diff --git a/Dockerfile b/Dockerfile index 5bf26dae..486dac16 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ -FROM node:22-alpine +# https://stackoverflow.com/questions/65612411/forcing-docker-to-use-linux-amd64-platform-by-default-on-macos/69636473#69636473 +FROM --platform=linux/amd64 node:22-alpine ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 355e658c..015b5f21 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -1,6 +1,7 @@ import express from "express"; import userRoutes from "./routes/user"; import chipRoutes from "./routes/chip"; +import healthRoutes from "./routes/health"; import { FRONTEND_URL } from "./constants"; const cors = require("cors"); @@ -18,6 +19,7 @@ app.use(express.json()); // Routes app.use("/api/user", userRoutes); app.use("/api/chip", chipRoutes); +app.use("/api/health", healthRoutes); const PORT = process.env.PORT || 8080; app.listen(PORT, () => { diff --git a/apps/backend/src/routes/health/index.ts b/apps/backend/src/routes/health/index.ts new file mode 100644 index 00000000..6fbe5b79 --- /dev/null +++ b/apps/backend/src/routes/health/index.ts @@ -0,0 +1,27 @@ +import express, {Request, Response} from "express"; +import {ErrorResponse} from "@types"; +import {z} from "zod"; + +export const HealthResponse = z.object({ + status: z.string(), +}); + +export type HealthResponse = z.infer< + typeof HealthResponse +>; + +const router = express.Router(); + +router.get( + "/status", + async ( + req: Request<{}, {}, null>, + res: Response + ) => { + return res.status(200).json({ + status: "live" + }); + } +); + +export default router; \ No newline at end of file diff --git a/deployment/README.md b/deployment/README.md index 607dcb56..035a1175 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -124,14 +124,17 @@ docker run -td -p 8080:8080 connections:1 export ACCOUNT_NUMBER="${AWS account number}" export AWS_REGION="ap-southeast-1" export ECR_REPO_NAME=${Repo name} -export $IMAGE-ID=${Image ID from docker image build} +export IMAGE_ID=${Image ID from docker image build} -aws ecr get-login-password --profile connections-admin-role --region $AWS_REGION +aws ecr get-login-password --profile connections-admin --region $AWS_REGION | docker login --username AWS --password-stdin $ACCOUNT_NUMBER.dkr.ecr.$AWS_REGION.amazonaws.com -docker login --username AWS --password-stdin $ACCOUNT_NUMBER.dkr.ecr.$AWS_REGION.amazonaws.com +docker tag $IMAGE_ID $ACCOUNT_NUMBER.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPO_NAME:1 +docker push $ACCOUNT_NUMBER.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPO_NAME:1 +``` -docker tag $IMAGE-ID $ACCOUNT_NUMBER.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPO_NAME:1` -docker push $ACCOUNT_NUMBER.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPO_NAME:1` +### Manually Apply Specific Tag to Infra +``` +terraform apply -var="image_tag=${tag number}" -auto-approve ``` ### TODO diff --git a/deployment/main.tf b/deployment/ecr.tf similarity index 100% rename from deployment/main.tf rename to deployment/ecr.tf diff --git a/deployment/ecs.tf b/deployment/ecs.tf new file mode 100644 index 00000000..f8f169ec --- /dev/null +++ b/deployment/ecs.tf @@ -0,0 +1,96 @@ +module "cloudwatch_logs" { + source = "cloudposse/cloudwatch-logs/aws" + version = "0.6.6" + + namespace = var.namespace + # stage = var.stage + name = var.name + + retention_in_days = 7 +} + +module "container_definition" { + source = "cloudposse/ecs-container-definition/aws" + version = "0.58.1" + + # container_name = "${var.namespace}-${var.stage}-${var.name}" + container_name = "${var.namespace}-${var.name}" + container_image = "${module.ecr.repository_url}:${var.image_tag}" + container_memory = 512 # optional for FARGATE launch type + container_cpu = 256 # optional for FARGATE launch type + essential = true + port_mappings = var.container_port_mappings + + # The environment variables to pass to the container. + #environment = [ + # { + # name = "ENV_NAME" + # value = "ENV_VALUE" + # }, + #] + + # Pull secrets from AWS Parameter Store. + # "name" is the name of the env var. + # "valueFrom" is the name of the secret in PS. + secrets = [ + # { + # name = "SECRET_ENV_NAME" + # valueFrom = "SECRET_ENV_NAME" + # }, + ] + + log_configuration = { + logDriver = "awslogs" + options = { + "awslogs-region" = var.region + "awslogs-group" = module.cloudwatch_logs.log_group_name + "awslogs-stream-prefix" = var.name + } + secretOptions = null + } +} + +resource "aws_ecs_cluster" "ecs_cluster" { + # name = "${var.namespace}-${var.stage}-${var.name}" + name = "${var.namespace}-${var.name}" + tags = { + Namespace = var.namespace + # Stage = var.stage + Name = var.name + } +} + +module "ecs_alb_service_task" { + source = "cloudposse/ecs-alb-service-task/aws" + version = "0.66.4" + + namespace = var.namespace + # stage = var.stage + name = var.name + + use_alb_security_group = true + alb_security_group = module.alb.security_group_id + container_definition_json = module.container_definition.json_map_encoded_list + ecs_cluster_arn = aws_ecs_cluster.ecs_cluster.arn + launch_type = "FARGATE" + vpc_id = module.vpc.vpc_id + security_group_ids = [module.vpc.vpc_default_security_group_id] + subnet_ids = module.subnets.private_subnet_ids # change to "module.subnets.public_subnet_ids" if "nat_gateway_enabled" is false + ignore_changes_task_definition = false + network_mode = "awsvpc" + assign_public_ip = false # change to true if "nat_gateway_enabled" is false + propagate_tags = "TASK_DEFINITION" + desired_count = var.desired_count + task_memory = 512 + task_cpu = 256 + force_new_deployment = true + container_port = var.container_port_mappings[0].containerPort + + ecs_load_balancers = [{ + # container_name = "${var.namespace}-${var.stage}-${var.name}" + container_name = "${var.namespace}-${var.name}" + container_port = var.container_port_mappings[0].containerPort + elb_name = "" + target_group_arn = module.alb.default_target_group_arn + }] +} \ No newline at end of file diff --git a/deployment/networking.tf b/deployment/networking.tf new file mode 100644 index 00000000..3fe24a35 --- /dev/null +++ b/deployment/networking.tf @@ -0,0 +1,45 @@ +module "vpc" { + source = "cloudposse/vpc/aws" + version = "2.0.0" + + namespace = var.namespace + # stage = var.stage + name = var.name + + ipv4_primary_cidr_block = "10.0.0.0/16" +} + +module "subnets" { + source = "cloudposse/dynamic-subnets/aws" + version = "2.0.4" + + namespace = var.namespace + # stage = var.stage + name = var.name + + availability_zones = ["ap-southeast-1a", "ap-southeast-1b", "ap-southeast-1c"] # change to your AZs + vpc_id = module.vpc.vpc_id + igw_id = [module.vpc.igw_id] + ipv4_cidr_block = [module.vpc.vpc_cidr_block] + nat_gateway_enabled = true + max_nats = 1 +} + +module "alb" { + source = "cloudposse/alb/aws" + version = "1.7.0" + + namespace = var.namespace + # stage = var.stage + name = var.name + + access_logs_enabled = false + vpc_id = module.vpc.vpc_id + ip_address_type = "ipv4" + subnet_ids = module.subnets.public_subnet_ids + security_group_ids = [module.vpc.vpc_default_security_group_id] + # https_enabled = true + # certificate_arn = aws_acm_certificate.cert.arn + # http_redirect = true + health_check_interval = 60 +} diff --git a/deployment/outputs.tf b/deployment/outputs.tf index 88a64663..49d2a818 100644 --- a/deployment/outputs.tf +++ b/deployment/outputs.tf @@ -1,4 +1,14 @@ output "github_actions_role_arn" { description = "The ARN of the role to be assumed by the GitHub Actions" value = aws_iam_role.github_actions_role.arn +} + +output "alb_dns_name" { + description = "DNS name of ALB" + value = module.alb.alb_dns_name +} + +output "ecr_repository_name" { + description = "The name of the ECR Repository" + value = module.ecr.repository_name } \ No newline at end of file diff --git a/deployment/provider.tf b/deployment/provider.tf index 7cbf0f97..0c6eb799 100644 --- a/deployment/provider.tf +++ b/deployment/provider.tf @@ -7,7 +7,7 @@ terraform { } provider "aws" { - region = "${var.aws_region}" + region = "${var.region}" default_tags { tags = { diff --git a/deployment/variables.tf b/deployment/variables.tf index e8fafbd9..b76649f0 100644 --- a/deployment/variables.tf +++ b/deployment/variables.tf @@ -1,4 +1,4 @@ -variable "aws_region" { +variable "region" { type = string default = "ap-southeast-1" // Singapore description = "aws region for current resource" @@ -26,4 +26,32 @@ variable "stage" { type = string default = "prod" description = "Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "image_tag" { + type = string + default = "latest" + description = "Docker image tag" +} + +variable "container_port_mappings" { + type = list(object({ + containerPort = number + hostPort = number + protocol = string + })) + default = [ + { + containerPort = 8080 + hostPort = 8080 + protocol = "tcp" + } + ] + description = "The port mappings to configure for the container. This is a list of maps. Each map should contain \"containerPort\", \"hostPort\", and \"protocol\", where \"protocol\" is one of \"tcp\" or \"udp\". If using containers in a task with the awsvpc or host network mode, the hostPort can either be left blank or set to the same value as the containerPort" +} + +variable "desired_count" { + type = number + description = "The number of instances of the task definition to place and keep running" + default = 1 } \ No newline at end of file