많은 조직에서는 소프트웨어 개발 및 인프라 관리 프로세스를 자동화하고 간소화하여 더 빠르게 혁신하기 위해 DevOps 방식을 채택한다. 데브옵스는 문화적 채택 외에도 특정 모범 사례를 따를 것을 제안하며, CI/CD(지속적 통합 및 지속적 전달)는 시작해야할 중요한 요소이다. CI/CD 작업은 배포 작업을 자동화하여 새로운 소프트웨어 업데이트를 리리즈하는데 걸리는 시간을 단축한다. 많은 툴들이 이 방법을 구현하는데 사용될 수 있다. AWS에는 CI/CD 목표를 달성하는 데 도움이 되는 기본 도구들이 있지만, 수많은 타사의 서드 파티와 통합할 수 있는 유연성과 확장성 또한 제공한다.
여기서는 Github Actions를 사용하여 CI/CD 워크플로를 생성하고 AWS CodeDeploy를 사용하여 EC2 인스턴스에 자바 SpringBoot 애플리케이션을 배포할 것이다.
DevOps는 Development Operations의 약어로, 소프트웨어 개발과 운영을 통합하여 효율성, 협력, 속도, 안정성을 개선하는 개발 및 운영 방법론이다. 전통적으로 소프트웨어 개발팀과 IT 운영팀은 분리되어 각자의 역할을 수행했지만, DevOps는 이러한 경계를 허물고 개발팀과 운영팀 사이의 협력과 커뮤니케이션을 강화한다.
DevOps는 소프트웨어 개발부터 배포, 운영, 모니터링까지의 전체 생명주기를 관리하며, 개발과 운영 간의 협업을 강화하여 릴리즈 주기를 단축하고 문제를 신속히 해결할 수 있도록 돕는다. 이를 통해 조직은 고객에게 더 빠르고 안정적인 제품 및 서비스를 제공할 수 있다.
CI/CD (Continuous Integration/Continuous Delivery)는 애플리케이션 개발 단계를 자동화하여 애플리케이션을 더욱 짧은 주기로 고객에게 제공하는 방법이다. CI/CD의 기본 개념은 지속적인 통합, 지속적인 서비스 제공, 지속적인 배포다. CI/CD는 새로운 코드 통합으로 인해 개발 및 운영팀에 발생하는 문제(일명 "통합 지옥(integration hell)")를 해결하기 위한 솔루션이다.
특히, CI/CD는 애플리케이션의 통합 및 테스트 단계에서부터 제공 및 배포에 이르는 애플리케이션의 라이프사이클 전체에 걸쳐 지속적인 자동화와 지속적인 모니터링을 제공한다. 이러한 구축 사례를 일반적으로 “CI/CD 파이프라인”이라 부르며, 개발 및 운영팀의 애자일 방식 협력을 통해 DevOps 또는 SRE(사이트 신뢰성 엔지니어링) 방식으로 지원된다.
https://docs.github.com/ko/actions/learn-github-actions/understanding-github-actions
Github Actions는 빌드, 테스트 및 배포 파이프라인을 자동화할 수 있는 CI/CD(지속적 통합 및 지속적 배포) 플랫폼이다. 리포지토리에 대한 모든 pull request를 빌드 및 테스트하거나 merged pull request를 프로덕션에 배포하는 워크플로를 생성할 수 있다.
GitHub Actions는 DevOps 이상의 기능을 제공하며 저장소에서 특정 이벤트가 발생할 때 정해진 워크플로를 실행할 수 있게 해준다. 예를 들어, 누군가 저장소에 새 이슈를 생성할 때마다 워크플로를 실행하여 적절한 레이블을 자동으로 추가할 수 있다.
GitHub에서는 워크플로를 실행하기 위한 Linux, Windows 및 macOS 가상 머신을 제공하며, 자체 데이터 센터 또는 클라우드 인프라에서 자체 호스팅 실행기를 호스팅할 수도 있다.
CodeDeploy에서 어떤 인스턴스에서 수행할지 구분하는 값으로 태그를 사용하기 때문에 추가해준다. 기존의 태그가 이미 있다면 이 과정은 생략해도 된다.
EC2 인스턴스에서 S3에 업로드되어 있는 파일에 접근하기 위해서는 권한이 필요하다. IAM 역할을 추가하고 AmazonS3FullAccess
권한을 할당한다.
위에서 추가한 IAM 역할을 EC2 인스턴스에 연결한다.
sudo yum update
# 루비 설치(Amazon Linux 기준)
sudo yum install ruby -y
sudo gem update --system
# CodeDeploy Agent 설치
sudo yum install wget
wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto > /tmp/logfile
# CodeDeploy Agent 설치 확인
sudo service codedeploy-agent status
EC2 인스턴스에서 CodeDeploy가 실행될 수 있도록 해주는 소프트웨어 패키지인 CodeDeploy Agent를 설치한다. 명령어는 인스턴스에 설치된 리눅스 버전에 따라 다를 수 있다. 이 글은 Amazon Linux 기준으로 작성되었다.
설치 완료 후 sudo service codedeploy-agent status
명령어로 소프트웨어가 정상적으로 설치되어 실행되고 있는지 확인할 수 있다. 만약 정상적으로 실행되고 있다면 The AWS CodeDeploy agent is running as PID [PID]
와 같이 떠야한다.
# ---------------------------------
# temurin jdk 17 설치
# ---------------------------------
# temurin-17-jdk rpm 다운로드
wget https://packages.adoptium.net/artifactory/rpm/amazonlinux/2/x86_64/Packages/temurin-17-jdk-17.0.7.0.0.7-1.x86_64.rpm
# rpm 설치
sudo yum localinstall temurin-17-jdk-17.0.7.0.0.7-1.x86_64.rpm
# java 버전 확인
java -version
배포 자동화할 스프링 부트 프로젝트에서 temurin 17 버전의 자바를 사용 중이기 때문에 EC2 인스턴스에도 똑같이 설치해주었다. 각자 사용하는 자바 버전을 설치하면 될 것 같다.
원하는 버킷 이름과 리전을 선택한다.
CodeDeploy에서 사용할 IAM 역할을 추가한다.
CodeDeploy 애플리케이션에서 배포 그룹을 생성한다. EC2 태그와 서비스 역할은 위에서 생성했던 것들로 설정해준다.
Github Actions 워크플로에서 AWS 리소스에 접근하기 위해서는 관련 IAM 사용자가 필요하다. AmazonS3FullAccess
와 AWSCodeDeployFullAccess
권한을 할당한다.
Github Actions에서 사용하기 위한 Access Key와 Secret Key를 발급받는다.
배포를 자동화할 프로젝트의 리포지토리에서 Settings - Security - Secrets and variables - Actions 페이지에 들어간다. 발급받은 키 값들은 New repository secret
으로 추가하면 된다. secret의 키 이름은 임의로 지정해도 상관없으며, 나중에 작성할 스크립트에서 매칭만 잘 시켜주면 된다.
version: 0.0
os: linux
# 배포 파일 설정
## source: 인스턴스에 복사할 디렉터리 경로
## destination: 인스턴스에서 파일이 복사되는 위치
## overwrite: 복사할 위치에 파일이 있는 경우 대체
files:
- source: /
destination: /home/ec2-user/apps/imad-server
overwrite: yes
# files 섹션에서 복사한 파일에 대한 권한 설정
## object: 권한이 지정되는 파일 또는 디렉터리
## pattern (optional): 매칭되는 패턴에만 권한 부여
## owner (optional): object 의 소유자
## group (optional): object 의 그룹 이름
permissions:
- object: /
pattern: "**"
owner: ec2-user
group: ec2-user
# 배포 이후에 실행할 일련의 라이프사이클
# 파일을 설치한 후 `AfterInstall` 에서 기존에 실행중이던 애플리케이션을 종료
# `ApplicationStart` 에서 새로운 애플리케이션을 실행
## location: hooks 에서 실행할 스크립트 위치
## timeout (optional): 스크립트 실행에 허용되는 최대 시간이며, 넘으면 배포 실패로 간주됨
## runas (optional): 스크립트를 실행하는 사용자
hooks:
AfterInstall:
- location: scripts/stop.sh
timeout: 60
runas: ec2-user
ApplicationStart:
- location: scripts/start.sh
timeout: 60
runas: ec2-user
CodeDeploy에서 배포를 위해 참조할 AppSpec 파일을 작성한다. AppSpec 파일을 사용해서 프로젝트의 어떤 파일들을 EC2의 어떤 경로에 복사할지 설정하고, 배포 작업 이후에 수행할 스크립트를 지정하여 자동으로 서버가 실행되게 할 수 있다. AppSpec 파일은 기본적으로 프로젝트의 루트 디렉터리에 위치해야 한다.
appspec.yml
에서 배포 이후에 호출하는 스크립트를 작성한다. 프로젝트의 루트 디렉터리에 scripts
디렉터리를 생성하고 거기에 파일을 추가한다.
#!/usr/bin/env bash
PROJECT_ROOT="/home/ec2-user/apps/spring-practice"
JAR_FILE="$PROJECT_ROOT/spring-webapp.jar"
APP_LOG="$PROJECT_ROOT/application.log"
ERROR_LOG="$PROJECT_ROOT/error.log"
DEPLOY_LOG="$PROJECT_ROOT/deploy.log"
TIME_NOW=$(date +%c)
# build 파일 복사
echo "$TIME_NOW > $JAR_FILE 파일 복사" >> $DEPLOY_LOG
cp $PROJECT_ROOT/build/libs/*.jar $JAR_FILE
# jar 파일 실행
echo "$TIME_NOW > $JAR_FILE 파일 실행" >> $DEPLOY_LOG
nohup java -jar $JAR_FILE > $APP_LOG 2> $ERROR_LOG &
CURRENT_PID=$(pgrep -f $JAR_FILE)
echo "$TIME_NOW > 실행된 프로세스 아이디 $CURRENT_PID 입니다." >> $DEPLOY_LOG
#!/usr/bin/env bash
PROJECT_ROOT="/home/ec2-user/apps/spring-practice"
JAR_FILE="$PROJECT_ROOT/spring-webapp.jar"
DEPLOY_LOG="$PROJECT_ROOT/deploy.log"
TIME_NOW=$(date +%c)
# 현재 구동 중인 애플리케이션 pid 확인
CURRENT_PID=$(pgrep -f $JAR_FILE)
# 프로세스가 켜져 있으면 종료
if [ -z $CURRENT_PID ]; then
echo "$TIME_NOW > 현재 실행중인 애플리케이션이 없습니다" >> $DEPLOY_LOG
else
echo "$TIME_NOW > 실행중인 $CURRENT_PID 애플리케이션 종료 " >> $DEPLOY_LOG
kill -15 $CURRENT_PID
fi
Spring Boot 2.5 버전부터는 빌드 시 일반 jar 파일 하나와 -plain.jar 파일 하나가 함께 만들어진다. 때문에 빌드 시 plain jar 파일은 만들어지지 않도록 build.gradle 파일에 다음 내용을 추가해야 한다.
jar {
enabled = false
}
프로젝트 루트 디렉터리에 .github/workflows
폴더를 만들고 그 안에 gradle.yml 파일을 생성한다. 아니면 Github의 프로젝트 리포지토리 페이지에서 Simple workflow를 생성하여 로컬에서 pull해도 된다.
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle
name: Spring Boot & Gradle CI/CD
# release 브랜치에 push 또는 pull request가 되면 스크립트 실행
on:
push:
branches: [ "release" ]
pull_request:
branches: [ release ]
# 해당 코드에서 사용될 변수 설정
env:
AWS_REGION: ap-northeast-2
PROJECT_NAME: imad-server
S3_BUCKET_NAME: ryuforaws01-github-actions-s3-bucket
CODE_DEPLOY_APP_NAME: spring-boot-codedeploy-app
CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: codedeploy-deployment-group
permissions:
contents: read
jobs:
build:
# Github의 워크플로에서 실행될 OS 선택
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
# create application.yml
- name: make application-aws.yml
if: contains(github.ref, 'release')
run: |
# spring의 resources 경로로 이동
cd ./src/main/resources
ls -al
touch ./application-aws.yml
# GitHub-Actions에서 설정한 값을 application-aws.yml 파일에 쓰기
echo "copy properties"
echo "${{ secrets.AWS_PROPERTIES }}" > ./application-aws.yml
shell: bash
# create key.p8 file
- name: make apple key.p8 file
if: contains(github.ref, 'release')
run: |
# spring의 resources 경로로 이동
mkdir ./src/main/resources/key
cd ./src/main/resources/key
ls -al
touch ./AuthKey.p8
# GitHub-Actions에서 설정한 값 파일에 쓰기
echo "copy APPLE_SECRET_KEY"
echo "${{ secrets.APPLE_SECRET_KEY }}" > ./AuthKey.p8
shell: bash
# gradlew 파일 실행권한 설정
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
shell: bash
# Gradle build (Test 제외)
- name: Build with Gradle
run: ./gradlew clean --stacktrace --info build
shell: bash
# AWS 인증 (IAM 사용자 Access Key, Secret Key 활용)
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
# 빌드 결과물을 S3 버킷에 업로드
- name: Upload to AWS S3
run: |
aws deploy push \
--application-name ${{ env.CODE_DEPLOY_APP_NAME }} \
--ignore-hidden-files \
--s3-location s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip \
--source .
# S3 버킷에 있는 파일을 대상으로 CodeDeploy 실행
- name: Deploy to AWS EC2 from S3
run: |
aws deploy create-deployment \
--application-name ${{ env.CODE_DEPLOY_APP_NAME }} \
--deployment-config-name CodeDeployDefault.AllAtOnce \
--deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
--s3-location bucket=$S3_BUCKET_NAME,key=$GITHUB_SHA.zip,bundleType=zip
현재 사용 중인 배포 스크립트다. 대부분은 주석에 설명되어 있고, 몇 개만 덧붙여서 이야기해보자.
# 해당 코드에서 사용될 변수 설정
env:
AWS_REGION: ap-northeast-2
PROJECT_NAME: imad-server
S3_BUCKET_NAME: ryuforaws01-github-actions-s3-bucket
CODE_DEPLOY_APP_NAME: spring-boot-codedeploy-app
CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: codedeploy-deployment-group
AWS에서 생성했던 S3 버킷, CodeDeploy 애플리케이션 등의 이름과 가용영역(AZ) 등을 지정한다. 이름은 정확하게 작성해야 한다.
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
Github Actinos에서는 이미 다른 사람이 작성해둔 스크립트를 불러와서 실행시킬 수 있다. Actions 팀이 공식적으로 배포하고 있는 스크립트부터, 개인이 작성한 것들도 있다. 필요한 것이 있다면 여기서 검색해보고 사용하면 좋을 것 같다.
# create application.yml
- name: make application-aws.yml
if: contains(github.ref, 'release')
run: |
# spring의 resources 경로로 이동
cd ./src/main/resources
ls -al
touch ./application-aws.yml
# GitHub-Actions에서 설정한 값을 application-aws.yml 파일에 쓰기
echo "copy properties"
echo "${{ secrets.AWS_PROPERTIES }}" > ./application-aws.yml
shell: bash
# create key.p8 file
- name: make apple key.p8 file
if: contains(github.ref, 'release')
run: |
# spring의 resources 경로로 이동
mkdir ./src/main/resources/key
cd ./src/main/resources/key
ls -al
touch ./AuthKey.p8
# GitHub-Actions에서 설정한 값 파일에 쓰기
echo "copy APPLE_SECRET_KEY"
echo "${{ secrets.APPLE_SECRET_KEY }}" > ./AuthKey.p8
shell: bash
프로젝트 폴더에는 외부에 노출되면 안 되는 API 키나 암호 파일 등의 파일들이 있고 보통 .gitignore
파일에 등록하여 관리할 것이다. 배포 또는 수정이 될 때마다 수동으로 업로드하게 되면 이렇게 자동화하는 의미가 없기 때문에 이 또한 스크립트에 추가해서 사용하고 있다.
먼저 보안이 필요한 파일의 내용을 Github의 secrets에 추가한다. 그리고 workflow 스크립트에서 불러와 EC2 인스턴스의 프로젝트 파일에 복사하면 배포 때마다 자동으로 갱신되게 할 수 있다.
나는 현재 진행 중인 프로젝트에서 브랜치를 release
, master
, feature-...
, bug-...
등으로 나누어 관리하고 있다. 로컬에서 테스트를 진행 후 문제가 없다면 master
브랜치로 병합을 하고, 배포를 하려면 release
브랜치에 pull request를 생성하여 merge한다. 이렇게 해주면 Github Actions에서 이를 감지하고 조건에 해당하는 스크립트를 실행한다.
merge를 하고 나면 이렇게 workflow가 실행되는 것을 확인할 수 있다.
AWS 콘솔의 CodeDeploy - 배포 탭에서도 CodeDeploy가 정상적으로 수행되었는지, 만약 실패했다면 원인이 무엇인지 파악할 수 있다.
ps -ef | grep java
EC2 인스턴스에서도 프로젝트 파일이 잘 업로드 됐고, 백그라운드에서 스프링 부트 서버가 실행되고 있다.