지난 포스팅까지 도커에 nginx를 올리고, reverse proxy 설정까지 마쳤다. 이제 Github Actions를 이용하여 스프링부트를 자동 배포해보자.
스프링 프로젝트를 Docker 컨테이너에 올리기 위해서는Dokerfile
을 작성해야 한다. Spring Boot로 만들어진 Web Application을 Docker Image로 만들어서 docker hub에 push하는 과정이다.
스프링부트 프로젝트 루트 경로에 Dockerfile
이라는 파일을 생성해서 아래와 같은 내용을 입력하자.
FROM openjdk:11
ARG JAR_FILE=./build/libs/{빌드한 jar 파일명}.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar", "--spring.profiles.active=prod"]
FROM openjdk:11
: base 이미지를 openjdk:11로 설정한다. 즉, Docker에 올릴 때 jdk11 버전을 이용해서 올리겠다고 선언해주는 커맨드이다.
ARG JAR_FILE=./build/libs/{빌드한 jar 파일명}.jar
: JAR 파일의 위치를 환경변수의 형태로 선언해주는 커맨드이다. 프로젝트를 빌드하게 되면 build/libs/{프로젝트명}-0.0.1-SNAPSHOT.jar 형태로 파일이 생성된다.
COPY ${JAR_FILE} app.jar
: 프로젝트 빌드 파일을 컨테이너의 루트 디렉토리의 app.jar 라는 이름으로 복사하는 커맨드이다.
ENTRYPOINT ["java","-jar","/app.jar", "--spring.profiles.active=prod"]
: 컨테이너에서 java -jar /app.jar를 실행하는 커맨드이다. 이때 --spring.profiles.active=prod 라는 옵션은 properties 파일에서 profiles를 활성화하는 경우 붙여준다.
지난번 EC2 인스턴스에서 docker-compose를 설치했으니 스킵하도록 하겠다.
docker-compose 파일을 작성하여 EC2 서버에 배포할 것이다. 프로젝트 루트 경로에 docker-compose.yml 파일을 생성하자.
docker-compose.yml을 작성하여 각각 독립된 컨테이너의 실행 정의를 실시한다.
version: "3" # 버전 지정
services: # 컨테이너 설정
database:
container_name: mysql # 컨테이너 이름
image: mysql/mysql-server:latest # 컨테이너에서 사용하는 base image 지정
environment: # 컨테이너 안의 환경변수 설정
MYSQL_DATABASE: {database name}
MYSQL_USER: {database user}
MYSQL_PASSWORD: {database pwd}
MYSQL_ROOT_HOST: '%'
MYSQL_ROOT_PASSWORD: rootpwd
command: # 명령어 설정
- --default-authentication-plugin=mysql_native_password
ports: # 접근 포트 설정
- 3305:3306 # Host:Container
networks:
- db_network
restart: always # 컨테이너 실행 시 재시작
mytamla:
build: .
expose:
- 8080
depends_on:
- database
networks: # 커스텀 네트워크 추가
db_network: # 네트워크 이름
driver: bridge
방금 작성한 Dockerfile
로 스프링부트 컨테이너를 실행하고, mysql은 기존 이미지를 사용해 컨테이너 실행을 할 것이다.
mysql config를 따로 빼서 볼륨 설정을 해주기도 하는데 필자는 생략했다.
기본적으로 Docker Compose는 하나의 디폴트 네트워크에 모든 컨테이너를 연결한다. 디폴트 네트워크의 이름은 docker-compose.yml가 위치한 디렉토리 이름 뒤에 _default가 붙는다.
커스텀 네트워크
컨테이너에 올리는 mysql은 호스트 컴퓨터에서 접속할 때는 3305포트를 사용해야 하고, 같은 디폴트 네트워크 내의 다른 컨테이너에서 접속할 때는 3306 포트를 사용해야 한다.
이렇게 해주면 database 서비스는 디폴트 네트워크 뿐만 아니라 db_network 네트워크에도 연결되게 된다.
여러가지 네트워크 드라이버가 있는데
- bridge : 하나의 호스트 컴퓨터에서 여러 개의 컨테이너들이 통신할 수 있게 한다.
- host : 호스트 컴퓨터와 동일한 네트워크에서 여러 개의 컨테이너들이 통신할 수 있게 한다.
- overlay : 여러 호스트 컴퓨터(다른 네트워크)에서 여러 개의 컨테이너들이 통신할 수 있게 한다.
CI(Continuous Integration)는 지속적 통합을 나타내는 용어이다.
어플리케이션의 새로운 코드 변경 사항이 정기적으로 빌드 및 테스트되어 공유 레포지토리에 통합되는 것을 의미한다.
클래스와 기능에서부터 전체 애플리케이션을 구성하는 서로 다른 모듈에 이르기까지 모든 것에 대한 테스트를 수행할 수 있으며, 코드를 병합하는 과정에서 충돌이 생긴다면 CI를 통해 버그를 수정할 수 있다.
이로 인해 개발하는 코드의 품질을 좀 더 향상시킬 수 있으며, 새로운 업데이트의 검증 및 릴리즈의 시간을 단축시킬 수 있다.
CD(Continuous Deliver, Countinuous Deplotment)는 지속적인 배포를 나타내는 용어이다.
(* 보통은 전자인 지속적 제공의 의미가 강하다.)
CI/CD는 지속적인 통합과 지속적인 배포의 결합된 관행을 뜻하며 이 과정들을 자동화하여 불필요한 공수를 줄이고 보다 빠른 서비스 제공을 할 수 있는 효과를 가진다.
어플리케이션 개발 단계를 자동화하면 보다 짧은 주기로 고객에게 제공할 수 있다. 자동화가 이루어지지 않는다면 모든 빌드와 배포 작업을 수동으로 조작해야 하는데 사소한 수정 사항 하나에도 모든 과정을 거쳐야하므로 매우 불편하고 시간 소요가 많다.
CI/CD 구축을 지원하는 툴은 시중에 많다.
각 툴은 요금체계부터 특성이 모두 다르므로 프로젝트에 적합한 것을 선택하면 된다.
Github Actions는 소프트웨어 개발 라이프사이클 안에서 PR, push 등의 이벤트 발생에 따라 자동화된 작업을 진행할 수 있게 해주는 기능이다.
CI/CD나 Testing, Cron Job 등 작업을 수행할 수 있다.
Github Actions를 활용하기 위해 구성 요소를 먼저 파악해보자.
.github/workflows
디렉토리에 YAML 형태를 저장합니다.필자는 팀원들과 Git을 공유하며 사용하는 환경이므로, 기능 추가와 같이 코드가 수정되고 난 후 feat
-> main
PR에서 CI가 동작하도록 구성하려고 한다.
(* 혼자 백엔드를 맡아서 feat, main으로만 브랜치를 구성했었다.)
CI 작업을 수행할 프로젝트의 레포지토리의 Actions 탭을 접속한다.
프로젝트 루트
.github/workflows
디렉토리 내에.yml
파일을생성해도 된다.
다양한 용도별 템플릿을 제공해주고 있다. 지금은 가장 기본적인 템플릿을 사용해볼 것이다. 상단에 set up a workflow yourself
를 클릭한다.
# Workflow 이름은 구별이 가능할 정도로 자유롭게 적어주어도 된다.
# 필수 옵션은 아니다.
name: Java CI with Gradle
# main 브랜치에 PR 이벤트가 발생하면 Workflow가 실행된다.
# 브랜치 구분이 없으면 on: [pull_request]로 해주어도 된다.
on:
pull_request:
branches: [ "main" ]
# 테스트 결과 작성을 위해 쓰기권한 추가
permissions: write-all
# 해당 Workflow의 Job 목록
jobs:
# Job 이름으로, build 라는 이름으로 Job이 표시된다.
build:
# Runner가 실행되는 환경을 정의
runs-on: ubuntu-latest
# build Job 내의 step 목록
steps:
# uses 키워드를 통해 Action을 불러올 수 있다.
# 해당 레포지토리로 check-out하여 레포지토리에 접근할 수 있는 Acion 불러오기
- uses: actions/checkout@v3
# 여기서 실행되는 커맨드에 대한 설명으로, Workflow에 표시된다.
# jdk 세팅
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
# gradle 캐싱
- 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-
### CI
#gradlew 권한 추가
- name: Grant Execute Permission For Gradlew
run: chmod +x gradlew
#test를 제외한 프로젝트 빌드
- name: Build With Gradle
run: ./gradlew build -x test
#test를 위한 mysql설정
- name: Start MySQL
uses: samin/mysql-action@v1.3
with:
host port: 3305
container port: 3305
mysql database: '{database name}'
mysql user: '{database user}'
mysql password: '{database pwd}'
#테스트를 위한 test properties 설정
- name: Make application-test.properties
run: |
cd ./src/test/resources
touch ./application.properties
echo "${{ secrets.PROPERTIES_TEST }}" > ./application.properties
shell: bash
#test코드 빌드
- name: Build With Test
run: ./gradlew test
#테스트 결과 파일 생성
- name: Publish Unit Test Results
uses: EnricoMi/publish-unit-test-result-action@v1
if: ${{ always() }}
with:
files: build/test-results/**/*.xml
CI/CD를 분리해 작성했다.
name: CD
on:
push: #해당 브랜치에 push(merge) 했을 때
branches:
- main
permissions: write-all #테스트 결과 작성을 위해 쓰기권한 추가
jobs:
build:
runs-on: ubuntu-latest
steps:
#jdk 세팅
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
#gradle 캐싱
- 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-
### CD
#배포를 위한 prod properties 설정
- name: Make application-prod.properties
run: |
cd ./src/main/resources
touch ./application-prod.properties
echo "${{ secrets.PROPERTIES_PROD }}" > ./application-prod.properties
shell: bash
#test를 제외한 프로젝트 빌드
- name: Build With Gradle
run: ./gradlew build -x test
#도커 빌드 & 이미지 push
- name: Docker build & Push
run: |
docker login -u ${{ secrets.DOCKER_ID }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -f Dockerfile -t ${{ secrets.DOCKER_REPO }}/goorm .
docker push ${{ secrets.DOCKER_REPO }}/goorm
#docker-compose 파일을 ec2 서버에 배포
- name: Deploy to Prod
uses: appleboy/ssh-action@master
id: deploy-prod
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
envs: GITHUB_SHA
script: |
docker stop goorm
docker rm goorm
sudo docker pull ${{ secrets.DOCKER_REPO }}/goorm
docker run -d --name goorm -p 8080:8080 ${{ secrets.DOCKER_REPO }}/goorm
docker rmi -f $(docker images -f "dangling=true" -q)
workflow 내의 ${{ ~~ }}
는 외부에 공개되어서는 안되는 민감 정보를 시크릿으로 저장해 놓은 것이다.
레포지토리 Settings > Secrets > Actions 에서 등록해주면 된다.
이처럼 Github Actions를 이용해 docker에 서버를 올리고, 배포를 자동화하였다.
안녕하세요 포스팅 감사히 잘 읽었습니다.
질문이 있어 댓글을 남깁니다.
질문1.
RDS 연동을 했음에도 불구하고 docker-compose.yml 에 MySQL 컨테이너 생성을 위한 코드를 작성하신 이유가 궁금합니다.
질문2.
docker-compose.yml 작성하실 때 MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD 값들은 RDS 엔드포인트, 마스터 사용자 이름, 마스터 암호와는 아무 관련이 없는건가요? MySQL 컨테이너에서 데이터베이스에 접속하기 위한 용도일 뿐인 것일까요?
질문3.
배포가 될 때마다 docker-compose 로 앱과 데이터베이스 컨테이너가 계속 새롭게 RUN 된다면 데이터베이스가 날아가는건가요?
CD Workflow에서
#도커 빌드 & 이미지 push 부분에
goorm 은 도커 아이디를 넣어주면 되나요?