GitHub Actions + Docker + AWS ECR + AWS EB를 활용한 무중단 배포

Jung·2021년 12월 12일
2

TIL

목록 보기
62/77
post-thumbnail

스프링 프로젝트를 Docker 이미지로 빌드해서 AWS ECR에 올리고 올라간 ECR의 이미지를 기반으로 AWS EB에 배포해보자!

GitHub Actions를 사용하기 위해서는 프로젝트의 최상단 경로에 .github라는 디렉토리를 만들고 그 아래에 workflows 디렉토리를 만든다. 이제 그 안에 yml 파일을 생성한다.

예시
.github/workflows/example.yml

일단 yml 파일의 내용은 아래와 같다.

name: Build and Push Docker Image
on:
  push:
    branches:
      - deploy
jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Set up JDK 1.8
        uses: actions/setup-java@v1
        with:
          java-version: 1.8

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Build with Gradle
        run: ./gradlew 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: ap-northeast-2

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ECR 리포지토리 이름
          IMAGE_TAG: ECR에 올라갈 이미지의 태그
        run: |
          docker buildx build --platform=linux/amd64 -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
          
      - name: Get current time
        uses: 1466587594/get-current-time@v2
        id: current-time
        with:
          format: YYYYMMDD_HH-mm-ss
          utcOffset: "+09:00"

      - name: Generate deployment package
        run: |
          mkdir -p deploy
          cp Dockerrun.aws.json deploy/Dockerrun.aws.json
          cd deploy && zip -r deploy.zip .
          
      - name: Beanstalk Deploy
        uses: einaregilsson/beanstalk-deploy@v14
        with:
          aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          application_name: AWS EB 애플리케이션 이름
          environment_name: AWS EB 환경 이름
          version_label: earth-docker-${{steps.current-time.outputs.formattedTime}}
          region: ap-northeast-2
          deployment_package: deploy/deploy.zip
          wait_for_environment_recovery: 200

📌 1

name: Build and Push Docker Image
on:
  push:
    branches:
      - deploy

Build and Push Docker Image라는 이름을 갖는 Workflow는 deploy 브랜치에 Push 될 경우 경우 실행된다.

📌 2

jobs:
  build-and-push-image:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      ... 

Workflow는 다양한 job으로 구성된다. 최소 한 개 이상의 job을 정의해야 한다.
build-and-push-image라는 job을 생성하고, 그 아래에 steps가 존재하는 구조다.
runs-on은 어떤 OS에서 실행될지 지정하는 것으로, ubuntu-latest최신 버전의 우분투 환경으로 지정했다.
stepsuses는 어떤 액션을 사용할지 지정하는 것이다. 이미 만들어진 액션을 사용할 때 지정한다.

actions/checkout@v2는 Workflow에서 접근할 수 있도록 리포지토리를 체크아웃하는 데 사용되는 공식 GitHub 작업이다.

체크아웃: 저장소(리포지토리)에서 파일을 받아오는 것
체크인: 체크아웃으로 받아온 파일을 수정 후, 저장소(리포지토리)를 새로운 버전으로 갱신하는 것

📌 3

- name: Set up JDK 1.8
  uses: actions/setup-java@v1
  with:
    java-version: 1.8

자바 버전을 설정한다. 1.8은 Java 8이라는 것인데 Java 11부터는 11로 쓰고 있다.
https://docs.gradle.org/current/javadoc/org/gradle/api/JavaVersion.html 에서 봐도 Java 11부터 11로 쓰고 있고, Java 10까지는 1.x로 쓰고 있다는 것을 알 수 있다.

📌 4

- name: Grant execute permission for gradlew
  run: chmod +x gradlew

  - name: Build with Gradle
  run: ./gradlew clean build -x test

run에는 커맨드를 넣으면 된다.
graldew 커맨드를 실행할 수 있는 권한을 부여한다. ./gradlew clean build -x test는 테스트 없이 빌드한다는 뜻이다.

📌 5

- 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

