Github Actions + CodeDeploy로 Spring Application CI/CD 구축

BaekSeungYun·2023년 8월 3일
0

우아한테크캠프 6기를 진행하며 최종 프로젝트를 위해 CI/CD를 구축하며 정리한 문서입니다.

혹시 간단하게 LoadBalancer를 사용하지 않고 단일 EC2에 배포할 예정이라면 EC2, S3, CodeDeploy 설정, GitHub 설정 항목만 진행하셔도 됩니다.
이 경우 CodeDeploy 설정 - 배포 그룹 생성로드 밸런싱 활성화 옵션을 Off 해주시면 됩니다.

Actions + CodeDeploy

Github Actions는 GitHub에서 제공하는 서비스로, 어떤 event가 발생하면 특정 작업을 실행하거나, 주기적으로 특정 작업을 실행할 수 있습니다.

Github Actions만 사용해서는 AWS 내 private한 인스턴스에 접근할 수 없기 때문에, AWS 내 배포를 지원하는 서비스인 CodeDeploy를 함께 사용해야 배포를 자동화할 수 있습니다.

이번 실습에서는 EC2 인스턴스는 private 서브넷에 두어 외부에서 접근할 수 없게 막고, public 서브넷에 로드밸런서를 두어 private 서브넷의 EC2 인스턴스들에게 요청을 분배하도록 설계하였습니다.

AWS Infra 설정

VPC

VPC는 AWS 내에서 이용 가능한 사설 가상 네트워크입니다.

VPC 서비스로 이동 후, VPC 생성 버튼을 클릭해 다음 절차를 따라 VPC를 생성합니다.

  • 생성할 리소스VPC 등으로 선택해 VPC 생성 시점에 서브넷과 라우팅 테이블, 인터넷 게이트웨이를 한꺼번에 생성합니다.

  • 로드밸런서 생성 시 2개 이상의 AZ(Availability Zone, 가용 영역)를 요구하기 때문에, 가용영역 수2개로 설정하겠습니다.

    NAT 게이트웨이는 1개 AZ에 위치하도록 하였습니다만, 요금이 많이 나올 수 있다고 합니다. NAT 게이트웨이를 사용하지 않고 NAT Instance를 사용하면 더 적은 비용으로 할 수 있다고 하니, 참고하시고 비용이 부담되신다면 NAT 인스턴스에 대해 검색하고 사용하시면 되겠습니다.

  • 퍼블릭 서브넷 수프라이빗 서브넷 수 모두 2로 설정하였습니다.

  • VPC 엔드포인트S3 게이트웨이로 설정해 CodeDeploy로 배포 시 S3에 접근할 수 있도록 했습니다.

생성 후 결과는 다음과 같습니다.

S3

애플리케이션 코드와 빌드 파일을 업로드하기 위한 S3를 준비해 줍니다.
S3-버킷 만들기 메뉴에서 버킷 이름과 리전을 설정하고, 나머지 설정은 기본값으로 생성합니다.

EC2

생성

다음 설정으로 Spring Application을 배포할 EC2 인스턴스를 생성해 줍니다. 실습을 위해 프리티어 인스턴스로 진행하였으며, 실제 배포 상황에 맞게 설정해 주시면 됩니다.

  • t2.micro Free tier
  • Ubuntu 20.04 LTS(x86 64bit)
  • 20GB Storage
  • 네트워크 설정에서 VPC를 위에서 생성한 VPC로 설정
  • 서브넷은 private subnet 중 하나로 설정
  • 퍼블릭 IP 자동할당 비활성화
  • 이 외 설정은 기본으로 설정

IAM 권한 설정

IAM(Identity and Access Management)는 AWS 리소스에 대한 엑세스를 안전하게 제어할 수 있도록 계정, 권한을 설정하는 서비스입니다.

EC2에서 CodeDeploy 및, S3 저장소에 접근할 수 있도록 Role을 부여해 주어야 합니다.

  • IAM 계정 우측 상단 닉네임 드롭다운 버튼을 클릭해 보안 자격 증명 버튼을 클릭합니다.
  • 엔터티 유형을 AWS 서비스, 사용 사례는 EC2를 선택해 줍니다.
  • 다음 정책을 추가하여 역할을 생성합니다. AmazonEC2RoleforAWSCodeDeploy

