CD — Terraform Apply + Deploy/Destroy (ECS) #97
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CD — Terraform Apply + Deploy/Destroy (ECS) | |
| on: | |
| workflow_dispatch: | |
| # checkov:skip=CKV_GHA_7 reason: controlled deploy inputs (mode/imageTag) for operator, not general user data | |
| inputs: | |
| mode: | |
| description: "apply: deploy image and scale to 1; destroy: cleanup everything" | |
| required: true | |
| type: choice | |
| options: [apply, destroy] | |
| default: apply | |
| imageTag: | |
| description: "Image tag to deploy (immutable tag in ECR)" | |
| required: true | |
| type: string | |
| env: | |
| AWS_REGION: us-east-1 | |
| CLUSTER_NAME: ecs-demo-cluster | |
| SERVICE_NAME: ecs-demo-svc | |
| ECR_REPOSITORY: ecs-demo-app | |
| LOG_GROUP: /ecs/ecs-demo | |
| permissions: | |
| id-token: write | |
| contents: read | |
| concurrency: | |
| group: cd-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| cd: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup jq | |
| run: sudo apt-get update && sudo apt-get install -y jq | |
| - name: Configure AWS (OIDC) | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| role-to-assume: arn:aws:iam::097635932419:role/github-actions-ecs-role | |
| aws-region: ${{ env.AWS_REGION }} | |
| - name: Setup Terraform | |
| uses: hashicorp/setup-terraform@v3 | |
| with: | |
| terraform_version: 1.7.5 | |
| - name: Terraform init (infra) | |
| working-directory: infra | |
| run: terraform init -input=false | |
| - name: Terraform import ECR if exists (before apply) | |
| if: ${{ inputs.mode == 'apply' }} | |
| working-directory: infra | |
| env: | |
| AWS_PAGER: "" | |
| run: | | |
| if aws ecr describe-repositories --repository-names "${{ env.ECR_REPOSITORY }}" --region "${{ env.AWS_REGION }}" >/dev/null 2>&1; then | |
| terraform state show aws_ecr_repository.this >/dev/null 2>&1 || terraform import aws_ecr_repository.this "${{ env.ECR_REPOSITORY }}" | |
| else | |
| echo "ECR not found — Terraform will create it." | |
| fi | |
| - name: Terraform apply (infra) | |
| if: ${{ inputs.mode == 'apply' }} | |
| working-directory: infra | |
| run: terraform apply -auto-approve -input=false | |
| - name: Compute ECR URL & Tag | |
| id: ecr | |
| run: | | |
| ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) | |
| ECR_URL="${ACCOUNT_ID}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY }}" | |
| TAG="${{ inputs.imageTag }}" | |
| echo "ECR_URL=$ECR_URL" >> "$GITHUB_OUTPUT" | |
| echo "TAG=$TAG" >> "$GITHUB_OUTPUT" | |
| - name: Assert image tag exists in ECR | |
| if: ${{ inputs.mode == 'apply' }} | |
| env: | |
| AWS_PAGER: "" | |
| run: | | |
| TAG="${{ steps.ecr.outputs.TAG }}" | |
| COUNT=$(aws ecr list-images \ | |
| --repository-name "${{ env.ECR_REPOSITORY }}" \ | |
| --region "${{ env.AWS_REGION }}" \ | |
| --filter tagStatus=TAGGED \ | |
| --query "length(imageIds[?imageTag=='${TAG}'])" \ | |
| --output text) | |
| if [ "$COUNT" -eq 0 ]; then | |
| echo "❌ Image tag not found in ECR: ${{ steps.ecr.outputs.ECR_URL }}:${TAG}" | |
| exit 1 | |
| else | |
| echo "✅ Found image tag: ${{ steps.ecr.outputs.ECR_URL }}:${TAG}" | |
| fi | |
| - name: Scale to 0 and wait (destroy) | |
| if: ${{ inputs.mode == 'destroy' }} | |
| continue-on-error: true | |
| run: | | |
| aws ecs update-service --cluster "${{ env.CLUSTER_NAME }}" --service "${{ env.SERVICE_NAME }}" --desired-count 0 --region "${{ env.AWS_REGION }}" || true | |
| aws ecs wait services-stable --cluster "${{ env.CLUSTER_NAME }}" --services "${{ env.SERVICE_NAME }}" --region "${{ env.AWS_REGION }}" || true | |
| echo "✅ Service scaled to 0." | |
| - name: Delete CloudWatch Log Group (destroy) | |
| if: ${{ inputs.mode == 'destroy' }} | |
| continue-on-error: true | |
| run: | | |
| aws logs delete-log-group --log-group-name "${{ env.LOG_GROUP }}" --region "${{ env.AWS_REGION }}" || true | |
| echo "🧹 CloudWatch log group deleted." | |
| - name: Terraform destroy (full cleanup) | |
| if: ${{ inputs.mode == 'destroy' }} | |
| working-directory: infra | |
| run: terraform destroy -auto-approve -input=false || true | |
| - name: ECR fallback cleanup (destroy) | |
| if: ${{ inputs.mode == 'destroy' }} | |
| continue-on-error: true | |
| env: | |
| AWS_PAGER: "" | |
| run: | | |
| if aws ecr describe-repositories --repository-names "${{ env.ECR_REPOSITORY }}" --region "${{ env.AWS_REGION }}" >/dev/null 2>&1; then | |
| IMAGES=$(aws ecr list-images --repository-name "${{ env.ECR_REPOSITORY }}" --region "${{ env.AWS_REGION }}" --query 'imageIds[*]' --output json || echo "[]") | |
| if [ "$IMAGES" != "[]" ]; then | |
| aws ecr batch-delete-image --repository-name "${{ env.ECR_REPOSITORY }}" --region "${{ env.AWS_REGION }}" --image-ids "$IMAGES" || true | |
| fi | |
| aws ecr delete-repository --repository-name "${{ env.ECR_REPOSITORY }}" --region "${{ env.AWS_REGION }}" --force || true | |
| echo "🗑️ Ensured ECR repo removed." | |
| fi | |
| - name: Verify state is empty (destroy) | |
| if: ${{ inputs.mode == 'destroy' }} | |
| working-directory: infra | |
| run: terraform state list || echo "✅ State is empty" | |
| - name: Get current TaskDefinition ARN | |
| if: ${{ inputs.mode == 'apply' }} | |
| id: svc | |
| run: | | |
| TD=$(aws ecs describe-services --cluster "${{ env.CLUSTER_NAME }}" --services "${{ env.SERVICE_NAME }}" --region "${{ env.AWS_REGION }}" --query "services[0].taskDefinition" --output text) | |
| echo "td=$TD" >> "$GITHUB_OUTPUT" | |
| - name: Download full TaskDefinition JSON | |
| if: ${{ inputs.mode == 'apply' }} | |
| run: | | |
| aws ecs describe-task-definition --task-definition "${{ steps.svc.outputs.td }}" --region "${{ env.AWS_REGION }}" --query "taskDefinition" > taskdef.json | |
| - name: Update image in TaskDefinition | |
| if: ${{ inputs.mode == 'apply' }} | |
| env: | |
| IMG: ${{ steps.ecr.outputs.ECR_URL }}:${{ steps.ecr.outputs.TAG }} | |
| run: | | |
| jq --arg IMG "$IMG" ' | |
| del(.revision,.status,.taskDefinitionArn,.requiresAttributes,.compatibilities,.registeredBy,.registeredAt,.deregisteredAt) | |
| | .containerDefinitions = (.containerDefinitions | map(if .name=="app" then .image=$IMG else . end)) | |
| ' taskdef.json > register.json | |
| echo "Using image: $IMG" | |
| - name: Register new TaskDefinition | |
| if: ${{ inputs.mode == 'apply' }} | |
| id: register | |
| run: | | |
| NEW_TD=$(aws ecs register-task-definition --region "${{ env.AWS_REGION }}" --cli-input-json file://register.json --query "taskDefinition.taskDefinitionArn" --output text) | |
| echo "new=$NEW_TD" >> "$GITHUB_OUTPUT" | |
| - name: Update Service & wait | |
| if: ${{ inputs.mode == 'apply' }} | |
| run: | | |
| aws ecs update-service --cluster "${{ env.CLUSTER_NAME }}" --service "${{ env.SERVICE_NAME }}" --task-definition "${{ steps.register.outputs.new }}" --desired-count 1 --region "${{ env.AWS_REGION }}" | |
| aws ecs wait services-stable --cluster "${{ env.CLUSTER_NAME }}" --services "${{ env.SERVICE_NAME }}" --region "${{ env.AWS_REGION }}" | |
| echo "✅ Deployed and service is stable." |