Github Actions + AWS EC2 Spring Boot 배포하기 위한 CI/CD 파이프라인 설정

NCOOKIE·2023년 12월 4일
0
post-thumbnail

개요

많은 조직에서는 소프트웨어 개발 및 인프라 관리 프로세스를 자동화하고 간소화하여 더 빠르게 혁신하기 위해 DevOps 방식을 채택한다. 데브옵스는 문화적 채택 외에도 특정 모범 사례를 따를 것을 제안하며, CI/CD(지속적 통합 및 지속적 전달)는 시작해야할 중요한 요소이다. CI/CD 작업은 배포 작업을 자동화하여 새로운 소프트웨어 업데이트를 리리즈하는데 걸리는 시간을 단축한다. 많은 툴들이 이 방법을 구현하는데 사용될 수 있다. AWS에는 CI/CD 목표를 달성하는 데 도움이 되는 기본 도구들이 있지만, 수많은 타사의 서드 파티와 통합할 수 있는 유연성과 확장성 또한 제공한다.

여기서는 Github Actions를 사용하여 CI/CD 워크플로를 생성하고 AWS CodeDeploy를 사용하여 EC2 인스턴스에 자바 SpringBoot 애플리케이션을 배포할 것이다.

DevOps

https://www.netapp.com/ko/devops-solutions/what-is-devops/

DevOps는 Development Operations의 약어로, 소프트웨어 개발과 운영을 통합하여 효율성, 협력, 속도, 안정성을 개선하는 개발 및 운영 방법론이다. 전통적으로 소프트웨어 개발팀과 IT 운영팀은 분리되어 각자의 역할을 수행했지만, DevOps는 이러한 경계를 허물고 개발팀과 운영팀 사이의 협력과 커뮤니케이션을 강화한다.

DevOps는 소프트웨어 개발부터 배포, 운영, 모니터링까지의 전체 생명주기를 관리하며, 개발과 운영 간의 협업을 강화하여 릴리즈 주기를 단축하고 문제를 신속히 해결할 수 있도록 돕는다. 이를 통해 조직은 고객에게 더 빠르고 안정적인 제품 및 서비스를 제공할 수 있다.

CI/CD

https://www.redhat.com/ko/topics/devops/what-is-ci-cd

CI/CD (Continuous Integration/Continuous Delivery)는 애플리케이션 개발 단계를 자동화하여 애플리케이션을 더욱 짧은 주기로 고객에게 제공하는 방법이다. CI/CD의 기본 개념은 지속적인 통합, 지속적인 서비스 제공, 지속적인 배포다. CI/CD는 새로운 코드 통합으로 인해 개발 및 운영팀에 발생하는 문제(일명 "통합 지옥(integration hell)")를 해결하기 위한 솔루션이다.

특히, CI/CD는 애플리케이션의 통합 및 테스트 단계에서부터 제공 및 배포에 이르는 애플리케이션의 라이프사이클 전체에 걸쳐 지속적인 자동화와 지속적인 모니터링을 제공한다. 이러한 구축 사례를 일반적으로 “CI/CD 파이프라인”이라 부르며, 개발 및 운영팀의 애자일 방식 협력을 통해 DevOps 또는 SRE(사이트 신뢰성 엔지니어링) 방식으로 지원된다.

Github Actions

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 가상 머신을 제공하며, 자체 데이터 센터 또는 클라우드 인프라에서 자체 호스팅 실행기를 호스팅할 수도 있다.

배포방법

서비스

  • GitHub Actions – 파이프라인을 호스팅할 워크플로 조정 도구
  • AWS CodeDeploy – Amazon EC2 AutoScaling 그룹의 배포를 관리하는 AWS 서비스
  • Amazon EC2 – 애플리케이션 배포를 위한 대상 컴퓨팅 서버
  • Amazon Simple Storage Service(Amazon S3) – 배포 아티팩트를 저장하는 Amazon S3

워크플로

  1. 개발자가 로컬 저장소의 코드 변경 사항을 Github 리포지토리에 push한다.
  2. Github Actions 작업은 빌드 단계를 트리거한다.
  3. IAM 사용자의 인증 정보로 AWS에 인증하고 리소스에 액세스한다.
  4. Github Actions는 배포 아티팩트를 S3에 업로드한다.
  5. Github Actions는 CodeDeploy를 호출한다.
  6. CodeDeploy는 EC2 인스턴스에 배포 명령을 내린다. (appspec.yml 참조)
  7. CodeDeploy는 Amazon S3에서 아티팩트를 다운로드하고 Amazon EC2 인스턴스에 배포한다.

