DevConda — blog-workspace

Deploy Spring Boot to ECS Fargate from GitHub Actions

Part 4 system architecture at a glance

When should you read this?

You already pushed an image to ECR, run it on Fargate, and put HTTPS on an ALB. Deploys may still mean local commands: docker build, docker push, register-task-definition, update-service. That breaks when every merge should ship the same path without someone at a keyboard.

Part 4 of 4 (ECR -> Fargate -> ALB HTTPS -> Actions). This post ends when a green GitHub Actions run on main pushes a new image tag and the service behind https://api.example.com serves it.

It is the Fargate twin of Deploy Spring Boot from GitHub Actions to EC2. That post copies a jar over SSH. Here the pipeline builds a container, pushes to ECR, and rolls the ECS service — no scp.

What you are building

push to main
  -> GitHub Actions
      -> docker build + push  ECR  api:<sha>
      -> new task definition revision (family api)
      -> ECS UpdateService (wait for stability)
  -> ALB :443  ->  new Fargate task :8080

Reuse cluster api, service api, repository api, region ap-northeast-2, and the ALB hostname from part 3. Tag with $GITHUB_SHA — not only latest.

Final system after parts 1–4 (traffic on the ALB; deploys from Actions into ECR and the ECS service):

Part 4: system architecture (request flow) – GitHub / Actions / OIDC / IAM

EC2 Actions path (jar + SSH) versus this Fargate Actions path (image + ECR + service roll):

EC2 jar Actions path versus Fargate image Actions path
EC2 jar Actions path versus Fargate image Actions path

Before you start

  1. Parts 1–3 work by hand: ECR has a tag, the service is stable, curl https://$DOMAIN/actuator/health (or /) succeeds.
  2. The Spring Boot repo has the same Dockerfile as part 1 (fat jar → app.jar, listen on 8080).
  3. A GitHub repo where you can add a workflow and Actions secrets (or OIDC).
ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
REGION=ap-northeast-2
CLUSTER=api
SERVICE=api
ECR_REPO=api
DOMAIN=api.example.com

IAM for Actions

Prefer OIDC so you do not store long-lived access keys in GitHub. Create an IAM role that GitHub can assume, with rights to:

  • ECR: GetAuthorizationToken, push/pull on repository api
  • ECS: DescribeServices, DescribeTaskDefinition, RegisterTaskDefinition, UpdateService, DescribeTasks
  • iam:PassRole on the task execution role (ecsTaskExecutionRole)
  • Optional: elasticloadbalancing:DescribeTargetHealth if you verify targets in the job

Trust policy (replace ACCOUNT and OWNER/REPO; optionally lock sub to repo:OWNER/REPO:ref:refs/heads/main):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::ACCOUNT:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:OWNER/REPO:*"
        }
      }
    }
  ]
}

Create the OIDC provider once per account if it is missing (token.actions.githubusercontent.com), create role github-actions-ecs-deploy, attach the permissions above, then:

ROLE_ARN=arn:aws:iam::$ACCOUNT:role/github-actions-ecs-deploy

If you already use static keys for the EC2 deploy post, you can put AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY in repo secrets instead — same steps after configure-aws-credentials. Prefer OIDC for new setups.

GitHub OIDC assumes IAM role for ECR and ECS
GitHub OIDC assumes IAM role for ECR and ECS

Workflow file

Add .github/workflows/deploy-fargate.yml:

name: Deploy to Fargate

on:
  push:
    branches: [main]

env:
  AWS_REGION: ap-northeast-2
  ECR_REPOSITORY: api
  ECS_CLUSTER: api
  ECS_SERVICE: api
  CONTAINER_NAME: api

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build, tag, and push image
        id: build
        env:
          REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo "image=$REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT

      - name: Download current task definition
        run: |
          aws ecs describe-task-definition \
            --task-definition api \
            --query taskDefinition > task-definition.json

      - name: Render new image into task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: ${{ env.CONTAINER_NAME }}
          image: ${{ steps.build.outputs.image }}

      - name: Deploy to Amazon ECS
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true

Store AWS_ROLE_ARN as a GitHub Actions secret. CONTAINER_NAME must match the container name in the task definition (api in parts 2–3).

describe-task-definition returns read-only fields (taskDefinitionArn, revision, status, …) that ECS rejects on register. The official render + deploy actions strip those. If you register by hand, delete those keys first.

Workflow steps from checkout to ECS deploy
Workflow steps from checkout to ECS deploy

What rolls in ECS

amazon-ecs-deploy-task-definition registers a new revision of family api with the new image URI, then updates the service. With wait-for-service-stability: true, the job waits until the new task is healthy under the ALB target group (same health path as part 3).

If the new task fails health checks, ECS keeps or returns to the previous revision depending on deployment configuration. Check CloudWatch /ecs/api and target health next.

aws ecs describe-services \
  --cluster $CLUSTER \
  --services $SERVICE \
  --region $REGION \
  --query 'services[0].{td:taskDefinition,running:runningCount,desired:desiredCount}' \
  --output table
ECS replaces old task with new image revision
ECS replaces old task with new image revision

Verify

curl -sf "https://$DOMAIN/actuator/health" || curl -sf "https://$DOMAIN/"

The task definition does not include the container named api means CONTAINER_NAME does not match. CannotPullContainerError after push usually means the execution role or image URI is wrong — same as part 2, now visible in the Actions log. wait-for-service-stability timeout → unhealthy targets or Spring crash on boot; open /ecs/api and describe-target-health.

Green job and HTTPS health through ALB
Green job and HTTPS health through ALB

Series close

Parts 1–4 end here: jar → ECR → Fargate → ALB HTTPS → Actions roll on every push to main.

Minimal checklist

  1. OIDC role (or keys) can push to ECR api and update ECS service api.
  2. Dockerfile builds the same fat jar path as part 1.
  3. Workflow pushes $REGISTRY/api:$GITHUB_SHA and deploys with wait-for-service-stability: true.
  4. CONTAINER_NAME matches the task definition container name.
  5. After a green run, curl https://$DOMAIN/actuator/health (or /) succeeds.

After that, every push to main ships a new image through ECR to Fargate behind the ALB — without a local docker push.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *