[AWS] - Spring Boot 프로젝트 배포(1)

개발 환경

💻 OS : M1 Mac Ventura 13.1

🍃 Spring : Spring Boot 2.7.7

🛠️ Java : Amazon corretto 17

📦 Stack : AWS : S3, CodeDeploy, EC2(Amazon Linux), RDS

해당 글은 개발 과정을 기록하기 위한 글입니다.
필요한 부분은 본인의 개발 환경 및 상황에 맞게 바꿔서 작성해야 합니다.

CI/CD Flow

스크린샷 2023-02-16 오후 11 56 12

1️⃣ : 배포 브랜치에 Push 될 때, Github Actions 실행

2️⃣ : Actions를 통해 빌드한 후 AWS IAM 인증을 거친 뒤, 전체 파일을 .zip 파일로 묶어 S3에 업로드

3️⃣ : S3에 올라간 파일명을 토대로 CodeDeploy에 배포 요청

4️⃣ : 배포 요청을 받으면 S3에서 파일을 받아와 EC2에 배포

5️⃣ : deploy.sh를 통해 애플리케이션 실행

CI 환경 구축

설명하기 앞서 application*.yml(properties) 파일들은 모두 .gitignore에 등록된 상태입니다.
또한 하나의 actions.yml 파일을 가지고 상황에 맞게 지속적으로 내용을 추가하는 방식으로 설명할 예정이며, 최종본은 가장 마지막에 추가해놓도록 하겠습니다!

Properties 파일 등록

위에서 설명한 것과 같이 설정 파일들은 .gitignore에 등록되어 있어 master 브랜치에 올라가있지 않은 상태다.

때문에 build를 하는 과정에서 에러가 날 수 있고, 최종적으로 배포 되었을 때 실행조차 할 수 없기 때문에, 설정 파일을 Actions Secret에 추가해준다.

image

위 사진과 같이 클릭하면 새로운 secret을 등록할 수 있다.

Name : CI/CD를 위해 사용할 actions.yml에서 사용할 이름(대문자 사용)
Secret : Name에 대한 상세 내용 작성

image

필자는 application.yml에 구동에 필요한 필수 항목을 작성하고, application-prod.yml에는 배포 환경에 필요한 RDS 정보와 같은 것을 작성해놓은 상태기 때문에 따로 구분했다.(+ 추후 개발 서버도 띄우기 위함)

# Actions Secrets
PROPERTIES_MAIN_PROD : appication.yml
PROPERTIES_PROD : application-prod.yml

GitHub Actions

image

배포를 하고자하는 Github Repository의 Actions에 들어가 actions.yml을 작성해준다.

# Actions에서 보여질 workflow 이름
name: CI/CD

# 트리거 지정
# master 브랜치에 push가 일어날 때 실행
on:
  push:
    branches:
      - master

jobs:
  CI-CD:
    runs-on: ubuntu-latest
    steps:

      ## Setting JDK
      ## 본인 spring 버전에 맞는 버전 지정
      ## https://github.com/actions/setup-java
      - uses: actions/checkout@v3
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

      ## Gradle Caching
      - name: Gradle Caching
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: $-gradle-$
          restore-keys: |
            $-gradle-

      ## Create application.yml
      - name: make prod application.yml
        if: contains(github.ref, 'master')
        run: |
          cd ./src/main/resources
          touch ./application.yml
          echo "$" > ./application.yml
        shell: bash

      ## Create application-prod.yml
      - name: make prod application-prod.yml
        if: contains(github.ref, 'master')
        run: |
          cd ./src/main/resources
          touch ./application-prod.yml
          echo "$" > ./application-prod.yml
        shell: bash

      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew
        shell: bash

      ## Gradle Build
      - name: Build with Gradle
        run: ./gradlew build

작성을 다 작성한 뒤에 Start Commit을 통해 master 브랜치에 바로 push 해준다.

이 과정에서 생기는 오류들은 미리 해결해 놓는게 맘 편하다!

