[CI/CD] CI/CD 로 배포 자동화하기

Kaite.Kang·2023년 4월 8일
0
post-thumbnail

* 목표

지금까지 코드는 빌드와 배포를 사용자가 직접 해주어야 했다. CI/CD 를 구축하면 빌드와 배포를 자동화할 수 있다. 이번 포스팅에서는 적절한 CI, CD 서비스를 선택하여 빌드, 배포, 실행까지 자동화하도록 설정해보자.

1. CI/CD 개념과 필요성

  • 필요성

이전에는 프로젝트를 서버에 배포하기 위해서는 스크립트를 개발자가 직접 실행해야 했다.

CI/CD 환경을 구축하면 다음과정을 개선할 수 있다.

CI → 여러 개발자가 개발한 코드를 병합하는 과정을 자동화

CD → 수백대의 서버에 빌드 파일을 배포하는 과정을 자동화

  • 개념
    • CI(Continuous Integration -지속적 통합) : 코드 버전 관리를 하는 VCS 시스템(Git, SVN 등)에 PUSH가 되면 자동으로 테스트와 빌드가 수행되어 안정적인 배포 파일을 만드는 과정
    • CD(Continuous Deployment -지속적인 배포) : 빌드 결과를 자동으로 운영 서버에 무중단 배포까지 진행되는 과정
    • 일반적으로 CI, CD를 함께 구축한 경우가 대부분이다.
  • 주의할 점

CI 도구를 도입했다고 해서 CI를 하고 있는 것은 아니다. 마틴 파일러의 블로그를 참고해보면 CI에 대해 다음과 같은 4가지 규칙을 이야기한다.

  • 모든 소스 코드가 현재 실행되고 누구든 현재의 소스에 접근할 수 있는 단일 지점을 유지할 것
  • 빌드 프로세스를 자동화해서 누구든 소스로부터 시스템을 빌드하는 단일 명령어를 사용할 수 있게 할 것
  • 테스팅을 자동화해서 단일 명령어로 언제든지 시스템에 대한 건전한 테스트 수트를 실행할 수 있게 할 것
  • 누구나 현재 실행 파일을 얻으면 지금까지 가장 완전한 실행 파일을 얻었다는 확인을 하게 할 것

여기서 특히 중요한 것을 테스팅 자동화이다. 지속적으로 통합하기 위해서는 무엇보다 이 프로젝트가 완전한 상태임을 보장하기 위해 테스트 코드가 구현되어 있어야만 한다.

2. 사전 준비

2-1. CI/CD 구성도


코드를 배포하기 위해서는 Code Commit → Code Build → Code Deploy 단계가 필요하다.

  • Code Commit: 코드 저장소의 역할(ex. 깃허브)
  • Code Build: 빌드용 서비스(ex. Github Action, Travis CI)
  • CodeDeploy: AWS의 배포 서비스

2-2. CI/CD 서비스 선택하기

CI 서비스는 다음 조건을 기준으로 선택하였다.

  • 별도의 서버가 필요하지 않은 서비스 → 젠키스는 별도의 서비스가 필요하다
  • 요금을 무료로 이용할 수 있는 서비스
  • AWS 와 연동이 가능한 서비스

이 조건에 맞는 CI 서비스 중에 Travis CI와 깃허브 액션(Github Action)이 있다.

Travis 요금 정책이 한달까지만 무료로 이용 가능하고, 처음 Github와 연동할 때 느린 이슈도 있어서 깃허브 액션을 선택하게 되었다. 깃허브 액션은 한달에 2000분까지는 무료이다.

AWS용 배포 서비스(CD 서비스)는 CodeDeploy 외에 대체재가 없다.

깃허브 액션이 코드를 빌드한 파일을 S3에 저장하면 AWS CodeDeploy는 S3에서 파일을 가져와 EC2에 배포한다.

2-3. AWS 서비스와 연동하려면 IAM 서비스를 사용해야 한다


AWS 서비스 연동을 위한 IAM 구성

이 포스팅에서 IAM 서비스를 빈번하게 구성하기 때문에 IAM이 무엇인지 알아야 본 포스팅을 이해하기에 수월하다.
AWS 외부 서비스와 AWS 내부 서비스를 연동하려면 IAM을 구성해야하고, AWS 내부 서비스끼리 연동을 위해서도 IAM을 구성해야 한다.

IAM은 AWS 리소스에 대한 접근제어를 담당하는 서비스이다. Identity and Access Management의 약어로 직역하자면 인증과 접근제어을 담당하는 관리 서비스라고 할 수 있다.

AWS 내부 서비스끼리 연동할 때IAM 역할을 생성해주면 된다. AWS 서비스인 CodeDeploy와 EC2를 연동하려면 EC2가 CodeDeploy에 접근할 수 있는 권한을 가진 IAM 역할과 CodeDeploy가 EC2에 접근할 수 있는 권한을 가진 IAM 역할을 생성하면 된다. 이렇게 생성한 두 개의 IAM 역할을 각각 EC2와 CodeDeploy에 추가해주면 된다. IAM 역할은 AWS 계정의 특정 AWS 리소스에 대한 임시 액세스 권한을 부여하는 도구라고 생각하면 된다.

