Search

[배포 자동화] Spring Boot + Docker + Docker-compose + Github Actions + CodeDeploy + AutoScaling + AWS EC2 Blue/Green 배포

Last update: @7/15/2023

CI, CD

코드 수정 후 다시 빌드하는 것을 CI(Continuous Integration), 서비스를 중단하지 않고 수정된 결과물을 배포해 반영하는 것을 CD(Continuous Delivery(or Deployment))라고들 부르는 것 같다. 여기서는 CI/CD를 그냥 배포 자동화라고 하겠다.

개요

Github Actions가 출시되면서 Travis CI, Jenkins 등 기존의 수많은 배포 자동화 툴을 대체할 수 있게 되었다. 기존 툴들에 Github를 연동시키는 과정을 생략할 수 있어 굉장히 편리하다.
이 글에서는 Github Actions를 이용해 Spring Boot 어플리케이션을 도커 이미지로 빌드하고 AWS CodeDeploy를 이용해 AWS에 배포하는 과정을 설명한다. 이 글은 기본적인 EC2, S3, IAM 등 AWS 서비스에 대한 사용법을 알고 있다고 가정하고 작성되었다.

서비스 구성

현재 내가 운영 중인 잉티 서비스는 위와 같이 구성되어 있다. 스프링 부트 어플리케이션과 Redis DB가 각각 도커 컨테이너에서 구동되며 하나의 EC2 인스턴스에 담겨있고, ELB에 의해 트래픽이 라우팅된다.

배포의 흐름