변경할 사항 정리

  • Setting JDK
    • 본인 Spring 버전에 맞도록 지정
  • Create *.yml
    • properties 파일은 외부에 공개하면 안 되는 정보를 담고 있다.
    • 때문에 Setting - Secretes - Actions에서 추가해준다.
    • 필요에 따라 사용하지 않으면 지워주고, yml이 아니면 properties로 바꿔주면 된다.

오류 정리

Build With Gradle
Run ./gradlew build
  ./gradlew build
  shell: /usr/bin/bash --noprofile --norc -e -o pipefail {0}
  env:
    JAVA_HOME: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/11.0.18-10/x64
    JAVA_HOME_11_X64: /opt/hostedtoolcache/Java_Temurin-Hotspot_jdk/11.0.18-10/x64
Error: Could not find or load main class org.gradle.wrapper.GradleWrapperMain
Caused by: java.lang.ClassNotFoundException: org.gradle.wrapper.GradleWrapperMain
  • Project 자바 버전과 동일하게 설정했는지 확인
/home/runner/work/.../src/main/java/com/.../app/config/security/CustomAuthenticationFailureHandler 2.java:
13: error: class CustomAuthenticationFailureHandler is public, should be declared in a file named CustomAuthenticationFailureHandler.java
  • 프로젝트에 ClassName 2.java과 같이 다른 클래스 파일과 중복되는 파일이 있는지 확인
* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: 
  • 실패한 테스트가 존재할 경우 위와 같이 뜰 수 있음
  • 혹은 특정 Test에서 사용되는 특정 파일, Properties를 불러올 수 없을 경우 발생 가능

AWS

IAM

AWS는 서비스의 보안을 위해 외부 접근을 기본적으로 차단한다. 때문에 IAM(Identify & Access Management)을 이용해서 외부에서 접근할 수 있는 루트를 만들어준다.

image

상단 검색창을 통해 IAM에 들어가 사용자 탭의 사용자 추가를 눌러준다.

image

image

AWSCodeDeployFullAccess, AmazonS3FullAccess 2가지 정책을 연결해준다.

다른 것은 건들이지 않고 다음을 누른 뒤, 사용자를 생성해준다.

image

이 IAM을 가지고 GitHub에서 증명을 해줘야하기 때문에 Access Key를 발급 받아야 한다.

image

image

설명은 선택이기 때문에 건들이지 않고 진행

image

Access Key, Secret Key는 따로 메모를 해놓고, .csv 파일은 다운을 받아놓자!

Secret 등록

까먹기 전에 위에서 발급 받은 키를 GitHub Secrets에 등록하자!

image
# Actions Secrets
AWS_IAM_ACCESS_KEY : 액세스 키
AWS_IAM_SECRET_KEY : 비밀 액세스 키

역할 추가

IAM을 가지고 S3, CodeDeploy, EC2에 접근을 할 것이기 때문에 각 서비스에 대해 역할을 추가해준다.

EC2 Role

EC2에서는 S3에 있는 파일을 받아와야하기 때문에 아래와 같이 등록해준다.

image

image

image

AmazonEC2RoleforAWSCodeDeploy 검색 후 체크

image

이 외 내용은 모두 그대로 두고 역할을 만든다.

CodeDeploy Role

CodeDeploy에서는 S3에 있는 것을 EC2에 보낸 뒤, 배포를 실행하도록 해야하기 때문에 아래와 같이 등록해준다.

image

image

CodeDeploy의 정책은 1가지만 존재하기 때문에 그대로 다음을 눌러준다.

image

동일하게 이름을 잘 지어주고 역할을 만든다.

S3

CI 과정을 통해 빌드된 파일들을 저장할 S3 Bucket을 생성해준다.

S3 Bucket 생성

image

image

image

이 외의 설정은 건들이지 않고 버킷 만들기를 눌러주면 끝!

EC2

뒤에서 설정할 CodeDeploy에서 파일을 받아와 애플리케이션을 실행하기 위한 역할

EC2 인스턴스 생성

image

image

image

image

