이전 글에서는 어떤 CI/CD 툴을 사용 할 지, 제가 생각한 CI, CD의 역할은 어떤건지에 대해 간략하게 소개했습니다. 이번엔 제가 구축한 PR, CI, CD에 대해 코드를 통해 자세하게 설명해드리겠습니다.
(참고: ci/cd에서 dev만 설명해드린 이유는 도커 이미지 명, ec2 서버, application.yml의 profile 등 dev에서 prod만 변경을하면되기 때문입니다.)
PR은 개발 협력에서 매우 중요합니다. 프로젝트는 개인이 아닌 팀으로 이루어져있고 본인이 맡은 역할의 코드가 팀원들의 코드와 합칠 때 문제가 없는 지 사전에 팀원들에게 알리고 검토/승인을 받는 과정입니다. 그래서 저는 PR도 자동화를 통해서 팀원들에게 알리기 전 충분히 자동으로 테스트를 해주고 팀원들이 코드를 보기전 사전 검증을 해주면 좋겠다고 생각했습니다.
name: Pull Request
on:
pull_request:
branches: [ main, develop ]
permissions:
checks: write
contents: read
pull-requests: write
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'corretto'
java-version: '17'
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
- name: Build with Gradle
run: ./gradlew build -x test
- name: Test
run: ./gradlew test
- name: add comments to a pull request
uses: mikepenz/action-junit-report@v3
if: always()
with:
report_paths: |
*/build/test-results/test/TEST-*.xml
build/test-results/test/TEST-*.xml
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: action-slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
author_name: serius-be
fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
if_mention: failure,cancelled
channel: '#be-pr'
env:
SLACK_WEBHOOK_URL: ${{ secrets.PR_SLACK_WEBHOOK }}
if: always()
on:
pull_request:
branches: [ main, develop ]
permissions:
checks: write
contents: read
pull-requests: write
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'corretto'
java-version: '17'
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
- name: Build with Gradle
run: ./gradlew build -x test
- name: Test
run: ./gradlew test
- name: add comments to a pull request
uses: mikepenz/action-junit-report@v3
if: always()
with:
report_paths: |
*/build/test-results/test/TEST-*.xml
build/test-results/test/TEST-*.xml
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: action-slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
author_name: serius-be
fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
if_mention: failure,cancelled
channel: '#be-pr'
env:
SLACK_WEBHOOK_URL: ${{ secrets.PR_SLACK_WEBHOOK }}
if: always()
CI는 통합한 코드에 문제가 없는 지 테스트 및 빌드를 하고 빌드한 아티펙트를 레지스트리에 푸쉬하는 과정입니다.
name: CI-Dev
on:
push:
branches: [ develop ]
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
permissions:
contents: read
actions: read
checks: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.SUBMODULE_TOKEN }}
submodules: true
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
distribution: 'corretto'
java-version: '17'
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
- name: Build with Gradle
run: ./gradlew build -x test -Dspring.profiles.active=dev
- name: Test
run: ./gradlew test
- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: |
*/build/test-results/test/TEST-*.xml
build/test-results/test/TEST-*.xml
check_name: "Test Results"
comment_mode: always
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v3
- name: Generate version tag
id: generate_tag
run: |
BUILD_DATE=$(date +'%Y%m%d')
RUN_ID=${{ github.run_id }}
SHORT_SHA=${GITHUB_SHA::7}
VERSION="${BUILD_DATE}-${RUN_ID}-${SHORT_SHA}"
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "DEV_LATEST_TAG=$VERSION" >> $GITHUB_ENV
- name: Store build info in Repository Secrets
env:
GH_TOKEN: ${{ secrets.PAT_TOKEN }}
run: |
echo "${{ env.DEV_LATEST_TAG }}" | gh secret set DEV_LATEST_TAG --repo ${{ github.repository }}
- name: Build and push
run: |
BASE_IMAGE="${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_DEV }}"
docker buildx build --platform linux/amd64 \
-t ${BASE_IMAGE}:latest \
-t ${BASE_IMAGE}:${{ env.VERSION }} \
--push .
- name: action-slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
author_name: serius-be
fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
if_mention: failure,cancelled
channel: '#be-ci'
text: |
*빌드 상태*: ${{ job.status }}
*이미지 태그*: ${{ env.VERSION }}
*이미지 URL*: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_DEV }}:${{ env.VERSION }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK }}
if: always()
일부분은 pull-request.yml과 동작이 똑같기에 다른점들만 설명하겠습니다~
on:
push:
branches: [ develop ]
- name: Create application files
run: |
mkdir -p ./src/main/resources
echo "${{ secrets.APPLICATION }}" > ./src/main/resources/application.yml
echo "${{ secrets.APPLICATION_DEV }}" > ./src/main/resources/application-dev.yml
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v3
- name: Generate version tag
id: generate_tag
run: |
BUILD_DATE=$(date +'%Y%m%d')
RUN_ID=${{ github.run_id }}
SHORT_SHA=${GITHUB_SHA::7}
VERSION="${BUILD_DATE}-${RUN_ID}-${SHORT_SHA}"
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "DEV_LATEST_TAG=$VERSION" >> $GITHUB_ENV
- name: Store build info in Repository Secrets
env:
GH_TOKEN: ${{ secrets.PAT_TOKEN }}
run: |
echo "${{ env.DEV_LATEST_TAG }}" | gh secret set DEV_LATEST_TAG --repo ${{ github.repository }}
- name: Build and push
run: |
BASE_IMAGE="${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_DEV }}"
docker buildx build --platform linux/amd64 \
-t ${BASE_IMAGE}:latest \
-t ${BASE_IMAGE}:${{ env.VERSION }} \
--push .
- name: action-slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
author_name: serius-be
fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
if_mention: failure,cancelled
channel: '#be-ci'
text: |
*빌드 상태*: ${{ job.status }}
*이미지 태그*: ${{ env.VERSION }}
*이미지 URL*: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_IMAGE_DEV }}:${{ env.VERSION }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK }}
if: always()
CD는 CI에서 생성한 도커 이미지를 서버에 배포 후 실행하는 과정입니다. 배포전략(블루-그린)을 사용할거기에 수동으로 배포합니다. 수동으로 진행 한 이유는 이전글에 자세히 작성해놓았으니 읽고 오시면 좋을것 같습니다~
name: CD-Dev
on:
workflow_dispatch:
inputs:
deployment_type:
description: '배포 유형'
required: true
default: 'latest_successful'
type: choice
options:
- latest_successful
- specific_version
specific_image_tag:
description: '특정 이미지 태그 (배포 유형이 specific_version인 경우에만 사용)'
required: false
deploy_message:
description: '배포 메시지'
required: false
jobs:
deploy:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
permissions:
contents: read
actions: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Determine image tag
id: determine_tag
run: |
DEPLOY_MESSAGE="${{ github.event.inputs.deploy_message }}"
DEPLOYMENT_TYPE="${{ github.event.inputs.deployment_type }}"
if [[ "$DEPLOYMENT_TYPE" == "latest_successful" ]]; then
echo "최신 성공한 CI 빌드 이미지 정보 읽기..."
DOCKER_IMAGE_TAG="${{ secrets.DEV_LATEST_TAG }}"
if [[ -z "$DOCKER_IMAGE_TAG" ]]; then
echo "::error::저장소 시크릿에서 최신 이미지 태그를 찾을 수 없습니다. CI 워크플로우가 성공적으로 완료되었는지 확인하세요."
exit 1
fi
echo "저장소 시크릿에서 이미지 태그 가져옴: $DOCKER_IMAGE_TAG"
elif [[ "$DEPLOYMENT_TYPE" == "specific_version" ]]; then
DOCKER_IMAGE_TAG="${{ github.event.inputs.specific_image_tag }}"
if [[ -z "$DOCKER_IMAGE_TAG" ]]; then
echo "::error::특정 버전 배포 선택 시 이미지 태그를 입력해야 합니다."
exit 1
fi
echo "지정된 이미지 태그 사용: $DOCKER_IMAGE_TAG"
else
echo "::error::알 수 없는 배포 유형: $DEPLOYMENT_TYPE"
exit 1
fi
echo "DOCKER_IMAGE_TAG=$DOCKER_IMAGE_TAG" >> $GITHUB_ENV
echo "DEPLOY_MESSAGE=$DEPLOY_MESSAGE" >> $GITHUB_ENV
echo "DEPLOYMENT_TYPE=$DEPLOYMENT_TYPE" >> $GITHUB_ENV
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Deploy to EC2
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_DEV_HOST }}
username: ${{ secrets.EC2_DEV_USERNAME }}
key: ${{ secrets.EC2_DEV_SSH_KEY }}
script: |
DOCKER_IMAGE_TAG=${{ env.DOCKER_IMAGE_TAG }}
DOCKER_USERNAME=${{ secrets.DOCKER_USERNAME }}
DOCKER_IMAGE=${{ secrets.DOCKER_DEV_IMAGE }}
echo "DOCKER_USERNAME=${DOCKER_USERNAME}" > /app/deploy.env
echo "DOCKER_IMAGE=${DOCKER_IMAGE}" >> /app/deploy.env
echo "DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG}" >> /app/deploy.env
echo "배포 스크립트 실행"
cd /app
chmod +x ./deploy.sh
./deploy.sh
if [ $? -eq 0 ]; then
echo "배포 성공"
echo "현재 실행 중인 컨테이너:"
docker ps
else
echo "배포 실패"
exit 1
fi
- name: Send Slack notification
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
author_name: serius-be-production
fields: repo,commit,author,action,eventName,workflow,job,took
if_mention: always
mention: here
channel: '#be-cd'
text: |
*배포 상태*: ${{ job.status == 'success' && ':white_check_mark: 성공' || ':x: 실패' }}
*배포 유형*: ${{ env.DEPLOYMENT_TYPE == 'latest_successful' && '최신 빌드' || '특정 버전 (롤백)' }}
*이미지 URL*: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_DEV_IMAGE }}:${{ env.DOCKER_IMAGE_TAG }}
*배포 메시지*: ${{ env.DEPLOY_MESSAGE }}
*배포자*: ${{ github.actor }}
*배포 시간*: $(date +'%Y-%m-%d %H:%M:%S')
env:
SLACK_WEBHOOK_URL: ${{ secrets.CD_SLACK_WEBHOOK }}
if: always()
on:
workflow_dispatch:
inputs:
deployment_type:
description: '배포 유형'
required: true
default: 'latest_successful'
type: choice
options:
- latest_successful
- specific_version
specific_image_tag:
description: '특정 이미지 태그 (배포 유형이 specific_version인 경우에만 사용)'
required: false
deploy_message:
description: '배포 메시지'
required: false
- name: Determine image tag
id: determine_tag
run: |
DEPLOY_MESSAGE="${{ github.event.inputs.deploy_message }}"
DEPLOYMENT_TYPE="${{ github.event.inputs.deployment_type }}"
if [[ "$DEPLOYMENT_TYPE" == "latest_successful" ]]; then
echo "최신 성공한 CI 빌드 이미지 정보 읽기..."
DOCKER_IMAGE_TAG="${{ secrets.DEV_LATEST_TAG }}"
if [[ -z "$DOCKER_IMAGE_TAG" ]]; then
echo "::error::저장소 시크릿에서 최신 이미지 태그를 찾을 수 없습니다. CI 워크플로우가 성공적으로 완료되었는지 확인하세요."
exit 1
fi
echo "저장소 시크릿에서 이미지 태그 가져옴: $DOCKER_IMAGE_TAG"
elif [[ "$DEPLOYMENT_TYPE" == "specific_version" ]]; then
DOCKER_IMAGE_TAG="${{ github.event.inputs.specific_image_tag }}"
if [[ -z "$DOCKER_IMAGE_TAG" ]]; then
echo "::error::특정 버전 배포 선택 시 이미지 태그를 입력해야 합니다."
exit 1
fi
echo "지정된 이미지 태그 사용: $DOCKER_IMAGE_TAG"
else
echo "::error::알 수 없는 배포 유형: $DEPLOYMENT_TYPE"
exit 1
fi
echo "DOCKER_IMAGE_TAG=$DOCKER_IMAGE_TAG" >> $GITHUB_ENV
echo "DEPLOY_MESSAGE=$DEPLOY_MESSAGE" >> $GITHUB_ENV
echo "DEPLOYMENT_TYPE=$DEPLOYMENT_TYPE" >> $GITHUB_ENV
- name: Deploy to EC2
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_DEV_HOST }}
username: ${{ secrets.EC2_DEV_USERNAME }}
key: ${{ secrets.EC2_DEV_SSH_KEY }}
script: |
DOCKER_IMAGE_TAG=${{ env.DOCKER_IMAGE_TAG }}
DOCKER_USERNAME=${{ secrets.DOCKER_USERNAME }}
DOCKER_IMAGE=${{ secrets.DOCKER_DEV_IMAGE }}
echo "DOCKER_USERNAME=${DOCKER_USERNAME}" > /app/deploy.env
echo "DOCKER_IMAGE=${DOCKER_IMAGE}" >> /app/deploy.env
echo "DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG}" >> /app/deploy.env
echo "배포 스크립트 실행"
cd /app
chmod +x ./deploy.sh
./deploy.sh
if [ $? -eq 0 ]; then
echo "배포 성공"
echo "현재 실행 중인 컨테이너:"
docker ps
else
echo "배포 실패"
exit 1
fi
- name: Send Slack notification
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
author_name: serius-be-production
fields: repo,commit,author,action,eventName,workflow,job,took
if_mention: always
mention: here
channel: '#be-cd'
text: |
*배포 상태*: ${{ job.status == 'success' && ':white_check_mark: 성공' || ':x: 실패' }}
*배포 유형*: ${{ env.DEPLOYMENT_TYPE == 'latest_successful' && '최신 빌드' || '특정 버전 (롤백)' }}
*이미지 URL*: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_DEV_IMAGE }}:${{ env.DOCKER_IMAGE_TAG }}
*배포 메시지*: ${{ env.DEPLOY_MESSAGE }}
*배포자*: ${{ github.actor }}
*배포 시간*: $(date +'%Y-%m-%d %H:%M:%S')
env:
SLACK_WEBHOOK_URL: ${{ secrets.CD_SLACK_WEBHOOK }}
if: always()