이제 본격적으로 CI/CD를 구축해 봅시당
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-actuator'
application.yml
# health-check
management:
endpoints:
web:
exposure:
include: health
SecurityConfig.java
.requestMatchers("/actuator/health").permitAll() // API
프로젝트에서 스프링 시큐리티를 사용 중이라 해당 path로의 요청을 사용자 검증하지 않고 접근 가능하도록 추가했습니다.
GET http://localhost:8080/actuator/health
다음과 같이 서버 상태를 확인할 수 있습니다.
스프링부트 루트 디렉토리에 다음 파일을 추가해주세요.
DockerFile
FROM openjdk:17
ARG JAR_FILE_PATH=build/libs/*.jar
WORKDIR /apps
COPY $JAR_FILE_PATH app.jar
CMD ["java", "--enable-preview" ,"-jar", "app.jar"]
EC2에 접속해서 /home/ubuntu에 다음 파일들을 추가해주세요.
docker-compose.blue.yml
version: '3.1'
services:
redis-server:
image: redis
container_name: redis-server
command: redis-server --port 6379
hostname: redis
ports:
- "6379:6379"
api:
image: 도커 허브 사용자이름/도커 허브 레포지토리 이름:latest
container_name: blue
environment:
- LANG=ko_KR.UTF-8
- HTTP_PORT=8080
ports:
- '8080:8080'
depends_on:
- redis-server
docker-compose.green.yml
version: '3.1'
services:
redis-server:
image: redis
container_name: redis-server
command: redis-server --port 6379
hostname: redis
ports:
- "6379:6379"
api:
image: 도커 허브 사용자이름/도커 허브 레포지토리 이름:latest
container_name: green
environment:
- LANG=ko_KR.UTF-8
- HTTP_PORT=8081
ports:
- '8081:8080'
depends_on:
- redis-server
Redis 서버도 ec2에서 직접 설치하지 않고 도커 컨테이너로 같이 올렸습니다. 여기서 삽질했던게 Redis 서버는 따로 다른 yml을 작성해서 컨테이너를 올렸더니 스프링부트 서버 컨테이너와 네트워크가 달라서 redis 호스트 이름을 인식을 못하더라구용.. 같은 Yml안에서 작성해줘야 동일한 네트워크로 할당됩니다.
이때 꼭 application.yml에서 redis 호스트 이름을 위에 docker-compose.yml에서 적은 호스트 이름으로 바꿔줘야 합니다!
deploy.sh
#!/bin/bash
# 환경 변수 읽기
IS_GREEN=$(docker ps -q -f "name=green")
IS_BLUE=$(docker ps -q -f "name=blue")
# 기존 컨테이너 종료 및 제거
if [ -n "$IS_GREEN" ]; then
echo "Stopping and removing green container"
sudo docker-compose -p green -f /home/ubuntu/docker-compose.green.yml down
fi
if [ -n "$IS_BLUE" ]; then
echo "Stopping and removing blue container"
sudo docker-compose -p blue -f /home/ubuntu/docker-compose.blue.yml down
fi
# 컨테이너 실행
if [ -z "$IS_BLUE" ]; then
echo "Running blue container"
sudo docker-compose -p blue -f /home/ubuntu/docker-compose.blue.yml up -d
BEFORE_COLOR="green"
AFTER_COLOR="blue"
BEFORE_PORT=8081
AFTER_PORT=8080
else
echo "Running green container"
sudo docker-compose -p green -f /home/ubuntu/docker-compose.green.yml up -d
BEFORE_COLOR="blue"
AFTER_COLOR="green"
BEFORE_PORT=8080
AFTER_PORT=8081
fi
echo "${AFTER_COLOR} server up(port:${AFTER_PORT})"
# 서버 상태 확인
for cnt in {1..10}
do
echo "Checking response from server(${cnt}/10)"
UP=$(curl -s http://127.0.0.1:${AFTER_PORT}/actuator/health)
if [[ "$UP" == *'"status":"UP"'* ]]; then
sleep 10
else
break
fi
done
if [ $cnt -eq 10 ]; then
echo "Server Error"
exit 1
fi
# Nginx 설정 업데이트
sudo sed -i "s/${BEFORE_PORT}/${AFTER_PORT}/" /etc/nginx/conf.d/service-url.inc
sudo nginx -s reload
echo "Deploy Completed!!"
sudo vi /etc/nginx/sites-available/default
server {
include /etc/nginx/conf.d/service-url.inc;
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html;
index index.html index.htm index.nginx-debian.html;
server_name _;
# Proxy all other requests to Spring Boot
location / {
proxy_pass $service_url;
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;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
# Actuator health endpoint
location /actuator/health {
proxy_pass $service_url;
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;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
}
}
sudo vi /etc/nginx/conf.d/service-url.inc
set $service_url http://127.0.0.1:8080;
name: footballGG cicd
on:
workflow_dispatch:
push:
branches:
- main
jobs:
docker-build-and-push:
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: Gradle Caching
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: Set environment values
run: |
cd ./src/main/resources # resources 폴더로 이동
touch ./env.properties # env.properties 파일 생성
echo "${{ secrets.ENV_VARS }}" > ./env.properties
shell: bash
- name: Gradlew auth setting
run: chmod +x ./gradlew
- name: Jar File build
run: ./gradlew bootJar
- name: Docker Image build & Push to DockerHub
run: |
docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_PASSWORD }}
docker build -f Dockerfile -t ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }} .
docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}
- name: Deploy to EC2
uses: appleboy/ssh-action@master
with:
key: ${{ secrets.EC2_PRIVATE_KEY }}
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
script: |
cd /home/ubuntu
sudo chmod +x deploy.sh
sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}
./deploy.sh
env.properties 부분 때문에 엄청 고생했습니다..제가 바보같이 깃허브 시크릿에 등록해놓고 자꾸 docker compose를 실행할 때 .env파일과 함께 실행되도록만 설정한 것입니다. 막상 스프링부트를 실행하면 env.properties라는 파일이 없다고 에러가 발생했습니다.
docker compose 실행시 --envfiles ./env 라는 명령어와 함께 실행하는 것은 docker-compose.yml에서 환경변수를 사용했을 때 필요합니다. 저는 보다시피 docker-compose.yml에서 사용한 것이 아니라 스프링부트 내의 application.yml에서 환경변수를 사용했기 때문에 직접 env.properties 파일을 깃허브 시크릿에서 복사하여 붙여넣는 과정이 필요했습니다.
항상 nginx 무중단 배포를 구현할 때마다 밤새면서 삽질하는 것 같네요 ㅠ 물론 무중단 배포를 구현안하면 github Action이 매우 간단하고 편리해서 금방 cicd를 구축할 수 있었습니다.
하지만 다른 프로젝트에서 무중단 배포를 구축해놓으니깐 과정은 힘들어도 추후 프로젝트를 진행할 때 매우 편했어요! 여러분들은 이 글 참고해서 삽질안하고 빠르게 구축하시길 바랍니당