Spring Boot, Github Actions, Docker, Docker Hub, AWS EC2로 CI/CD 파이프라인 구성하기
# 서론
스프링 부트 프로젝트를
Github Actions + Docker + Docker Hub + AWS EC2로 CI/CD 파이프라인 구성하는 방법을 기록한다.
기본적인 흐름도는 다음과 같다.
1. 로컬에서 작업 -> 깃허브 푸쉬
2. Github Actions이 해당 프로젝트를 빌드하고, 도커 이미지로 생성 후 도커 허브에 Push
3. Github Actions이 AWS EC2에 접속하여 해당 이미지를 Pull 받고, 실행
사전 환경
- JDK 17
- AWS EC2 t2.micro (Ubuntu 20.04 LTS)
- Docker Hub 무료 계정
먼저 AWS EC2를 생성하고, 해당 인스턴스에 JDK 17를 설치하는 과정은 생략한다.
# 도커, 도커 허브 세팅
1. 도커 설치
인스턴스에 도커를 설치하는 과정은 아래 블로그를 참조하도록 하자.
https://insight.infograb.net/docs/aws/installing-docker-on-aws-ec2/
2. 도커 허브 레포지토리 생성
도커 허브에 해당 이미지들을 Push할 레포지토리를 생성한다.
# 프로젝트 세팅 및 Dockerfile 작성
build.gradle 하단에 아래 코드를 추가한다.
bootJar로 jar 빌드 파일 생성시 -plain.jar이 붙은 파일이 생성되는데 이를 없애기 위한 코드와
SNAPSHOT-0.0.1.jar 형태의 이름을 고정적으로 변환하기 위한 코드이다.
다음은 Dockerfile이다.
FROM openjdk:17-jdk
ENV APP_HOME=/home/app/
WORKDIR $APP_HOME
COPY build/libs/*.jar plub-server.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","plub-server.jar"]
해당 스크립트를 프로젝트 루트 디렉토리에 Dockerfile (별도의 확장자가 없다.)로 생성해서 넣어준다.
이제 도커는 이 Dockerfile에 명시된 스크립트를 통해서 도커 이미지를 생성한다.
# Github Actions 설정
workflow를 작성하기 전에 workflow 스크립트에서 사용할 비밀 값들을 먼저 세팅해 준다.
깃허브 레포로 가서 Settings 탭으로 들어가면 설정할 수 있다.
DOCKER_PASSWORD = 도커 허브 계정 비밀번호
DOCKER_USERNAME = 도커 허브 계정 이름 (ex. plub2023)
EC2_IP = AWS EC2 인스턴스의 공개 IP (접속 주소)
EC2_PEM_KEY = EC2 인스턴스 생성시 지정하는 키페어다. 해당 키페어를 메모장으로 연 다음에 안에 있는 모든 내용물 (===로 시작되고 ===로 끝나는것 까지 포함해야함)을 복사한 값
YML_SECRET = DB연결정보와 같은 깃허브에 올라가면 안 되는 내용물을 base64 인코딩한 결과물을 저장한 값
만약 오류가 발생한다면 이 부분에서 발생했을 확률이 높으므로 명칭을 제대로 입력했는지, 값을 제대로 넣었는지 다시 한번 확인한다.
이제 workflow를 작성한다.
name: CI/CD Pipeline using Docker Hub
on:
push:
branches: [ "develop" ]
# pull_request:
# branches: [ "master" ]
permissions:
contents: read
jobs:
build:
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: Set up yml secrets file
env:
YAML_SECRET: ${{ secrets.YML_SECRET }}
YAML_DIR: src/main/resources
YAML_FILE_NAME: application-secret.yml
run: echo $YAML_SECRET | base64 --decode > $YAML_DIR/$YAML_FILE_NAME
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew bootJar
## 3) Docker Hub에 이미지 push 하기
- name: Docker build
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -t ${{ secrets.DOCKER_USERNAME }}/plub-server .
docker tag ${{ secrets.DOCKER_USERNAME }}/plub-server ${{ secrets.DOCKER_USERNAME }}/plub-server:${GITHUB_SHA::7}
docker push ${{ secrets.DOCKER_USERNAME }}/plub-server:${GITHUB_SHA::7}
## 4) Docker Hub에 Push한 이미지를 리눅스 서버에 받아와서 run
- name: Deploy
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_IP }}
username: ubuntu
key: ${{ secrets.EC2_PEM_KEY }}
envs: GITHUB_SHA
script: |
docker pull ${{ secrets.DOCKER_USERNAME }}/plub-server:${GITHUB_SHA::7}
docker tag ${{ secrets.DOCKER_USERNAME }}/plub-server:${GITHUB_SHA::7} plub-server
docker stop server
docker run -d --rm --name server -p 8080:8080 plub-server
앞서 설정했던 비밀 값들은 secrets.이름 으로 해당 스크립트에서 접근할 수 있다.
한 단계씩 차례로 살펴보자면
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Set up yml secrets file
env:
YAML_SECRET: ${{ secrets.YML_SECRET }}
YAML_DIR: src/main/resources
YAML_FILE_NAME: application-secret.yml
run: echo $YAML_SECRET | base64 --decode > $YAML_DIR/$YAML_FILE_NAME
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew bootJar
Github Actions가 실행되는 컨테이너에 JDK 17을 세팅한다.
그리고 우리가 빌드시 동적으로 첨부해야할 파일 (application-secret.yml)을 아까 설정한 secrets에서 가져와서 넣고 빌드를 진행한다.
빌드를 시작하기 위해서는 gradlew를 실행해야 하기 때문에, 실행에 필요한 권한을 설정하고,
이제 bootJar을 실행한다.
## 3) Docker Hub에 이미지 push 하기
- name: Docker build
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -t ${{ secrets.DOCKER_USERNAME }}/plub-server .
docker tag ${{ secrets.DOCKER_USERNAME }}/plub-server ${{ secrets.DOCKER_USERNAME }}/plub-server:${GITHUB_SHA::7}
docker push ${{ secrets.DOCKER_USERNAME }}/plub-server:${GITHUB_SHA::7}
해당 과정은 이미지를 만들어서 도커 허브에 Push하는 과정을 자동화하는 스크립트이다.
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
-> 도커 허브에 로그인 한다. 도커 허브 계정을 통해서 로그인을 진행한다.
docker build -t ${{ secrets.DOCKER_USERNAME }}/plub-server .
-> 도커 허브 계정/이미지 이름으로 이미지를 생성한다. 이때, 도커 허브 레포지토리와 동일하게 이름을 생성해야 Push가 가능하니 주의하자.
docker tag ${{ secrets.DOCKER_USERNAME }}/plub-server ${{ secrets.DOCKER_USERNAME }}/plub-server:${GITHUB_SHA::7}
-> 도커 허브에 Push한 이미지를 분별할 수 있도록 일종의 태깅 작업을 하는 과정이다. (굳이 필요 없긴하지만, 만약 새로운 이미지를 동일한 이름으로 Push하면 덮어씌워진다.)
docker push ${{ secrets.DOCKER_USERNAME }}/plub-server:${GITHUB_SHA::7}
-> 생성된 파일을 도커 허브에 업로드
## 4) Docker Hub에 Push한 이미지를 리눅스 서버에 받아와서 run
- name: Deploy
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_IP }}
username: ubuntu
key: ${{ secrets.EC2_PEM_KEY }}
envs: GITHUB_SHA
script: |
docker pull ${{ secrets.DOCKER_USERNAME }}/plub-server:${GITHUB_SHA::7}
docker tag ${{ secrets.DOCKER_USERNAME }}/plub-server:${GITHUB_SHA::7} plub-server
docker stop server
docker run -d --rm --name server -p 8080:8080 plub-server
AWS EC2에 지정해 놨던 키페어를 통해서 SSH 접속을 한다.
올렸던 이미지를 Pull 해서 이름을 plub-server로 변경하고,
기존에 실행중이던 컨테이너를 중지한 후에 다시 실행하는 스크립트이다.
여기까지 설정하면 정상적으로 파이프라인 구축에 성공한 것이다!