새 키페어를 생성하면, ec2-keypair.pem 파일을 다운받게 된다. 다운로드한 파일을 바탕화면에 aws 폴더를 만들어 보관해놓자!

폴더 이름이나 경로는 본인이 원하는 곳으로 바꿔도 된다! 단, ec2-keypair.pem 파일에 대한 이름 수정은 하면 안 된다.

image

기존 위치 무관에서 내 IP로 변경!

image

프리티어 기준 30기가까지 지원하기 때문에 30기가를 사용하고, 다른 옵션은 아무것도 건들지 말자
gp2 같은 경우 범용이 아닌 것을 사용할 경우 과금이 발생할 수 있음!

다른 문제가 없다면 인스턴스 시작을 눌러 새로운 인스턴스를 시작해주자

IAM 역할 부여

image

새로 만든 인스턴스를 우클릭하여 보안 - IAM 역할 수정을 클릭해준다.

image

앞서 만들었던 EC2의 IAM 역할을 적용해준다.

image

역할을 재적용하기 위해 인스턴스를 재부팅해준다. 재부팅 과정은 1 ~ 3분 정도 소요되니 다음 과정을 천천히 진행하자!

탄력적 IP 적용

인스턴스가 재부팅될 때마다 퍼블릭 주소는 계속해서 변경되기 때문에 탄력적 IP 주소를 통해 고정 퍼블릭 IP를 생성해준다.

image

image

아무것도 건들이지 않고 할당을 클릭해 부여해준다!

image

image

앞서 만든 인스턴스를 선택해주고, 해당 인스턴스에 대한 프라이빗 IP를 선택한 뒤, 연결 버튼을 눌러준다.

ssh 접속

이제 정상적으로 접근이 가능해졌는지 확인해보자!

image

인스턴스 상세 페이지에서 우측 상단에 있는 연결 버튼을 클릭하고, SSH 클라이언트 텝에 가장 아래 예시 ssh 연결 주소를 복사해 터미널에 입력해주자!

해당 과정은 .pem이 있는 디렉터리에서 진행해야 합니다.

image

CodeDeploy 설치

S3에 올라간 파일을 CodeDeploy를 통해 EC2에 전달해 배포를 해주기 때문에 ssh에 연결된 상태에서 설치를 해준다.

image

# CodeDeploy 설치
aws s3 cp s3://aws-codedeploy-ap-northeast-2/latest/install . --region ap-northeast-2
# 설치 파일에 권한 부여
chmod +x ./install
# 다운 받은 파일을 설치하기 위해 ruby 설치
sudo yum install ruby;
# CodeDeploy 설치
sudo ./install auto
# 서비스 상태 확인
sudo service codedeploy-agent status

CodeDeploy

S3에 올라온 .zip 파일을 EC2에 배포하는 역할

애플리케이션 생성

image

image

애플리케이션 이름은 나중에 actions.yml에 들어가니 다른 이름과 헷갈리지 않게 지어준다!

배포 그룹 생성

image

image

배포 그룹 이름은 마음대로 지어주고, 서비스 역할은 IAM에서 만들어놨던 codedeploy-role을 적용해준다.

image

위 과정에서 만든 EC2 인스턴스를 연결해준다.

image

마지막으로 CodeDeployDefault.AllAtOnce로 설정 되어있는지 확인하고, 로드 밸런서는 비활성화 해준다.

RDS

image

image

image

엔진 버전은 현재 Spring Boot에서 사용 중인 엔진 버전과 동일하게 맞춰야 한다!

image

image

DB 인스턴스 식별자는 host와 관련되어 있기 때문에 적당히 잘 지어주고, 마스터 사용자 이름과 비밀번호를 직접 지정해준다.

image

스토리지 자동 조정을 활성화하면 과금이 발생할 수 있기 때문에 비활성화 해준다!

image

EC2에서 들어가는 요청만 허용하기 위해 EC2 컴퓨팅 리소스에 연결을 선택해주고, 앞서 생성한 EC2 인스턴스를 선택해준다. VPC는 Default로 두면 된다!

