diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index f0ffe54d2..915d1929c 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -19,9 +19,6 @@ jobs: cicd-role: 'arn:aws:iam::964715690079:role/plumber-prod-github-oidc-role' ecr-repository: 'plumber-prod' ecs-cluster-name: 'plumber-prod-ecs' - ecs-service-name: 'plumber-prod-ecs-service' - ecs-worker-service-name: 'plumber-prod-ecs-worker-service' - ecs-container-name: 'app' codedeploy-application: 'plumber-prod-ecs-app' codedeploy-deployment-group: 'plumber-prod-ecs-dg' - release-version: ${{ github.sha }} \ No newline at end of file + release-version: ${{ github.sha }} diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 60c8b6de0..f4777d4e2 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -19,9 +19,6 @@ jobs: cicd-role: 'arn:aws:iam::964715690079:role/plumber-staging-github-oidc-role' ecr-repository: 'plumber-staging' ecs-cluster-name: 'plumber-staging-ecs' - ecs-service-name: 'plumber-staging-ecs-service' - ecs-worker-service-name: 'plumber-staging-ecs-worker-service' - ecs-container-name: 'app' codedeploy-application: 'plumber-staging-ecs-app' codedeploy-deployment-group: 'plumber-staging-ecs-dg' release-version: ${{ github.sha }} diff --git a/.github/workflows/deploy-test.yml b/.github/workflows/deploy-test.yml new file mode 100644 index 000000000..2f9646dd8 --- /dev/null +++ b/.github/workflows/deploy-test.yml @@ -0,0 +1,24 @@ +name: Deploy to test + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - build/ecs-deployment + +jobs: + deploy: + name: Deploy + uses: ./.github/workflows/deploy.yml + with: + environment: 'test' + aws-account-id: '394714924170' + cicd-role: 'arn:aws:iam::394714924170:role/oidc-admin' + ecr-repository: 'plumber-test' + ecs-cluster-name: 'plumber-test-ecs' + codedeploy-application: 'plumber-test-ecs-app' + codedeploy-deployment-group: 'plumber-test-ecs-dg' + release-version: ${{ github.sha }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ec0ba57e2..da517cdda 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -22,18 +22,6 @@ on: description: 'ECS cluster to deploy to' required: true type: string - ecs-service-name: - description: 'ECS service for server' - required: true - type: string - ecs-worker-service-name: - description: 'ECS service for worker' - required: true - type: string - ecs-container-name: - description: 'Name of container in ECS task definition' - required: true - type: string codedeploy-application: description: 'CodeDeploy application to use' required: true @@ -89,11 +77,6 @@ jobs: docker push ${{ steps.ecr-image-uri.outputs.image }} deploy: - strategy: - matrix: - service: - - server - - worker needs: build runs-on: ubuntu-latest steps: @@ -121,50 +104,46 @@ jobs: IMAGE_TAG: ${{ inputs.environment }}-${{ inputs.release-version }} run: echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT + - name: Merge ECS task definition templates + id: merge-ecs-templates + run: | + cp ecs/task-definition.json ecs-task-definition.json + jq --argjson environment "$(jq '.environment' ecs/env.json)" '.containerDefinitions[0].environment += $environment' ecs-task-definition.json + jq --argjson environment "$(jq '.secrets' ecs/env.json)" '.containerDefinitions[0].environment += $environment' ecs-task-definition.json + jq --argjson environment "$(jq '.environment' ecs/env.json)" '.containerDefinitions[1].environment += $environment' ecs-task-definition.json + jq --argjson environment "$(jq '.secrets' ecs/env.json)" '.containerDefinitions[1].environment += $environment' ecs-task-definition.json + cp ecs/appspec.json appspec.json + - name: Replace variables in task definition file run: | sed -i 's//${{ inputs.aws-account-id }}/g' ecs-task-definition.json sed -i 's//${{ inputs.environment }}/g' ecs-task-definition.json - sed -i 's//${{ matrix.service }}/g' ecs-task-definition.json sed -i 's//${{ inputs.aws-account-id }}/g' appspec.json sed -i 's//${{ inputs.environment }}/g' appspec.json - sed -i 's//${{ matrix.service }}/g' appspec.json - - - name: Replace start command in task definition file (server) - if: matrix.service == 'server' - run: | - sed -i 's//start/g' ecs-task-definition.json - - - name: Replace start command in task definition file (worker) - if: matrix.service == 'worker' - run: | - sed -i 's//start:worker/g' ecs-task-definition.json - name: Fill in the new image ID in the Amazon ECS task definition id: server-task-def uses: aws-actions/amazon-ecs-render-task-definition@v1 with: task-definition: ecs-task-definition.json - container-name: ${{ inputs.ecs-container-name }} + container-name: server + image: ${{ steps.ecr-image-uri.outputs.image }} + + - name: Fill in the new image ID in the Amazon ECS task definition + id: full-task-def + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition: ${{ steps.server-task-def.outputs.task-definition }} + container-name: worker image: ${{ steps.ecr-image-uri.outputs.image }} - name: Deploy Amazon ECS task definition to server uses: aws-actions/amazon-ecs-deploy-task-definition@v1 - if: matrix.service == 'server' with: - task-definition: ${{ steps.server-task-def.outputs.task-definition }} + task-definition: ${{ steps.full-task-def.outputs.task-definition }} cluster: ${{ inputs.ecs-cluster-name }} service: ${{ inputs.ecs-service-name }} wait-for-service-stability: false codedeploy-appspec: appspec.json codedeploy-application: ${{ inputs.codedeploy-application }} codedeploy-deployment-group: ${{ inputs.codedeploy-deployment-group }} - - - name: Deploy Amazon ECS task definition to worker - uses: aws-actions/amazon-ecs-deploy-task-definition@v1 - if: matrix.service == 'worker' - with: - task-definition: ${{ steps.server-task-def.outputs.task-definition }} - cluster: ${{ inputs.ecs-cluster-name }} - service: ${{ inputs.ecs-worker-service-name }} - wait-for-service-stability: false diff --git a/ecs-task-definition.json b/ecs-task-definition.json deleted file mode 100644 index 69a551bf9..000000000 --- a/ecs-task-definition.json +++ /dev/null @@ -1,218 +0,0 @@ -{ - "containerDefinitions": [ - { - "name": "app", - "cpu": 0, - "command": ["npm", "run", ""], - "portMappings": [ - { - "containerPort": 8080, - "hostPort": 8080, - "protocol": "tcp" - } - ], - "essential": true, - "environment": [ - { - "name": "TZ", - "value": "Asia/Singapore" - }, - { - "name": "NODE_ENV", - "value": "production" - }, - { - "name": "DD_SOURCE", - "value": "nodejs" - }, - { - "name": "APP_ENV", - "value": "" - }, - { - "name": "POSTGRES_ENABLE_SSL", - "value": "true" - }, - { - "name": "REDIS_TLS", - "value": "true" - }, - { - "name": "REDIS_CLUSTER_MODE", - "value": "true" - }, - { - "name": "SERVE_WEB_APP_SEPARATELY", - "value": "false" - }, - { - "name": "POSTMAN_FROM_ADDRESS", - "value": "donotreply@plumber.gov.sg" - } - ], - "secrets": [ - { - "name": "PORT", - "valueFrom": "plumber--port" - }, - { - "name": "BASE_URL", - "valueFrom": "plumber--base-url" - }, - { - "name": "WEB_APP_URL", - "valueFrom": "plumber--base-url" - }, - { - "name": "WEBHOOK_URL", - "valueFrom": "plumber--base-url" - }, - { - "name": "POSTGRES_HOST", - "valueFrom": "plumber--rds-host" - }, - { - "name": "POSTGRES_DATABASE", - "valueFrom": "plumber--rds-database" - }, - { - "name": "POSTGRES_USERNAME", - "valueFrom": "plumber--rds-username" - }, - { - "name": "POSTGRES_PASSWORD", - "valueFrom": "plumber--rds-password" - }, - { - "name": "SESSION_SECRET_KEY", - "valueFrom": "plumber--session-secret-key" - }, - { - "name": "ENCRYPTION_KEY", - "valueFrom": "plumber--encryption-key" - }, - { - "name": "REDIS_PORT", - "valueFrom": "plumber--redis-port" - }, - { - "name": "REDIS_HOST", - "valueFrom": "plumber--redis-host" - }, - { - "name": "REDIS_USERNAME", - "valueFrom": "plumber--redis-username" - }, - { - "name": "REDIS_PASSWORD", - "valueFrom": "plumber--redis-password" - }, - { - "name": "POSTMAN_API_KEY", - "valueFrom": "plumber--postman-api-key" - }, - { - "name": "FORMSG_API_KEY", - "valueFrom": "plumber--formsg-api-key" - }, - { - "name": "WORKER_ACTION_CONCURRENCY", - "valueFrom": "plumber--worker-action-concurrency" - }, - { - "name": "S3_COMMON_BUCKET", - "valueFrom": "plumber--s3-common-bucket" - }, - { - "name": "SGID_CLIENT_ID", - "valueFrom": "plumber--sgid-client-id" - }, - { - "name": "SGID_CLIENT_SECRET", - "valueFrom": "plumber--sgid-client-secret" - }, - { - "name": "SGID_PRIVATE_KEY", - "valueFrom": "plumber--sgid-private-key" - }, - { - "name": "POSTMAN_RATE_LIMIT", - "valueFrom": "plumber--postman-rate-limit" - } - ], - "logConfiguration": { - "logDriver": "awslogs", - "options": { - "awslogs-group": "plumber-/ecs/application-", - "awslogs-region": "ap-southeast-1", - "awslogs-stream-prefix": "ecs" - } - } - }, - { - "name": "dd-agent", - "image": "public.ecr.aws/datadog/agent:latest", - "portMappings": [ - { - "containerPort": 8126, - "hostPort": 8126, - "protocol": "tcp" - } - ], - "essential": true, - "environment": [ - { - "name": "TZ", - "value": "Asia/Singapore" - }, - { - "name": "DD_APM_NON_LOCAL_TRAFFIC", - "value": "true" - }, - { - "name": "ECS_FARGATE", - "value": "true" - }, - { - "name": "DD_APM_ENABLED", - "value": "true" - }, - { - "name": "DD_SITE", - "value": "datadoghq.com" - }, - { - "name": "DD_SERVICE", - "value": "plumber--ecs-" - }, - { - "name": "DD_TAGS", - "value": "env: service:plumber team:plumber" - } - ], - "mountPoints": [], - "volumesFrom": [], - "secrets": [ - { - "name": "DD_API_KEY", - "valueFrom": "plumber-dd-api-key" - } - ], - "logConfiguration": { - "logDriver": "awslogs", - "options": { - "awslogs-group": "plumber-/dd-agent/", - "awslogs-region": "ap-southeast-1", - "awslogs-stream-prefix": "ecs" - } - } - } - ], - "family": "plumber--ecs-", - "executionRoleArn": "arn:aws:iam:::role/plumber--ecs-task-exec-role", - "taskRoleArn": "arn:aws:iam:::role/plumber--ecs-task-role", - "networkMode": "awsvpc", - "requiresCompatibilities": ["FARGATE"], - "cpu": "1024", - "memory": "2048" -} diff --git a/appspec.json b/ecs/appspec.json similarity index 83% rename from appspec.json rename to ecs/appspec.json index 327a58d14..4854596ab 100644 --- a/appspec.json +++ b/ecs/appspec.json @@ -5,7 +5,7 @@ "TargetService": { "Type": "AWS::ECS::Service", "Properties": { - "TaskDefinition": "arn:aws:ecs:ap-southeast-1::task-definition/plumber--ecs-:1", + "TaskDefinition": "arn:aws:ecs:ap-southeast-1::task-definition/plumber--ecs-server:1", "LoadBalancerInfo": { "ContainerName": "app", "ContainerPort": 8080 diff --git a/ecs/env.json b/ecs/env.json new file mode 100644 index 000000000..b1d091cb1 --- /dev/null +++ b/ecs/env.json @@ -0,0 +1,130 @@ +{ + "environment": [ + { + "name": "TZ", + "value": "Asia/Singapore" + }, + { + "name": "NODE_ENV", + "value": "production" + }, + { + "name": "DD_SOURCE", + "value": "nodejs" + }, + { + "name": "APP_ENV", + "value": "" + }, + { + "name": "POSTGRES_ENABLE_SSL", + "value": "true" + }, + { + "name": "REDIS_TLS", + "value": "true" + }, + { + "name": "REDIS_CLUSTER_MODE", + "value": "true" + }, + { + "name": "SERVE_WEB_APP_SEPARATELY", + "value": "false" + }, + { + "name": "POSTMAN_FROM_ADDRESS", + "value": "donotreply@plumber.gov.sg" + } + ], + "secrets": [ + { + "name": "PORT", + "valueFrom": "plumber--port" + }, + { + "name": "BASE_URL", + "valueFrom": "plumber--base-url" + }, + { + "name": "WEB_APP_URL", + "valueFrom": "plumber--base-url" + }, + { + "name": "WEBHOOK_URL", + "valueFrom": "plumber--base-url" + }, + { + "name": "POSTGRES_HOST", + "valueFrom": "plumber--rds-host" + }, + { + "name": "POSTGRES_DATABASE", + "valueFrom": "plumber--rds-database" + }, + { + "name": "POSTGRES_USERNAME", + "valueFrom": "plumber--rds-username" + }, + { + "name": "POSTGRES_PASSWORD", + "valueFrom": "plumber--rds-password" + }, + { + "name": "SESSION_SECRET_KEY", + "valueFrom": "plumber--session-secret-key" + }, + { + "name": "ENCRYPTION_KEY", + "valueFrom": "plumber--encryption-key" + }, + { + "name": "REDIS_PORT", + "valueFrom": "plumber--redis-port" + }, + { + "name": "REDIS_HOST", + "valueFrom": "plumber--redis-host" + }, + { + "name": "REDIS_USERNAME", + "valueFrom": "plumber--redis-username" + }, + { + "name": "REDIS_PASSWORD", + "valueFrom": "plumber--redis-password" + }, + { + "name": "POSTMAN_API_KEY", + "valueFrom": "plumber--postman-api-key" + }, + { + "name": "FORMSG_API_KEY", + "valueFrom": "plumber--formsg-api-key" + }, + { + "name": "WORKER_ACTION_CONCURRENCY", + "valueFrom": "plumber--worker-action-concurrency" + }, + { + "name": "S3_COMMON_BUCKET", + "valueFrom": "plumber--s3-common-bucket" + }, + { + "name": "SGID_CLIENT_ID", + "valueFrom": "plumber--sgid-client-id" + }, + { + "name": "SGID_CLIENT_SECRET", + "valueFrom": "plumber--sgid-client-secret" + }, + { + "name": "SGID_PRIVATE_KEY", + "valueFrom": "plumber--sgid-private-key" + }, + { + "name": "POSTMAN_RATE_LIMIT", + "valueFrom": "plumber--postman-rate-limit" + } + ] +} diff --git a/ecs/task-definition.json b/ecs/task-definition.json new file mode 100644 index 000000000..0698c6751 --- /dev/null +++ b/ecs/task-definition.json @@ -0,0 +1,111 @@ +{ + "containerDefinitions": [ + { + "name": "server", + "cpu": 0, + "command": ["npm", "run", "start"], + "portMappings": [ + { + "containerPort": 8080, + "hostPort": 8080, + "protocol": "tcp" + } + ], + "essential": true, + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "plumber-/ecs/application-server", + "awslogs-region": "ap-southeast-1", + "awslogs-stream-prefix": "ecs" + } + } + }, + { + "name": "worker", + "cpu": 0, + "command": ["npm", "run", "start:worker"], + "portMappings": [ + { + "containerPort": 8081, + "hostPort": 8081, + "protocol": "tcp" + } + ], + "essential": true, + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "plumber-/ecs/application-worker", + "awslogs-region": "ap-southeast-1", + "awslogs-stream-prefix": "ecs" + } + } + }, + { + "name": "dd-agent", + "image": "public.ecr.aws/datadog/agent:latest", + "portMappings": [ + { + "containerPort": 8126, + "hostPort": 8126, + "protocol": "tcp" + } + ], + "essential": true, + "environment": [ + { + "name": "TZ", + "value": "Asia/Singapore" + }, + { + "name": "DD_APM_NON_LOCAL_TRAFFIC", + "value": "true" + }, + { + "name": "ECS_FARGATE", + "value": "true" + }, + { + "name": "DD_APM_ENABLED", + "value": "true" + }, + { + "name": "DD_SITE", + "value": "datadoghq.com" + }, + { + "name": "DD_SERVICE", + "value": "plumber--ecs-" + }, + { + "name": "DD_TAGS", + "value": "env: service:plumber team:plumber" + } + ], + "mountPoints": [], + "volumesFrom": [], + "secrets": [ + { + "name": "DD_API_KEY", + "valueFrom": "plumber-dd-api-key" + } + ], + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "plumber-/dd-agent/", + "awslogs-region": "ap-southeast-1", + "awslogs-stream-prefix": "ecs" + } + } + } + ], + "family": "plumber--ecs", + "executionRoleArn": "arn:aws:iam:::role/plumber--ecs-task-exec-role", + "taskRoleArn": "arn:aws:iam:::role/plumber--ecs-task-role", + "networkMode": "awsvpc", + "requiresCompatibilities": ["FARGATE"], + "cpu": "1024", + "memory": "2048" +}