ECS + ECR + GitHub Actions로 Spring Boot 배포 자동화(CI/CD) 구축하기

Nevgiveup·2026년 2월 9일

Backend

목록 보기
7/8
post-thumbnail

0. 목표

0.1 요약

  1. 먼저 ECR에 리포지토리 만들고 docker 빌드 파일 업로드하기
  2. ECS에서 클러스터를 하나 만들고 태스크 정의를 ECR에서 만든 docker로 설정하기
  3. GitHub Actions Workflow로 배포 설정하기

0.2 목표

로컬에서 docker-compose up으로 돌리던 Spring Boot 서버를, CI/CD 자동화 구축을 하게 만들어 보고자 했다.

git tag v0.0.1
git push --tags

나는 이런식으로 태그를 붙여서 push하면 자동으로 배포까지 되는 것을 원했다.

  1. GitHub Actions이 Docker 이미지를 빌드해서 Amazon ECR에 push한다.
  2. Amazon ECS가 새 이미지를 가져와서 배포한다.
  3. RDS(MySQL) 비밀번호와 JWT 시크릿은 AWS Secrets Manager로 관리하고, 태스크 실행 시 주입한다.

1. 아키텍처

아래 사진은 CI/CD 흐름도이다.

Secrets Manager는 태스크가 시작될 때 ECS가 값을 조회해 컨테이너 환경변수로 주입한다.
CloudWatch Logs는 컨테이너 표준 출력 로그를 전송해 확인한다.

2. 준비

2.1 애플리케이션 포트/환경변수

Spring Boot 컨테이너는 8080을 포트로 썼다.

DB 정보는 ECS Task 환경변수로 주입한다.

SPRING_DATASOURCE_URL
SPRING_DATASOURCE_USERNAME
SPRING_DATASOURCE_PASSWORD (Secrets manager로 넣기)

3. ECR 리포지토리 만들기

ECR에서 리포지토리를 생성한다.

이 리포지토리에 GitHub Actions가 빌드한 Docker이미지를 푸시한다.

4. RDS(MySQL)

서버에서 사용할 RDS의 URL이나 비밀번호 같은것을 정리해서

4.1 RDS 정보

ECS 태스크는 스프링 부트 컨테이너를 실행하고, 그 컨테이너는 DB에 접속하기 때문에 DB정보를 태스크에 넣어줘야 한다. 사용하는 RDS의 URL, password, username등을 잘 알아두어야한다.

  • SPRING_DATASOURCE_PASSWORD
  • SPRING_DATASOURCE_URL
  • SPRING_DATASOURCE_USERNAME

4.2 보안그룹(Security Group)

RDS 인바운드 규칙을 설정해줘야한다.

Type: MySQL/Aurora (3306)
Source: CS Task가 사용하는 보안그룹
이런식으로 ECS 보안 그룹을 허용해주면 된다. 그러면 ECS에서만 DB접근이 가능하게된다.

ECS서비스를 만들 때 어떤 보안그룹을 붙였는지 확인하고 그 보안그룹을 RDS 인바운드에 넣어야한다.

5. Secrets Manager 만들기

5.1 Secret 생성 타입

Amazon RDS 데이터베이스 자격 증명 타입으로 만들면 편하다.

새 보안 암호 저장을 누르고 DB에서 사용하는 비번같은 비밀정보를 넣어서 사용하면 된다.

레포지토리가 public이기때문에, 비밀번호/JWT 시크릿을 코드나 GitHub 저장소에 직접 적지 않는다.

5.2 ECS Task Definition에서 valueFrom으로 주입하기

secrets사용법에서 조금 헷갈렸는데 보안 암호 세부 정보에서 보안암호 ARN 정의에서 secret 사용은 이렇게 했다.

ECS 태스크 정의에서 secrets에 valueFrom으로 ARN을 넣는다.
Secrets Manager ARN은 :키이름:: 형태로 특정 키를 지정할 수 있다.

ex) arn:~~~~:secret:service_name:root_password::