생성 결과는 다음과 같습니다.

  • 생성한 인스턴스를 선택해서 작업 - 보안 - IAM 역할 수정을 클릭해 역할 수정 페이지에 접근합니다.

  • 이후 생성한 IAM 역할을 EC2에 부여합니다.

  • 역할을 부여한 이후에는 재부팅을 해 주어야 하기 때문에, 인스턴스 중지 - 인스턴스 시작을 차례로 실행해 인스턴스를 재부팅해 줍니다.

CodeDeploy Agent 설치

이제 CodeDeploy Agent를 설치할 차례입니다. 다음 절차를 따라 CodeDeploy Agent를 설치합니다.

# apt-get update
sudo apt-get update

# ruby 설치
sudo apt-get install ruby

# wget 설치
sudo apt-get install wget

# wget 이용해 한국 리전 Codedeploy Agent 설치
wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install

# 실행 권한 부여
chmod +x ./install

# agent 설치
sudo ./install auto

이후 다음 명령어들을 이용해 Agent를 관리합니다.

# agent 상태 체크
sudo service codedeploy-agent status

# agent 시작
sudo service codedeploy-agent start

# agent 재시작
sudo service codedeploy-agent restart

Java 설치

Spring Application 실행을 위한 Java를 설치해 줍니다.
Amazon Corretto 11 JDK를 설치해 보겠습니다.

# Corretto Apt 레포지터리를 추가합니다.
wget -O- https://apt.corretto.aws/corretto.key | sudo apt-key add - 
sudo add-apt-repository 'deb https://apt.corretto.aws stable main'

# apt-get update
sudo apt-get update

# jdk 설치
sudo apt-get install -y java-11-amazon-corretto-jdk

이후 Java가 잘 설치되었는지 버전을 체크합니다.

java -version

Target Group

로드밸런서가 요청을 분배할 target instance들의 그룹입니다. 대상 그룹 메뉴에서 create target group 버튼을 클릭해 생성합니다.

  • target typeInstances로 선택해 EC2 인스턴스를 선택할 수 있게 합니다.
  • Protocol, PortHTTP:8080으로 설정하였습니다. Spring에서 설정한 포트 번호에 맞게 설정하면 됩니다.
  • Protocol Version은 상황에 맞게 설정합니다.
  • Health check path는 Spring Application이 항상 200 OKStatus로 응답할 수 있는 엔드포인트를 제공합니다. Spring Actuator를 이용해 health check용 엔드포인트를 만들 수도 있습니다. 생성 이후에도 변경할 수 있습니다.

Load Balancer

EC2 왼쪽 메뉴에서 로드밸런서 메뉴에서 Create load balancer버튼을 클릭해 로드밸런서를 생성합니다. 저는 L7에서 동작하는 Application Load Balancer를 이용하였습니다.

  • 체계는 로드밸런서가 인터넷으로부터 오는 리퀘스트를 처리할지, 아니면 내부 네트워크에서 private하게 사용할지 선택하는 옵션입니다. 저는 인터넷 경계으로 설정하여 인터넷에서 오는 요청을 처리할 수 있도록 했습니다.
  • IP 주소 유형IPv4로 설정하였습니다.
  • VPC는 설정한 VPC를 이용합니다.

  • 네트워크 매핑은 로드밸런서가 트래픽을 보내줄 서브넷을 설정하는 옵션입니다. 반드시 2개 이상의 서로다른 Availability Zone과, 각 AZ당 하나 이상의 Subnet을 설정해야 합니다. 각 AZ의 Public Subnet을 설정해 두었습니다.

  • 리스너 및 라우팅에서는 HTTP:80을 위에서 생성한 타겟 그룹에 매핑했습니다.
  • HTTPS 프로토콜을 받기 위해서는 SSL/TLS 인증서를 설정해야합니다.

CodeDeploy 설정

IAM 역할 생성

