우아한테크캠프 6기
를 진행하며 최종 프로젝트를 위해 CI/CD를 구축하며 정리한 문서입니다.
혹시 간단하게 LoadBalancer를 사용하지 않고 단일 EC2에 배포할 예정이라면 EC2, S3, CodeDeploy 설정, GitHub 설정 항목만 진행하셔도 됩니다.
이 경우 CodeDeploy 설정 - 배포 그룹 생성 시 로드 밸런싱 활성화 옵션을 Off 해주시면 됩니다.
Github Actions는 GitHub에서 제공하는 서비스로, 어떤 event가 발생하면 특정 작업을 실행하거나, 주기적으로 특정 작업을 실행할 수 있습니다.
Github Actions만 사용해서는 AWS 내 private한 인스턴스에 접근할 수 없기 때문에, AWS 내 배포를 지원하는 서비스인 CodeDeploy를 함께 사용해야 배포를 자동화할 수 있습니다.
이번 실습에서는 EC2 인스턴스는 private 서브넷에 두어 외부에서 접근할 수 없게 막고, public 서브넷에 로드밸런서를 두어 private 서브넷의 EC2 인스턴스들에게 요청을 분배하도록 설계하였습니다.
VPC는 AWS 내에서 이용 가능한 사설 가상 네트워크입니다.
VPC 서비스로 이동 후, VPC 생성
버튼을 클릭해 다음 절차를 따라 VPC를 생성합니다.
생성할 리소스를 VPC 등
으로 선택해 VPC 생성 시점에 서브넷과 라우팅 테이블, 인터넷 게이트웨이를 한꺼번에 생성합니다.
로드밸런서 생성 시 2개 이상의 AZ(Availability Zone, 가용 영역)를 요구하기 때문에, 가용영역 수는 2
개로 설정하겠습니다.
NAT 게이트웨이는 1개 AZ에 위치하도록 하였습니다만, 요금이 많이 나올 수 있다고 합니다. NAT 게이트웨이를 사용하지 않고 NAT Instance를 사용하면 더 적은 비용으로 할 수 있다고 하니, 참고하시고 비용이 부담되신다면 NAT 인스턴스에 대해 검색하고 사용하시면 되겠습니다.
퍼블릭 서브넷 수와 프라이빗 서브넷 수 모두 2
로 설정하였습니다.
VPC 엔드포인트는 S3 게이트웨이
로 설정해 CodeDeploy로 배포 시 S3에 접근할 수 있도록 했습니다.
생성 후 결과는 다음과 같습니다.
애플리케이션 코드와 빌드 파일을 업로드하기 위한 S3를 준비해 줍니다.
S3-버킷 만들기
메뉴에서 버킷 이름과 리전을 설정하고, 나머지 설정은 기본값으로 생성합니다.
다음 설정으로 Spring Application을 배포할 EC2 인스턴스를 생성해 줍니다. 실습을 위해 프리티어 인스턴스로 진행하였으며, 실제 배포 상황에 맞게 설정해 주시면 됩니다.
t2.micro
Free tierIAM(Identity and Access Management)는 AWS 리소스에 대한 엑세스를 안전하게 제어할 수 있도록 계정, 권한을 설정하는 서비스입니다.
EC2에서 CodeDeploy 및, S3 저장소에 접근할 수 있도록 Role을 부여해 주어야 합니다.
보안 자격 증명
버튼을 클릭합니다.AWS 서비스
, 사용 사례는 EC2
를 선택해 줍니다.AmazonEC2RoleforAWSCodeDeploy
생성 결과는 다음과 같습니다.
작업 - 보안 - IAM 역할 수정
을 클릭해 역할 수정 페이지에 접근합니다.인스턴스 중지
- 인스턴스 시작
을 차례로 실행해 인스턴스를 재부팅해 줍니다.이제 CodeDeploy Agent를 설치할 차례입니다. 다음 절차를 따라 CodeDeploy Agent를 설치합니다.
# apt-get update
sudo apt-get update
# ruby 설치
sudo apt-get install ruby
# wget 설치
sudo apt-get install wget
# wget 이용해 한국 리전 Codedeploy Agent 설치
wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
# 실행 권한 부여
chmod +x ./install
# agent 설치
sudo ./install auto
이후 다음 명령어들을 이용해 Agent를 관리합니다.
# agent 상태 체크
sudo service codedeploy-agent status
# agent 시작
sudo service codedeploy-agent start
# agent 재시작
sudo service codedeploy-agent restart
Spring Application 실행을 위한 Java를 설치해 줍니다.
Amazon Corretto 11 JDK를 설치해 보겠습니다.
# Corretto Apt 레포지터리를 추가합니다.
wget -O- https://apt.corretto.aws/corretto.key | sudo apt-key add -
sudo add-apt-repository 'deb https://apt.corretto.aws stable main'
# apt-get update
sudo apt-get update
# jdk 설치
sudo apt-get install -y java-11-amazon-corretto-jdk
이후 Java가 잘 설치되었는지 버전을 체크합니다.
java -version
로드밸런서가 요청을 분배할 target instance들의 그룹입니다. 대상 그룹
메뉴에서 create target group
버튼을 클릭해 생성합니다.
Instances
로 선택해 EC2 인스턴스를 선택할 수 있게 합니다.HTTP:8080
으로 설정하였습니다. Spring에서 설정한 포트 번호에 맞게 설정하면 됩니다.200 OK
Status로 응답할 수 있는 엔드포인트를 제공합니다. Spring Actuator
를 이용해 health check용 엔드포인트를 만들 수도 있습니다. 생성 이후에도 변경할 수 있습니다.EC2 왼쪽 메뉴에서 로드밸런서 메뉴에서 Create load balancer
버튼을 클릭해 로드밸런서를 생성합니다. 저는 L7에서 동작하는 Application Load Balancer
를 이용하였습니다.
인터넷 경계
으로 설정하여 인터넷에서 오는 요청을 처리할 수 있도록 했습니다.IPv4
로 설정하였습니다.HTTP:80
을 위에서 생성한 타겟 그룹에 매핑했습니다.CodeDeploy에서 배포를 위해 필요한 Role을 부여해 주어야 합니다.
보안 자격 증명 - 역할 메뉴에서 역할 만들기
버튼을 클릭해 역할을 생성합니다.
AWS 서비스
로 설정합니다.CodeDeploy
로 설정합니다.CodeDeploy는 Application 단위로 서비스를 배포하고 관리합니다.
CodeDeploy 서비스에서 애플리케이션 생성
버튼을 클릭해 애플리케이션을 생성합니다.
EC2/온프레미스
를 선택합니다.애플리케이션을 선택하고 배포 그룹 생성
버튼을 클릭해 배포 대상 그룹을 생성합니다.
현재 위치
를 사용하겠습니다.현재 위치
옵션을 사용하면 현재 동작 중인 EC2 인스턴스를 잠시 멈추고 업데이트를 진행합니다.블루/그린
옵션은 blue/green 방식으로 무중단 배포를 할 수 있는 옵션입니다. AutoScaling을 사용하거나, green fleet에 미리 EC2를 설정해 두면 사용할 수 있습니다.CodeDeployDefault.AllAtOnce
로 설정했습니다.로드 밸런싱 활성화
옵션을 켜 배포 중인 인스턴스에게 트래픽이 전달되지 않도록 합니다.대상 그룹 선택
옵션에서 로드밸런서 대상 그룹을 설정합니다.GitHub Actions에서 Codedeploy 배포 프로세스를 invoke하기 위해 AWS에 접근할 수 있는 계정을 설정해 주겠습니다.
루트 계정의 액세스 키를 사용해도 동작하지만, 보안상 새로운 계정을 생성하고, 해당 계정의 액세스 키를 사용하겠습니다.
보안 자격 증명
버튼을 클릭합니다.액세스 관리
메뉴에서 사용자
메뉴에 접근해 사용자 추가
버튼을 클릭해 사용자 생성을 시작합니다.권한 설정
단계에서 직접 정책 연결
을 선택하고 다음 두 정책을 검색해서 추가합니다.AmazonS3FullAccess
AWSCodeDeployFullAccess
생성 결과는 다음과 같습니다.
보안 자격 증명
탭에서 액세스 키를 추가합니다. 생성된 액세스 키는 조심히 보관해 주세요.(한번 발급받으면 다시 참조할 수 없습니다)생성된 key는 repository의 Settings
- Secrets and variables
- Actions
에서 New repository secret
버튼을 눌러 각 secret key들을 생성해 줍니다. 저는 아래와 같은 이름으로 생성해 주었습니다.
생성한 secret key는 이후 Actions yml 스크립트 작성 시 사용될 예정입니다.
.github/workflows/
디렉터리 하위에 .yml
파일을 위치하면 Actions가 스크립트를 인식하여 스크립트에서 설정된 이벤트에 대해 job을 실행합니다.
아래 스크립트는 develop 브랜치에 push 이벤트가 발생하면 배포 과정을 실행하는 스크립트입니다.
env 항목들을 적절히 수정하여 사용하면 됩니다.
# .github/workflows/develop-deploy.yml
name: Java CD with Gradle for Develop
on:
push:
branches: [ "develop" ]
env:
AWS_REGION: <AWS 리전>
S3_BUCKET_NAME: <S3 버킷 이름>
CODE_DEPLOY_APPLICATION_NAME: <배포 애플리케이션 이름>
CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: <배포 그룹 이름>
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'corretto'
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: clean build -x test
- 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: ${{ env.AWS_REGION }}
- name: Upload to AWS S3
run: |
aws deploy push \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--ignore-hidden-files \
--s3-location s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip \
--source .
- name: Deploy to AWS EC2 from S3
run: |
aws deploy create-deployment \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--deployment-config-name CodeDeployDefault.AllAtOnce \
--deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
--s3-location bucket=$S3_BUCKET_NAME,key=$GITHUB_SHA.zip,bundleType=zip
actions/checkout@v3
: Actions 환경에 코드를 가져오는 기능을 제공합니다.gradle/gradle-build-action@v2
: gradle을 이용해 빌드하는 기능을 제공합니다. -x test
옵션을 주어 항상 테스트를 실행하도록 설정했습니다.CodeDeploy가 S3에서 코드를 EC2로 옮긴 후, Java Spring Application을 실행, 중단하기 위한 스크립트가 필요합니다.
저는 프로젝트 루트에 /script
디렉터리를 만들고 start.sh
, stop.sh
를 작성하였습니다.
start.sh
#!/usr/bin/env bash
PROJECT_ROOT="/home/ubuntu/app"
JAR_FILE="$PROJECT_ROOT/build/libs/<프로젝트 jar 파일 이름>.jar"
APP_LOG="$PROJECT_ROOT/application.log"
ERROR_LOG="$PROJECT_ROOT/error.log"
DEPLOY_LOG="$PROJECT_ROOT/deploy.log"
TIME_NOW=$(date +%c)
echo "[ $TIME_NOW ] Copy file $JAR_FILE to project root" >> $DEPLOY_LOG
cp $PROJECT_ROOT/build/libs/*.jar $JAR_FILE
echo "[ $TIME_NOW ] Run java application : $JAR_FILE" >> $DEPLOY_LOG
nohup java -jar $JAR_FILE > $APP_LOG 2> $ERROR_LOG &
CURRENT_PID=$(pgrep -f $JAR_FILE)
echo "[ $TIME_NOW ] Application running PID : $CURRENT_PID" >> $DEPLOY_LOG
stop.sh
#!/usr/bin/env bash
PROJECT_ROOT="/home/ubuntu/app"
JAR_FILE="$PROJECT_ROOT/build/libs/<프로젝트 jar 파일 이름>.jar"
DEPLOY_LOG="$PROJECT_ROOT/deploy.log"
TIME_NOW=$(date +%c)
CURRENT_PID=$(pgrep -f $JAR_FILE)
if [ -z $CURRENT_PID ]; then
echo "[ $TIME_NOW ] Running application not found." >> $DEPLOY_LOG
else
echo "$[ TIME_NOW ] Terminate application PID : $CURRENT_PID" >> $DEPLOY_LOG
kill -15 $CURRENT_PID
fi
프로젝트 루트에 위에서 작성한 start.sh
, stop.sh
를 어느 시점에 실행할지에 대한 정보를 담은 appspec.yml
파일을 작성해야 합니다.
version: 0.0
os: linux
files:
- source: /
destination: /home/ubuntu/app
overwrite: yes
permissions:
- object: /
pattern: "**"
owner: ubuntu
group: ubuntu
hooks:
AfterInstall:
- location: scripts/stop.sh
timeout: 60
runas: ubuntu
ApplicationStart:
- location: scripts/start.sh
timeout: 60
runas: ubuntu
이제 모든 설정이 끝났습니다. 길고 복잡한 과정을 따라오시느라 고생 많으셨습니다 ㅎㅎ. GitHub에 푸시해서 배포가 정상적으로 되는지 확인해 봅시다!
A. 이 경우 대상 그룹(Target Group)
메뉴에서 Health Check가 정상적으로 되는지 확인해 봅시다. Health Check 설정을 다시 확인해 보고, 스프링 서버에서 Health Check Path에 대해 올바른 응답코드로 응답하는지 확인해 봅시다. Spring Actuator를 이용하면 Health Check용 엔드포인트를 쉽게 만들 수 있습니다.
A. 대상 그룹
에서 배포에 사용하는 로드밸런서 Target Group을 선택하고, 속성
탭에서 대상 등록 해제 관리
옵션을 낮추면 BlockTraffic 단계에서 대상 등록 해제 관리(Deregistration Delay)
를 짧게 설정해 줍니다. 이 옵션은 트래픽을 차단한 후에도 연결이 남아있는 경우, 연결이 자연스럽게 끊기도록 기다려주는 Delay입니다. 이 시간을 조절하면 BlockTraffic 단계의 시간을 빠르게 넘어갈 수 있습니다. 하지만 너무 짧게 설정하면 남은 커넥션이 강제로 종료되니 적절한 값을 찾아 튜닝하면 좋을 것 같습니다.
A. Health Check가 오래 걸리기 때문입니다. 대상이 Healthy한지 검증할 때 다음 옵션이 적용된 상태라면, 30초 간격의 5번의 Health Check를 성공해야하기 때문에, 150초 이상이 걸리는 것을 알 수 있습니다. 정상 임계값(Healthy threshhold)와 간격(interval)를 적절히 튜닝하여 속도를 개선할 수 있습니다.
A. EC2에 IAM권한 설정을 나중에 한 경우, codedeploy agent가 권한이 없는 상태로 동작하고 있을 수도 있습니다. EC2인스턴스를 종료하고 다시 시작한 후, codedeploy agent도 restart 해 보세요!
좋은 글이네요. 공유해주셔서 감사합니다.