image

퍼블릭 엑세스 가능으로 변경! 만약 가능으로 변경이 안 된다면, 불가능으로 진행한 뒤, RDS 생성 후 진행할 VPC 설정을 마치고 다시 수정을 통해 변경하면 된다!

image

나중에 로컬에서 접속이 가능하도록 규칙을 넣을 것이기 때문에 새로운 vpc 보안 그룹을 생성해준다.

image

자동 백업을 비활성화 시켜주고, 초기 DB를 생성하려면 이름을 작성해주고, 필요없다면 빈 칸으로 냅두자!

image

버전이 자동으로 올라가면, Spring Boot에서도 변경이 필요하기 때문에 고정 버전으로 사용하기 위해 비활성화 해주자!

이 상태로 데이터베이스 생성하면 끝!

규칙 정의

image

image

생성된 DB에 들어가서 연결 & 보안 탭 아래에 보안그룹 규칙에서 유형이 CIDR/IP Inbound인 것을 클릭해서 들어가자!

image

기존에 등록되어 있던 MYSQL/Aurora모든 트래픽으로 변경 후 규칙 저장!

VPC 세팅

image

상단 검색창에 VPC를 검색한 뒤, 라우팅 테이블 탭에 들어가보자. 보통 RDS-Pvt-rt로 된 테이블이 1개만 존재할 것이다. (필자는 RDS 2개를 띄워놔서 2개임)

해당 테이블 ID를 클릭해 상세 페이지로 들어간 다음 라우팅 편집을 눌러주자

image

local은 그대로 냅두고, 라우팅 추가를 눌러 사진과 같이 맞춰주면 igw-012345678 이런 코드가 생성될 것이다. 이 상태로 변경사항 저장!

DB 연동

image

다시 RDS로 돌아와서 엔드포인트를 복사해서 DB 연결을 해보자!

엔드포인트의 주소를 Host에 넣어주고, 기존에 설정한 User와 Passowrd를 맞게 입력해주자!

image

CD 환경 구축

앞서 CI 과정은 actions.yml에 등록되어 있으니 구축한 내용을 토대로 CD 과정을 추가하자!

GitHub Actions

# Actions에서 보여질 workflow 이름
name: CI/CD

# 트리거 지정
# master 브랜치에 push가 일어날 때 실행
on:
  push:
    branches:
      - master

# env
env:
  S3_BUCKET_NAME: deploy-bucket
  PROJECT_NAME: project