"secrets": [
  {
    "name": "SPRING_DATASOURCE_PASSWORD",
    "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:<ACCOUNT_ID>:secret:XXXXXX"
  }
]

6. CloudWatch Logs

log를 남겨서 보고 싶어서 cloudWatch로 로그를 남겨놓도록 설정해놨다. 이부분은 사실 하지 않아도 된다.

ECS 컨테이너 로그를 CloudWatch로 보내려면 CloudWatch Log Group을 미리 만들어야한다.

Task Definition 로그 설정에 awslogs를 넣고 CloudWatch에서 같은 이름의 로그 그룹을 만들어 주면 된다.

예: /ecs/service/log 로 로그 그룹을 만들었다면

"logConfiguration": {
  "logDriver": "awslogs",
  "options": {
    "awslogs-group": "/ecs/service/log",
    "awslogs-region": "ap-northeast-2",
    "awslogs-stream-prefix": "ecs",
    "awslogs-create-group": "true"
  }
}

7. IAM 권한 구성

권한은 두 종류로 나눠서 만든다.

7.1 ECS Task Execution Role

아래 사진을 보면 weblog-ecs-secrets-read이라고 커스텀 권한이 있는데 Secrets Manager read write 아님 GetSecretValue 같은 것을 넣어줘도 될 것이다. ecs는 배포하고 실행까지 하기 때문에 RDS에 접근해야한다. 그래서 RDS비밀번호가 있는 Secrets Manager 권한이 필요하다.

7.2 GitHub Actions 배포 Role (OIDC)

GitHub Actions가 AWS에 배포를 하려면 OIDC Role이 필요하다. 나는 Access Key를 저장하지 않고, OIDC로 임시 자격 증명을 발급받는 방법을 사용했다.

8. ECS 클러스터 생성

쉽게 말해보면 ECS는 우리가 사용하는 컨테이너(Docker 같은 것)를 관리하기 위한 도구이다. 컨테이너를 쉽게 실행, 중지하는 등의 관리가 가능하다.

9. Task Definition 작성

나는 레포에 .aws/ecs/task-definition.json로 고정을 해두었다.

9.1 레포 구조

9.2 Task Definition

내가 사용하는 Def구조이다. 여기를 참고하면 더 자세히 볼 수 있다.

{
  "family": "여기에 task이름",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "executionRoleArn": "arn:aws:iam::<AWS_ACCOUNT_ID>:role/ecsTaskExecutionRole 여기에 역할 ARN넣기",
  "taskRoleArn": "arn:aws:iam::<AWS_ACCOUNT_ID>:role/ecsTaskExecutionRole 여기도",
  "containerDefinitions": [
    {
      "name": "태스크 안 컨테이너 이름",
      "image": "<AWS_ACCOUNT_ID>.dkr.ecr.ap-northeast-2.amazonaws.com/service:latest",
      "essential": true,
      "portMappings": [{ "containerPort": 8080, "protocol": "tcp" }],
      "environment": [
        { "name": "SPRING_DATASOURCE_URL", "value": "jdbc:mysql://<RDS_ENDPOINT>:3306/serviceDB?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&sslMode=REQUIRED" RDS URL넣기},
        { "name": "SPRING_DATASOURCE_USERNAME", "value": "service_root" RDS에서 사용하는 이름}
      ],
      "secrets": [
        { "name": "SPRING_DATASOURCE_PASSWORD", "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:<AWS_ACCOUNT_ID>:secret:service-db-password-XXXXXX:password::" 여기는 secretmanager에 비번 ARN}
      ],
      "logConfiguration": { CloudWatch를 안쓰면 없애야함
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/weblog",
          "awslogs-region": "ap-northeast-2",
          "awslogs-stream-prefix": "ecs",
          "awslogs-create-group": "true"
        }
      }
    }
  ]
}

DB 비번은 environment가 아니라 secrets을 썼다.
로그를 설정하면 CloudWatch log group이랑 IAM권한도 맞춰줘야 한다.

10. ECS 서비스 생성

클러스터 위에 서비스를 만들면, 서비스는 태스크를 항상 N개 유지하고, 새 task definition이 올라오면 롤링 배포를 한다.

