main
브랜치에 푸시 이벤트가 발생한다.github action
으로 작성한 CICD.yml
이 푸시 이벤트를 감지하여 Job1, Job2를 실행한다.JDK 17
을 설치한다.JAR파일
을 뽑아낸다Dockerfile
로 하여금 JAR파일을 도커 이미지 빌드를 한다.서버IP/env
를 확인해서 서버가 살아있는지 확인한다.docker pull
시 이미지가 쌓이는데, 이를 방치하면 용량이 초과될 수 있으니 쓰지 않는 도커 이미지를 삭제한다.GITHUB SECRETS
의 환경변수들을 전달받아 docker compose up
을 수행한다. -> 도커 이미지로 하여금 컨테이너가 RUN 된다.Nginx
실행. docker-compose-blue.yml
, docker-compose-green.yml
작성/etc/nginx/conf.d/service-env.inc
으로 하여금 Nginx Change Upstream을 자동으로 바인딩 되도록 작성내용을 추가한다.EC2 원격 접속을 위한 key, 서버와 데이터베이스 연결을 위한 RDS username, pw 같은 것들을 업로드 해놓아야 한다.
도커 파일(Dockerfile)
을 프로젝트 루트 경로에 작성해야한다. 이때 Github secrets -> github action -> docker-compose로 이어져서 docker-compose가 컴포즈때 소지한 환경변수들을 받는 공간이 필요하다.
도커 파일-도커 이미지 프로세스는 공간만 마련해둔다. 이 공간에 환경변수를 집어넣는 일은 컴포즈 파일이 수행한다.
Dockerfile은 빈 집과 같다. 하지만 이 집에 들어올 사람의 부류는 명확하다. 예를 들어 무조건 음악방이 필요한 음악가가 들어오게 된다. 피아노나 기타 혹은 첼로를 보관하고 플레이할 수 있는 방을 확보해놓아야 한다.
이때 도커파일은 이 음악가용 하우스의 설계도가 되며, 도커 이미지는 하우스 껍데기 그자체가 된다. 도커 컴포즈 설정 파일을 통해 도커 이미지로 하여금 docker compose 명령을 수행하게 되면 어떤 음악가가 하우스를 계약하고 사용하는 단계로 해석할 수 있다.
당연하게도 배포 환경에서 필요로하는 스프링부트 설정 파일을 구성해주어야 한다.
profile로 하여금 local, blue, green으로 구분할 수 있으며
blue, green은 배포용 설정으로,
local에는 로컬개발용 설정으로 설정파일을 구성해야한다.
FROM amazoncorretto:17-alpine
ARG JAR_FILE=build/libs/*.jar
ARG PROFILES
ARG ENV
ARG AWS_ACCESS_KEY_ID
ARG AWS_SECRET_ACCESS_KEY
ARG AWS_REGION
ARG S3_BUCKET_NAME
ARG RDS_ENDPOINT
ARG RDS_NAME
ARG RDS_PASSWORD
ARG RDS_USERNAME
ARG LIVE_SERVER_IP
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["sh", "-c", "java \
-Dspring.profiles.active=${PROFILES} \
-Dserver.env=${ENV} \
-Dcloud.aws.credentials.access-key=${AWS_ACCESS_KEY_ID} \
-Dcloud.aws.credentials.secret-key=${AWS_SECRET_ACCESS_KEY} \
-Dcloud.aws.region.static=${AWS_REGION} \
-Ds3.bucket=${S3_BUCKET_NAME} \
-Dspring.datasource.url=jdbc:mysql://${RDS_ENDPOINT}:3306/${RDS_NAME} \
-Dspring.datasource.username=${RDS_USERNAME} \
-Dspring.datasource.password=${RDS_PASSWORD} \
-jar app.jar"]
솔직히 이 파일의 이름이 CICD
라고 하는게 맞는지는 잘 모르겠다. 가장 정확한 표현은 무중단을 곁들인 배포 자동화라고 표현할 수 있을 것 같다.
name: CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set working directory
working-directory: ./noteJ
run: echo "Switched to ./noteJ"
- name: Install JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build with Gradle
working-directory: ./noteJ
run: |
chmod +x gradlew
./gradlew clean build -x test
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build Docker
run: docker build --platform linux/amd64 -t ${{ secrets.DOCKERHUB_USERNAME }}/live_server ./noteJ
- name: Push Docker
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/live_server:latest
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Set target IP
run: |
STATUS=$(curl -o /dev/null -w "%{http_code}" "http://${{ secrets.LIVE_SERVER_IP }}/env")
echo $STATUS
if [ $STATUS = 200 ]; then
CURRENT_UPSTREAM=$(curl -s "http://${{ secrets.LIVE_SERVER_IP }}/env")
else
CURRENT_UPSTREAM=green
fi
echo CURRENT_UPSTREAM=$CURRENT_UPSTREAM >> $GITHUB_ENV
if [ $CURRENT_UPSTREAM = blue ]; then
echo "CURRENT_PORT=8080" >> $GITHUB_ENV
echo "STOPPED_PORT=8081" >> $GITHUB_ENV
echo "TARGET_UPSTREAM=green" >> $GITHUB_ENV
else
echo "CURRENT_PORT=8081" >> $GITHUB_ENV
echo "STOPPED_PORT=8080" >> $GITHUB_ENV
echo "TARGET_UPSTREAM=blue" >> $GITHUB_ENV
fi
- name: Remove unused Docker images
uses: appleboy/ssh-action@master
with:
username: ubuntu
host: ${{ secrets.LIVE_SERVER_IP }}
key: ${{ secrets.EC2_SSH_KEY }}
script_stop: true
script: |
echo "Removing unused Docker images..."
sudo docker image prune -af
echo "Unused Docker images removed."
- name: Docker compose
uses: appleboy/ssh-action@master
with:
username: ubuntu
host: ${{ secrets.LIVE_SERVER_IP }}
key: ${{ secrets.EC2_SSH_KEY }}
script_stop: true
script: |
sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/live_server:latest
# 환경 변수를 직접 docker-compose 실행 시 전달
sudo AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} \
AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} \
AWS_REGION=${{ secrets.AWS_REGION }} \
S3_BUCKET_NAME=${{ secrets.S3_BUCKET_NAME }} \
RDS_ENDPOINT=${{ secrets.RDS_ENDPOINT }} \
RDS_NAME=${{ secrets.RDS_NAME }} \
RDS_PASSWORD=${{ secrets.RDS_PASSWORD }} \
RDS_USERNAME=${{ secrets.RDS_USERNAME }} \
LIVE_SERVER_IP=${{ secrets.LIVE_SERVER_IP }} \
docker-compose -f docker-compose-${{ env.TARGET_UPSTREAM }}.yml up -d
- name: Check deploy server URL
uses: jtalk/url-health-check-action@v3
with:
url: http://${{ secrets.LIVE_SERVER_IP }}:${{env.STOPPED_PORT}}/env
max-attempts: 5
retry-delay: 12s
- name: Change nginx upstream
uses: appleboy/ssh-action@master
with:
username: ubuntu
host: ${{ secrets.LIVE_SERVER_IP }}
key: ${{ secrets.EC2_SSH_KEY }}
script_stop: true
script: |
sudo docker exec -i nginxserver bash -c 'echo "set \$service_url ${{ env.TARGET_UPSTREAM }};" > /etc/nginx/conf.d/service-env.inc && nginx -s reload'
- name: Stop current server
uses: appleboy/ssh-action@master
with:
username: ubuntu
host: ${{ secrets.LIVE_SERVER_IP }}
key: ${{ secrets.EC2_SSH_KEY }}
script_stop: true
script: |
sudo docker stop ${{env.CURRENT_UPSTREAM}}
sudo docker rm ${{env.CURRENT_UPSTREAM}}
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
permissions:
contents: read
on
: main 브랜치의 push
, pull request
시에 워크 플로우가 실행된다.permissions
: 워크플로우가 리포지토리에 대해 읽기 만 가능하다.jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set working directory
working-directory: ./noteJ
run: echo "Switched to ./noteJ"
runs-on: ubuntu-latest
steps
working-directory: ./noteJ
설정으로 이 스텝은 ./noteJ
디렉토리에서 실행된다.run: echo "Switched to ./noteJ"
명령은 단순히 디렉토리 변경이 제대로 적용되었는지 확인하기 위한 로깅이다.- name: Install JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
이 스텝은 GitHub Actions에서 제공하는 actions/setup-java@v3
액션을 사용해 빌드 머신에 JDK 17을 설치하는 작업이다.
java-version: '17'
Java 17 설치
distribution: 'temurin'
Temurin은 오픈 소스 JDK 배포판으로, 안정적이고 널리 사용되는 배포판이므로 선택
- name: Build with Gradle
working-directory: ./noteJ
run: |
chmod +x gradlew
./gradlew clean build -x test
이 스텝은 Gradle을 사용해 spring 애플리케이션을 빌드하는 작업이다.
작업 디렉토리:
working-directory: ./noteJ
로 지정되어, ./noteJ
폴더에서 명령어들이 실행된다.
실행 권한 부여:
chmod +x gradlew
명령어로 Gradle Wrapper 파일인 gradlew
에 실행 권한을 부여한다.
Gradle 빌드 실행:
./gradlew clean build -x test
명령어로 다음 작업을 수행한다.
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build Docker
run: docker build --platform linux/amd64 -t ${{ secrets.DOCKERHUB_USERNAME }}/live_server ./noteJ
- name: Push Docker
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/live_server:latest
Login to DockerHub
docker/login-action@v1
액션을 사용하여, GitHub Secrets에 저장된 DOCKERHUB_USERNAME
과 DOCKERHUB_TOKEN
을 통해 로그인Build Docker
docker build
명령어를 사용해 ./noteJ
디렉토리에 있는 Dockerfile을 기반으로 이미지를 빌드--platform linux/amd64
옵션은 이미지가 Linux의 amd64 아키텍처용으로 빌드되도록 보장${{ secrets.DOCKERHUB_USERNAME }}/live_server
로 지정되어, DockerHub 저장소와 연동Push Docker
docker push
명령어를 사용해 앞서 빌드한 이미지를 latest
태그로 DockerHub에 푸시이 과정을 통해 애플리케이션의 최신 Docker 이미지가 DockerHub에 저장되며, 이후 배포 단계에서 이 이미지를 기반으로 컨테이너를 실행한다.
이 부분은 GitHub Actions 워크플로우의 또 다른 Job인 deploy
를 정의하는 부분이다.
needs: build
deploy
Job은 build
Job이 완료되어야 실행된다. 즉, 빌드가 성공적으로 끝난 후 배포 작업이 진행된다.
runs-on: ubuntu-latest
deploy
Job 역시 GitHub Actions가 제공하는 최신 Ubuntu 가상 머신 환경에서 실행된다.
이 머신은 빌드용 임시 환경과 동일하게 워크플로우 실행 후 종료된다.
- name: Set target IP
run: |
STATUS=$(curl -o /dev/null -w "%{http_code}" "http://${{ secrets.LIVE_SERVER_IP }}/env")
echo $STATUS
if [ $STATUS = 200 ]; then
CURRENT_UPSTREAM=$(curl -s "http://${{ secrets.LIVE_SERVER_IP }}/env")
else
CURRENT_UPSTREAM=green
fi
echo CURRENT_UPSTREAM=$CURRENT_UPSTREAM >> $GITHUB_ENV
if [ $CURRENT_UPSTREAM = blue ]; then
echo "CURRENT_PORT=8080" >> $GITHUB_ENV
echo "STOPPED_PORT=8081" >> $GITHUB_ENV
echo "TARGET_UPSTREAM=green" >> $GITHUB_ENV
else
echo "CURRENT_PORT=8081" >> $GITHUB_ENV
echo "STOPPED_PORT=8080" >> $GITHUB_ENV
echo "TARGET_UPSTREAM=blue" >> $GITHUB_ENV
fi
이 스텝은 배포할 대상 서버의 현재 상태를 확인하고, Blue-Green 배포를 위해 어떤 환경(blue 또는 green)으로 전환할지 결정하는 작업이다.
curl
명령어를 사용하여 http://${{ secrets.LIVE_SERVER_IP }}/env
URL에 요청을 보내고, HTTP 상태 코드(예: 200)를 STATUS
변수에 저장한다.CURRENT_UPSTREAM
에 저장한다.CURRENT_UPSTREAM
을 green
으로 설정한다. (응답이 없다면 현재 실행되고 있는 서버가 없으니 무중단 로직을 위한 전환 로직이 아닌 단순히 그냥 서버를 켜야한다.)응답이 없을 경우 green으로 설정하는 이유
CURRENT_UPSTREAM
은 현재 어떤 서버가 켜져있는지에 대한 정보이다.
blue
도green
도 실행되지 않은 상태에서는 우리는blue
를 처음으로 배포하게 된다.
이때green
을 현재 서버로 둘 경우 이후 프로세스에서 다음 서버는 자동적으로blue
가 되므로 green으로 설정해서 blue가 띄워질 수 있게 만드는 것이다.
즉, 아무것도 없을 경우 green이 허상으로 떠있도록 논리적인 상황을 만들어 그다음 blue가 실행되도록 하여 아무것도 없는 상황과, green or blue가 떠있는 상황을 모두 다음 프로세스에서 하나의 논리로 처리할 수 있게 된다.
환경 변수 설정:
CURRENT_UPSTREAM
값을 GitHub Actions의 환경 변수 파일에 기록하여 이후 스텝에서 사용할 수 있도록 한다.포트 및 타겟 환경 결정:
CURRENT_UPSTREAM
이 blue라면:CURRENT_PORT
)는 8080, 중지할 포트(STOPPED_PORT
)는 8081로 설정하고, 새로 배포할 대상 환경(TARGET_UPSTREAM
)을 green으로 지정한다.이 과정을 통해 현재 활성화된 환경과 비활성 환경을 구분하여, 새로운 버전을 배포할 때 서비스 중단 없이 전환할 수 있도록 준비한다.
- name: Remove unused Docker images
uses: appleboy/ssh-action@master
with:
username: ubuntu
host: ${{ secrets.LIVE_SERVER_IP }}
key: ${{ secrets.EC2_SSH_KEY }}
script_stop: true
script: |
echo "Removing unused Docker images..."
sudo docker image prune -af
echo "Unused Docker images removed."
이 스텝은 원격 서버에 SSH로 접속하여 사용하지 않는 Docker 이미지를 정리하는 작업을 수행한다.
SSH 접속:
appleboy/ssh-action@master
액션을 사용해, 지정된 사용자(ubuntu
), 서버 주소(LIVE_SERVER_IP
), 그리고 SSH 키(EC2_SSH_KEY
)로 원격 서버에 접속한다.스크립트 실행:
sudo docker image prune -af
명령어를 실행 script_stop: true:
- name: Docker compose
uses: appleboy/ssh-action@master
with:
username: ubuntu
host: ${{ secrets.LIVE_SERVER_IP }}
key: ${{ secrets.EC2_SSH_KEY }}
script_stop: true
script: |
sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/live_server:latest
# 환경 변수를 직접 docker-compose 실행 시 전달
sudo AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} \
AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} \
AWS_REGION=${{ secrets.AWS_REGION }} \
S3_BUCKET_NAME=${{ secrets.S3_BUCKET_NAME }} \
RDS_ENDPOINT=${{ secrets.RDS_ENDPOINT }} \
RDS_NAME=${{ secrets.RDS_NAME }} \
RDS_PASSWORD=${{ secrets.RDS_PASSWORD }} \
RDS_USERNAME=${{ secrets.RDS_USERNAME }} \
LIVE_SERVER_IP=${{ secrets.LIVE_SERVER_IP }} \
docker-compose -f docker-compose-${{ env.TARGET_UPSTREAM }}.yml up -d
이 스텝은 원격 서버에 SSH로 접속해 Docker 관련 배포 작업을 수행한다.
SSH 접속 및 Docker 이미지 업데이트
appleboy/ssh-action
액션을 사용해, 서버에 SSH로 접속. sudo docker pull
명령어로 DockerHub에 있는 최신 live_server
이미지를 가져와 서버의 이미지를 업데이트환경 변수와 함께 docker-compose 실행
docker-compose
명령어를 실행하는데, 사용하는 Compose 파일은 docker-compose-${{ env.TARGET_UPSTREAM }}.yml
로, TARGET_UPSTREAM 값(예: blue 또는 green)에 따라 선택up -d
옵션은 컨테이너를 백그라운드에서 실행새로운 이미지를 기반으로 설정된 환경 변수와 함께 docker-compose를 실행하여, 배포 대상 컨테이너를 기동한다.
- name: Check deploy server URL
uses: jtalk/url-health-check-action@v3
with:
url: http://${{ secrets.LIVE_SERVER_IP }}:${{env.STOPPED_PORT}}/env
max-attempts: 5
retry-delay: 12s
이 스텝은 배포된 서버의 상태를 확인하기 위해 헬스 체크를 수행하는 역할을 한다.
헬스 체크 액션 사용:
jtalk/url-health-check-action@v3
액션을 사용해 지정된 URL에 접속해 응답 상태를 확인한다.URL:
http://${{ secrets.LIVE_SERVER_IP }}:${{env.STOPPED_PORT}}/env
로, LIVE_SERVER_IP와 STOPPED_PORT 변수에 따라 결정된다.(STOPPED_PORT
가 이 스크립트에서 최종적으로 실행될 컨테이너의 포트이다.)시도 횟수와 재시도 간격:
max-attempts: 5
로 설정되어 있어, 최대 5번까지 헬스 체크를 시도retry-delay: 12s
로 12초의 간격을 두고 재시도를 수행즉, 이 스텝은 새로 배포된 서버가 정상적으로 기동하여 /env
엔드포인트가 올바른 응답을 반환하는지 확인하는 과정이다.
- name: Change nginx upstream
uses: appleboy/ssh-action@master
with:
username: ubuntu
host: ${{ secrets.LIVE_SERVER_IP }}
key: ${{ secrets.EC2_SSH_KEY }}
script_stop: true
script: |
sudo docker exec -i nginxserver bash -c 'echo "set \$service_url ${{ env.TARGET_UPSTREAM }};" > /etc/nginx/conf.d/service-env.inc && nginx -s reload'
이 스텝은 원격 서버의 nginx 설정을 업데이트하여 트래픽이 새 배포 대상으로 전환되도록 합니다. 즉 nginx가 바라보는 방향을 방금 띄운 서버(STOPPED_PORT)
로 전환해야한다.
ubuntu
, 서버 IP(LIVE_SERVER_IP
), 그리고 SSH 키(EC2_SSH_KEY
)로 원격 서버에 접속한다.sudo docker exec -i nginxserver bash -c '... '
명령을 통해, 이름이 nginxserver
인 Docker 컨테이너 내부에서 bash 명령어를 실행한다.echo "set \$service_url ${{ env.TARGET_UPSTREAM }};" > /etc/nginx/conf.d/service-env.inc
service-env.inc
파일에 set $service_url [TARGET_UPSTREAM 값];
라는 내용을 기록한다.$TARGET_UPSTREAM
은 배포 과정에서 결정된 새 환경(예: blue 또는 green)을 나타내며, nginx가 해당 값을 사용해 업스트림 서버를 결정한다.nginx -s reload
명령어로 nginx를 재시작 없이 설정을 재로드하여, 새로운 업스트림 설정이 즉시 반영되도록 한다. - name: Stop current server
uses: appleboy/ssh-action@master
with:
username: ubuntu
host: ${{ secrets.LIVE_SERVER_IP }}
key: ${{ secrets.EC2_SSH_KEY }}
script_stop: true
script: |
sudo docker stop ${{env.CURRENT_UPSTREAM}}
sudo docker rm ${{env.CURRENT_UPSTREAM}}
이 시점에 blue 서버와 green 서버가 동시에 떠있다.
혹은 첫 배포라 green이 논리적으로만 떠있을 수도 있다.
어쨋든 green을 꺼줘야 한다. 만약 첫 배포라면 이 step에서 실패할 것이다.(첫 배포만 그냥 넘어가도록 하자. 스크립트를 개선한다면 이 또한 감지할 수 있을 것이다.)
이 스텝은 배포 후 기존에 실행 중이던 Docker 컨테이너(현재 활성화된 서버)를 종료하고 삭제하는 역할을 한다.
appleboy/ssh-action@master
액션을 사용해, ubuntu
사용자로 지정된 라이브 서버(LIVE_SERVER_IP
)에 SSH로 접속한다.sudo docker stop ${{env.CURRENT_UPSTREAM}}
CURRENT_UPSTREAM
에 저장된 컨테이너(현재 활성화된 서버)를 중지sudo docker rm ${{env.CURRENT_UPSTREAM}}
이 과정은 Blue-Green 배포 방식에서 새 서버로 트래픽 전환 후, 기존 서버를 안전하게 정리하기 위해 수행된다.