CodeDeploy에서 배포를 위해 필요한 Role을 부여해 주어야 합니다.
보안 자격 증명 - 역할 메뉴에서 역할 만들기 버튼을 클릭해 역할을 생성합니다.

  • 신뢰할 수 있는 엔터티 유형을 AWS 서비스로 설정합니다.
  • 사용 사례를 CodeDeploy로 설정합니다.
  • 이후 다른 설정은 그대로 두고, 역할 이름을 설정하고 설정을 마무리합니다.

애플리케이션 생성

CodeDeploy는 Application 단위로 서비스를 배포하고 관리합니다.
CodeDeploy 서비스에서 애플리케이션 생성 버튼을 클릭해 애플리케이션을 생성합니다.

  • 애플리케이션 이름을 설정합니다. 이 설정값은 이후 Actions 스크립트 작성 시 사용하게 됩니다.
  • 컴퓨팅 플랫폼은 EC2/온프레미스를 선택합니다.

배포 그룹 생성

애플리케이션을 선택하고 배포 그룹 생성 버튼을 클릭해 배포 대상 그룹을 생성합니다.

  • 배포 그룹 이름을 설정합니다. 배포 그룹 이름은 추후 Actions스크립트 작성 시 사용됩니다.
  • 서비스 역할은 위에서 생성한 CodeDeploy용 역할을 선택합니다.

  • 배포 유형을 설정합니다. Autoscaling을 사용하지 않고, 인스턴스가 하나이기 때문에 무중단 배포를 포기하고 현재 위치를 사용하겠습니다.
    - 현재 위치 옵션을 사용하면 현재 동작 중인 EC2 인스턴스를 잠시 멈추고 업데이트를 진행합니다.
    - 블루/그린 옵션은 blue/green 방식으로 무중단 배포를 할 수 있는 옵션입니다. AutoScaling을 사용하거나, green fleet에 미리 EC2를 설정해 두면 사용할 수 있습니다.
  • 태그 그룹은 EC2에 설정된 키/값을 설정해 줍니다. 태그가 일치하는 모든 인스턴스에 배포를 시도합니다.

  • 배포 설정 옵션은 CodeDeployDefault.AllAtOnce로 설정했습니다.
    - AllAtOnce : 한번에 모든 인스턴스들에 배포를 시도합니다.
    - HalfAtATime : 50%의 인스턴스는 항상 동작 중인 상태로 조금씩 배포합니다.
    - OneAtATime : 한번에 하나씩의 인스턴스들에게 배포합니다.
  • 로드 밸런싱 활성화 옵션을 켜 배포 중인 인스턴스에게 트래픽이 전달되지 않도록 합니다.
  • 대상 그룹 선택 옵션에서 로드밸런서 대상 그룹을 설정합니다.

GitHub 설정

IAM for Actions

GitHub Actions에서 Codedeploy 배포 프로세스를 invoke하기 위해 AWS에 접근할 수 있는 계정을 설정해 주겠습니다.

루트 계정의 액세스 키를 사용해도 동작하지만, 보안상 새로운 계정을 생성하고, 해당 계정의 액세스 키를 사용하겠습니다.

  • 우측 상단 닉네임 드롭다운 버튼을 클릭해 보안 자격 증명 버튼을 클릭합니다.
  • 좌측 액세스 관리 메뉴에서 사용자 메뉴에 접근해 사용자 추가 버튼을 클릭해 사용자 생성을 시작합니다.
  • 사용자 이름을 입력합니다
  • 권한 설정 단계에서 직접 정책 연결을 선택하고 다음 두 정책을 검색해서 추가합니다.
    - AmazonS3FullAccess
    - AWSCodeDeployFullAccess

생성 결과는 다음과 같습니다.

  • 이후 생성이 완료된 사용자의 보안 자격 증명탭에서 액세스 키를 추가합니다. 생성된 액세스 키는 조심히 보관해 주세요.(한번 발급받으면 다시 참조할 수 없습니다)

생성된 key는 repository의 Settings - Secrets and variables - Actions 에서 New repository secret버튼을 눌러 각 secret key들을 생성해 줍니다. 저는 아래와 같은 이름으로 생성해 주었습니다.