그렇다면 외부 서비스가 AWS 내부 서비스로 접근할 때는 어떻게 해야될까? IAM 사용자를 생성하면 된다. IAM 사용자는 AWS 제품에 대한 API 호출을 수행해야 하는 사람 또는 애플리케이션을 의미한다. 외부 서비스는 AWS 서비스를 이용하기 위해 API를 호출해야 하므로 AWS 사용자를 생성하고 해당 사용자에 대한 Key를 발급 받아서 사용하면 AWS 서비스를 API 호출을 할 수 있게 된다.
예를 들어 Github Actions이 AWS S3와 AWS CodeDeploy에 접근이 필요한 상황이라고 하자. IAM 사용자를 생성해서 S3와 CodeDeploy 접근 권한을 추가하고, 해당 사용자의 Key를 발급 받아서 Github Actions에 적용해주면 된다. 그럼 Github Actions은 AWS S3와 AWS CodeDeploy를 호출할 수 있는 API를 사용할 수 있다.

IAM의 구성 요소인 사용자와 역할의 개념과 사용 예시를 알아보았다.
이제 본격적으로 CI/CD를 구성해보자.

3. Github Actions 구성하기

구글 액션과 연동할 프로젝트에 들어가서 Actions 탭에서 “Publish Java Package with Gradle”에 들어간다.

<프로젝트명> / .github / workflows / gradle-publish.yml 파일을 편집할 수 있다. 이 파일 이름은 자유롭게 지정할 수 있다. 이 파일에는 구글액션이 어떤 동작을 수행하도록 할 것인지 순서대로 실행할 프로세스를 적어주어야 한다. 문법은 YAML 문법을 사용한다.

워크플로의 구성은 메뉴얼 페이지를 참고하여 작성하면 된다.

name: deploy #0. GitHub Action에서 보여질 이름을 지정한다.

on:
  release:
    types: [push] #1. push 시 자동으로 실행된다.
  push:
    branches: [master] #2. master 브랜치에서 동작한다.
  workflow_dispatch:   #3. 수동으로도 실행이 가능하다.

jobs:
  build:
    runs-on: ubuntu-latest #4. 해당 스크립트를 작동할 OS
    permissions:
      contents: read
      packages: write

    steps:
    - name: Checkout
      uses: actions/checkout@v3 #5. 프로젝트 코드를 CheckOut한다.
      
    - name: Set up JDK 11
      uses: actions/setup-java@v3 #6. Java 11 버전을 설치한다.
      with:
        java-version: '11'
        distribution: 'temurin'
      
    - name: Grant execute permission for gradlew
      run: chmod +x ./gradlew #7. gradle wrapper를 실행할 수 있도록 실행 권한을 부여한다.
      shell: bash

    - name: Build with Gradle
      run: ./gradlew clean build -x test #8. gradle wrapper를 통해 해당 프로젝트를 build한다.
      shell: bash

💡 주의
처음에 run-on 값을 Amazon Linux 2로 적었는데 해당 value는 지원하는 파라미터가 따로 정해져 있었다.
적합한 YAML workflow Label을 선택하여 사용하면 된다.

위 스크립트를 commit 하면 자동으로 배포가 된다. Actions 메뉴에서 Action을 확인할 수 있다.

Action > build 을 클릭하면 자세한 정보를 확인할 수 있다.

당장은 해당 Action을 사용하지 않을 것 이기 때문에 사용하지 않는 Action은 바로바로 삭제한다. 프리 요금제의 경우 1달에 2000시간을 초과하면 사용이 제한된다.

4. Github Actions와 AWS S3 연동하기


깃허브 액션은 CI서비스로서 코드 빌드를 수행해준다. 코드를 AWS로 배포는 AWS CodeDeploy를 역할이므로 깃허브 액션과 CodeDeploy를 연동해야 한다. 하지만 CodeDeploy는 코드 저장 기능이 없기 때문에 파일 서버인 AWS S3와 연동하여 빌드 파일(.jar)을 파일을 받아 배포해야 한다.

S3(Simple Storage Service)는 오브젝트 스토리지로 주로 파일을 저장하는데 사용된다.

4-1. Github Actions을 위한 IAM 사용자 생성

깃허브 액션같은 외부 서비스가 AWS S3에 파일을 저장하기 위해서는 접근 권한을 가진 AWS Key를 발급해야 한다.
IAM(Identity and Access Management)에서 접근 제어를 담당한다. IAM 서비스에서 S3에 접근 권한을 가진 사용자를 생성해야 한다. 그 다음 사용자에 대한 Key를 발급하여 깃허브 액션 서비스에 Key를 추가해 주면 된다.

[사용자] > [사용자 추가] 버튼을 눌러 AWS에 접근할 사용자를 생성한다.

사용자 이름을 지정한다.

이 사용자는 깃허브가 사용할 사용자로 S3와 CodeDeploy에 접근할 수 있는 권한이 필요하다.

“AmazonS3FullAccess”와 “AWSCodeDeployFullAccess”에 권한을 추가해준다.