배포하기

EC2 관련 설정태그 추가

CodeDeploy에서 어떤 인스턴스에서 수행할지 구분하는 값으로 태그를 사용하기 때문에 추가해준다. 기존의 태그가 이미 있다면 이 과정은 생략해도 된다.

S3용 IAM 역할 추가

EC2 인스턴스에서 S3에 업로드되어 있는 파일에 접근하기 위해서는 권한이 필요하다. IAM 역할을 추가하고 AmazonS3FullAccess 권한을 할당한다.

EC2 인스턴스에 IAM 연결

위에서 추가한 IAM 역할을 EC2 인스턴스에 연결한다.

CodeDeploy Agent 설치

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

https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/codedeploy-agent-operations-install-ubuntu.html

EC2 인스턴스에서 CodeDeploy가 실행될 수 있도록 해주는 소프트웨어 패키지인 CodeDeploy Agent를 설치한다. 명령어는 인스턴스에 설치된 리눅스 버전에 따라 다를 수 있다. 이 글은 Amazon Linux 기준으로 작성되었다.

설치 완료 후 sudo service codedeploy-agent status 명령어로 소프트웨어가 정상적으로 설치되어 실행되고 있는지 확인할 수 있다. 만약 정상적으로 실행되고 있다면 The AWS CodeDeploy agent is running as PID [PID]와 같이 떠야한다.

Java 설치

https://hothoony.tistory.com/1749

# ---------------------------------
# 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 인스턴스에도 똑같이 설치해주었다. 각자 사용하는 자바 버전을 설치하면 될 것 같다.

S3 버킷 생성

원하는 버킷 이름과 리전을 선택한다.

CodeDeploy 설정

IAM 역할 생성

CodeDeploy에서 사용할 IAM 역할을 추가한다.

CodeDeploy 애플리케이션 생성

CodeDeploy 배포 그룹 생성

CodeDeploy 애플리케이션에서 배포 그룹을 생성한다. EC2 태그와 서비스 역할은 위에서 생성했던 것들로 설정해준다.

Github Actions 설정

IAM 사용자 추가

Github Actions 워크플로에서 AWS 리소스에 접근하기 위해서는 관련 IAM 사용자가 필요하다. AmazonS3FullAccessAWSCodeDeployFullAccess 권한을 할당한다.

Access Key 및 Secret Key 발급

Github Actions에서 사용하기 위한 Access Key와 Secret Key를 발급받는다.

Github Secrets 설정

배포를 자동화할 프로젝트의 리포지토리에서 Settings - Security - Secrets and variables - Actions 페이지에 들어간다. 발급받은 키 값들은 New repository secret으로 추가하면 된다. secret의 키 이름은 임의로 지정해도 상관없으며, 나중에 작성할 스크립트에서 매칭만 잘 시켜주면 된다.

프로젝트 파일 추가 및 수정

AppSepc 파일 작성

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 디렉터리를 생성하고 거기에 파일을 추가한다.

start.sh

#!/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

stop.sh

#!/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

build.gradle 수정

Spring Boot 2.5 버전부터는 빌드 시 일반 jar 파일 하나와 -plain.jar 파일 하나가 함께 만들어진다. 때문에 빌드 시 plain jar 파일은 만들어지지 않도록 build.gradle 파일에 다음 내용을 추가해야 한다.

jar {
    enabled = false
}

deploy.yml 파일 작성

프로젝트 루트 디렉터리에 .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

현재 사용 중인 배포 스크립트다. 대부분은 주석에 설명되어 있고, 몇 개만 덧붙여서 이야기해보자.

AWS ENV

# 해당 코드에서 사용될 변수 설정
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) 등을 지정한다. 이름은 정확하게 작성해야 한다.

외부 action 호출

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

https://github.com/marketplace?type=actions

Github Actinos에서는 이미 다른 사람이 작성해둔 스크립트를 불러와서 실행시킬 수 있다. Actions 팀이 공식적으로 배포하고 있는 스크립트부터, 개인이 작성한 것들도 있다. 필요한 것이 있다면 여기서 검색해보고 사용하면 좋을 것 같다.

.gitignore에 있는 파일 배포

      # 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 인스턴스에서도 프로젝트 파일이 잘 업로드 됐고, 백그라운드에서 스프링 부트 서버가 실행되고 있다.

참고

profile
일단 해보자

0개의 댓글