jobs:
  CI-CD:
    runs-on: ubuntu-latest
    steps:

      ## Setting JDK
      ## 본인 spring 버전에 맞는 버전 지정
      ## https://github.com/actions/setup-java
      - uses: actions/checkout@v3
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

      ## Gradle Caching
      - name: Gradle Caching
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: $-gradle-$
          restore-keys: |
            $-gradle-

      ## Create application.yml
      - name: make prod application.yml
        if: contains(github.ref, 'master')
        run: |
          cd ./src/main/resources
          touch ./application.yml
          echo "$" > ./application.yml
        shell: bash

      ## Create application-prod.yml
      - name: make prod application-prod.yml
        if: contains(github.ref, 'master')
        run: |
          cd ./src/main/resources
          touch ./application-prod.yml
          echo "$" > ./application-prod.yml
        shell: bash

      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew
        shell: bash

      ## Gradle Build
      - name: Build with Gradle
        run: ./gradlew build

      - name: Make zip file
        run: zip -r ./$GITHUB_SHA.zip .
        shell: bash

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: $
          aws-secret-access-key: $
          aws-region: ap-northeast-2

      # script files 복사
      - name: Copy script
        run: cp ./scripts/*.sh ./deploy
      
      # S3에 업로드
      - name: Upload to S3
        run: aws s3 cp --region ap-northeast-2 ./$GITHUB_SHA.zip s3://$S3_BUCKET_NAME/$PROJECT_NAME/$GITHUB_SHA.zip

      # Deploy
      - name: Deploy
        run: |
          aws deploy create-deployment \
          --application-name deploy-dev-app \
          --deployment-config-name CodeDeployDefault.AllAtOnce \
          --deployment-group-name deploy-dev \
          --file-exists-behavior OVERWRITE \
          --s3-location bucket=deploy-bucket,bundleType=zip,key=project/$GITHUB_SHA.zip \
          --region ap-northeast-2 \

변경할 사항 정리

  • env
    • S3_BUCKET_NAME : S3 버킷 이름
    • PROJECT_NAME : S3 버킷 내부에 빌드된 파일을 저장할 폴더(프로젝트) 이름
  • Deploy
    • application-name : CodeDeploy의 application 이름
    • deployment-group-name : 해당 application의 배포 그룹 이름
    • s3-location
      • bucket : S3 버킷 이름
      • key : env에 있는 PROJECT_NAME의 value와 동일하게 작성

deploy.sh 생성

CodeDeploy를 통해 S3에서 EC2로 보내는 과정에서 배포를 요청해야하기 때문에 작성해준다.

SpringBoot 프로젝트 최상위에 scripts 디렉터리를 만든 뒤, 아래 내용의 주석은 지우고 작성

image
#!/bin/bash
BUILD_JAR=$(ls /home/ec2-user/action/build/libs/*.jar)
JAR_NAME=$(basename $BUILD_JAR)
echo "> build 파일명: $JAR_NAME" >> /home/ec2-user/action/deploy.log

echo "> build 파일 복사" >> /home/ec2-user/action/deploy.log
DEPLOY_PATH=/home/ec2-user/action/
cp $BUILD_JAR $DEPLOY_PATH

echo "> 현재 실행중인 애플리케이션 pid 확인" >> /home/ec2-user/action/deploy.log
CURRENT_PID=$(pgrep -f $JAR_NAME)

if [ -z $CURRENT_PID ]
then
  echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다." >> /home/ec2-user/action/deploy.log
else
  echo "> kill -15 $CURRENT_PID"
  kill -15 $CURRENT_PID
  sleep 5
fi

DEPLOY_JAR=$DEPLOY_PATH$JAR_NAME
echo "> DEPLOY_JAR 배포"    >> /home/ec2-user/action/deploy.log
nohup java -jar $DEPLOY_JAR >> /home/ec2-user/deploy.log 2>/home/ec2-user/action/deploy_err.log &

appspec.yml 생성

image

해당 파일은 프로젝트 구조 최상위에 따로 만들어 준다.

최종적으로 CodeDeploy가 EC2 환경에 S3에서 가져온 jar 파일을 배포할 수 있도록 만들어주는 파일

version: 0.0
os: linux
# S3에 있는 zip 파일이 EC2에 배포될 위치를 지정
files:
  # CodeDeploy에서 전달해 준 파일 중 destination으로 이동시킬 대상을 루트로 지정(전체파일)
  - source: /
    # source에서 지정된 파일을 받을 위치, 이후 jar를 실행하는 등은 destination에서 옮긴 파일들로 진행
    destination: /home/ec2-user/action/ 
    overwrite: yes

# CodeDeploy에서 EC2서버로 넘겨준 파일들을 모두 ec2-user권한을 갖도록 합니다.
permissions: 
  - object: /
    pattern: "**"
    owner: ec2-user
    group: ec2-user

# ApplicationStart 단계에서 deploy.sh를 실행시키도록 합니다.
# CodeDeploy배포 단계에서 실행할 명령어를 지정합니다.
hooks:
  # deploy.sh를 ec2-user권한으로 실행합니다.
  ApplicationStart: 
    - location: scripts/deploy.sh
      # 스크립트 실행 60초 이상 수행되면 실패가 됩니다.
      timeout: 60 
      runas: ec2-user

레퍼런스

주형님 블로그 - 가비아 + Amazon Linux + Nginx + Cerbot/SSL을 활용한 https 설정
Github Actions Variable
뱀귤님 블로그
김재성님 블로그
danuri님 블로그
zzang9ha님 블로그

댓글남기기