일단 생소하고 복잡한 개념들이 많이 나오고, 배포 방법에 경우의 수가 많기 때문에 전체적인 흐름을 알아두어야 언제 무엇을 해야할지 알 수 있다. 내가 구성한 흐름은 다음과 같다.
Github에 push → Github Actions 트리거 → 앱 및 도커 이미지 빌드 → S3 및 도커 허브에 파일 배포 → CodeDeploy에 의해 배포 트리거 → AutoScaling Group 생성 → Launch Template에 의해 EC2 Instance 생성 → Launch Template User Data로 docker-compose 실행
조금 더 상세한 과정은 아래와 같다.
1.
내 어플리케이션의 코드를 수정, 커밋하고 Github에 Push
2.
Github는 내 프로젝트 경로에 .github/workflows/*.yml 파일이 있으면 열어서 실행 조건을 확인하고, 조건이 맞다면 Github Actions를 실행
a.
이때 Github Actions는 사용자의 Github secret 변수들을 yml 파일의 placeholder(${{…}})에 넣어줌
3.
Github Actions는 깃헙의 클라우드 컴퓨터(이하 깃헙 컴퓨터)에 내 프로젝트 폴더를 다운로드
4.
깃헙 컴퓨터는 .github/workflows/*.yml 파일에 적힌 작업을 수행. 나의 경우는 다음과 같음
a.
Gradle로 스프링 부트 어플리케이션(.jar 파일) 빌드
b.
도커 이미지 빌드 후 도커 허브로 push
c.
배포에 필요한 아래 파일들을 zip 파일로 압축해서 AWS의 저장소인 S3로 업로드
i.
docker-compose.yml (EC2에서 docker-compose 실행 시 필요한 설정 파일)
ii.
.app-env (docker-compose에서 사용한 환경변수를 정의한 파일. 깃헙 브랜치별로 스프링 프로필을 개발, 운영 등으로 구분하기 위해 사용함)
iii.
appspec.yml (CodeDeploy가 사용)
d.
깃헙 컴퓨터는 AWS CodeDeploy를 호출해 배포를 요청하고 역할을 마침
5.
명령을 받은 CodeDeploy는 기존의 Auto Scaling Group(Green fleet)을 복제한 새로운 Auto Scaling Group(Blue fleet)을 생성
AWS에서 running은 초록색, pending(준비중)은 파란색인 것에서 유래한 것 같다.
6.
Auto Scaling Group은 내부에 설정된 개수만큼 EC2 인스턴스를 Launch Template에 따라 생성
7.
각 EC2 인스턴스는 생성된 후 Launch Template의 User Data에 따라 초기 명령어를 실행하여 어플리케이션 가동
8.
CodeDeploy는 트래픽을 받을 Auto Scaling 그룹이 준비되면 로드 밸런서를 통해 요청을 받을 수 있도록 하고, 기존 Auto Scaling Group은 종료시킴

Auto scaling과 Blue/Green 배포

다른 포스트를 참조하면서 가장 헷갈렸던 부분이 appspec.yml이다. appspec.yml은 CodeDeploy가 배포 시 새로 생성하는 인스턴스에 들어가 작업할 내용들을 정의하는 파일이다. CodeDeploy가 Auto Scaling Group을 통해 배포를 진행하지만, Auto Scaling Group은 무중단 배포 뿐만 아니라 늘어나는 트래픽에 따라 자동으로 새로운 인스턴스를 만드는 본연의 기능도 할 수 있어야 한다. 따라서 트래픽이 늘어나 Auto Scaling이 작동하여 새로운 EC2가 추가되려면 CodeDeploy를 거치지 않고도 스스로 어플리케이션을 구동할 수 있어야 하는데, appspec.yml에 어플리케이션 구동 로직을 추가한다는 것이 이해가 가지 않았다. 나는 Auto Scale-out 기능을 살려놓고 싶었기 때문에 appspec.yml에 배포 관련 로직을 추가하지 않고 Launch Template의 User data만으로 배포 자동화가 되도록 했다.

절차

EC2 인스턴스 세팅 및 이미지 생성

배포에 필요한 프로그램들이 미리 설치된 인스턴스의 이미지를 만들어 두자. AWS EC2 이미지(AMI)를 만들기 위해 새로운 EC2 Ubuntu Instance를 생성한 후 접속한다. EC2에 접속하여 아래 코드를 통해 배포를 위해 필요한 프로그램들을 미리 설치한다.
CodeDeploy Agent설치
sudo apt update sudo apt install -y ruby-full wget cd /home/ubuntu wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install chmod +x ./install # install the latest version of the CodeDeploy agent on any supported version of Ubuntu Server except 20.04: sudo ./install auto #check that the service is running sudo service codedeploy-agent status #error: No AWS CodeDeploy agent running 발생할 경우 #sudo service codedeploy-agent start
Shell
복사
Docker 설치
sudo apt-get update sudo apt-get install -y apt-transport-https ca-certificates curl gnupg-agent software-properties-common curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" sudo apt-get install -y docker-ce docker-ce-cli containerd.io
Shell
복사
Docker-compose 설치
sudo curl -L "https://github.com/docker/compose/releases/download/v2.12.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo mv /usr/local/bin/docker-compose /usr/bin/docker-compose sudo chmod +x /usr/bin/docker-compose
Java
복사
AWS cli 설치
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip awscliv2.zip sudo ./aws/install
Shell
복사
설치 후 아래 명령어를 입력하고 IAM ACCESS_KEY와 SECRET_KEY 그리고 지역(ap-northeast-2)을 입력해 IAM 유저를 등록한다. format 어쩌고는 text로 해줬다.
aws configure
Shell
복사
인스턴스를 만들었으면 AWS 대시보드에서 인스턴스 우클릭 → image and template → create image 눌러 이미지를 만들어준다

IAM 설정

기존 사용하던 IAM user에 아래 두 정책이 있는 지 확인하고, 없으면 붙여준다.
AWSCodeDeployFullAccess
AmazonS3FullAccess
IAM 서비스로 가서 2개의 role을 만들어줘야 한다.
Code Deploy에 부여할 role 생성 후 아래 두 policy를 부여(code-deploy-role)
AmazonS3FullAccess
AWSCodeDeployFullAccess
EC2에 부여할 role 생성(ec2-role)
AutoScalingFullAccess
AWSCodeDeployRole
커스텀 제작 inline policy 추가 (없으면 The IAM role ~ does not give you permission to perform operations in the following AWS service: AmazonAutoScaling 오류 발생)
{ "Version": "2012-10-17", "Statement": [ { "Sid": "VisualEditor0", "Effect": "Allow", "Action": [ "iam:PassRole", "ec2:CreateTags", "ec2:RunInstances" ], "Resource": "*" } ] }
JSON
복사

Launch Template 생성

Launch Template은 EC2 인스턴스를 런칭할 때의 설정들을 모아놓은 것이다.
Application and OS Images
: 아까 만들어놓은 이미지를 설정
Instance type, Key pair, Security group 등은 통상 EC2 설정과 같이 하면 된다.
Advanced details
IAM instance profile : 위에서 만들었던 ec2-role을 설정해준다. EC2를 런칭시킬 때 EC2에 해당 role이 붙어 권한이 부여된다.
 User data
#!/bin/bash sudo aws s3 cp s3://engt-s3/deploy/deploy.zip ./ sudo unzip deploy.zip -d ./deploy cd ./deploy sudo docker login -u 도커허브아이디 -p 도커허브토큰 sudo docker-compose --env-file .engt-env up -d
Bash
복사
EC2가 런칭되고 실행되는 명령어. S3에서 배포에 필요한 파일을 받아와 압축을 풀고 docker-compose로 실행하는게 전부다.

Auto Scaling Group 생성

Launch Template : 방금 만든 Launch template 설정. 혹시 몇 차례 수정했다면 버전은 Latest 선택
Availability Zones and subnets : 전부 선택
Load balancing : Attach to an existing load balancer
Attach to an existing load balancer : 현재 로드밸런서가 서비스중인 target group 선택
Health checks : 체크해줌. 300 seconds
Additional settings : 체크 안함
Group size : 각자의 서비스 사정에 맞게 설정. 나는 min 1, desire 1, max 3으로 해놓음
Scaling policies : Average CPU Utilization 50% 300 seconds 설정함
Instance scale-in protection : 체크 안 함

Code Deploy Application 및 deploy group 생성

Code Deploy → Application → Create Application 클릭
Compute platform : EC2/On-premises
해당 어플리케이션으로 이동 후 create deployment group 클릭
Service Role : CodeDeploy role(custom)
Deployment type : Blue/Green
Environment configuration : Automatically copy Amazon EC2 Auto Scaling group
Deployment settings
traffic routing : Reroute traffic immediately
Terminate the original instances in the deployment group : 0 Days 0 Hours 0 Minutes
Deployment configuration : CodeDeployDefault.AllAtOnce
Load balancer : Application Load Balancer or Network Load Balancer
사용중인 로드밸런서 선택

Dockerfile 생성

FROM --platform=linux/amd64 openjdk:17-jdk-alpine ARG JAR_FILE=./build/libs/engt-1.0.0.jar WORKDIR /usr/src/app COPY ${JAR_FILE} ./engt-app.jar CMD ["java","-jar","./engt-app.jar","--spring.profiles.active=prod"] EXPOSE 8080
Docker
복사
깃헙 컴퓨터에서 도커 이미지를 빌드할 때 사용될 도커파일을 프로젝트 루트 경로에 생성한다.

docker-compose.yml 생성

version : "3.3" services: redis-server: image: "redis" container_name: redis-server ports: - "6379:6379" engt: image: gyuraydev/engt container_name: engt ports: - "8080:8080" depends_on: - redis-server command: ["java","-jar","./engt-app.jar","--spring.profiles.active=${SPRING_PROFILE}","2>&1 & sleep 1 ; tail -f nohup.out"] logging: driver: awslogs options: awslogs-group: "engt-log_group" awslogs-region: "ap-northeast-2" awslogs-stream: "engt-log_stream"
YAML
복사
프로젝트 루트 경로에 EC2에서 도커 허브로부터 이미지를 내려받고 이미지로부터 컨테이너를 띄울 docker-compose.yml 파일을 생성한다.
꼭 루트 경로에 만들어야할 필요는 없고, 나중에 압축파일에만 들어가면 된다.
Redis 이미지를 받아서 컨테이너를 하나 띄우고, 깃헙 컴퓨터가 도커 허브에 push해놓은 어플리케이션 이미지를 받아서 컨테이너를 또 하나 띄운다.
command 부분에 환경변수를 주입받아 스프링 프로필을 세팅한다. Dockerfile의 CMD는 무시되고, docker-compose의 command 명령어가 대신 실행되면서(override) 스프링 어플리케이션이 가동된다.
프로젝트를 도커 이미지로 빌드해서 실행하는 것이라면 docker-compose.yml에 build 부분이 들어가겠지만, 이미 깃헙 컴퓨터에서 이미지를 빌드해 도커 허브로 올려두었기 때문에 다운받아 실행만 하면 되고, 따라서 빌드 관련 설정이 전혀 없다.
logging은 AWS CloudWatch를 통해 로그를 볼 수 있도록 설정해놓았다. CloudWatch에서 만들어 놓은 Log group 이름과 Log stream 이름만 넣으면 도커가 알아서 로그를 전송해줘서 편리하고, EC2 인스턴스가 삭제돼도 로그를 남길 수 있다.

appspec.yml

version: 0.0 os: linux files: - source: / destination: /home/ubuntu/deploy permissions: - object: / pattern: "**" owner: ubuntu group: ubuntu
YAML
복사
프로젝트 루트 경로에 Code Deploy가 참조할 appspec.yml을 만든다.
꼭 루트 경로에 만들어야할 필요는 없고, 나중에 S3에 올리는 배포용 압축파일(deploy.zip)에만 들어가면 된다.
CodeDeploy는 새로운 Auto Scaling Group을 런칭시키고 트래픽을 바꾸는 것 이외는 아무런 역할을 맡지 않도록 했다.

.github/workflows/deploy.yml

name: Java CI with Gradle on: push: branches: [ "main" ] pull_request: branches: [ "main" ] permissions: contents: read jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up JDK 17 uses: actions/setup-java@v3 with: java-version: '17' distribution: 'temurin' - name: Build with Gradle uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 with: arguments: build -x test - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Login to Docker Hub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v4 with: context: . push: true tags: gyuraydev/engt:latest - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-2 - name: Set profile env variable to deploy.sh run: echo "SPRING_PROFILE=prod" >> .engt-env - name: Make deploy zip file run: zip -r ./deploy.zip appspec.yml docker-compose.yml .engt-env - name: Upload deploy zip file to S3 run: | aws s3 cp ./deploy.zip s3://engt-s3/deploy/deploy.zip - name: Call CodeDeploy run: | aws deploy create-deployment \ --application-name engt \ --deployment-group-name engt-deployment_group \ --deployment-config-name CodeDeployDefault.AllAtOnce \ --region ap-northeast-2 \ --s3-location bucket=engt-s3,bundleType=zip,key=deploy/deploy.zip
YAML
복사
Github Actions가 참조할 yml 파일을 만든다. 프로젝트 루트 경로에 ./github/workflows/ 폴더를 만들고 그 안에 작성한다.
yml 파일 이름은 아무렇게나 지어도 상관없다.
uses 뒤의 docker/login-action@v2 같은 이름들은 깃헙 저장소 주소(이름)이고, 깃헙 컴퓨터에서 해당 저장소를 다운받아 동 저장소의 actions.yml 파일에 적힌 절차에 따라 작업을 실행하는 원리다. 이미 다른 사람들이 만들어 놓은 작업들에 변수만 입력해 편리하게 작업을 할 수 있다.
${{ secrets.DOCKERHUB_USERNAME }} 같은 것은 placeholder인데, 깃헙 저장소 settings → Secrets and Variables → Actions → Secrets 에서 추가할 수 있다.
각각의 step을 보면 다른 사람들의 프로그램/명령어 모음에 인자를 전달하거나 직접 명령어를 사용해서 필요한 작업들을 순차적으로 진행한다.

.engt-env

빈 환경변수 파일인데, docker-compose를 구동할 때 인자로 넘긴다.
아래처럼 깃헙 workflows 내에 브랜치별로 yml 파일을 만들어 깃헙 컴퓨터가 이 파일에 각각 다른 프로필 환경변수를 집어넣도록 했다.
- name: Set profile env variable to deploy.sh run: echo "SPRING_PROFILE=prod" >> .engt-env
YAML
복사
위 User Data 란에도 있지만 docker compose에 아래처럼(--env-file .engt-env) 넘기면 docker-compose.yml에서 환경변수로 쓸 수 있다.
sudo docker-compose --env-file .engt-env up -d
YAML
복사

후기

꼬박 5일은 걸린 것 같다. 괜히 DevOps라는 직군을 별도로 두는 게 아니구나 싶을 정도로 복잡하고 오류도 많이 만났다. 배포 방법의 경우의 수가 많아서 내 상황에 맞는 포스트도 찾기 어려웠고, 배포 흐름에 대해 잘 설명된 글을 찾기도 어려웠다. 도커, CodeDeploy, AutoScaling, GitHub Actions 등 낯선 개념들을 익혀서 한 번에 적용하려니 인지부하가 꽤나 컸다.
일단 해놓고 나니 정말 편하다. 기존에 코드를 수정하면 인스턴스를 하나 새로 만들어서 스크립트를 돌려 어플을 띄우고, 기존 인스턴스를 정리하던 귀찮은 작업이 push와 동시에 자동으로 이뤄지니, 자잘한 업데이트를 더 편하게 할 수 있게 됐다.

References

관련 문서