어제 ~ 오늘에 걸쳐서 Github Action으로 배포를 자동화해 봤다.
Releases · hyerijang/daily-pay
개인프로젝트 - 예산 관리 어플리케이션 (v1.1.0 진행중). Contribute to hyerijang/daily-pay development by creating an account on GitHub.
github.com
hotfix 1.0.0 : Docker 적용
hotfix 1.0.2 : github submodule 적용
hotfix 1.0.3 : git action 1 - CI/CD 적용
hotfix 1.0.4 : git action 2 - Release Tag 자동화
develop 브랜치에서는 이미 1.1.x 버전의 개발이 진행중이므로 feature 브랜치가 아닌 hotfix 브랜치를 분기해서 사용했다. hotfix 버전이 4개로 구분된건, 원래는 docker 만 적용하려고 했는데 하다보니까 점점 하고싶은게 늘어나서... 그렇게 됐다!
github Submodule로 환경 분리한 이유
github action으로 CICD를 하려면 application-xxxxxx.yml 파일들을 github action이 접근할 수 있는 어딘가에 저장해 두고 사용해야 했다. (jar 파일 빌드할 때 필요)
가장 간단한 건 public reposory에 그대로 push 하는 거지만 EC2 비밀번호. jwt secrest key 같은 중요 정보가 들어있어서 심각한 보안 문제가 발생할 수 있다. (매우 위험!)
간단한 해결법으로는 repository secret에 yml 파일을 통째로 등록해 두고 사용하는 방법도 있었는데, yml 파일을 업데이트할 때마다 수동으로 secret도 업데이트 해줘야 한다는 문제점이 있었다.
그래서 결국 github submodule을 써서 private 리포지토리에 따로 저장하는 방식으로 결정했다! yml 에 업데이트가 생기면 daily-pay-config 리포지토리에 변경사항을 push 해주면 된다.
Github Action을 선택한 이유
처음에는 Jenkins로 CI/CD를 해볼까 했는데
1. Jenkins용 EC2 서버가 한 대 더 필요
2. Github Action이 구성이 더 쉽다고 함
3. Github Action 써본적 없어서 한번 써보고 싶음
라는 이유로 Github Action으로 CI/CD를 하기로 결정했다.
프로젝트 아키텍처
내 프로젝트에서는 총 2개의 Github Action을 사용했다.
1. master 브랜치에 새로운 커밋 생성 시, 커밋 메시지의 버전정보를 읽어서 Github Release Tag 생성
2. 새로운 Release 생성 시, CICD 시작
Github Action 1 : Github Release Tag 생성
name: Release Tag
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 버전 정보 추출
run: echo "##[set-output name=version;]$(echo '${{ github.event.head_commit.message }}' | egrep -o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}')"
id: extract_version_name
- name: Release 생성
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.CONFIG_TOKEN }}
with:
tag_name: ${{ steps.extract_version_name.outputs.version }}
release_name: ${{ steps.extract_version_name.outputs.version }}
아래 블로그의 코드를 참조해서 약간만 수정했다.
10분만에 만드는 Docker image 저장 자동화 (feat. Github Action)
devocean.sk.com
17번째 줄의 GITHUB_TOKEN: ${{ secrets.CONFIG_TOKEN }} 부분은 action 실행 시, Repository - Setting - Secrets and vatiables - Action에 등록된 secret을 참조하게 된다. 본인 secret에 맞게 변경해서 사용할 것!
CONFIG_TOKEN 내의 값으로는 Gihub에서 Fine-grained personal access tokens을 생성해서 넣어줘야 한다.
토큰 생성 시에는 Release tag를 수정해야하므로 Content 접근 권한을 Read and write로 해야 함에 주의하도록 하자.
해당 Action이 수행되면 커밋메시지를 읽어서 버전 정보를 추출하고, Release Tag를 만들어주게 된다. (예를들어, 커밋 title이 Merge branch 'hotfix-1.0.4' 이면 "1.0.4" 만 추출해서 Release Tag 생성)
Github Action 2 : CI/CD
내 경우에는 Submodule로 환경을 분리해서 cicd.yml (= github action 설정)에 Submodule관련 설정이 들어있다. 안 쓰는 경우 제외해도 된다.
name: CI/CD using github actions & docker
# event trigger
# 새로운 release tag 생성 시 수행
on:
create:
tag:
- v*
permissions:
contents: read
# 실제 실행될 내용들을 정의합니다.
jobs:
build:
runs-on: ubuntu-latest
steps:
# open jdk 17 버전 환경을 세팅
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: "17"
distribution: "temurin"
# gradle caching - 빌드 시간 향상
- 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-
#Set up Gradle
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
# Github Submodule에서 환경변수 가져오기
- name: Checkout repo
uses: actions/checkout@v4
with:
token: ${{secrets.CONFIG_TOKEN}}
submodules: true
# 프로젝트 메타정보 추출해서 Docker image에 태그 생성
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{secrets.DOCKER_USERNAME}}/daily-pay
# 가장 최신 image 에 latest tag 달기
flavor: |
latest=true
# Git short commit, use git version tag
tags: |
type=semver,pattern={{version}}
# gradle build
- name: Build with Gradle
run: |
chmod +x ./gradlew
./gradlew clean build -x test
# DockerHub 로그인
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{secrets.DOCKER_USERNAME}}
password: ${{secrets.DOCKER_PASSWORD}}
# Docker image Build해서 DockerHub에 Push
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
# EC2 서버에 배포
- name: Deploy to prod
uses: appleboy/ssh-action@master
id: deploy-prod
with:
host: ${{ secrets.SSH_KNOWN_HOSTS }} # EC2 퍼블릭 IPv4 DNS
username: ${{secrets.SSH_USER}}
key: ${{ secrets.SSH_PEM_KEY }}
envs: GITHUB_SHA
# 주의 : 실행중인 컨테이너 전부 종료
script: |
sudo docker stop $(sudo docker ps -a -q)
sudo docker pull ${{secrets.DOCKER_USERNAME}}/daily-pay
sudo docker run -d -p 80:8080 ${{secrets.DOCKER_USERNAME}}/daily-pay
sudo docker image prune -f
여기서도 마찬가지로 ${{ secrets.~~~~}} 이런 값들은 다 본인 프로젝트에 등록된 secret에 맞게 수정이 필요하다.
참고로 ${{secrets.DOCKER_USERNAME}}/daily-pay에서 /daily-pay는 내 이미지 명이니 이 역시 본인의 docker image 명으로 수정하도록 하자.
내 리포지토리에 secret으로 등록해 둔 정보들은 다음과 같다
CONFIG_TOKEN
|
Gihub의 Fine-grained personal access tokens |
DOCKER_PASSWORD
|
docker hub 비밀번호 |
DOCKER_USERNAME
|
docker hub ID |
SSH_KNOWN_HOSTS
|
EC2 ip 주소 |
SSH_PEM_KEY
|
EC2 ssh 접속 할 때 쓰는 .pem 파일의 내용 그대로 복붙 |
SSH_USER
|
EC2 host name (e.g. ubuntu) |
해당 Action이 수행되면 Release Tag 정보를 읽어서 Docker 이미지를 빌드한 뒤 Docker hub에 latest 태그, 버전 태그를 함께 등록해서 push 하게 된다. ( latest 는 늘 가장 최신 버전으로 업데이트되도록 했다.)
몇몇 블로그 코드를 보면 최신 이미지를 업로드 할 때 lastest를 업데이트 하지 않는 경우가 많이 보이는데... 1회만 배포한다면 모를까, 여러 버전을 배포하는 경우에는 latest 를 반드시 지정해줘야한다. latest 태그가 없으면 이미지 pull할 때 오류가 발생하고, lastest 가 최신 버전으로 업데이트 되지 않으면 예전 버전의 이미지를 가져오기 때문.
그 뒤 EC2 서버에서 해당 image의 latest 버전을 받아와서 배포를 진행하게 된다.
이미지를 run 하기 전에 sudo docker stop $(sudo docker ps -a -q)로 실행되고 있는 모든 컨테이너를 종료하게 했는데, 내 경우에는 해당 이미지만 컨테이너로 띄워놔서 이렇게 해도 되지만, 만약 해당 EC2 서버 내에 다른 컨테이너도 띄워져 있다면 절대 이렇게 하면 안 된다!
특정 포트의 프로세스만 죽이는 명령어가 있는지 구글링을 해봤는데... 응 없어. 이래서 사람들이 쿠버네티스를 쓰나보다. 내 프로젝트에서는 컨테이너 자동화까진 필요가 없고, 다른 리팩토링할 것도 많아서 일단 전체 컨테이너를 종료하도록 하고 마무리 했다.
앞서 말했듯이 Docker compose 나 쿠버네티스로 컨테이너 오케스트레이션을 하면 컨테이너 lifecycle을 자동 관리해주므로 삭제도 알아서 해줄 것이다. 이건 나중에 시간날 때 해봐야지.
Dockerfile
Docker image를 빌드할 때 쓰는 Dockerfile은 아래와 같이 설정해 뒀다.
# Dockerfile
# jdk17 Image Start
FROM eclipse-temurin:17-jdk-jammy
# 인자 설정 - JAR_File
ARG JAR_FILE=build/libs/*.jar
# jar 파일 복제
COPY ${JAR_FILE} app.jar
# 실행 명령어
ENTRYPOINT ["java", "-Dspring.profiles.active=prod,security,swagger", "-jar", "app.jar"]
"-Dspring.profiles.active=prod,security,swagger"
나는 application.yml을 여러 개로 분리해 놓고 환경에 따라 profile을 다르게 쓰고 있다. (예를 들어, dev 환경에서는 8000 포트를, prod 환경에서는 8080 포트를 사용하고, p6spy는 부하가 크므로 prod 환경에서는 사용하지 않는 식)
따라서 이미지 실행 시에 "-Dspring.profiles.active=prod, security, swagger" 옵션으로 EC2 서버 상에서 사용할 profile을 별도로 지정해 뒀다.
Github Action 사용 소감
Github Action의 장점
1. Github 내에서 CICD 관리 가능
2. 별도의 서버나 외부 서비스를 필요로 하지 않음
3. yml 파일로 간단하게 파이프라인 구성 가능
가장 큰 장점은 아무래도 Github 내에서 CI/CD를 관리할 수 있다는 거 같다. 졸업 프로젝트 당시에 Travis CI로 CICD를 했었는데, 그때는 CI/CD 결과를 확인하려면 Travis CI 페이지로 들어가야했었다. 귀찮다 또 Github Release Tag를 자동 생성 해주는 건... Github Action에서만 가능한 기능이다. 일일히 만들어 주기 좀 귀찮았는데 이런게 있다는 걸 알게되서 좋았다.
AWS EC2, Codedeploy 같은 별도의 아키텍처를 필요로 하지 않는 것도 큰 장점이다. 저런 것도 결국 다 비용이고, 문제가 생기면 하나하나 들어가서 확인해 봐야한다. (귀찮음22)
마지막으로 사용법이 무척 간단해서 좋았다! yml 파일 만들어서 workflow 폴더에 넣고 push하면 끝이니까. 앞으로도 간단하게 CICD를 하고 싶을때 애용할 것 같다.
'프로젝트 > 예산 관리 어플리케이션 (개인)' 카테고리의 다른 글
Stream을 List로 변환하는 방법들의 차이 (0) | 2023.12.14 |
---|---|
[Spring Security + JWT] Access, Refresh 토큰을 DB에 저장해야하는 이유 (2) | 2023.11.27 |
DAILY PAY : 예산관리 애플리케이션 회고 (1) | 2023.11.15 |