태그는 일반적으로 Name 값을 지정하는데 본인이 구분할 수 있는 이름으로 만들면 된다.

사용자 생성이 완료되었다.

사용자의 엑세스 키를 발급해야 한다. [사용자 이름] > [보안 자격 증명] > [액세스 키 만들기]에서 액세스키를 만든다.

깃허브 액션은 AWS 외부에서 실행되는 애플리케이션이므로 “AWS 외부에서 실행되는 애플리케이션”을 선택한다.

엑세스키 태그를 설정 하고 나면 엑세스 키가 생성된다.

엑세스 키와 비밀 엑세스 키는 개인으로 보관해야 하며, 해당 창을 넘어가면 비밀 엑세스 키는 더이상 저장할 수 없다. 반드시 따로 저장해두어야 한다.

만들어진 엑세스 키를 Github Actions에 등록한다.

깃허브 프로젝트 페이지에서 [Setting] > [Secrets and Variables] > [Actions] > [New repository secret] 에서 키를 등록할 수 있다.

Name에는 각각 AWS_ACCESS_KEY, AWS_SECRET_KEY 를, Secret에는 IAM 사용자에서 발급받은 ACCESS_KEY와 SECRET_ACCESS_KEY를 등록한다.

저장이 완료되면 다음과 같은 화면을 볼 수 있다.

여기까지 Github Actions이 S3와 Codedeploy에 접근할 수 있도록 해당 리소스의 접근권한을 가진 사용자 ”freelec-githubAction-codedeploy”를 만들어서 Github Actions에 사용자 액세스키 를 등록해주었다.

이제 Github Actions은 S3와 Codedeploy에 접근할 때 사용자 ”freelec-githubAction-codedeploy”의 권한을 이용할 수 있다.

4-2. AWS S3 버킷 생성

이제 S3버킷을 생성해보자.

S3는 배포할 Zip 파일을 저장할 것이므로 이 목적에 맞게 이름을 지정하자.

버킷의 보안과 권한 설정 부분이다. 실제 서비스에서는 jar 파일이 퍼블릭일 경우, 누구나 내려 받을 수 있어 코드나 설정값, 주요 키값들이 노출될 위험이 있다. 권한이 퍼블릭이 아니더라도 IAM 사용자로 발급받은 키를 사용하여 접근 가능하기 때문에 모든 액세스를 차단하는 설정에 체크한다.

이제 S3가 생성되었으니 S3로 배포파일을 전달해보자.

4-3. GitHub Action과 AWS S3를 연동 코드 추가

GitHub Action과 AWS S3를 연동하도록 yml 파일을 gradle-publish.yml 파일에 다음 코드를 추가한다.
그 전에 시간을 동기화 해주어 빌드 시간이 한국시간인 KST로 설정될 수 있도록 하자.(action의 기준 시간이 UTC인거 같다.)

...code...

    #8. gradle wrapper를 통해 해당 프로젝트를 build한다.
    - name: Build with Gradle
      run: ./gradlew clean build -x test 
      shell: bash

    # 이전에 추가했던 내용 아래에 작성하면 됩니다. 

    #9.utcOffset: "+09:00": 해당 action의 기준이 UTC이므로 한국시간인 KST로 진행 하기 위해 offset에 +09:00 를 해준다.
    - name: Get current time
      uses: 1466587594/get-current-time@v2  
      id: current-time
      with:
        format: YYYY-MM-DDTHH-mm-ss 
        utcOffset: "+09:00"

    #10. 현재 시간을 출력한다.
    - name: Show Current Time
      run: echo "CurrentTime=${{steps.current-time.outputs.formattedTime}}" 
      shell: bash

그 다음 GitHub Action과 AWS S3를 연동 부분을 추가해주자.

# main.yml의 최상단입니다.
#새로 추가된 내용입니다. (11) 
env:  
  S3_BUCKET_NAME: freelec-springboot-build-file
  PROJECT_NAME: springboot2-webservice

name: deploy #0. GitHub Action에서 보여질 이름을 지정한다.

on:
  release:
    types: [push] #1. push 시 자동으로 실행된다.
  push:
    branches: [master] #2. master 브랜치에서 동작한다.
  workflow_dispatch:   #3. 수동으로도 실행이 가능하다.

...code...

    #10. 현재 시간을 출력한다.
    - name: Show Current Time
      run: echo "CurrentTime=${{steps.current-time.outputs.formattedTime}}" 
      shell: bash

    # 아래부분이 추가되는 내용입니다.
    #12. 프로젝트 이름으로 해당 폴더를 모두 압축시킨다.
    - name: Make zip file
      run: zip -r ./$PROJECT_NAME.zip .
      shell: bash

    #13. aws에 해당 키 값으로 접속을 진행한다.
    - 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: ap-northeast-2

    #14. s3 에 프로젝트 이름에 해당하는 폴더에 zip파일을 저장한다.
    - name: Upload to S3
      run: aws s3 cp --region ap-northeast-2 ./deploy/$PROJECT_NAME.zip s3://$S3_BUCKET_NAME/$PROJECT_NAME/$PROJECT_NAME.zip