11. GitHub Actions Workflow

11.1 레포 변수/시크릿

나는 Github에있는 레포 변수값을 사용했다 위치는 아래 사진과 같다.

Variables

  • AWS_REGION
  • ECR_REPOSITORY
  • ECS_CLUSTER
  • ECS_SERVICE
  • ECS_TASK_DEFINITION
  • CONTAINER_NAME

Secrets

  • AWS_ROLE_TO_ASSUME (OIDC role arn)

11.2 워크플로우

내가 사용한 워크플로우는 아래와 같다.

name: Deploy to ECS

on:
  push:
    tags: ["v*"]
  workflow_dispatch:

env:
  AWS_REGION: ${{ vars.AWS_REGION }}
  ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
  ECS_CLUSTER: ${{ vars.ECS_CLUSTER }}
  ECS_SERVICE: ${{ vars.ECS_SERVICE }}
  ECS_TASK_DEFINITION: .aws/ecs/task-definition.json
  CONTAINER_NAME: ${{ vars.CONTAINER_NAME }}

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          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 to ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.ref_name }}
        run: |
          IMAGE_URI="$ECR_REGISTRY/${{ env.ECR_REPOSITORY }}:$IMAGE_TAG"
          echo "IMAGE_URI=$IMAGE_URI" >> $GITHUB_OUTPUT
          
          docker build -t "$IMAGE_URI" .
          docker push "$IMAGE_URI"

      - name: Render Amazon ECS task definition
        id: render-task
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: ${{ env.ECS_TASK_DEFINITION }}
          container-name: ${{ env.CONTAINER_NAME }}
          image: ${{ steps.build-image.outputs.IMAGE_URI }}

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

11-3. 워크플로우 설명

on:은 워크플로우 트리거이다. tag에 v가 붙은게 푸시된다면 실행이 된다. (v0.0.1, v1,2,4같은 태그)
env:는 워크플로우 전역 환경변수이다.. 여기에서 github variables에 있는것을 가져와서 썼다.
jobs:는 수행할 작업을 넣는 곳이다. 서로 병렬 실행도 가능하다고 한다.
jobs: 안 steps: 위에서 아래로 순서대로 실행된다. 각 step에 무슨 이름(name), 어떤 액션(uses), 어떤 쉘 명령(run)하는지 들어간다.

나는 워크플로우 안에 순서대로
1. Checkout (여기에서 레포 소스코드 내려받음)
2. Configure AWS credentials (여기에서 AWS 자격증명을 한다. 저기 넣어놓은 AWS_ROLE_TO_ASSUME으로 임시 자격 증명을 받아 다음 작업들을 할 수 있다)
3. Login to Amazon ECR (ECR에 docker push를 함)
4. Build, tag, and push image to ECR (도커 이미지를 만들고, 이미지를 빌드하고, 태그를 붙인 후 ECR에 올림)
5. Render Amazon ECS task definition (task-definition.json을 읽고 container-name에 해당하는 이미지로 태스크를 정의함)
6. Deploy Amazon ECS task definition (정의된 태스크를 ECS에 등록하고 여기에 들어가있는 클러스터의 서비스를 그 새 태스크로 업데이트를 함)

12. 태그 푸시 후 배포

12.1 태그 푸시

git tag v0.0.1
git push --tags

12.2 성공

13. 결론

  • 배포 방식을 태그 푸시 기반 자동 배포로 바꿨다.
  • ECS_CLUSTER, ECS_SERVICE는 ECS 콘솔에 표시된 실제 이름을 그대로 사용해야 한다.
  • Secrets Manager는 ARN으로 주입하며, Task Execution Role에 GetSecretValue 권한이 필요했다.
  • IAM과 보안 그룹 설정은 너무 어려웠고 중요하다라는 걸 알게됐다. 권한이랑 인바운드 규칙이 달라지면 바로 에러를 쏟아냈다.

다음 글에서는 이 인프라를 구축하면서 겪었던 오류 유형과 해결 과정을 정리해보겠다.

profile
while( true ) { study(); }

0개의 댓글