AWS 자격을 증명하는 것이다. AWS iam 사용자의 access keysecret key를 넣으면 되는데 이 부분은 외부에 노출되면 아주 위험하므로 GitHub의 Secrets를 이용한다. 리포지토리 Settings -> Secrets로 가면 설정할 수 있다.

📌 6

- name: Login to Amazon ECR
  id: login-ecr
  uses: aws-actions/amazon-ecr-login@v1

- name: Build, tag, and push image to Amazon ECR
  id: build-image
  env:
    ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
    ECR_REPOSITORY: ECR 리포지토리 이름
    IMAGE_TAG: ECR에 올라갈 이미지의 태그
  run: |
    docker buildx build --platform=linux/amd64 -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
    docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
    echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"

로컬 Docker 클라이언트에서 하나 이상의 Amazon ECR 레지스트리에 로그인한다.
도커 이미지로 빌드하고 ECR 리포지토리에 push 한다.

AWS EB로 생성되는 EC2는 linux/amd64 환경이다.

FROM openjdk:8-jdk-alpine
COPY build/libs/*.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

도커 이미지로 빌드 시에 필요한 Dockerfile 파일이다.
alpine을 붙이면 생성된 도커 이미지의 용량이 더 작다.
gradle로 프로젝트를 빌드해서 생성된 jar 파일을 도커 이미지로 빌드할 때 필요한 파일이다.

gradle로 빌드하면 jar 파일이 2개가 생성되는데 하나는 plain.jar다. plain.jar는 의존성 관련 내용이 포함되어 있지 않고 모듈의 클래스와 리소스만 포함하는 파일이다.
(참고: https://stackoverflow.com/questions/67935064/difference-between-spring-boot-2-5-0-generated-jar-and-plain-jar)

build.gradle

jar {
    enabled = false
}

를 작성하면 gradle build 시에 plain.jar가 생성되지 않는다.

📌 7

- name: Get current time
  uses: 1466587594/get-current-time@v2
  id: current-time
  with:
    format: YYYYMMDD_HH-mm-ss
    utcOffset: "+09:00"

우리나라 시간은 UTC 기준으로 9시간이 빠르기 때문에 9시간을 더한다.

📌 8

- name: Generate deployment package
  run: |
    mkdir -p deploy
    cp Dockerrun.aws.json deploy/Dockerrun.aws.json
    cd deploy && zip -r deploy.zip .

Dockerrun.aws.json 파일을 zip 파일로 만드는 과정이다.

{
  "AWSEBDockerrunVersion": "1",
  "Image": {
    "Name": "이미지 URI(AWS ECR에서 확인 가능)",
    "Update": "true"
  },
  "Ports": [
    {
      "ContainerPort": 8080
    }
  ]
}

Dockerrun.aws.json 파일이다. AWS ECR에 올라간 도커 이미지를 실행하는 데 필요한 설정을 하는 파일이다.

📌 9

- name: Beanstalk Deploy
  uses: einaregilsson/beanstalk-deploy@v14
  with:
    aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    application_name: AWS EB 애플리케이션 이름
    environment_name: AWS EB 환경 이름
    version_label: earth-docker-${{steps.current-time.outputs.formattedTime}}
    region: ap-northeast-2
    deployment_package: deploy/deploy.zip
    wait_for_environment_recovery: 200

아까 생성한 zip 파일을 AWS EB에 업로드 및 배포하는 과정이다.

도커 환경의 AWS EB를 만들고 애플리케이션에 업로드 및 배포를 하려면 아래 사진의 업로드 및 배포를 클릭해 Dockerrun.aws.json를 압축한 파일을 올리면 된다.

  • Dockerrun.aws.json만 올리는 경우에는 zip으로 압축하지 않고 올려도 된다고 하는데 아직 직접 해보지 않아 확실하진 않다.

위 사진처럼 AWS EB console에 들어가 수동적으로 업로드 및 배포를 할 수 있지만 GitHub Actions를 활용한 무중단 배포를 구축할 것이기 때문에 .github/workflows/example.yml 에 위 내용을 작성한다.

이제 99%의 작업은 끝났고 AWS EB의 구성 -> 보안으로 가서

이 역할에 권한을 하나 추가해야 한다. AWS IAM -> 역할로 가서 위 사진의 역할로 들어간다.

AmazonEC2ContainerRegistryReadOnly를 추가하면 정상적으로 작동이 될 것이다.

끝! 👍

+

EB 배포는 기본적으로 한 번에 모두 방법을 사용한다. 이는 아주 잠깐의 다운타임이 발생할 수 있고 이를 사용하면 위에서 설명한 방법이 무중단 배포라고 할 수 없을 것 같다. 한 번에 모두 방법은 사용되고 있는 인스턴스에 새로운 버전을 배포하기 때문에 잠깐의 다운타임이 생겼다고 하면 변경 불가능 방법은 새로운 인스턴스에 새로운 버전을 배포하고 이전 버전을 종료시킨다. 그래서 배포 방법을 변경 불가능으로 수정했다.

위 사진은 변경 불가능으로 수정하고 새로운 버전을 배포했을 때 인스턴스 상태 화면이다.

한 번에 모두(All at once) - 가장 빠른 배포 방법이다. 단기간의 서비스 손실이 허용될 수 있고 빠른 배포가 중요한 경우에 적합하다. 이 방법을 사용하면 Elastic Beanstalk에서 각 인스턴스에 새 애플리케이션 버전을 배포한다. 그런 다음 웹 프록시 또는 애플리케이션 서버를 다시 시작해야 할 수 있다. 결과적으로 짧은 시간 동안 사용자가 애플리케이션을 사용할 수 없거나 가용성이 감소할 수 있다.

롤링(Rolling) - 가동 중지를 방지하고 가용성 감소를 최소화하는 대신 배포 시간이 길어진다. 완전한 서비스 손실이 허용될 수 없는 경우에 적합하다. 이 방법을 사용하면 애플리케이션이 한 번에 한 인스턴스 배치로 사용자 환경에 배포된다. 배포 전반에 걸쳐 대부분의 대역폭이 유지된다.

추가 배치를 사용한 롤링(Rolling with additional batch) - 가용성 감소를 방지하지만 배포 시간이 롤링 방법보다도 오래 걸립니다. 배포 전반에 걸쳐 동일한 대역폭을 유지해야 하는 경우에 적합합니다. 이 방법을 사용하면 Elastic Beanstalk에서 추가 인스턴스 배치를 시작한 다음 롤링 배포를 수행합니다. 추가 배치를 시작하는 데는 시간이 걸리며 배포 전반에 걸쳐 동일한 대역폭이 유지됩니다.

변경 불가능(Immutable) - 기존 인스턴스를 업데이트하는 대신 새 애플리케이션 버전이 항상 새 인스턴스에 배포되도록 하는 더 느린 배포 방법입니다. 또한 배포가 실패할 경우 빠르고 안전하게 롤백할 수 있다는 추가 이점이 있습니다. 이 방법을 사용하면 Elastic Beanstalk에서 변경 불가능한 업데이트를 수행하여 애플리케이션을 배포합니다. 변경이 불가능한 업데이트에서 두 번째 Auto Scaling 그룹이 사용자 환경에서 시작되고 새 인스턴스가 상태 확인을 통과할 때까지 새 버전과 기존 버전이 함께 트래픽을 처리합니다.

트래픽 분할(Traffic splitting) - canary 테스트 배포 방법입니다. 이전 애플리케이션 버전을 통해 나머지 트래픽을 계속 처리하면서 수신 트래픽의 일부를 사용하여 새 애플리케이션 버전의 상태를 테스트하려는 경우에 적합합니다.

참고: https://docs.aws.amazon.com/ko_kr/elasticbeanstalk/latest/dg/using-features.deploy-existing-version.html

profile
97kim.github.io

2개의 댓글

comment-user-thumbnail
2022년 9월 11일

혹시 EB 환경은 어떻게 설정하셨는지 알 수 있을까요?

답글 달기
comment-user-thumbnail
2024년 8월 27일

감사합니다. 도움이 많이 되었습니다!

답글 달기