(#11) env

환경 변수를 이름 : 값 으로 지정한다. 자주 사용되거나, 변경될 수 있는 부분은 환경 변수로 지정하면 편하게 사용할 수 있다.

  • S3_BUCKET_NAME: 내가 설정한 S3 버킷 이름
  • PROJECT_NAME: 내 Git Hub 프로젝트 이름

수정한 코드를 커밋과 푸시를 한 후 Actions으로 이동해 Action을 확인한다.
이후 Action을 삭제해준다.

AWS S3를 확인해 보면 해당 압축 파일이 들어있는 것을 확인할 수 있다.

5. EC2에 CodeDeploy 연동하기

5-1. EC2에 IAM 역할 추가

“IAM > 역할 > 역할 만들기”에서 EC2가 사용할 IAM 을 생성한다.

권한 정책은 “AmazonEC2RoleforAWSCodeDeploy”을 추가한다.

마지막으로 역할 이름을 설정한다.

최종적으로 역할이 잘 생성되었다.

이렇게 만든 역할을 EC2 서비스에 등록해야 한다. EC2 인스턴스 목록에서 인스턴스명을 우클릭한 뒤, [보안 > IAM 역할 수정]으로 이동한다.

IAM 역할에서 이전에 만든 역할을 선택한 후 업데이트 한다.

EC2 재부팅을 해야만 역할이 정상적으로 적용되니 인스턴스 재부팅을 진행한다.

5-2. EC2에 CodeDeploy 에이전트 설치

EC2가 AWS CodeDeploy의 요청을 받을 수 있도록 EC2 내부에 에이전트를 설치해야 한다.

EC2에 접속하여 다음 명령어를 실행한다.

### CodeDeploy 설치 파일을 다운로드한다.
$ aws s3 cp s3://aws-codedeploy-ap-northeast-2/latest/install . --region ap-northeast-2
download: s3://aws-codedeploy-ap-northeast-2/latest/install to ./install

### 실행 권한은 추가하고, install 파일로 설치를 진행한다.
$ chmod +x ./install
$ ./install auto
/usr/bin/env: ruby: No such file or directory

### 만약 위와 같은 ruby 메시지가 나왔다면 ruby를 설치한 후 다시 실행하면 된다.
$ yum list ruby
Loaded plugins: extras_suggestions, langpacks, priorities, update-motd
Available Packages
ruby.x86_64                  2.0.0.648-36.amzn2.0.3                   amzn2-core

$ sudo yum install ruby
...
Installed:
  ruby.x86_64 0:2.0.0.648-36.amzn2.0.3

Dependency Installed:
  ruby-irb.noarch 0:2.0.0.648-36.amzn2.0.3
  ruby-libs.x86_64 0:2.0.0.648-36.amzn2.0.3
  rubygem-bigdecimal.x86_64 0:1.2.0-36.amzn2.0.3
  rubygem-io-console.x86_64 0:0.4.2-36.amzn2.0.3
  rubygem-json.x86_64 0:1.7.7-36.amzn2.0.3
  rubygem-psych.x86_64 0:2.0.0-36.amzn2.0.3
  rubygem-rdoc.noarch 0:4.0.0-36.amzn2.0.3
  rubygems.noarch 0:2.0.14.1-36.amzn2.0.3

Complete!

### ruby 패키지가 설치되었는지 확인한다.
$ rpm -qa|grep ruby-
ruby-irb-2.0.0.648-36.amzn2.0.3.noarch
ruby-2.0.0.648-36.amzn2.0.3.x86_64
ruby-libs-2.0.0.648-36.amzn2.0.3.x86_64

### 다시 한번 CodeDeploy 설치 파일을 실행한다.
$ sudo ./install auto
...
pre hook : 1
Checking if there is already a process named codedeploy-agent running.
  Installing : codedeploy-agent-1.5.0-57.noarch                             1/1

post hook : 1
Check if there is a codedeployagent config file.
Start codedeploy-agent in post hook if this is a first install.
  Verifying  : codedeploy-agent-1.5.0-57.noarch                             1/1

Installed:
  codedeploy-agent.noarch 0:1.5.0-57

Complete!

### codedeploy가 잘 설치 되었는지 확인한다.
$ systemctl list-units |grep -i codedeploy
codedeploy-agent.service                                      loaded active running   AWS CodeDeploy Host Agent

$ systemctl status codedeploy-agent
● codedeploy-agent.service - AWS CodeDeploy Host Agent
   Loaded: loaded (/usr/lib/systemd/system/codedeploy-agent.service; enabled; vendor preset: disabled)
   Active: active (running) since Tue 2023-03-28 12:36:09 KST; 1min 34s ago
  Process: 12917 ExecStart=/bin/bash -a -c [ -f /etc/profile ] && source /etc/profile; /opt/codedeploy-agent/bin/codedeploy-agent start (code=exited, status=0/SUCCESS)
 Main PID: 12927 (ruby)
   CGroup: /system.slice/codedeploy-agent.service
           ├─12927 codedeploy-agent: master 12927
           └─12931 codedeploy-agent: InstanceAgent::Plugins::CodeDeployPlugin::CommandPoller of...

Mar 28 12:36:09 freelec-springboot2-webservice systemd[1]: Starting AWS CodeDeploy Host Agent...
Mar 28 12:36:09 freelec-springboot2-webservice systemd[1]: Started AWS CodeDeploy Host Agent.

6. AWS CodeDeploy 구성

6-1. CodeDeploy IAM 역할 생성하기

CodeDeploy에서 EC2로 접근하려면 권한이 필요하다. 권한 생성을 위해 IAM에서 역할을 생성한다.

  1. 엔터티 유형은 [AWS 서비스]를 선택한다.

  2. [사용 사례] > [다른 AWS 서비스의 사용 사례]에서 CodeDeploy를 선택한다.

  3. 권한은 따로 선택할 것이 없다. “AWSCodeDeployRole” 이 선택되어 있다.

  4. 역할 이름을 설정한다.

최종적으로 CodeDeploy를 위한 역할이 생성되었다.

6-2. CodeDeploy 생성하기

이제 자동 배포의 마지막 단계이다.

CodeDeploy를 검색하여 들어가서 [애플리케이션 > 애플리케이션 생성]으로 이동한다.

애플리케이션 이름을 작성하고, 컴퓨팅 플랙폼은 [EC2/온프레미스]를 선택하고, [애플리케이션 생성]을 한다.

아래 [배포 그룹 생성]을 선택한다.

아래 항목을 입력하여 CodeDeploy에 배포를 위한 설정을 한다.

  1. 배포 그룹 이름 입력
  2. 서비스 역할은 이전에 IAM에서 만든 codedeploy 역할을 추가한다.
  3. 배포 방법은 현재 위치로 진행한다.
  4. 환경 구성은 Amazon EC2 인스턴스로 진행합니다(EC2를 만들었던 키/값을 이용합니다. 반드시 키와 값은 EC2 태그와 일치하여야합니다. (대소문자구분)
  5. 에이전트 구성은 사용하지 않는다.
  6. 배포 설정은 CodeDeployDefault.AllAtOnce 이다.
  7. 로드 밸런서는 비활성화한다.

7. Github Actions, S3, CodeDeploy 연동

EC2에 zip 파일을 저장할 디렉토리를 생성한다. 이 압축파일은 S3에서 EC2로 넘겨줄 파일이다.

$ mkdir -p ~/app/step2/zip

Github Actions에서 Build 가 끝나면 S3에 zip 파일이 전송되고, 이 zip 파일은 /home/ec2-user/app/step2/zip로 복사되어 압축을 풀게 될 것이다.

이전에 Github Actions 설정은 gradle-publish.yml 으로 진행하였다.

AWS CodeDeploy 설정은 appspec.yml 에 설정한다. appspec.yml 파일은 CodeDeploy에서 배포를 관리하는 데 사용하는 YAML 형식 또는 JSON 형식의 파일이다. appspec.yml파일은 파일에 정의된 일련의 수명 주기 이벤트 후크로 각 배포를 관리하는 데 사용된다.

프로젝트 제일 상단에 appspec.yml 파일을 작성한다.

version: 0.0
os: linux
files:
  - source: /
    destination: /home/ec2-user/app/step2/zip/ 
    overwrite: yes

source는 전달해준 파일 중 destination으로 이동시킬 대상이다. / 인 경우 전체 파일을 지정하게 된다.

destination는 파일을 받을 위치이다.

Github Actions 설정에서도 CodeDeploy 내용을 추가한다.

gradle-publish.yml에 다음 코드를 추가한다.

# 코드의 맨 처음 부분입니다
env:  #11. 환경 변수 
  S3_BUCKET_NAME: freelec-springboot-build-file
  PROJECT_NAME: springboot2-webservice
  CODE_DEPLOY_APP_NAME: freelec-springboot2-webservice     #15. codedeploy의 어플리케이션과 그룹 이름 추가
  CODE_DEPLOY_GROUP_NAME: freelec-springboot-webservice-group

name: deploy #0. GitHub Action에서 보여질 이름을 지정한다.

...code...

    #13. aws에 해당 키 값으로 접속을 진행한다.
    - 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: ap-northeast-2

    #14. s3 에 프로젝트 이름에 해당하는 폴더에 zip파일을 저장한다.
    - name: Upload to S3
      run: aws s3 cp --region ap-northeast-2 ./deploy/$PROJECT_NAME.zip s3://$S3_BUCKET_NAME/$PROJECT_NAME/$PROJECT_NAME.zip  #(14)

    #16. 배포 그룹으로 해당 애플리케이션을 보낸다.
    - name: Code Deploy
      run: aws deploy create-deployment --application-name $CODE_DEPLOY_APP_NAME --deployment-config-name CodeDeployDefault.AllAtOnce --deployment-group-name $CODE_DEPLOY_GROUP_NAME --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$PROJECT_NAME/$PROJECT_NAME.zip

전체 코드는 다음과 같다.

env:  #11. 환경 변수 
  S3_BUCKET_NAME: freelec-springboot-build-file
  PROJECT_NAME: springboot2-webservice
  CODE_DEPLOY_APP_NAME: freelec-springboot2-webservice     #15. codedeploy의 어플리케이션과 그룹 이름
  CODE_DEPLOY_GROUP_NAME: freelec-springboot-webservice-group

name: deploy #0. GitHub Action에서 보여질 이름을 지정한다.

on:
  release:
    types: [push] #1. push 시 자동으로 실행된다.
  push:
    branches: [master] #2. master 브랜치에서 동작한다.
  workflow_dispatch:   #3. 수동으로도 실행이 가능하다.

jobs:
  build:
    runs-on: ubuntu-latest #4. 해당 스크립트를 작동할 OS
    permissions:
      contents: read
      packages: write

    steps:
    #5. 프로젝트 코드를 CheckOut한다.
    - name: Checkout
      uses: actions/checkout@v3 
      
    #6. Java 11 버전을 설치한다.
    - name: Set up JDK 11
      uses: actions/setup-java@v3 
      with:
        java-version: '11'
        distribution: 'temurin'
      
    #7. gradle wrapper를 실행할 수 있도록 실행 권한을 부여한다.
    - name: Grant execute permission for gradlew
      run: chmod +x ./gradlew 
      shell: bash

    #8. gradle wrapper를 통해 해당 프로젝트를 build한다.
    - name: Build with Gradle
      run: ./gradlew clean build -x test 
      shell: bash

    #9.utcOffset: "+09:00": 해당 action의 기준이 UTC이므로 한국시간인 KST로 진행 하기 위해 offset에 +09:00 를 해준다.
    - name: Get current time
      uses: 1466587594/get-current-time@v2  
      id: current-time
      with:
        format: YYYY-MM-DDTHH-mm-ss 
        utcOffset: "+09:00"

    #10. 현재 시간을 출력한다.
    - name: Show Current Time
      run: echo "CurrentTime=${{steps.current-time.outputs.formattedTime}}" 
      shell: bash

    #12. 프로젝트 이름으로 해당 폴더를 모두 압축시킨다.
    - name: Make zip file
      run: zip -r ./$PROJECT_NAME.zip .
      shell: bash

    #13. aws에 해당 키 값으로 접속을 진행한다.
    - 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: ap-northeast-2

    #14. s3 에 프로젝트 이름에 해당하는 폴더에 zip파일을 저장한다.
    - name: Upload to S3
      run: aws s3 cp --region ap-northeast-2 ./deploy/$PROJECT_NAME.zip s3://$S3_BUCKET_NAME/$PROJECT_NAME/$PROJECT_NAME.zip  #(14)

    #16. 배포 그룹으로 해당 애플리케이션을 보낸다.
    - name: Code Deploy
      run: aws deploy create-deployment --application-name $CODE_DEPLOY_APP_NAME --deployment-config-name CodeDeployDefault.AllAtOnce --deployment-group-name $CODE_DEPLOY_GROUP_NAME --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$PROJECT_NAME/$PROJECT_NAME.zip

내용을 모두 작성한 후, 프로젝트를 커밋하고 푸시한다.

Github가 푸시되면 Github Actions이 빌드하고, 빌드가 끝나면 CodeDeploy 화면에 배포가 진행되는 것을 확인할 수 있다.

CodeDeploy로 이동하면 배포가 성공한 것을 확인할 수 있다.

EC2으로 이동해, 파일이 있는지 확인한다.

cd /home/ec2-user/app/step2/zip

$ ls -l ~/app/step2/zip/
total 24
-rw-r--r-- 1 root root  111 Mar 28 20:02 appspec.yml
drwxr-xr-x 7 root root  106 Mar 28 20:03 build
-rw-r--r-- 1 root root 1560 Mar 28 20:02 build.gradle
drwxr-xr-x 3 root root   21 Mar 28 20:03 gradle
-rwxr-xr-x 1 root root 5774 Mar 28 20:02 gradlew
-rw-r--r-- 1 root root 2674 Mar 28 20:02 gradlew.bat
drwxr-xr-x 4 root root   36 Mar 28 20:03 out
-rw-r--r-- 1 root root   45 Mar 28 20:02 settings.gradle
drwxr-xr-x 4 root root   30 Mar 28 20:03 src

Github Action, S3, CodeDeploy 연동이 완료되었다!

8. 배포 자동화 구성하기

앞서 Github Actions, S3, CodeDeploy 연동까지 구현되었다.

이것을 기반으로 실제로 Jar 파일을 배포하여 실행까지 진행해보자.

8-1. deploy.sh 파일 추가

scripts 디렉토리를 생성해서 여기에 deploy.sh 스크립트를 생성한다.

이 스크립트는 step2에서 실행될 스크립트로 step2에서 작성된 deploy.sh와 크게 다르지 않다.

우선 git pull을 통해 직접 빌드했던 부분을 제거하였다. 그리고 Jar을 실행하는 단계에서 몇가지 코드가 추가되었다.

#!/bin/bash

REPOSITORY=/home/ec2-user/app/step2
PROJECT_NAME=aws-web-service #해당 위치에 properties에 작성한 프로젝트명과 동일하게 작성합니다.

echo "> Build 파일 복사"
cp $REPOSITORY/zip/*.jar $REPOSITORY/

echo "> 현재 구동 중인 애플리케이션 pid 확인"
CURRENT_PID=$(pgrep -f $PROJECT_NAME)

echo "현재 구동 중인 애플리케이션 pid: $CURRENT_PID"

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

echo "> 새 애플리케이션 배포"
JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 1)

echo "> JAR_NAME: $JAR_NAME"
echo "> $JAR_NAME 에 실행권한 추가"
chmod +x $JAR_NAME

echo "> $JAR_NAME 실행"
nohup java -jar \
  -Dspring.config.location=classpath:/application.properties,/home/ec2-user/app/application-oauth.properties,/home/ec2-user/app/application-real-db.properties \
  -Dspring.profile.active=real \
  $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &

8-2. appspec.yml 파일 수정

codedeploy가 자동으로 배포할 수 있도록 appscpec.yml 에도 내용을 추가한다.

permissions 에서는 CodeDeploy에서 EC2서버로 넘겨준 파일들을 모두 ec2-user 권한을 갖도록한다.

hooks 는 배포 단계에서 실행할 명령어를 지정한 구문이다. ApplicationStart라는 단계에서 deploy를 ec2-user 권한으로 실행하게 한다. “timeout: 60”으로 스크립트 실행 60초 이상이 수행되면 실패가 된다. 무한정으로 기다릴 수 없으니 제한 시간을 둬야한다.

version: 0.0
os: linux
files:
  - source: /
    destination: /home/ec2-user/app/step2/zip/ 
    overwrite: yes

permissions:
  - object: /
    pattern: "**"
    owner: ec2-user
    group: ec2-user

hooks:
  ApplicationStart:
    - location: deploy.sh
      timeout: 60
      runas: ec2-user

8-3. gradle-publish.yml 파일 수정

마지막으로 gradle-publish.yml (빌드 과정에서 실행할 내용들)에 아래 코드를 추가한다.

#(final) 에는 현재 프로젝의 모든 파일을 zip 파일로 만드는 과정을 포함한다. 실제 필요한 파일은 Jar, appspec.yml, 배포를 위한 스크립트 이므로 이외에 나머지 파일은 제외하고 zip 파일로 묶어야 한다.

참고로 S3에는 디렉토리 단위로 업로드가 가능하기 때문에 deploy 디렉토리는 항상 생성해야 한다.

env:  #11. 환경 변수 
  S3_BUCKET_NAME: freelec-springboot-build-file
  PROJECT_NAME: springboot2-webservice
  CODE_DEPLOY_APP_NAME: freelec-springboot2-webservice     #15. codedeploy의 어플리케이션과 그룹 이름
  CODE_DEPLOY_GROUP_NAME: freelec-springboot-webservice-group

name: deploy #0. GitHub Action에서 보여질 이름을 지정한다.

on:
  release:
    types: [push] #1. push 시 자동으로 실행된다.
  push:
    branches: [master] #2. master 브랜치에서 동작한다.
  workflow_dispatch:   #3. 수동으로도 실행이 가능하다.

jobs:
  build:
    runs-on: ubuntu-latest #4. 해당 스크립트를 작동할 OS
    permissions:
      contents: read
      packages: write

    steps:
    #5. 프로젝트 코드를 CheckOut한다.
    - name: Checkout
      uses: actions/checkout@v3 
      
    #6. Java 11 버전을 설치한다.
    - name: Set up JDK 11
      uses: actions/setup-java@v3 
      with:
        java-version: '11'
        distribution: 'temurin'
      
    #7. gradle wrapper를 실행할 수 있도록 실행 권한을 부여한다.
    - name: Grant execute permission for gradlew
      run: chmod +x ./gradlew 
      shell: bash

    #8. gradle wrapper를 통해 해당 프로젝트를 build한다.
    - name: Build with Gradle
      run: ./gradlew clean build -x test 
      shell: bash

    #9.utcOffset: "+09:00": 해당 action의 기준이 UTC이므로 한국시간인 KST로 진행 하기 위해 offset에 +09:00 를 해준다.
    - name: Get current time
      uses: 1466587594/get-current-time@v2  
      id: current-time
      with:
        format: YYYY-MM-DDTHH-mm-ss 
        utcOffset: "+09:00"

    #10. 현재 시간을 출력한다.
    - name: Show Current Time
      run: echo "CurrentTime=${{steps.current-time.outputs.formattedTime}}" 
      shell: bash

    - name: Generate deployment package # (final)
      run: |
        mkdir -p before-deploy
        cp scripts/*.sh before-deploy/
        cp appspec.yml before-deploy/
        cp build/libs/*.jar before-deploy/
        cd before-deploy && zip -r before-deploy *
        cd ../ && mkdir -p deploy
        mv before-deploy/before-deploy.zip deploy/$PROJECT_NAME.zip
      shell: bash

    #12. 프로젝트 이름으로 해당 폴더를 모두 압축시킨다.
    - name: Make zip file
      run: zip -r ./$PROJECT_NAME.zip .
      shell: bash

    #13. aws에 해당 키 값으로 접속을 진행한다.
    - 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: ap-northeast-2

    #14. s3 에 프로젝트 이름에 해당하는 폴더에 zip파일을 저장한다.
    - name: Upload to S3
      run: aws s3 cp --region ap-northeast-2 ./deploy/$PROJECT_NAME.zip s3://$S3_BUCKET_NAME/$PROJECT_NAME/$PROJECT_NAME.zip  #(14)

    #16. 배포 그룹으로 해당 애플리케이션을 보낸다.
    - name: Code Deploy
      run: aws deploy create-deployment --application-name $CODE_DEPLOY_APP_NAME --deployment-config-name CodeDeployDefault.AllAtOnce --deployment-group-name $CODE_DEPLOY_GROUP_NAME --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$PROJECT_NAME/$PROJECT_NAME.zip

모든 설정이 완료되었으면 Github로 커밋과 푸시를 진행한다.

배포된 애플리케이션이 정상적으로 동작하고 있는지 확인하려면 EC2에서 애플리케이션 프로세스가 잘 떠있는지 확인해보면 된다. 애플리케이션의 pid를 조회해보자.

### 해당 위치에 어플리케이션이 잘 배포되었다.
$ ls -l ~/app/step2/zip/
total 44884
-rw-r--r-- 1 ec2-user ec2-user      290 Mar 28 20:49 appspec.yml
-rw-r--r-- 1 ec2-user ec2-user     1064 Mar 28 20:49 deploy.sh
-rw-r--r-- 1 ec2-user ec2-user 45919183 Mar 28 20:49 springboot2-webservice-1.0-SNAPSHOT.jar
-rw-r--r-- 1 ec2-user ec2-user    32249 Mar 28 20:49 springboot2-webservice-1.0-SNAPSHOT-plain.jar

### <pgrep -f + jar-파일명> 으로 pid를 조회할 수 있다.
$ pgrep -f springboot2-webservice-1.0-SNAPSHOT.jar
30322

### 배포 로그는 아래 경로에서 확인할 수 있다.
$ ls -l /opt/codedeploy-agent/deployment-root/
total 0
drwxr-xr-x 7 root root 101 Mar 28 20:56 2d379fa4-d97c-4e1e-ab62-447c6d7d1662
drwxr-xr-x 2 root root 247 Mar 28 20:57 deployment-instructions
drwxr-xr-x 2 root root  46 Mar 28 20:49 deployment-logs
drwxr-xr-x 2 root root   6 Mar 28 20:57 ongoing-deployment

$ tail /opt/codedeploy-agent/deployment-root/deployment-logs/codedeploy-agent-deployments.log
[2023-03-28 21:04:35.347] [d-C8V7GRCMM]LifecycleEvent - ApplicationStart
[2023-03-28 21:04:35.348] [d-C8V7GRCMM]Script - deploy.sh
[2023-03-28 21:04:35.384] [d-C8V7GRCMM][stdout]> Build 파일 복사
[2023-03-28 21:04:35.550] [d-C8V7GRCMM][stdout]> 현재 구동 중인 애플리케이션 pid 확인
[2023-03-28 21:04:35.556] [d-C8V7GRCMM][stdout]현재 구동 중인 애플리케이션 pid:
[2023-03-28 21:04:35.556] [d-C8V7GRCMM][stdout]> 현재 구동 중인 애플리케이션이 없으므로 종료하지 않습니다
[2023-03-28 21:04:35.556] [d-C8V7GRCMM][stdout]> 새 애플리케이션 배포
[2023-03-28 21:04:35.559] [d-C8V7GRCMM][stdout]> JAR_NAME: /home/ec2-user/app/step2/springboot2-webservice-1.0-SNAPSHOT-plain.jar
[2023-03-28 21:04:35.559] [d-C8V7GRCMM][stdout]> /home/ec2-user/app/step2/springboot2-webservice-1.0-SNAPSHOT-plain.jar 에 실행권한 추가
[2023-03-28 21:04:35.560] [d-C8V7GRCMM][stdout]> /home/ec2-user/app/step2/springboot2-webservice-1.0-SNAPSHOT-plain.jar 실행

마지막으로 Git Hub Action으로 이동해, 우선 기존에 진행했던 action을 모두 삭제하여 정리해준다.

9. 최종 확인

commit 변경점이 확인하고 싶다면, index.mustach의 타이틀을 ver.3으로 변경한다.

코드 변경 사항이 깃허브에 커밋과 푸쉬가 완료되었다면, Github Actions에서 빌드가 완료되고 배포가 성공적으로 완료되고 정상적으로 업데이트 됨을 확인 할 수 있다.

EC2 도메인으로 접속하면 변경된 화면을 확인할 수 있다.

참고

도서 - 스프링 부트와 AWS로 혼자 구현하는 웹 서비스

깃허브 이슈 - github actions과 CodeDeploy 연동 가이드

블로그 - AWS IAM 서비스 개념

블로그 - appspec.yml: AWS CodeDeploy 설정 파일

0개의 댓글