이번에 github actions와 docker를 활용해 자동 배포 환경을 구축해보았는데 그 과정을 기록으로 남기려 합니다.
이전 프로젝트에서는 github actions를 통해 jar 파일을 빌드해 S3에 업로드하고 AWS Code Deploy를 통해 EC2에 배포하는 작업을 수행했습니다.
그런데 일반적인 웹 서비스는 WAS를 다중화하고 Load Balancing을 통해 HA를 확보할 것입니다. 이렇게 다중화한 매 EC2 서버의 환경을 맞추고, Code Deploy를 설정해주고 배포하는 것은 여간 귀찮은 작업이 아닐 수 없습니다.
또한 앞으로의 확장된 서버의 OS 환경이 항상 동일할 것이라는 보장이 없습니다.
이를 위해 컨테이너 기술인 Docker를 사용해보려 합니다. Docker를 사용하면 애플리케이션이 구동될 수 있는 환경
을 미리 구축해두고 호스트 머신의 환경과 무관하게 애플리케이션을 항상 동일한 환경에서 구동할 수 있게 됩니다. 또한 애플리케이션이 구동될 수 있는 환경
을 이미지로 빌드해 도커 registry 에 등록한 후 서버를 늘릴 때 해당 이미지를 pull 받아 손쉽게 scale-out을 할 수 있는 장점이 있습니다.
Dockerfile을 통해 docker image
를 만들 수 있습니다. 어떤 docker base image
에 파일을 작성하고 컨테이너를 실행시키는 과정으로 이미지를 빌드할 수 있습니다. Dockerfile은 결국 이 과정을 담는 스크립트의 파일명이 됩니다.
먼저 작성한 Dockerfile은 다음과 같습니다.
# server base image - java 11
FROM adoptopenjdk/openjdk11
# copy .jar file to docker
COPY ./build/libs/novelpark-0.0.1-SNAPSHOT.jar app.jar
# always do command
ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-jar", "app.jar"]
주요한 문법 몇 가지를 알아보겠습니다.
FROM 절을 수행할 때 마치 여러 조각을 쌓는 것처럼 보이는데 이를 레이어(Layer)라고 합니다.
FROM을 이해하기 위해서는 먼저 컨테이너의 레이어구조에 대해서 아는 것이 좋습니다. 우리가 여러 컨테이너를 실행시키고 싶을 때는 어떻게 해야할까요?
docker 컨테이너가 실행되면 모든 읽기 전용 레이어들을 순서대로 쌓은 후 쓰기 가능한 신규 레이어를 쌓게 됩니다. 그 다음 컨테이너 안에서 발생한 결과물들이 쓰기 가능 레이어를 기록되게 합니다.
즉, 아무리 많은 도커 컨테이너를 실행해도 기존 읽기 전용 레이어는 변하지 않고 컨테이너마다 생성된 쓰기 가능 레이어에 데이터가 쌓여 서로 겹치지 않게 됩니다.
FROM 명령어는 이 Base image를 지정하기 위한 명령어 입니다.
컨테이너에서 커맨드를 실행하기 위해 사용됩니다.
RUN echo "HELLO WORLD!"
Dockerfile에서 고정된 값을 사용하고 싶은 경우가 있을 수 있는데, 이때 사용할 수 있습니다.
ENV KEY VALUE
ENV KEY=VALUE
파일 혹은 디렉토리를 추가할 수 있는 명령어입니다.
호스트 컴퓨터의 파일을 도커 컨테이너 내부의 파일 시스템으로 복사하기 위해 사용되는 명령어 입니다.
작업 디렉토리의 전환을 위해 사용되는 명령어 입니다. 이 명령어를 사용하면 이후 사용되는 모든 명령어는 지정한 디렉토리에서 수행되게 됩니다.
이미지가 실행되었을 때 항상 실행되어야 하는 커맨드를 정의할 때 사용됩니다. 컨테이너를 실행파일처럼 사용할 수 있을 때 유용합니다.
# shell
ENTRYPOINT java -jar app.jar
# exec
ENTRYPOINT ["java", "-jar", "app.jar"]
ENTRYPOINT와 유사하게 생성된 이미지를 바탕으로 컨테이너 내부에서 수행될 작업이나 명령을 실행합니다. 주의할 점은 Dockerfile에 하나의 CMD 명령만 기술 가능하기 때문에 여러 개를 기록하게 된다면 마지막 명령만 유효합니다.
아래와 같이 shell 형식과 exec형식을 지원합니다.
# shell
CMD java -jar app.jar
# exec
CMD ["java", "-jar", "app.jar"]
# server base image - java 11
FROM adoptopenjdk/openjdk11
# copy .jar file to docker
COPY ./build/libs/novelpark-0.0.1-SNAPSHOT.jar app.jar
# always do command
ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-jar", "app.jar"]
다시 Dockerfile로 돌아와서 각 명령어를 보겠습니다.
adoptopenjdk/openjdk11
를 사용했습니다.name: CI/CD
on:
push:
pull_request:
branches:
- main
jobs:
backend-deploy:
runs-on: ubuntu-latest
steps:
# SOURCE 단계 - 저장소 Checkout
- name: Checkout-source code
uses: actions/checkout@v3
# Gradle 실행권한 부여
- name: Grant execute permission to gradlew
run: chmod +x ./gradlew
# Spring boot application 빌드
- name: Build with gradle
run: ./gradlew clean build
# docker image 빌드
- name: Build docker image
run: docker build -t <docker_hub_username>/<docker_image_name> .
# docker hub 로그인
- name: Login docker hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# docker hub 퍼블리시
- name: Publish to docker hub
run: docker push <docker_hub_username>/<docker_image_name>
# WAS 인스턴스 접속 & 애플리케이션 실행
- name: Connect to WAS & Execute Application
uses: appleboy/ssh-action@v0.1.6
with:
host: ${{ secrets.WAS_HOST }}
username: ${{ secrets.WAS_USERNAME }}
key: ${{ secrets.SSH_KEY }}
port: ${{ secrets.WAS_SSH_PORT }}
script: |
docker stop $(docker ps -a -q)
docker rm $(docker ps -a -q)
docker pull <docker_hub_username>/<docker_image_name>
docker run -d -p 8080:8080 --name <container_name> <docker_hub_username>/<docker_image_name>
이후 github actions의 workflow 파일을 작성합니다. 각 내용에 대해 설명하면 다음과 같습니다.
그런데 여전히 문제가 하나 남아있습니다. 프로젝트에 노출되면 안되는 JWT 토큰 값이나 OAuth Secret Key등이 존재합니다. 이런 민감한 파일들이 docker hub에 public하게 올라가게 되면 누군가 악의적으로 Jar파일을 압축해제하여 정보를 추출할 수 있게 됩니다.
이를 해결하는 방법은 다음 포스팅에서 다뤄보도록 하겠습니다.
최종적으로 수행되는 흐름은 다음과 같습니다.
https://woochan-autobiography.tistory.com/468