68번의 시도끝에 완성해버렸다...
docker, github actions, nginx 모두 처음이라 사용법을 익히는데 많은 시간과 노력이 소모되었는데, 지금 생각해보니, 낯설었을뿐 생각보다 쉬우니 차근차근 공부하면 어려울 부분은 없다 생각합니다!
그럼 본론으로 들어가, 제가 구성한 아키텍처는 다음과 같습니다.
- git push -> Github Actions에 의해 배포 스크립트가 실행되고 Blue-Green 무중단 배포가 진행됩니다.
EC2에 대한 내용은 다음 포스팅을 참고해주세요.
특히, 프리티어라면 꼭 swap파일을 생성해주세요!
https://velog.io/@carrotbat410/EC2-Spring-Boot-%EC%84%9C%EB%B2%84-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0
추가적인 참고사항은 다음과 같습니다.
$ sudo apt update
$ sudo apt install nginx
아래 명령어를 실행하여 정상적으로 실행 중인지 확인할 수 있습니다.
$ systemctl status nginx
또한 브라우저 주소창에 서버의 ip 혹은 도메인 이름을 입력하여 아래와 같은 기본 Nginx 랜딩 페이지가 표시되는지 확인합니다.
위 랜딩 페이지가 표시된다면 Nginx가 정상적으로 동작하고 있다는 뜻입니다.
Blue-Green 배포를 위해 미리 설정 파일을 수정하겠습니다.
다음 명령어로 관리자 권한을 얻은 다음.
$ sudo -s
upstream.conf파일을 생성합니다.
$ vi /etc/nginx/upstream.conf
upstream.conf
upstream app_servers {
#ip_hash; (기본 분배 알고리즘은 round robin입니다.)
server 127.0.0.1:8081; # App1 컨테이너
server 127.0.0.1:8082; # App2 컨테이너
server 127.0.0.1:8083; # App3 컨테이너
server 127.0.0.1:8084; # App4 컨테이너
}
sites-available/default파일에 다음 내용을 추가해줍니다.
$ vi /etc/nginx/sites-available/default
default
server {
listen 8080 default_server;
listen [::]:8080 default_server;
root /var/www/html;
index index.html index.htm index.nginx-debian.html;
server_name _;
location / {
proxy_pass http://app_servers; # Blue-Green 배포를 위한 proxy_pass 설정
proxy_set_header Host $host; # Blue-Green 배포를 위한
proxy_set_header X-Real-IP $remote_addr; # Blue-Green 배포를 위한
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Blue-Green 배포를 위한
proxy_set_header X-Forwarded-Proto $scheme; # Blue-Green 배포를 위한
}
}
nginx 기본 설정 파일에 다음 내용을 추가해줍니다.
vi /etc/nginx/nginx.conf
nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
}
http {
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
include /etc/nginx/upstream.conf; # Blue-Green 배포를 위한 추가 설정 파일
}
다음 명령어를 실행합니다.
nginx 재시작
$ systemctl restart nginx
제대로 실행되고 있는지 확인.
$ systemctl status nginx
nginx 세팅 끝!
다음 명령어를 사용해서, docker, docker compose를 설치하겠습니다.
# 패키지 업데이트 및 필요한 패키지 설치.
sudo apt-get update
sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
software-properties-common
# Docker의 공식 GPG 키 추가.
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
# Docker 저장소를 추가.
sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"
# 패키지 데이터베이스 업데이트.
sudo apt-get update
# Docker CE 설치.
sudo apt-get install docker-ce docker-ce-cli containerd.io
# Docker를 실행 중인 사용자 그룹에 추가.
# root 권한을 가지고 실행하는 것은 권장되지 않으므로, 사용자를 docker group에 포함시켜주면 된다.
($USER 환경 변수는 현재 로그인한 사용자 아이디를 나타내므로 그대로 입력하면 된다.)
출처: https://technote.kr/369
sudo usermod -aG docker ${USER}
# /var/run/docker.sock 파일의 권한을 666으로 변경하여 그룹 내 다른 사용자도 접근 가능하게 변경
(github actions에서 docker-compose접근시 /var/run/docker.sock에 대해서 permission denied이
발생하여 이 명령어 추가함)
sudo chmod 666 /var/run/docker.sock
# Docker 설치 확인.
docker --version
# Docker Compose의 최신 버전을 다운로드.
sudo curl -L "https://github.com/docker/compose/releases/download/$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep 'tag_name' | cut -d\" -f4)/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# 실행 권한 추가.
sudo chmod +x /usr/local/bin/docker-compose
# Docker Compose 설치 확인.
docker-compose --version
docker-compose.yml파일을 작성합니다.
$ vi /home/ubuntu/docker-compose.yml
docker-compose.yml
services:
app1:
container_name: container1
image: carrotbat410/spring-lol-repository:latest
ports:
- "8081:8080"
depends_on:
- mysql-db
networks:
- spring-lol-network
app2:
container_name: container2
image: carrotbat410/spring-lol-repository:latest
ports:
- "8082:8080"
depends_on:
- mysql-db
networks:
- spring-lol-network
app3:
container_name: container3
image: carrotbat410/spring-lol-repository:latest
ports:
- "8083:8080"
depends_on:
- mysql-db
networks:
- spring-lol-network
app4:
container_name: container4
image: carrotbat410/spring-lol-repository:latest
ports:
- "8084:8080"
depends_on:
- mysql-db
networks:
- spring-lol-network
mysql-db:
container_name: mysql-db
image: mysql:latest
environment:
MYSQL_DATABASE: lol_prod
MYSQL_ROOT_PASSWORD: tmpPassword
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
networks:
- spring-lol-network
volumes:
mysql-data:
networks:
spring-lol-network:
external: true
driver: bridge
본인 상황에 맞게 적절하게 작성해주시면 됩니다.
추가 작업으로 저는 mysql이미지를 먼저 불러오고 컨테이너를 띄운후, 아래명령어들로 접속 권한에 대해 설정을 진행했습니다.
$ docker exec -it {container_id} bash
$ mysql
$ CREATE DATABASE IF NOT EXISTS lol_prod;
$ CREATE USER 'root'@'%' IDENTIFIED BY 'tmpPassword';
$ GRANT ALL PRIVILEGES ON lol_prod.* TO 'root'@'%';
$ FLUSH PRIVILEGES;
배포 스크립트를 생성해줍니다.
git push를 하면 Github Actions가 EC2에 접속해 배포 스크립트를 실행해줄겁니다.
vi /home/ubuntu/deploy.sh
deploy.sh
#!/bin/bash
IS_APP1=$(docker ps | grep container1)
MAX_RETRIES=20
check_service() {
local RETRIES=0
local URL=$1
while [ $RETRIES -lt $MAX_RETRIES ]; do
echo "Checking service at $URL... (attempt: $((RETRIES+1)))"
sleep 3
REQUEST=$(curl $URL)
if [ -n "$REQUEST" ]; then
echo "health check success"
return 0
fi
RETRIES=$((RETRIES+1))
done;
echo "Failed to check service after $MAX_RETRIES attempts."
return 1
}
if [ -z "$IS_APP1" ];then
echo "### App3 App4 => APP1 App2 ###"
echo "1. App1 이미지 받기"
docker-compose pull app1
echo "2. App1 App2 컨테이너 실행"
docker-compose up -d app1 app2
echo "3. health check"
if ! check_service "http://127.0.0.1:8081" || ! check_service "http://127.0.0.1:8082"; then
echo "APP1 또는 APP2 health check 가 실패했습니다."
exit 1
fi
echo "4. APP3 APP4 컨테이너 내리기"
docker-compose stop app3 app4
docker-compose rm -f app3 app4
else
echo "### App1 App2 => App3 App4 ###"
echo "1. App3 이미지 받기"
docker-compose pull app3
echo "2. App3 App4 컨테이너 실행"
docker-compose up -d app3 app4
echo "3. health check"
if ! check_service "http://127.0.0.1:8083" || ! check_service "http://127.0.0.1:8084"; then
echo "APP3 또는 APP4 health check 가 실패했습니다."
exit 1
fi
echo "4. APP1 APP2 컨테이너 내리기"
docker-compose stop app1 app2
docker-compose rm -f app1 app2
fi
저장후 아래 명령어를 실행합니다.
권한 부여
$ chmod +x ./deploy.sh
작성된 배포 스크립트는 다음과 같이 동작합니다.
- 위에서 작성한 docker-compose.yml내용을 토대로 컨테이너를 띄웁니다.
- health check를 진행합니다.(3초마다, 20번 시도.)
- health check가 성공하면, 기존에 떠있던 컨테이너를 내립니다.
Github Actions의 workflow를 구성해줍니다.
./github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Set up Local MySQL for Test
uses: mirromutth/mysql-action@v1.1
with:
host port: 3306
container port: 3306
mysql database: 'lol-test'
mysql root password: 'tmpPassword'
- name: Create application.properties for test
run: |
mkdir -p src/main/resources
echo "${{ secrets.APPLICATION_PROPERTIES }}" > src/main/resources/application.properties
echo "${{ secrets.APPLICATION_PROPERTIES_PROD }}" > src/main/resources/application-prod.properties
echo "${{ secrets.APPLICATION_PROPERTIES_SECRET }}" > src/main/resources/application-secret.properties
#GitHub Actions에서 steps.<step_id>.conclusion은 해당 단계의 결과를 나타냄.
# conclusion 값은 GitHub Actions 자체에서 할당하며, 가능한 값 종류는 success / failure / cancelled / skipped 이 있음.
- name: Test with Gradle
id: gradle-test
run: SPRING_PROFILES_ACTIVE=secret ./gradlew test
- name: Build with Gradle
if: steps.gradle-test.conclusion == 'success'
run: SPRING_PROFILES_ACTIVE=secret ./gradlew build
- name: Login to Docker Hub
if: steps.gradle-test.conclusion == 'success'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build Docker image
if: steps.gradle-test.conclusion == 'success'
run: docker build . -t carrotbat410/spring-lol-repository
- name: Push Docker image
if: steps.gradle-test.conclusion == 'success'
run: docker push carrotbat410/spring-lol-repository
- name: Deploy to Server
if: steps.gradle-test.conclusion == 'success'
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SERVER_HOST }}
username: ubuntu
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
/home/ubuntu/deploy.sh
docker rmi -f $(docker images -f "dangling=true" -q)
# tag:none인 이미지 지우는 명령어
github repository -> Setting에서 필요한 github actions secret들을 추가합니다.
ci-cd.yml의 내용은 다음과 같습니다.
- github actions runner위에 스프링 서버를 띄우기 위해 세팅을 해줍니다.(JDK, DB)
- 테스트코드를 실행후, 테스트에 성공하면 다음 과정을 진행합니다.
- 빌드 파일을 만들고, docker hub에 docker image를 업로드합니다.
- EC2 인스턴스에 접속하여, deploy.sh를 실행합니다.
main 브랜치에 git push하게 되면 CI/CD가 진행됩니다.
EC2 인스턴스에 접속해 아래 명령어로 컨테이너가 제대로 작동하는지 확인합니다.
$ docker ps
$ docker ps 결과
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2391f1e45c5d carrotbat410/spring-lol-repository:latest "java -jar -Dspring.…" 23 hours ago Up 23 hours 0.0.0.0:8082->8080/tcp, :::8082->8080/tcp container2
9019a209b7e4 carrotbat410/spring-lol-repository:latest "java -jar -Dspring.…" 23 hours ago Up 23 hours 0.0.0.0:8081->8080/tcp, :::8081->8080/tcp container1
609ff1e0a69c mysql:latest "docker-entrypoint.s…" 24 hours ago Up 24 hours 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp mysql-db
spring 서버에 접속 로그가 찍히도록 기능을 추가한 뒤 본인의 주소로 요청을 여러번 보내면
public ip + 포트 80 (포트 80번은 생략 가능)
2개의 컨테이너에 요청을 분산시키는 것을 확인할 수 있습니다!
아직 부족한 부분이 있습니다.
health check에서 단순히 root endpoint인 127.0.0.1을 확인했는데, 이건 "스프링이 켜지긴 했다." 라는 뜻입니다. 데이터베이스 연결 상태, 외부 서비스와의 통신 상태 등 필요한 부분을 health check하는 개선된 과정이 필요합니다.
참고자료
https://technote.kr/369
https://jaehyeon48.github.io/nginx/configure-nginx-on-ubuntu-2004/
https://yeonyeon.tistory.com/52
https://velog.io/@hooni_/Blue-Green-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC
https://www.joinc.co.kr/w/man/12/proxy
https://github.com/occidere/TIL/issues/116