이번 포스팅에서는 Github action
과 Docker
를 이용하여 DoggyWalky
를 배포하는 시간을 가져보도록 하겠습니다.
Github Actions
은 Github
에서 코드 변경이 발생했을 때 변경된 코드를 자동으로 배포 서버에 반영해주는 기능을 제공합니다. 이와 함께 Docker
를 통해 컨테이너 이미지화 하여 배포 서버에 해당 컨테이너를 실행시켜 배포해보도록 하겠습니다.
Travis CI
, Jenkins
등 다양한 CI/CD 도구가 있지만 대부분 유료이거나, 부분적 유료인 경우가 많습니다.
그에 비해 Github Actions
는 public repository
인 경우 무료이며 Github
에 공식적으로 내장된 기능이므로, Github
와 함께 관리하기 편하기 때문에 선택하게 되었습니다. Jenkins
같은 경우 동시에 진행중인 EverTrip
프로젝트에서 사용해보기로 하였습니다.
또한 Docker
를 사용한 이유는 OS 환경에 종속적이지 않도록 하기 위함이었습니다. 로컬 서버는 윈도우 OS를 사용하였지만 배포 서버는 ubuntu OS
였기에 애플리케이션을 OS 환경에 종속적이지 않게 Docker
를 활용하여 애플리케이션 이미지를 빌드하고 Docker hub
저장소에 올리고 배포 서버에서 해당 이미지를 다운받고 실행하도록 구현하였습니다.
이제 CI/CD를 적용해보도록 하겠습니다. 먼저 Github Actions
의 Workflow
를 작성하고, 환경 변수를 설정해준 이후 배포 서버에서 확인해보도록 하겠습니다.
우선 프로젝트 루트 경로 아래에 .github/workflows/github-actions.yml
파일을 생성합니다.
github-actions.yml
파일에 자동화 배포에 대한 Workflow
를 작성하게 됩니다. 해당 Workflow
의 순서는 아래와 같습니다.
# github repository actions 페이지에 나타날 이름
name: CI/CD using github actions & docker
# 1. event trigger
# main 브랜치에 push가 되었을 때 실행
on:
push:
branches: [ "main"]
permissions:
contents: read
jobs:
CI-CD:
runs-on: ubuntu-latest
steps:
# 2. JDK setting - github actions에서 사용할 JDK 설정 (프로젝트나 AWS의 java 버전과 달라도 무방)
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
# 3. 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-
# 4. gradle build
- name: Build Test with Gradle
env:
JAVA_TOOL_OPTIONS: "-Djasypt.encryptor.password=${{ secrets.JASYPT_PASSWORD }} -Dcom.amazonaws.sdk.disableEc2Metadata=true"
run: ./gradlew clean build --info
# 5. docker build & push to production
- name: Docker build & push to prod
if: contains(github.ref, 'main')
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -f Dockerfile --build-arg JASYPT_PASSWORD=${{ secrets.JASYPT_PASSWORD }} -t ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky:latest .
docker push ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky:latest
# 6. deploy to production
- name: Deploy to prod
uses: appleboy/ssh-action@master
id: deploy-prod
if: contains(github.ref, 'main')
with:
host: ${{ secrets.HOST_PROD }} # EC2 퍼블릭 IPv4 DNS
username: ubuntu
key: ${{ secrets.PRIVATE_KEY }}
envs: GITHUB_SHA
script: |
sudo docker ps -q --filter "name=doggywalky-main" | grep -q . && sudo docker stop doggywalky-main && sudo docker rm doggywalky-main || true
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky:latest
sudo docker run -d -p 8080:8080 --name doggywalky-main -e JASYPT_PASSWORD=${{ secrets.JASYPT_PASSWORD }} -e DISABLE_EC2_METADATA=${{ secrets.DISABLE_EC2_METADATA }} ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky:latest
sudo docker image prune -f
github-actions.yml
파일을 전체적으로 살펴보도록 하겠습니다.
# github repository actions 페이지에 나타날 이름
name: CI/CD using github actions & docker
# 1. event trigger
# main 브랜치에 push가 되었을 때 실행
on:
push:
branches: [ "main"]
permissions:
contents: read
main
브랜치에 push가 발생했을 시 배포 스크립트가 실행된다는 것을 의미합니다.
저희 프로젝트는 Git Flow
전략을 사용하여 main
브랜치는 배포 서버용, develop
브랜치는 개발 서버용, feature
는 각 기능 개발용으로 분류하였습니다.
feature
브랜치에서 기능을 개발하고 develop
에 병합하여 테스트 후 최종적으로 main
브랜치에 푸쉬하게 되고 이때 배포 스크립트가 실행되게 됩니다.
permissions
섹션에서 contents: read
설정을 하여 GitHub Actions가 레포지토리의 콘텐츠에 접근할 수 있도록 허용하도록 하였습니다.
# 2. JDK setting - github actions에서 사용할 JDK 설정 (프로젝트나 AWS의 java 버전과 달라도 무방)
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
Github Actions
가 사용할 JDK
를 설정하고 있습니다.
# 3. 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-
Gradle을 캐싱해주는 작업을 진행합니다.
해당 코드는 없어도 무방하지만, 적용했을 시 빌드 시간을 단축할 수 있습니다.
# 4. gradle build
- name: Build Test with Gradle
env:
JAVA_TOOL_OPTIONS: "-Djasypt.encryptor.password=${{ secrets.JASYPT_PASSWORD }} -Dcom.amazonaws.sdk.disableEc2Metadata=true"
run: ./gradlew clean build --info
Gradle build를 진행하여 테스트 및 빌드 작업을 하게 됩니다. 프로젝트의 환경 변수 값을 설정하여 넣어주고 로그 출력을 위해 ./gradlew clean build --info
명령어를 사용하였습니다.
# 5. docker build & push to production
- name: Docker build & push to prod
if: contains(github.ref, 'main')
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -f Dockerfile --build-arg JASYPT_PASSWORD=${{ secrets.JASYPT_PASSWORD }} -t ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky:latest .
docker push ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky:latest
배포하려는 애플리케이션을 컨테이너 이미지로 빌드하고 Docker Hub
레포지토리에 푸시하는 작업을 진행합니다.
컨테이너 이미지를 빌드할 때 Dockerfile
이 필요하게 됩니다. Dockerfile
에 작성한 명령어들은 이미지를 빌드하는 과정에서 실제로 실행되며, 그 결과는 이미지에 반영됩니다. 이후 이미지가 나중에 컨테이너를 실행할 때 사용되게 됩니다.
--build-arg JASYPT_PASSWORD=${{ secrets.JASYPT_PASSWORD }}
코드는 컨테이너 이미지 빌드 시 사용할 build argument
를 지정합니다. 해당 build argument
는 Dockerfile
에서는 ARG argument_name
와 같은 형식으로 참조할 수 있습니다.
위 코드에서 docker-doggywalky:latest
는 Docker Hub
에 올라갈 컨테이너 이미지 이름입니다.
# 6. deploy to production
- name: Deploy to prod
uses: appleboy/ssh-action@master
id: deploy-prod
if: contains(github.ref, 'main')
with:
host: ${{ secrets.HOST_PROD }} # EC2 퍼블릭 IPv4 DNS
username: ubuntu
key: ${{ secrets.PRIVATE_KEY }}
envs: GITHUB_SHA
script: |
sudo docker ps -q --filter "name=doggywalky-main" | grep -q . && sudo docker stop doggywalky-main && sudo docker rm doggywalky-main || true
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky:latest
sudo docker run -d -p 8080:8080 --name doggywalky-main -e JASYPT_PASSWORD=${{ secrets.JASYPT_PASSWORD }} -e DISABLE_EC2_METADATA=${{ secrets.DISABLE_EC2_METADATA }} ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky:latest
sudo docker image prune -f
배포하려는 EC2 서버에 원격 접속한 후 Docker Hub
에 푸시했던 프로젝트 컨테이너 이미지를 pull 땡겨서 실행시키는 작업입니다.
EC2 원격 접속에는 appleboy
를 사용합니다.
이 때 기입할 정보는 아래와 같습니다.
host
: EC2의 IP 주소 혹은 DNS
username
: 인스턴스 생성 시 선택한 OS의 기본 사용자 이름
key
: EC2 생성 시 받은 pem 파일의 내용
Dockerfile
도 마찬가지로 프로젝트 루트 경로의 하위에 생성해주시면 됩니다.
# OpenJDK 17 버전의 환경을 구성
FROM eclipse-temurin:17
# 빌드 시 환경 변수를 설정하기 위해 ARG 사용
ARG JAR_FILE=build/libs/*.jar
# JAR_FILE을 app.jar로 복사
COPY ${JAR_FILE} /app/app.jar
# 런타임에 사용할 환경 변수를 설정하기 위해 ENV 사용
ARG JASYPT_PASSWORD
ENV JASYPT_PASSWORD_ENV=${JASYPT_PASSWORD}
# 환경 변수를 사용하여 애플리케이션 실행
ENTRYPOINT ["sh", "-c", "java -jar -Djasypt.encryptor.password=${JASYPT_PASSWORD_ENV} -Dcom.amazonaws.sdk.disableEc2Metadata=true /app/app.jar"]
컨테이너 이미지를 빌드할 때 위의 명령어들이 차례로 실행되게 됩니다.
FROM eclipse-temurin:17
eclipse-temurin:17 이미지를 기반으로 새로운 이미지를 생성합니다.
ARG JAR_FILE=build/libs/*.jar
빌드 인수(build argument)로 JAR_FILE을 설정합니다. 기본값은 build/libs/*.jar입니다. 애플리케이션의 JAR 파일 경로를 지정합니다.
COPY ${JAR_FILE} /app/app.jar
ARG로 지정된 JAR_FILE 경로에 있는 파일을 도커 이미지의 /app/app.jar 위치로 복사합니다.
ARG JASYPT_PASSWORD
ENV JASYPT_PASSWORD_ENV=${JASYPT_PASSWORD}
빌드 인수 JASYPT_PASSWORD를 설정하고
환경 변수 JASYPT_PASSWORD_ENV를 JASYPT_PASSWORD 값으로 설정합니다. 이 환경 변수는 애플리케이션 실행 시 사용됩니다.
ENTRYPOINT ["sh", "-c", "java -jar -Djasypt.encryptor.password=${JASYPT_PASSWORD_ENV} -Dcom.amazonaws.sdk.disableEc2Metadata=true /app/app.jar"]
컨테이너가 시작될 때 실행할 명령을 정의합니다. 환경 변수로 JASYPT_PASSWORD_ENV
를 사용하여 애플리케이션을 실행하게 됩니다.
Github
의 Secret Key
를 설정하여 환경 변수를 세팅할 수 있습니다.
레포지토리의 Settings
태그에서
다음과 같이 Secret Key
를 설정할 수 있습니다. 설정한 Secret Key
들은 github-actions.yml
파일에서 ${{ secrets.VARIABLE_NAME }}
다음과 같이 사용할 수 있습니다.
이제 main
브랜치에 푸쉬하여 CI/CD를 적용해보도록 하겠습니다.
Githun Actions
의 Workflow
을 확인하려면 아래로 들어가시면 됩니다. 현재 동작하고 있는 Workflow
를 클릭하시면 순차적으로 동작하고 있는 Job
들을 확인하실 수 있습니다.
위 사진과 같이 CI/CD가 정상적으로 완료된 것을 확인하였고, 배포 서버에 가서 컨테이너 실행 여부를 확인해보도록 하겠습니다.
docker ps
명령어 실행 결과 doggywalky-main
이름의 컨테이너가 실행 중인 것을 확인할 수 있습니다.
Spring Rest Docs
를 사용하여 만든 api.html
페이지를 확인해보도록 하겠습니다.
배포 서버의 api.html
역시 정상적으로 접속이 가능한 것을 확인할 수 있었습니다.
DoggyWalky
프로젝트는 Main Server
와 ChatServer
로 분류되어 있기 때문에 ChatServer
도 배포해주었습니다.
아직 로드 밸런싱을 적용하기 전이라 같은 EC2 서버에 8081
포트로 배포해주었습니다. 추후에 로드 밸런싱을 적용해볼 예정입니다.
ChatServer
배포는 github-actions.yml
파일의 deploy to production
부분만 수정하였습니다.
# deploy to production!!
- name: Deploy to prod
uses: appleboy/ssh-action@master
id: deploy-prod
if: contains(github.ref, 'main')
with:
host: ${{ secrets.HOST_PROD }} # EC2 퍼블릭 IPv4 DNS
username: ubuntu
key: ${{ secrets.PRIVATE_KEY }}
envs: GITHUB_SHA
script: |
sudo docker ps -q --filter "name=doggywalky-chat" | grep -q . && sudo docker stop doggywalky-chat && sudo docker rm doggywalky-chat || true
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky-chat:latest
sudo docker run -d --network host --name doggywalky-chat -e JASYPT_PASSWORD=${{ secrets.JASYPT_PASSWORD }} -e DISABLE_EC2_METADATA=${{ secrets.DISABLE_EC2_METADATA }} ${{ secrets.DOCKER_USERNAME }}/docker-doggywalky-chat:latest
sudo docker image prune -f
sudo docker run -d --network host ...
에서 네트워크 옵션을 사용하여 도커 컨테이너와 호스트가 동일한 IP 주소를 사용하게 되며, 모든 네트워크 인터페이스와 포트를 공유하도록 설정해주었습니다.
Github Actions
의 Workflow
확인을 해보겠습니다.
정상적으로 CI/CD가 완료되었음을 확인할 수 있습니다. 이제 배포 서버로 가서 컨테이너 실행 여부를 확인해보겠습니다.
현재 실행되고 있는 컨테이너는 doggywalky-main
과 doggywalky-chat
이 있음을 확인할 수 있습니다. 마지막으로 배포 서버의 8081
포트로 접속하여 api.html
을 확인해보도록 하겠습니다.
ChatServer
에 대한 API 명세가 제대로 나오는 것을 확인할 수 있습니다.
이번 포스팅에서는 Docker
와 Github Actions
를 활용하여 CI/CD를 적용해보는 시간을 가졌습니다. 해당 과정을 거치면서 로컬에서 작성한 개발 코드들의 배포 과정을 자동화하여 효율성을 높일 수 있었습니다.
CI/CD 파이프라인을 설정하는 데 다소 시간이 걸렸지만, 일단 완성되고 나니 자동화된 CI/CD가 수동 배포와 비교하여 훨씬 간편하고 효율적이라는 것을 직접 깨달을 수 있었습니다.