생성한 secret key는 이후 Actions yml 스크립트 작성 시 사용될 예정입니다.

yml 스크립트 작성

.github/workflows/ 디렉터리 하위에 .yml파일을 위치하면 Actions가 스크립트를 인식하여 스크립트에서 설정된 이벤트에 대해 job을 실행합니다.

deploy workflow

아래 스크립트는 develop 브랜치에 push 이벤트가 발생하면 배포 과정을 실행하는 스크립트입니다.
env 항목들을 적절히 수정하여 사용하면 됩니다.

# .github/workflows/develop-deploy.yml

name: Java CD with Gradle for Develop

on:
  push:
    branches: [ "develop" ]

env:
  AWS_REGION: <AWS 리전>
  S3_BUCKET_NAME: <S3 버킷 이름>
  CODE_DEPLOY_APPLICATION_NAME: <배포 애플리케이션 이름>
  CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: <배포 그룹 이름>

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'corretto'

      - name: Build with Gradle
        uses: gradle/gradle-build-action@v2
        with:
          arguments: clean build -x test

      - 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 }}

      - name: Upload to AWS S3
        run: |
          aws deploy push \
            --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
            --ignore-hidden-files \
            --s3-location s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip \
            --source .

      - name: Deploy to AWS EC2 from S3
        run: |
          aws deploy create-deployment \
            --application-name ${{ env.CODE_DEPLOY_APPLICATION_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
  • actions/checkout@v3 : Actions 환경에 코드를 가져오는 기능을 제공합니다.
  • JDK를 17 Corretto로 설정하였습니다. version, distribution은 프로젝트 상황에 맞게 설정해 주시면 됩니다.
  • gradle/gradle-build-action@v2 : gradle을 이용해 빌드하는 기능을 제공합니다. -x test옵션을 주어 항상 테스트를 실행하도록 설정했습니다.
  • Actions Secrets에 등록한 ACCESS KEY를 이용해 AWS credentials를 설정합니다.
  • 이후 S3에 빌드된 프로젝트 폴더 전체를 업로드합니다.
  • 업로드가 완료되면 CodeDeploy에 다음 과정을 위임합니다.

start.sh, stop.sh

CodeDeploy가 S3에서 코드를 EC2로 옮긴 후, Java Spring Application을 실행, 중단하기 위한 스크립트가 필요합니다.
저는 프로젝트 루트에 /script 디렉터리를 만들고 start.sh, stop.sh를 작성하였습니다.

start.sh

#!/usr/bin/env bash

PROJECT_ROOT="/home/ubuntu/app"
JAR_FILE="$PROJECT_ROOT/build/libs/<프로젝트 jar 파일 이름>.jar"

APP_LOG="$PROJECT_ROOT/application.log"
ERROR_LOG="$PROJECT_ROOT/error.log"
DEPLOY_LOG="$PROJECT_ROOT/deploy.log"

TIME_NOW=$(date +%c)

echo "[ $TIME_NOW ] Copy file $JAR_FILE to project root" >> $DEPLOY_LOG
cp $PROJECT_ROOT/build/libs/*.jar $JAR_FILE

echo "[ $TIME_NOW ] Run java application : $JAR_FILE" >> $DEPLOY_LOG
nohup java -jar $JAR_FILE > $APP_LOG 2> $ERROR_LOG &

CURRENT_PID=$(pgrep -f $JAR_FILE)
echo "[ $TIME_NOW ] Application running PID : $CURRENT_PID" >> $DEPLOY_LOG

stop.sh

#!/usr/bin/env bash

PROJECT_ROOT="/home/ubuntu/app"
JAR_FILE="$PROJECT_ROOT/build/libs/<프로젝트 jar 파일 이름>.jar"

DEPLOY_LOG="$PROJECT_ROOT/deploy.log"

TIME_NOW=$(date +%c)

CURRENT_PID=$(pgrep -f $JAR_FILE)

if [ -z $CURRENT_PID ]; then
  echo "[ $TIME_NOW ] Running application not found." >> $DEPLOY_LOG
else
  echo "$[ TIME_NOW ] Terminate application PID : $CURRENT_PID" >> $DEPLOY_LOG
  kill -15 $CURRENT_PID
fi

appspec.yml

프로젝트 루트에 위에서 작성한 start.sh, stop.sh를 어느 시점에 실행할지에 대한 정보를 담은 appspec.yml 파일을 작성해야 합니다.

version: 0.0
os: linux

files:
  - source:  /
    destination: /home/ubuntu/app
    overwrite: yes

permissions:
  - object: /
    pattern: "**"
    owner: ubuntu
    group: ubuntu

hooks:
  AfterInstall:
    - location: scripts/stop.sh
      timeout: 60
      runas: ubuntu
  ApplicationStart:
    - location: scripts/start.sh
      timeout: 60
      runas: ubuntu

마무리

배포 성공

이제 모든 설정이 끝났습니다. 길고 복잡한 과정을 따라오시느라 고생 많으셨습니다 ㅎㅎ. GitHub에 푸시해서 배포가 정상적으로 되는지 확인해 봅시다!

TroubleShooting

Q. AllowTraffic에서 배포가 더이상 진행되지 않습니다.

A. 이 경우 대상 그룹(Target Group) 메뉴에서 Health Check가 정상적으로 되는지 확인해 봅시다. Health Check 설정을 다시 확인해 보고, 스프링 서버에서 Health Check Path에 대해 올바른 응답코드로 응답하는지 확인해 봅시다. Spring Actuator를 이용하면 Health Check용 엔드포인트를 쉽게 만들 수 있습니다.

Q. BlockTraffic이 너무 느려요.

A. 대상 그룹에서 배포에 사용하는 로드밸런서 Target Group을 선택하고, 속성 탭에서 대상 등록 해제 관리 옵션을 낮추면 BlockTraffic 단계에서 대상 등록 해제 관리(Deregistration Delay)를 짧게 설정해 줍니다. 이 옵션은 트래픽을 차단한 후에도 연결이 남아있는 경우, 연결이 자연스럽게 끊기도록 기다려주는 Delay입니다. 이 시간을 조절하면 BlockTraffic 단계의 시간을 빠르게 넘어갈 수 있습니다. 하지만 너무 짧게 설정하면 남은 커넥션이 강제로 종료되니 적절한 값을 찾아 튜닝하면 좋을 것 같습니다.

Q. AllowTraffic이 너무 느려요.

A. Health Check가 오래 걸리기 때문입니다. 대상이 Healthy한지 검증할 때 다음 옵션이 적용된 상태라면, 30초 간격의 5번의 Health Check를 성공해야하기 때문에, 150초 이상이 걸리는 것을 알 수 있습니다. 정상 임계값(Healthy threshhold)와 간격(interval)를 적절히 튜닝하여 속도를 개선할 수 있습니다.

Q. GitHub에서는 문제가 없는데, Pending에서 배포가 멈춥니다.

A. EC2에 IAM권한 설정을 나중에 한 경우, codedeploy agent가 권한이 없는 상태로 동작하고 있을 수도 있습니다. EC2인스턴스를 종료하고 다시 시작한 후, codedeploy agent도 restart 해 보세요!

profile
우테캠 6기

3개의 댓글

comment-user-thumbnail
2023년 8월 3일

좋은 글이네요. 공유해주셔서 감사합니다.

답글 달기
comment-user-thumbnail
2023년 8월 4일

우윳빛깔 백.승.윤

답글 달기
comment-user-thumbnail
2023년 9월 13일

백승윤, 그는 신인가? 백승윤, 그는 신인가? 백승윤, 그는 신인가? 백승윤, 그는 신인가? 백승윤, 그는 신인가? 백승윤, 그는 신인가? 백승윤, 그는 신인가? 백승윤, 그는 신인가? 백승윤, 그는 신인가? 백승윤, 그는 신인가? 백승윤, 그는 신인가? 백승윤, 그는 신인가? 백승윤, 그는 신인가? 백승윤, 그는 신인가? 백승윤, 그는 신인가? 백승윤, 그는 신인가? 백승윤, 그는 신인가? 백승윤, 그는 신인가? 백승윤, 그는 신인가? 백승윤, 그는 신인가?

답글 달기