
이전에 github actions + codedeploy + Docker + ecr + s3 + asg로 CI/CD 구축을 힘겹게 진행했다. 당시에는 CI/CD가 무엇인지도 제대로 이해하지 못하고 구축했으며, 구축 과정이 너무 힘들어서 그냥 수동 배포하는게 낫겠다고 생각했었다.
그러나 막상 CI/CD를 구축하지 않고 수동 배포를 진행하니 단순한 push 하나에도 프로젝트를 빌드하고, jar 파일을 bastion host에 올리고, 이를 프라이빗 서브넷 내의 ec2에 전송해야 해 굉장히 번거로웠다.
이번 기회에 CI/CD의 소중함을 크게 느꼈으며... 제대로 학습해서 구축하기로 결심했다.
CI/CD는 지속적 통합(Continuous Integration) 및 지속적 제공/배포(Continuous Delivery/Deployment)을 말한다.
CI는 애플리케이션에 대한 새로운 변경 사항을 정기적으로 빌드 및 테스트되어 공유 리포지토리에 통합하는 것을 말한다.
CD는 지속적 제공(Continuous Delivery)과 지속적 배포(Continuous Deployment)로 의미가 나뉜다. 둘은 CI단계에서 빌드, 테스트가 진행된 후 배포 준비상태를 확인하는 것은 동일하나 최종 단계에서 지속적 제공은 수동으로 배포를 진행하고, 지속적 배포는 자동으로 배포가 진행된다. 이번 포스트에서 사용하는 CD는 지속적 배포(Continuous Deployment)이다.
github actions는 workflow → job → step -> action으로 구성되어 있다.
Runner
workflow
job
step
action
AWS에는 CI/CD를 위한 여러 서비스가 제공된다.
AWS 서비스와의 높은 통합을 갖는 CI/CD를 구축하고 싶다면, CodePipeline을 사용하는 것을 추천한다.
CI/CD 구축에 있어 고려했던 방법은 다음과 같다.
github actions → EC2 배포
github actions + CodeDeploy → EC2 배포
CodePipeline → ECR 배포
github actions → ECR 배포
CI/CD는 github actions + ECR + ECS를 사용해 구축했다.
CI는 다음과 같이 진행된다.
CD는 다음과 같이 진행된다.
도커 이미지를 빌드하는데 필요한 Dockerfile을 Spring boot 루트 디렉토리에 생성한다.
FROM openjdk:17-jdk
ARG JAR_FILE=./build/libs/*-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
name: CICD
on:
push:
branches: [ "dev" ]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Gradle Cache
uses: actions/cache@v4.2.0
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build -x test
- name: Configure AWS IAM credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: ap-northeast-2
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, and push docker image to Amazon ECR
id: build-image
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: ${{ secrets.AWS_REPOSITORY_NAME }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG .
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
echo "IMAGE_URI=$REGISTRY/$REPOSITORY:$IMAGE_TAG" >> $GITHUB_ENV
- name: Retrieve most recent ECS task definition JSON file
env:
TASK_DEFINITION: [태스크 정의 이름]
run: |
aws ecs describe-task-definition --task-definition $TASK_DEFINITION --query taskDefinition > task-definition.json
- name: Create ECS task definition
id: setting-task-definition
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: [컨테이너 이름]
image: ${{ env.IMAGE_URI }}
- name: Clean up task definition file
run: |
rm task-definition.json
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.setting-task-definition.outputs.task-definition }}
service: [ecs 서비스명]
cluster: [ecs 클러스터명]
wait-for-service-stability: true
github actions 워크플로우 코드는 위와 같은 코드를 사용했다. 각각의 action이 무엇을 실행하는지 하나씩 설명해보겠다.
name: CICD
on:
push:
branches: [ "dev" ]
workflow 이름과 트리거 조건을 설정한다. 트리거 조건은 dev 브랜치에 push되는 경우로 설정했다.
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
가상 러너와 권한을 설정한다. 가상 러너는 우분투로, 권한은 읽기 전용으로 한정한다.
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
github actions의 가상 러너에 JDK 17을 설치한다.
- name: Gradle Cache
uses: actions/cache@v4.2.0
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
gradle 캐싱 작업을 실행한다. actions의 Caches에 캐싱되며, CI 속도가 향상된다.
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build -x test
빌드를 위한 권한을 부여하고, Spring boot 애플리케이션을 빌드한다.
- name: Configure AWS IAM credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
aws-region: ap-northeast-2
Access Key와 Secret Key를 사용해 IAM 자격 증명을 인증한다.
Secret에서 AWS_ACCESS_KEY, AWS_SECRET_KEY 환경변수를 생성해야 한다.

- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
ECR에 로그인한다.
- name: Build, tag, and push docker image to Amazon ECR
id: build-image
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: [ecr 리포지토리 이름]
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG .
docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
echo "IMAGE_URI=$REGISTRY/$REPOSITORY:$IMAGE_TAG" >> $GITHUB_ENV
Docker 이미지를 빌드하고, ECR에 이미지를 push한다.
도커 이미지 URI를 환경변수로 저장한다.
- name: Retrieve most recent ECS task definition JSON file
env:
TASK_DEFINITION: [태스크 정의 이름]
run: |
aws ecs describe-task-definition --task-definition $TASK_DEFINITION --query taskDefinition > task-definition.json
ECS 태스크 정의 중 가장 최신 개정을 task-definition.json 파일로 가져온다.
- name: Create ECS task definition
id: setting-task-definition
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: task-definition.json
container-name: [컨테이너 이름]
image: ${{ env.IMAGE_URI }}
- name: Clean up task definition file
run: |
rm task-definition.json
가져온 태스크 정의와 위에서 생성한 도커 이미지의 URI를 바탕으로 새로운 태스크 정의를 생성한다. 태스크 정의를 생성한 후 task-definition.json 파일은 삭제한다.
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
task-definition: ${{ steps.setting-task-definition.outputs.task-definition }}
service: [ecs 서비스명]
cluster: [ecs 클러스터명]
wait-for-service-stability: true
생성한 태스크 정의를 바탕으로 ECS 서비스에 태스크를 배포한다.