약 3달전에 2주간 짧은 사이드 프로젝트를 진행했다.
뭘 시도해보면 좋을까 하다가 Spring Security와 CI/CD 구축에 집중적으로 도전해보고 싶었다.
저번 글에서 Spring Security에 대한 기본 활용에 필요한 내용들을 정리했다.
이번에는 또 다른 주제인 CI/CD에 대해 적어보려고 한다.
일단 CI/CD를 구축하는 방법에는 정말 다양한 방법이 있다.
CI/CD를 지원해주는 다양한 툴들이 있지만, 2주라는 짧은 기간의 프로젝트였고, Github로 형상관리를 하고 있다는 점에서 가장 접근하기 좋은 Github Actions를 활용하고자 했다.
서버를 구축하기 위해서는 컴퓨팅 엔진 또한 고민해야 한다.
이 또한 짧은 프로젝트 특성상 구축, 관리가 용이한 클라우드 서비스를 생각했다.
많은 클라우드 서비스가 있지만 가장 익숙하고 짧은 기간 프리티어를 활용할 수 있는 AWS 채택했다.
본래 과거에 진행했던 프로젝트에서는 AWS의 EC2를 직접 수동 생성하고, Nginx를 직접 설정하여 서버를 구축하곤 했다.
하지만 이번에는 AWS의 Elastic BeanStalk 서비스를 통해 서버를 구축해보았다.
전체 프로젝트 구조 및 CI/CD
전체적인 서버 아키텍처는 다음과 같다.
여기서 가장 주의깊게 봐야할 부분은 CI/CD 부분이다.
프로젝트 Master branch에 Merged가 일어나면, Github Actions의 스크립트가 실행되도록 했다.
스크립트 내부적으로 프로젝트 빌드 후 Jib 플러그인을 통해 Docker hub 이미지에 Push 하도록 했다.
이 이미지를 AWS ElasticBeanstalk에서 Pull 받아 EC2 내부에 Docker를 구축하고 Springboot 애플리케이션을 실행시키는 순서이다.
별개로 프론트 서버는 AWS S3와 CloudFront를 활용해서 배포했다.
해당 내용과 관련된 글은 차후 작성해보려고 한다.
AWS ElasticBeanstalk
AWS ElasticBeanstalk는 서버 구축 및 운영에 필요한 다양한 서비스 자원들을 편리하게 이용할 수 있도록 해주는 서비스이다.
본래 서버 구축을 하기 위해서는 EC2, RDS와 같은 서버 구축에 필요한 서비스는 물론, 로깅, 모니터링과 같이 서버 운영에 필요한 서비스 등의 설정을 직접 해줘야 한다.
그러나 ElasticBeanstalk을 활용하면 서버 구축 및 운영에 필요한 서비스들을 간단한 설정을 통해 세팅해주고, 패키지 처럼 묶어서 관리할 수 있게 해주는 서포트 서비스라고 보면 된다.
로드밸런싱, 오토스케일링 등과 같은 설정 등도 제공해주기 때문에 잘 활용하면 개발에 유용하게 쓸 수 있다고 생각한다.
ElasticBeanstalk 서비스 자체에 비용은 따로 청구되지 않는다.
다만 ElasticBeanstalk을 통해 생성된 EC2, RDS 등의 자원들은 해당 자원들의 정책에 따라 요금이 부과된다.
따라서 프리티어로 ElasticBeanstalk을 이용할 유저라면, EC2, RDS 등의 인스턴스가 1개만 생성되도록 ElasticBeanstalk 설정을 해주어야 한다.
필자는 AWS 프리티어를 사용하기 때문에 과금이 되지 않도록 프리티어 과금 정책 내에서 동작하도록 ElasticBeanstalk 설정을 해주었다.
또한 Docker를 활용하여 서버를 빌드, 배포할 예정이기 때문에 애플리케이션 앱 종류를 'Docker'로 설정해주었다.
ElasticBeanstalk은 AWS에서 별도의 서비스 페이지를 제공해주기 때문에 간편하게 설정하고 생성할 수 있다.
구체적인 설정 방법은 프로젝트 마다 상이하고, 양이 많기 때문에 이 글에서 다루진 않을 것이다.
ElasticBeanstalk을 설정하기 전에 유의할 점은 해당 서비스에 대한 IAM 권한이 허용되어 있어야 한다.
권한 설정은 아래 이미지르 참고하길 바란다.
Docker File 설정
본 프로젝트에서 도커를 활용하여 빌드, 배포를 진행할 것이기 때문에 도커 파일이 필수적이다.
도커 파일과 도커 컴포즈 파일은 다음과 같이 세팅하였다.
FROM adoptopenjdk:17-jdk-hotspot
COPY build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]
version: "3.9"
services:
backend:
image: "snowdrop6342/tell-me:latest"
ports:
- "80:8080"
restart: "always"
Jib 플러그인
어떻게 하면 스프링 부트 애플리케이션을 효율적으로 Docker를 통해 배포할 수 있을까 고민하다가 Jib 플러그인에 대해 알게되었다.
Jib은 Spring 애플리케이션을 빌드함과 동시에 Docker 파일로 빌드 및 Dockerhub에 업로드까지 해주는 툴이다.
Maven, Gradle 등의 빌드 툴에서 플러그인을 통해 활용할 수 있다.
빌드, 배포 과정에서 도커 데몬을 이용하여 도커 이미지를 빌드하고, 이를 이용해 배포를 하는 작업이 필요하다.
이 과정을 Jib 플러그인이 아주 간편하게 만들어준다고 보면된다.
심지어 Jib 플러그인을 활용하여 도커 이미지를 빌드하면, 빌드 할 때 변경 사항만 이미지에 반영하기 때문에 자동으로 캐싱 처리까지 해준다고 하니, 쓰지 않을 이유가 없을 것 같다.
Jib 플러그인 설정은 Gradle의 Build.gradle 파일에서 설정할 수 있다.
다음은 필자가 세팅한 Jib 플러그인의 예제 코드이다.
JVM 옵션도 설정할 수 있어서 Spring의 profiles를 설정하는데 활용하였다.
plugins {
id 'com.google.cloud.tools.jib' version '3.2.1'
}
jib {
from {
image = "eclipse-temurin:17-jre-alpine"
}
to {
image = "snowdrop6342/tell-me"
tags = ["latest"]
}
container {
creationTime = "USE_CURRENT_TIMESTAMP"
// JVM 옵션들(실행 옵션 설정)
jvmFlags = ['-Dspring.profiles.active=prod', '-XX:+UseContainerSupport', '-Dfile.encoding=UTF-8']
}
}
Github Actions Workflow 생성
이제 전체적인 애플리케이션 배포 설정은 끝났다.
Github Actions을 통해 CI/CD를 구축하기 위해 Github Actions에서 Workflow 스크립트를 작성해줘야 한다.
필자는 이 과정에서 환경변수 처리에 대한 부분을 크게 고민했다.
여기서 의미하는 환경변수는 각종 Access Key, JWT Secret key 등 외부로 노출되면 안되는 민감 정보들을 의미한다.
이러한 정보들을 직접 프로젝트의 Properties 파일 안에 넣을 순 없다.
이들을 어떻게하면 CI/CD가 이루어지는 과정에서 동적으로 주입해 줄 수 있을까 고민했고, linux echo 명령어를 활용했다.
민감 정보는 Github Secret에 환경변수로 등록 후, CI/CD 스크립트에서 linux echo 명령어로 미리 프로젝트에 만들어둔 env.properties에 옮겨 적는 방식으로 이를 해결했다.
일단 Github Actions에 미리 환경변수들을 등록해둔다.
환경변수 등록은 Github Repository - Settings - Secrets and variables - Actions에서 할 수 있다.
환경변수를 등록하면 다음과 같은 화면이 뜬다.
등록한 환경변수들을 옮겨담을 env.properties를 프로젝트 내에 빈 파일로 생성해둔다.
그리고 해당 파일을 프로젝트의 application.yml 파일에 import 한다.
spring:
profiles:
group:
local: common, local
dev: common, dev
prod: common, prod
config:
import: classpath:/env.properties
이제 모든 준비는 끝났다.
마지막으로 Github Actions 스크립트를 작성해주면 된다.
필자가 작성한 스크립트 문은 다음과 같다.
# This is a basic workflow to help you get started with Actions
name: deploy
on:
push:
branches: [ "master" ] # master branch push가 될 때 CD가 일어나도록 핸들링
env:
DOCKER_HUB_USERNAME: snowdrop6342
AWS_REGION: ap-northeast-2
EB_ENVIRONMENT_NAME: Tell-me-application-env-2
EB_APPLICATION_NAME: tell-me-application
EB_DEPLOYMENT_PACKAGE: ./tellme/docker-compose.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3.3.0
with:
java-version: 17
distribution: 'adopt'
- name: Build Number
id: build-number
run: echo "::set-output name=BUILD_NUMBER::$(date '+%-d.%-m.%Y.%-H.%-M.%-S')"
- name: write JWT_SECRET_KEY
run: echo JWT_SECRET_KEY=${{secrets.JWT_SECRET_KEY}} >> env.properties
working-directory: ./tellme/src/main/resources
- name: write KAKAO_CLIENT_PW
run: echo KAKAO_CLIENT_PW=${{secrets.KAKAO_CLIENT_PW}} >> env.properties
working-directory: ./tellme/src/main/resources
- name: write KAKAO_CLINET_ID
run: echo KAKAO_CLINET_ID=${{secrets.KAKAO_CLINET_ID}} >> env.properties
working-directory: ./tellme/src/main/resources
- name: write KAKAO_REDIRECT_URI
run: echo KAKAO_REDIRECT_URI=${{secrets.KAKAO_REDIRECT_URI}} >> env.properties
working-directory: ./tellme/src/main/resources
- name: write PROD_DATABASE_PASSWORD
run: echo PROD_DATABASE_PASSWORD=${{secrets.PROD_DATABASE_PASSWORD}} >> env.properties
working-directory: ./tellme/src/main/resources
- name: write PROD_DATABASE_URL
run: echo PROD_DATABASE_URL=${{secrets.PROD_DATABASE_URL}} >> env.properties
working-directory: ./tellme/src/main/resources
- name: write PROD_DATABASE_USERNAME
run: echo PROD_DATABASE_USERNAME=${{secrets.PROD_DATABASE_USERNAME}} >> env.properties
working-directory: ./tellme/src/main/resources
- name: Docker Login
uses: docker/login-action@f3364599c6aa293cdc2b8391b1b56d0c30e45c8a
with:
username: ${{ env.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: Push to Dockerhub
working-directory: ./tellme
run: ./gradlew jib
- name: Beanstalk Deploy
uses: einaregilsson/beanstalk-deploy@ebe3476a4ce991d54336935e75e78dd9d86f9408
with:
aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
region: ${{ env.AWS_REGION }}
environment_name: ${{ env.EB_ENVIRONMENT_NAME }}
application_name: ${{ env.EB_APPLICATION_NAME }}
deployment_package: ${{ env.EB_DEPLOYMENT_PACKAGE }}
version_label: ${{ steps.build-number.outputs.BUILD_NUMBER }}
version_description: Version ${{steps.build-number.outputs.BUILD_NUMBER}} deployed via github actions ${{ github.sha }}
wait_for_deployment: 60
'👨💻 개발' 카테고리의 다른 글
Spring Security 요약 정리 (0) | 2023.08.12 |
---|---|
깃(Git), 깃허브(Github) 활용하기 (0) | 2021.12.28 |
스프링과 스프링부트 차이(Spring vs Spring boot) (0) | 2021.12.11 |
웹 애플리케이션 서버(WAS)란? 웹 서버(WS)와의 차이 (0) | 2021.12.11 |
서블릿(Servlet)이란? 서블릿에서 스프링까지 (0) | 2021.12.11 |