✅ 개요
지난 포스트에서 기본적인 인프라 구성은 완료했었다.
이번 포스트에선 Github Actions를 통해 본격적으로 CI/CD 파이프라인을 구축해보겠다.
✅ 1. Github Actions 사용자 생성
Github Actions는 다양한 AWS 인프라에 접근해야 한다. 따라서 그 권한을 가진 사용자를 설정해준다.
해당 사용자는 다음의 세 가지 정책을 가진다.
- AmazonEC2ContainerRegistryFullAccess: ECR에 Spring Boot 이미지를 업로드하기 위함
- AmazonS3FullAccess: S3에 CodeDeploy 관련 파일 및 기타 파일을 업로드 하기 위함
- AWSCodeDeployFullAccess: CodeDeploy에게 배포명령을 내리기 위함
액세스 키 발급
✅ 2. Dockerfile 작성
스프링부트 프로젝트를 빌드해서 해당 파일을 이미지로 만든다.
FROM openjdk:17-jdk
COPY build/libs/*SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
✅ 3. Docker-Compose 파일 작성
블루 서버, 그린 서버에 대한 Docker-Compose 파일을 작성한다.
1️⃣ compose.blue.yml
services:
blue:
image: 211125297893.dkr.ecr.ap-northeast-2.amazonaws.com/sscanner-backend
container_name: blue-server
ports:
- "8081:8080"
environment:
- HOME_URL
- HOME_USERNAME
- HOME_PASSWORD
- AWS_ACCESS_KEY
- AWS_SECRET_KEY
- AWS_REGION
- REDIS_HOST
- REDIS_PORT
- BUCKET_NAME
- BASE_URL
2️⃣ compose.green.yml
services:
green:
image: 211125297893.dkr.ecr.ap-northeast-2.amazonaws.com/sscanner-backend
container_name: green-server
ports:
- "8082:8080"
environment:
- HOME_URL
- HOME_USERNAME
- HOME_PASSWORD
- AWS_ACCESS_KEY
- AWS_SECRET_KEY
- AWS_REGION
- REDIS_HOST
- REDIS_PORT
- BUCKET_NAME
- BASE_URL
기본적으로 다 똑같지만 port번호만 다르다.
✅ 3. CodeDeploy 파일 작성
1️⃣ appspec.yml
CodeDeploy의 핵심이 되는 파일이다.
version: 0.0
os: linux
files:
- source: /
destination: /home/ubuntu/sscanner
permissions:
- object: /home/ubuntu/sscanner
owner: ubuntu
group: ubuntu
hooks:
ApplicationStart:
- location: scripts/start-server.sh
timeout: 60
runas: ubuntu
files:
- source: /
destination: /home/ubuntu/sscanner
- source: S3에서 어떤 파일들을 다운 받을지
- destination: 다운받은 파일을 EC2 어느 폴더에 넣을지
이 파일을 보고 나는 처음에 의문이 들었다. S3에 대한 정보를 기재한 적이 없는데 어떻게 파일을 읽어올까?
이는 아래 github actions에서 CodeDeploy에게 명령을 내릴 때 기재한다.
자세한건 github actions 코드를 설명할 때 이야기하겠다.
hooks:
ApplicationStart:
- location: scripts/start-server.sh
timeout: 60
runas: ubuntu
다운이 완료되고 나면 실행되는 hook이다
scripts/start-server.sh의 명령어들을 실행한다.
2️⃣ scripts/start-server.sh
해당 파일은 S3에서 관련 파일들을 업로드 한 뒤, EC2에서 실행될 명령어 들이다.
#!/bin/bash
NGINX_CONF="/etc/nginx/sites-available/default"
cd /home/ubuntu/sscanner/docker-compose/
# 이미지 업데이트
docker pull 211125297893.dkr.ecr.ap-northeast-2.amazonaws.com/sscanner-backend
# 현재 실행 중인 서버 확인 (블루 서버가 실행 중인지 확인)
if docker ps | grep -q "blue-server"; then
echo "블루 서버가 실행 중입니다. 그린 서버로 배포합니다."
# 그린 서버 배포
docker compose -f compose.green.yml up -d green --build
# 블루 서버 삭제
docker compose -f compose.blue.yml down
# 사용하지 않는 이미지 삭제
docker image prune -af
NEW_SERVER="green"
else
echo "그린 서버가 실행 중입니다. 블루 서버로 배포합니다."
# 블루 서버 배포
docker compose -f compose.blue.yml up -d blue --build
# 그린 서버 삭제
docker compose -f compose.green.yml down
# 사용하지 않는 이미지 삭제
docker image prune -af
NEW_SERVER="blue"
fi
echo "------- $NEW_SERVER 서버 배포 완료 --------"
복잡해보이는데 로직은 간단하다.
- 먼저 ECR에서 Spring boot 이미지를 다운받는다.(있다면 새로운 걸로 업데이트)
- 현재 실행 중인 서버가 블루 서버라면 블루 서버 동작을 멈추고 그린 서버를 실행한다.
- 반대로 현재 실행 중인 서버가 그린 서버라면 그린 서버 동작을 멈추고 블루 서버를 실행한다.
✅ 3. Nginx 프록시 설정
EC2에서 /etc/nginx/sites-available/default 이동
다음 부분을 추가한다.
upstream backend {
least_conn;
# 블루와 그린 서버를 정의합니다.
server 127.0.0.1:8081 max_fails=3 fail_timeout=10s; # Blue
server 127.0.0.1:8082 max_fails=3 fail_timeout=10s; # Green
}
server {
listen 443 ssl; # managed by Certbot
listen [::]:443 ssl ipv6only=on; # managed by Certbot
server_name ___; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/___/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/___/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
# 블루/그린 서버로 트래픽 분산
location / {
proxy_pass http://backend;
proxy_next_upstream error timeout http_502;
proxy_next_upstream_timeout 3s;
proxy_next_upstream_tries 10;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
현재 실행 중인 서버로 리다이렉트 되도록 설정했다.
✅ 4. github actions yml 파일 작성
많은 과정이 있지만 블루-그린 배포에 있어서 핵심적인 내용 위주로 설명하겠다.
name: CI-CD Pipeline
on:
push:
branches:
[ main ]
jobs:
CI-CD:
runs-on: ubuntu-latest
env:
# 임시 데이터베이스 환경변수(추후 RDS로 전환할 예정)
HOME_URL: ${{ secrets.HOME_URL }}
HOME_USERNAME: ${{ secrets.HOME_USERNAME }}
HOME_PASSWORD: ${{ secrets.HOME_PASSWORD }}
# AWS 환경변수
AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ap-northeast-2
# Redis 환경변수
REDIS_HOST: ${{ secrets.REDIS_HOST }}
REDIS_PORT: 6379
# Bucker 환경변수
BUCKET_NAME: ${{ secrets.BUCKET_NAME}}
BASE_URL: ${{ secrets.BASE_URL }}
steps:
- name: Github Repository 파일 불러오기
uses: actions/checkout@v4
- name: JDK 17 버전 설치
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- uses: actions/setup-node@v3
with:
node-version: '20'
- name: Gradle 캐싱
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: 빌드 권한 부여
run: chmod +x ./gradlew
shell: bash
- name: 빌드 및 테스트
run: ./gradlew clean build
- name: AWS Resource에 접근할 수 있게 AWS credentials 설정
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ap-northeast-2
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: ECR에 로그인하기
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Docker 이미지 생성
run: docker build -t sscanner-backend .
- name: Docker 이미지에 tag 붙이기
run: docker tag sscanner-backend:latest 211125297893.dkr.ecr.ap-northeast-2.amazonaws.com/sscanner-backend:latest
- name: ECR에 Docker 이미지 Push
run: docker push 211125297893.dkr.ecr.ap-northeast-2.amazonaws.com/sscanner-backend:latest
- name: .env 파일 작성
run: |
echo "HOME_URL=${{ secrets.HOME_URL }}" >> docker-compose/.env
echo "HOME_USERNAME=${{ secrets.HOME_USERNAME }}" >> docker-compose/.env
echo "HOME_PASSWORD=${{ secrets.HOME_PASSWORD }}" >> docker-compose/.env
echo "AWS_ACCESS_KEY=${{ secrets.AWS_ACCESS_KEY }}" >> docker-compose/.env
echo "AWS_SECRET_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}" >> docker-compose/.env
echo "AWS_REGION=ap-northeast-2" >> docker-compose/.env
echo "REDIS_HOST=${{ secrets.REDIS_HOST }}" >> docker-compose/.env
echo "REDIS_PORT=6379" >> docker-compose/.env
echo "BUCKET_NAME=${{ secrets.BUCKET_NAME }}" >> docker-compose/.env
echo "BASE_URL=${{ secrets.BASE_URL }}" >> docker-compose/.env
- name: 압축하기
run: tar -czvf $GITHUB_SHA.tar.gz appspec.yml scripts docker-compose
- name: S3에 프로젝트 폴더 업데이트 하기
run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.tar.gz s3://sscanner-bucket-codedeployfiles/$GITHUB_SHA.tar.gz
- name: CodeDeploy를 활용해 EC2에 프로젝트 코드 배포
run: aws deploy create-deployment
--application-name sscanner-code_deploy
--deployment-group-name Production
--deployment-config-name CodeDeployDefault.AllAtOnce
--s3-location bucket=sscanner-bucket-codedeployfiles,bundleType=tgz,key=$GITHUB_SHA.tar.gz
1️⃣ 빌드 및 테스트
- name: 빌드 및 테스트
run: ./gradlew clean build
- name: AWS Resource에 접근할 수 있게 AWS credentials 설정
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ap-northeast-2
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
스프링부트 프로젝트 빌드 파일을 Docker 이미지로 만들기 위해 빌드 및 테스트를 진행한다.
2️⃣ ECR에 스프링 부트 빌드 파일 Docker 이미지 업로드
- name: ECR에 로그인하기
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Docker 이미지 생성
run: docker build -t sscanner-backend .
- name: Docker 이미지에 tag 붙이기
run: docker tag sscanner-backend:latest 211125297893.dkr.ecr.ap-northeast-2.amazonaws.com/sscanner-backend:latest
- name: ECR에 Docker 이미지 Push
run: docker push 211125297893.dkr.ecr.ap-northeast-2.amazonaws.com/sscanner-backend:latest
3️⃣ .env 파일 작성
- name: .env 파일 작성
run: |
echo "HOME_URL=${{ secrets.HOME_URL }}" >> docker-compose/.env
echo "HOME_USERNAME=${{ secrets.HOME_USERNAME }}" >> docker-compose/.env
echo "HOME_PASSWORD=${{ secrets.HOME_PASSWORD }}" >> docker-compose/.env
echo "AWS_ACCESS_KEY=${{ secrets.AWS_ACCESS_KEY }}" >> docker-compose/.env
echo "AWS_SECRET_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}" >> docker-compose/.env
echo "AWS_REGION=ap-northeast-2" >> docker-compose/.env
echo "REDIS_HOST=${{ secrets.REDIS_HOST }}" >> docker-compose/.env
echo "REDIS_PORT=6379" >> docker-compose/.env
echo "BUCKET_NAME=${{ secrets.BUCKET_NAME }}" >> docker-compose/.env
echo "BASE_URL=${{ secrets.BASE_URL }}" >> docker-compose/.env
docker는 독립적인 환경이기 때문에 환경변수를 컨테이너 별로 설정해주어야 한다. 이때 .env파일은 반드시 docker-compose 파일과 동일한 디렉토리 폴더에 위치해야 한다.
앞서 작성했던 블루,그린 서버 Docker Compose 파일을 다시보자.
compose.blue.yml
services:
blue:
image: 211125297893.dkr.ecr.ap-northeast-2.amazonaws.com/sscanner-backend
container_name: blue-server
ports:
- "8081:8080"
environment:
- HOME_URL
- HOME_USERNAME
- HOME_PASSWORD
- AWS_ACCESS_KEY
- AWS_SECRET_KEY
- AWS_REGION
- REDIS_HOST
- REDIS_PORT
- BUCKET_NAME
- BASE_URL
compose.green.yml
services:
green:
image: 211125297893.dkr.ecr.ap-northeast-2.amazonaws.com/sscanner-backend
container_name: green-server
ports:
- "8082:8080"
environment:
- HOME_URL
- HOME_USERNAME
- HOME_PASSWORD
- AWS_ACCESS_KEY
- AWS_SECRET_KEY
- AWS_REGION
- REDIS_HOST
- REDIS_PORT
- BUCKET_NAME
- BASE_URL
해당 Compose 파일에 환경변수를 모두 기재해준 것을 알 수 있는데 각 컨테이너는 독립적인 환경에서 동작하기 때문에 일일이 추가해야 각 컨테이너에서 정상적으로 스프링부트 서버가 돌아간다.
4️⃣ S3에 CodeDeploy 관련 폴더 및 Docker Compose 폴더 업로드
- name: 압축하기
run: tar -czvf $GITHUB_SHA.tar.gz appspec.yml scripts docker-compose
- name: S3에 프로젝트 폴더 업데이트 하기
run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.tar.gz s3://sscanner-bucket-codedeployfiles/$GITHUB_SHA.tar.gz
5️⃣ CodeDeploy에 배포 명령
- name: CodeDeploy를 활용해 EC2에 프로젝트 코드 배포
run: aws deploy create-deployment
--application-name sscanner-code_deploy
--deployment-group-name Production
--deployment-config-name CodeDeployDefault.AllAtOnce
--s3-location bucket=sscanner-bucket-codedeployfiles,bundleType=tgz,key=$GITHUB_SHA.tar.gz
여기서 관련 파일을 읽어올 S3 bucket을 명시해준다.
appspec.yml에서는 해당 버킷의 파일을 다운 받아 복사한다.
✅ 배포 테스트
[Github Actions]
[CodeDeploy]
[EC2]
배포할 때 마다 포트가 변경되면서 실행되는 것을 알 수 있다.
하지만…
배포 과정 중 새로고침을 눌러보면 일정시간동안 502 Bad Gateway 오류가 발생한다.
이는 내가 의도했던 바가 아니다. 단 한순간 이라도 서비스가 끊기지 않는 것이 무중단 배포의 최종 목적이다.
다음 포스트에서는 이 문제를 해결하는 과정을 적어보도록 하겠다.
'데브코스 > 실습 & 프로젝트' 카테고리의 다른 글
[2-3차 프로젝트] Sonar를 통해 코드 정적분석하기 - 1. SonarCloud 환경설정하기 (1) | 2024.10.28 |
---|---|
[2-3차 프로젝트] 블루 - 그린 배포 과정 중 502 BadGateway 발생 해결 여정 (0) | 2024.10.28 |
[2-3차 프로젝트] 블루/그린 배포 방식으로 CI/CD 파이프라인 구축하기 - 1. 인프라 구성하기 (0) | 2024.10.27 |
[1차 프로젝트] - Toss Payments 결제 API를 통해 Spring boot 서버 구현하기 (4) | 2024.09.12 |
[1차 프로젝트] 코드리뷰 피드백 반영하기 (0) | 2024.09.09 |