
GitHub Actions Workflow로 배포 설정하기로컬에서 docker-compose up으로 돌리던 Spring Boot 서버를, CI/CD 자동화 구축을 하게 만들어 보고자 했다.
git tag v0.0.1
git push --tags
나는 이런식으로 태그를 붙여서 push하면 자동으로 배포까지 되는 것을 원했다.
GitHub Actions이 Docker 이미지를 빌드해서 Amazon ECR에 push한다.Amazon ECS가 새 이미지를 가져와서 배포한다.아래 사진은 CI/CD 흐름도이다.

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

DB 정보는 ECS Task 환경변수로 주입한다.
SPRING_DATASOURCE_URL
SPRING_DATASOURCE_USERNAME
SPRING_DATASOURCE_PASSWORD (Secrets manager로 넣기)

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

이 리포지토리에 GitHub Actions가 빌드한 Docker이미지를 푸시한다.
서버에서 사용할 RDS의 URL이나 비밀번호 같은것을 정리해서
ECS 태스크는 스프링 부트 컨테이너를 실행하고, 그 컨테이너는 DB에 접속하기 때문에 DB정보를 태스크에 넣어줘야 한다. 사용하는 RDS의 URL, password, username등을 잘 알아두어야한다.
SPRING_DATASOURCE_PASSWORDSPRING_DATASOURCE_URLSPRING_DATASOURCE_USERNAMERDS 인바운드 규칙을 설정해줘야한다.

Type: MySQL/Aurora (3306)
Source: CS Task가 사용하는 보안그룹
이런식으로 ECS 보안 그룹을 허용해주면 된다. 그러면 ECS에서만 DB접근이 가능하게된다.
ECS서비스를 만들 때 어떤 보안그룹을 붙였는지 확인하고 그 보안그룹을 RDS 인바운드에 넣어야한다.
Amazon RDS 데이터베이스 자격 증명 타입으로 만들면 편하다.

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

레포지토리가 public이기때문에, 비밀번호/JWT 시크릿을 코드나 GitHub 저장소에 직접 적지 않는다.
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"
}
]
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"
}
}
권한은 두 종류로 나눠서 만든다.
아래 사진을 보면 weblog-ecs-secrets-read이라고 커스텀 권한이 있는데 Secrets Manager read write 아님 GetSecretValue 같은 것을 넣어줘도 될 것이다. ecs는 배포하고 실행까지 하기 때문에 RDS에 접근해야한다. 그래서 RDS비밀번호가 있는 Secrets Manager 권한이 필요하다.

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

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

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

내가 사용하는 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권한도 맞춰줘야 한다.
클러스터 위에 서비스를 만들면, 서비스는 태스크를 항상 N개 유지하고, 새 task definition이 올라오면 롤링 배포를 한다.

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

Variables
Secrets
내가 사용한 워크플로우는 아래와 같다.
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
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에 등록하고 여기에 들어가있는 클러스터의 서비스를 그 새 태스크로 업데이트를 함)
git tag v0.0.1
git push --tags


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