CodeDeploy란?
Amazon EC2 인스턴스, 온프레미스 인스턴스, 서버리스 Lambda 함수 또는 Amazon ECS 서비스로 애플리케이션 배포를 자동화하는 배포 서비스이다.
CodeDeploy의 이점
왜 CodeDeploy로 배포하는가?
무중단 배포 불가능
무중단 배포란?
NestJS 서버로 진행했을 때는 pm2를 이용하여 무중단 배포(zero down time deployment)가 가능했는데 JAVA 기반 프로젝트에서는 pm2와 같은 역할을 해주는 Process Manager가 존재하지 않는 것으로 보인다. (혹시 발견하게 된다면 알려주세요..!)
nginx를 이용한 무중단 배포 방법
Nginx를 이용한 무중단 배포 방식도 있긴하지만 다소 복잡해 보였고, 한 인스턴스에서 서버를 여러 개를 가동해야 하는 것이기 때문에 (우리 서비스랑은 관련이 없겠지만) 트래픽이 몰릴 경우 대처가 어려울 것 같다고 생각했다.
CodeDeploy를 이용하면 다른 AWS 서비스와 연동해서 사용하기가 좋다. 때문에 AWS EC2의 Auto Scaling 과 Elastic Load Balancing(ELB) 기능을 이용하면 (비용적인 부분이 더 발생하겠지만) 앞에서 언급한 상황에 대처가 쉬울 것으로 판단했다.
ELB와 Auto Scaling
CodeDeploy를 이용하면 EC2의 ELB과 Auto Scaling 기능을 이용하여 Blue-Green 방식으로 무중단 배포를 진행할 수 있다.
다른 AWS 서비스와의 연계
Github Actions의 워크플로우를 이용한 방식은 다른 AWS 서비스와의 연계가 편하지 않다. 그런데 CodeDeploy를 이용하여 배포하면 EC2, S3, CloudFront 등과 같은 다른 AWS 서비스와의 연계가 쉬워진다.
추후 배포 과정을 CloudWatch를 이용하여 모니터링 하기에 용이한 측면이 있다.
이러한 이유들로 CodeDeploy를 활용하여 배포를 진행하기로 결정하였고, 기존에 Github Actions에서 CD 워크플로우가 CodeDeploy로 대체되었다고 이해하면 좋을 것 같다!
CodeDeploy를 이용한 배포 과정
배포 전략
기본적인 배포 전략은 다음과 같다.
1.
Git Repository에 커밋이 발생한다.
2.
Github Actions에서 CI 워크플로우가 트리거된다.
3.
빌드와 테스트가 이루어진다.
4.
브랜치가 dev나 main인 경우 S3에 zip 파일을 업로드한다.
5.
CodeDeploy 배포를 트리거시킨다.
6.
CodeDeploy는 S3에 업로드된 zip파일을 AWS EC2에 다운로드시켜 배포 스크립트를 실행시켜 프로젝트를 배포한다.
전제 조건
진행을 위해 기본적으로 AmazonEC2FullAccess, AmazonS3FullAccess, AWSCodeDeployFullAccess 권한 정책이 필요하다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iam:ListRoles",
"iam:ListPolicies",
"iam:CreateRole",
"iam:AttachRolePolicy",
"iam:CreateInstanceProfile",
"iam:AddRoleToInstanceProfile",
"iam:PassRole",
"iam:GetRole",
"iam:ListInstanceProfiles",
"ssm:CreateAssociation"
],
"Resource": "*"
}
]
}
YAML
복사
추가로 다음과 같은 권한이 필요하다.
배포용 S3 버킷 생성
먼저 배포용 S3 버킷을 생성해야 한다.
dev 서버를 배포하기 위한 버킷과 main 용 버킷을 구분하기 위해서 dev prefix를 붙여주었다.
S3 버킷에는 jar 파일을 업로드할 것이다. jar 파일에는 서버에서 사용하는 credential한 값이 들어있기 때문에 반드시 모든 퍼블릭 액세스를 차단해주어야 한다.
이름만 작성하고 다 기본 설정으로 해두어도 무방하다!
Github Actions Secrets 설정
Github Actions에서 aws-cli를 이용하여 S3에 배포용 zip파일을 업로드할 것이다. 따라서 key_id와 secret_key 같은 credential한 값을 secret한 환경변수로 사용할 수 있도록 설정을 해주어야 한다.
New repository secret 버튼을 누르면 key-value 형태로 secret 값을 저정할 수 있다. 이렇게 하면 워크플로우에서 ${{ secrets.AWS_ACCESS_KEY_ID }} 같은 형태로 secret 값을 사용할 수 있고, 이 값은 외부로 노출되지 않는다.
region과 s3 버킷 이름은 꼭 가릴 필요는 없지만 그래도 혹시 모르니 secret 값으로 관리해주었다!
Spring 애플리케이션에서 사용할 환경변수인 application-*.yml 파일의 내용도 gradlew로 빌드할 때 담겨야한다. 따라서 yml 파일의 내용도 secret 값으로 추가해주었다.
yml파일은 base64로 인코딩 후 저장하였다.
CI 워크플로우 구성
name: CI/CD
on:
push:
paths:
- "backend/**"
workflow_dispatch:
jobs:
backend-CI:
runs-on: ubuntu-latest
steps:
- name: 체크아웃
uses: actions/checkout@v2
- name: JDK 11 설정
uses: actions/setup-java@v3
with:
java-version: "11"
distribution: "corretto"
- name: Application YML 생성
run: |
mkdir -p backend/src/main/resources
echo "${{ secrets.APPLICATION_PROD_YML }}" | base64 --decode > backend/src/main/resources/application-prod.yml
echo "${{ secrets.APPLICATION_DEV_YML }}" | base64 --decode > backend/src/main/resources/application-dev.yml
echo "${{ secrets.APPLICATION_AUTH_YML }}" | base64 --decode > backend/src/main/resources/application-auth.yml
echo "${{ secrets.APPLICATION_MAIL_YML }}" | base64 --decode > backend/src/main/resources/application-mail.yml
- name: Gradle 빌드
run: |
cd backend
mkdir -p build/generated-snippets/
chmod +x gradlew
./gradlew build
shell: bash
- name: Configure AWS credentials
if: ${{ github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' }}
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: ${{ secrets.AWS_REGION }}
- name: Dev S3에 업로드
if: ${{ github.ref == 'refs/heads/dev' }}
run: |
mkdir -p before-deploy
cp backend/build/libs/cabinet-*.jar before-deploy/
cd before-deploy && zip -r before-deploy *
cd ../ && mkdir -p deploy
mv before-deploy/before-deploy.zip deploy/deploy.zip
aws s3 cp deploy/deploy.zip s3://${{ secrets.AWS_S3_DEV_BUCKET_NAME }}/deploy.zip
YAML
복사
CI 워크플로우를 다음과 같이 구성하였다.
on:
push:
paths:
- "backend/**"
workflow_dispatch:
YAML
복사
backend 디렉토리에 내용이 github에 푸쉬되었을 때, workflow_dispatch 이벤트가 트리거 되었을 때, 이를 감지하여 워크플로우가 동작하도록 하였다.
workflow_dispatch란?
jobs:
backend-CI:
runs-on: ubuntu-latest
steps:
- name: 체크아웃
uses: actions/checkout@v2
- name: JDK 11 설정
uses: actions/setup-java@v3
with:
java-version: "11"
distribution: "corretto"
YAML
복사
actions/checkout 라이브러리를 이용하여 커밋을 올린 브랜치로 체크아웃 후, actions/setup-java 라이브러리를 이용하여 JDK-11-corretto 버전으로 JDK를 설정할 수 있도록 하였다.
- name: Application YML 생성
run: |
mkdir -p backend/src/main/resources
echo "${{ secrets.APPLICATION_PROD_YML }}" | base64 --decode > backend/src/main/resources/application-prod.yml
echo "${{ secrets.APPLICATION_DEV_YML }}" | base64 --decode > backend/src/main/resources/application-dev.yml
echo "${{ secrets.APPLICATION_AUTH_YML }}" | base64 --decode > backend/src/main/resources/application-auth.yml
echo "${{ secrets.APPLICATION_MAIL_YML }}" | base64 --decode > backend/src/main/resources/application-mail.yml
- name: Gradle 빌드
run: |
cd backend
mkdir -p build/generated-snippets/
chmod +x gradlew
./gradlew build
shell: bash
YAML
복사
secrets에 저장된 yml 내용을 디코딩하여 파일 형태로 저장 후, gradlew 빌드를 진행한다.
빌드 시에 test도 함께 동작하도록 하였다.
- name: Configure AWS credentials
if: ${{ github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' }}
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: ${{ secrets.AWS_REGION }}
- name: Dev S3에 업로드
if: ${{ github.ref == 'refs/heads/dev' }}
run: |
mkdir -p before-deploy
cp backend/build/libs/cabinet-*.jar before-deploy/
cd before-deploy && zip -r before-deploy *
cd ../ && mkdir -p deploy
mv before-deploy/before-deploy.zip deploy/deploy.zip
aws s3 cp deploy/deploy.zip s3://${{ secrets.AWS_S3_DEV_BUCKET_NAME }}/deploy.zip
YAML
복사
마지막으로 aws-actions/configure-aws-credentials 라이브러리를 이용하여 AWS Configuration을 env에 등록한 후 S3 버킷에 jar 파일을 업로드 시키도록 하였다.
워크플로우가 정상적으로 실행되었다!
이후 정상적으로 S3 버킷에 업로드되는 것을 확인할 수 있었다!
EC2에서 CodeDeploy를 사용할 수 있도록 연동
EC2에서 CodeDeploy를 사용할 수 있도록 하려면 IAM에서 역할을 생성하여 EC2 인스턴스에 등록해주어야 한다. IAM의 역할에 대해서는 아래 포스트에서 상세히 작성해두었다.
먼저 필요한 역할을 생성해보자. IAM에서 역할 탭을 클릭 후 역할 만들기 버튼을 클릭한다.
신뢰할 수 있는 엔티티 유형으로 AWS 서비스를 체크하고, 사용 사례로 EC2를 체크 후 다음 버튼을 클릭한다.
AmazonEC2RoleforAWSCodeDeploy 정책을 체크하고 다음 버튼을 클릭한다. 해당 정책에는 EC2 인스턴스가 S3 버킷을 다운로드할 권한이 포함되어 있다.
후설 하겠지만 EC2 인스턴스에 CodeDeploy agent라는 데몬 프로그램을 설치하게 된다. 이 프로그램이 해당 권한을 이용하여 CodeDeploy 서비스를 사용하게되는데 이를 이용할 수 있는 권한으로 이해하면 된다.
역할 생성 버튼을 클릭하면 역할이 만들어진다!
이제 EC2 인스턴스에 앞에서 만들었던 역할을 등록시켜주어야 한다.
EC2 인스턴스에서 작업의 보안의 IAM 역할 수정을 클릭한다.
앞에서 생성한 역할을 클릭하고, IAM 역할 업데이트 버튼을 클릭하면 등록이 완료된다.
적용을 위해서는 인스턴스를 재부팅 시켜야 한다.
종료를 누르면 그대로 인스턴스가 삭제되니 주의하자..!
aws s3 cp s3://aws-codedeploy-ap-northeast-2/latest/install . --region ap-northeast-2
Bash
복사
재부팅 후 EC2 인스턴스에 접속해서 다음 명령어를 실행한다.
앞에서 언급한 CodeDeploy Agent를 설치하기 위한 과정이다.
download: s3:// 어쩌구저쩌구가 뜨면 설치가 완료된 것이다.
chmod +x ./install
sudo ./install auto
Bash
복사
install에 실행 권한을 부여하고 설치를 진행한다.
/usr/bin/env: ruby: No such file or directory 이런 오류가 발생한다면 이는 ruby가 설치되지 않아서 생기는 오류이다.
sudo yum install ruby -y
Bash
복사
ruby를 설치하고 다시 설치를 진행한다.
sudo service codedeploy-agent status
Bash
복사
이후 이 명령어를 실행하면 CodeDeploy가 정상적으로 설치되어서 실행중인지 확인이 가능하다.
이런식으로 실행중인 PID가 출력된다면 정상적으로 작동중인 것이다!
CodeDeploy 관련 역할 생성
앞에서와 마찬가지로 CodeDeploy에도 필요한 역할을 부여해주어야 한다.
앞에서와 마찬가지로 역할 생성 버튼을 클릭 후, 사용 사례에 CodeDeploy를 선택하고, 다음 버튼을 클릭한다.
이는 CodeDeploy가 다른 AWS 서비스를 이용할 권한을 역할에 등록 후 해당 역할을 CodeDeploy에게 부여하는 과정이라고 이해하면 된다!
AWSCodeDeployRole이라는 정책이 방금 말한 그 권한들을 묶고 있는 정책이다. 다음 버튼을 클릭한다.
어떤 권한이 부여되는 것인지 궁금하다면 + 버튼을 눌러 확인해볼 수 있다!
마찬가지로 역할 이름을 작성 후 역할 생성을 클릭하면 역할이 만들어진다.
CodeDeploy 애플리케이션 및 배포 그룹 생성
애플리케이션을 생성하여 CodeDeploy를 시작할 수 있다. 애플리케이션 안에 배포 그룹이라는 것을 만들게되는데 이는 배포를 하는 단위 정도로 이해하면 될 것 같다!
한 애플리케이션에 여러 배포 그룹이 포함될 수 있다.
먼저 애플리케이션 생성 버튼을 클릭한다.
이름을 작성하고, 컴퓨팅 플랫폼을 EC2/온프레미스로 설정 후, 애플리케이션 생성 버튼을 클릭한다.
만들어진 애플리케이션을 클릭하면 배포 그룹을 생성할 수 있다. 배포 그룹 생성 버튼을 클릭한다.
배포 그룹 이름을 지정하고, 서비스 역할로 아까 생성한 역할을 선택한다.
배포 유형을 선택할 수 있다. Blue-Green 배포 방식을 목표로 하고 있지만 이는 당장 Auto-Scaling과 ELB 설정이 되어있어야 하기 때문에 일단 현재 위치 배포 방식을 선택해주었다.
다음은 환경 구성이다. Amazon EC2 인스턴스를 체크하고, Name을 키로 하여 연동할 인스턴스 이름을 선택한다.
EC2에 설치했던 CodeDeploy Agent를 자동으로 업데이트할 것인지 묻는 파트이다. 기본 설정되어 있던 14일 주기로 자동 업데이트 되도록 설정하였다.
배포 구성을 어떻게 할 지 설정하는 파트이다. AllAtOnce로 하면 모든 배포 대상 인스턴스에 동시에 배포한다. 규모가 큰 시스템에서는 OneAtATime(하나의 인스턴스에 배포 후 다음 인스턴스로 이동)이나 HalfAtATime(배포 대상 인스턴스의 절반에만 배포하고, 다 끝나면 나머지 절반에 배포)를 사용하는게 좋다고 하지만 일단 AllAtOnce를 선택해주었다.
로드 밸런스를 활성화할 것인지, 활성화한다면 어떤 로드밸런서를 사용할 것인지 묻는 파트이다. 일단 비활성화를 해두고 배포 그룹 생성 버튼을 클릭하였다.
Appspec.yml 설정 및 배포 스크립트 작성 + Dockerfile 작성
version: 0.0
os: linux
files:
- source: /
destination: /home/ec2-user/deploy/zip/
overwrite: yes
permissions:
- object: /
pattern: "**"
owner: ec2-user
group: ec2-user
hooks:
ApplicationStart:
- location: deploy.sh
timeout: 60
runas: ec2-user
Bash
복사
appspec.yml
이는 CodeDeploy Agent가 사용하는 배포 스펙 파일이다.
files 필드에서는 배포용 파일을 EC2 인스턴스 어디에 다운받을 지 경로를 지정하고, 덮어쓰기를 할 지 말지 등을 선택할 수 있다.
permissions 필드에서는 파일이나 디렉토리에 대한 권한을 지정하도록 할 수 있다.
hooks 필드는 특정 이벤트에 대한 훅을 지정할 수 있다.
hooks에 대한 상세 설명
#!/bin/bash
mkdir -p /home/ec2-user/deploy/zip/
echo "> 현재 실행 중인 Docker 컨테이너 pid 확인" >> /home/ec2-user/deploy/deploy.log
CURRENT_PID=$(docker container ls -q)
if [ -z $CURRENT_PID ]
then
echo "> 현재 구동중인 Docker 컨테이너가 없으므로 종료하지 않습니다." >> /home/ec2-user/deploy/deploy.log
else
echo "> docker stop $CURRENT_PID" # 현재 구동중인 Docker 컨테이너가 있다면 모두 중지
docker stop $CURRENT_PID
sleep 5
fi
cd /home/ec2-user/deploy/zip/
docker build -t cabi_dev ./ # Docker Image 생성
docker run -d -p 4242:4242 cabi_dev # Docker Container 생성
Bash
복사
deploy.sh
ApplicationStart 이벤트에 대한 hook으로 실행될 스크립트 파일이다.
이 스크립트 파일에서 실행중인 컨테이너가 있으면 해당 컨테이너를 중지시키고, 새로 컨테이너를 생성하도록 하였다.
FROM amazoncorretto:11
COPY cabinet-0.0.1-SNAPSHOT.jar .
CMD java -jar -Dspring.profiles.active=dev cabinet-0.0.1-SNAPSHOT.jar
Docker
복사
Dockerfile
Dockerfile은 다음과 같이 작성하였다. 호스트의 jar 파일을 컨테이너로 복사 후, CMD에서 java로 해당 jar 파일을 실행하게 하고, 프로필을 dev로 지정하여 dev용 서버를 가동시킨다.
EC2에서 Docker 설치 방법
최종 CI YML 파일
cp scripts/*.sh before-deploy/
cp appspec.yml before-deploy/
cp Dockerfile before-deploy/
YAML
복사
zip 파일을 만들기 전 디렉토리에 deploy.sh 파일과 appspec.yml 파일, Dockerfile을 담도록 변경하였다.
(appspec.yml과 deploy.sh 파일은 S3에 꼭 함께 업로드 되어야 한다.)
aws deploy create-deployment \
--application-name ${{ secrets.AWS_CODEDEPLOY_DEV_APP_NAME }} \
--deployment-config-name CodeDeployDefault.AllAtOnce \
--deployment-group-name ${{ secrets.AWS_CODEDEPLOY_DEV_GROUP_NAME }} \
--file-exists-behavior OVERWRITE \
--s3-location bucket=${{ secrets.AWS_S3_DEV_BUCKET_NAME }},bundleType=zip,key=deploy.zip
YAML
복사
name: CI/CD
on:
push:
paths:
- "backend/**"
workflow_dispatch:
jobs:
backend-CI:
runs-on: ubuntu-latest
services:
mariadb:
image: mariadb:10.3.39
env:
MYSQL_DATABASE: test_db
MYSQL_USER: test_user
MYSQL_PASSWORD: test_password
MYSQL_ROOT_PASSWORD: test_password
ports:
- 3310:3306
options: >-
--health-cmd "mysqladmin status -h 127.0.0.1 -P 3306 -u root -ptest_password"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: 체크아웃
uses: actions/checkout@v2
- name: DB에 샘플 데이터 삽입
run: >-
mysql --force
--host="127.0.0.1"
--port="3310"
--database="test_db"
--user="test_user"
--password="test_password"
< "backend/src/test/resources/database/spring_test_db.sql"
- name: JDK 11 설정
uses: actions/setup-java@v3
with:
java-version: "11"
distribution: "corretto"
- name: Application YML 생성
run: |
mkdir -p backend/src/main/resources
echo "${{ secrets.APPLICATION_PROD_YML }}" | base64 --decode > backend/src/main/resources/application-prod.yml
echo "${{ secrets.APPLICATION_DEV_YML }}" | base64 --decode > backend/src/main/resources/application-dev.yml
echo "${{ secrets.APPLICATION_AUTH_YML }}" | base64 --decode > backend/src/main/resources/application-auth.yml
echo "${{ secrets.APPLICATION_MAIL_YML }}" | base64 --decode > backend/src/main/resources/application-mail.yml
- name: Gradle 빌드
run: |
cd backend
mkdir -p build/generated-snippets/
chmod +x gradlew
./gradlew build
shell: bash
- name: Configure AWS credentials
if: ${{ github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' }}
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: ${{ secrets.AWS_REGION }}
- name: Dev S3에 업로드
if: ${{ github.ref == 'refs/heads/dev' }}
run: |
mkdir -p before-deploy
cp backend/build/libs/cabinet-*.jar before-deploy/
cp scripts/*.sh before-deploy/
cp appspec.yml before-deploy/
cp Dockerfile before-deploy/
cd before-deploy && zip -r before-deploy *
cd ../ && mkdir -p deploy
mv before-deploy/before-deploy.zip deploy/deploy.zip
aws s3 cp deploy/deploy.zip s3://${{ secrets.AWS_S3_DEV_BUCKET_NAME }}/deploy.zip
aws deploy create-deployment \
--application-name ${{ secrets.AWS_CODEDEPLOY_DEV_APP_NAME }} \
--deployment-config-name CodeDeployDefault.AllAtOnce \
--deployment-group-name ${{ secrets.AWS_CODEDEPLOY_DEV_GROUP_NAME }} \
--file-exists-behavior OVERWRITE \
--s3-location bucket=${{ secrets.AWS_S3_DEV_BUCKET_NAME }},bundleType=zip,key=deploy.zip
YAML
복사
최종적으로 CI YML 파일은 다음과 같이 구성하였다!
다시 한 번 CI 워크플로우를 트리거 시키면 정상적으로 S3에 배포용 zip파일이 업로드되고, CodeDeploy로 트리거가 된다!
CodeDeploy도 정상적으로 실행되었고,
도커 컨테이너도 정상적으로 가동되었다!
이제 정상적으로 브라우저로 접속이 가능하다!
마치며
오늘은 CodeDeploy가 무엇인지, 까비 팀이 왜 스프링 프로젝트를 CodeDeploy를 활용하여 배포하는지에 대해서 알아보고, CodeDeploy를 적용하는 과정에 대해서 소개해보았습니다!
그러나 아직 끝난 것은 아닙니다.
지금 방식으로는 무중단 배포라고 부를 수 없겠죠? 왜냐하면 인스턴스 하나만 사용하고 있고, CodeDeploy가 트리거 될 때마다 도커 컨테이너가 내렸다가 다시 올리기 때문에 그 사이에 중단이 되게 때문입니다.
다음 시간에는 ELB와 AutoScaling을 이용하여 Blue-Green 방식의 무중단 배포를 진행하는 방법을 소개하겠습니다!