Github Actions, CodeDeploy를 이용한 EC2 무중단 배포 자동화

mminjg·2022년 8월 18일
8
post-thumbnail

소스파일을 S3에 올리고, CodeDeploy를 이용하여 EC2에 스프링 도커 컨테이너를 만들어 배포하고자 한다.

과정
1. Github main 브랜치에 Push
2. Github Actions에서 AWS S3에 빌드 파일 및 Dockerfile, deploy.sh 등 업로드
3. Github Actions이 AWS CodeDeploy에 배포 요청
4. CodeDeploy가 배포 실행
5. 도커 빌드 및 실행

Github Actions으로 S3에 업로드

버킷 생성

소스코드를 저장할 버킷을 생성한다.

IAM 사용자 추가

IAM > 사용자 > 사용자 추가

사용자 이름 설정

사용자 이름은 github-action-s3-codedeploy으로 설정하였다.

정책 추가

github actions에서 AWS S3, CodeDeploy에 접근할 권한이 필요하다. AmazonS3FullAccessAWSCodeDeployFullAccess를 선택하여 추가한다.

완료하고 Access key ID와 Access Key를 확인한다.

Github Secrets 설정

  • APPLICATION: application.yml의 내용
  • AWS_ACCESS_KEY_ID: Access key ID
  • AWS_SECRET_ACCESS_KEY: Secret access key

Github Actions Workflow

Github Repository > Actions > set up a workflow yourself
main-deploy.yml 파일을 작성한다.

name: Deploy to Production

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source code
        uses: actions/checkout@master

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

      - name: Gradle Caching
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: Make application.yml
        run: |
          cd ./src/main
          mkdir resources
          cd ./resources
          touch ./application.yml
          echo "${{ secrets.APPLICATION }}" > ./application.yml
        shell: bash
        
      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Build with Gradle
        run: ./gradlew build
        
      - name: Make zip file
        run: |
          mkdir deploy
          cp ./build/libs/*.jar ./deploy/
          zip -r -qq -j ./spring-build.zip ./deploy
          
      - 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: Upload to S3
        run: |
          aws s3 cp \
            --region ap-northeast-2 \
            ./spring-build.zip s3://tcat-spring
  • Make application.yml: Github Secrets의 저장된 secrets.APPLICATION으로 application.yml 파일을 만든다.

  • Make zip file: 압축을 위한 deploy 디렉토리를 만들어 .jar 파일을 복사한 후 spring-build.zip 파일을 만든다.

  • Upload to S3: tcat-spring 버킷에 zip파일을 업로드 한다.

버킷에 올라온 zip파일을 확인할 수 있다.

AWS CodeDeploy 설정

CodeDeploy는 Amazon EC2 인스턴스, 온프레미스 인스턴스, 서버리스 Lambda 함수 또는 Amazon ECS 서비스로 애플리케이션 배포를 자동화하는 배포 서비스이다.

배포하는 환경에 CodeDeploy Agent를 설치해야 하며, 배포 시 정의한 appspec.yml에 따라 동작하게 된다.

EC2 IAM Role 생성

EC2에서 CodeDeploy를 사용하기 위해 IAM Role을 생성한다. IAM Role은 주로 AWS 서비스들이 직접 다른 AWS 서비스를 제어하기 위해 사용한다.

IAM > 역할 > 역할 생성

AmazonEC2RoleforAWS-CodeDeploy 권한을 추가한다.

이름은 role-ec2-codedeploy로 지정하였다.

CodeDeploy IAM Role 생성

마찬가지로 CodeDeploy도 IAM Role을 생성한다.

사용사례에서 CodeDeploy를 선택한다.

권한이 자동으로 추가되어있다.

이름을 role-codedeploy로 지정하고 생성한다.

EC2 IAM 역할 수정

EC2 > 인스턴스 > 작업 > 보안 > IAM 역할 수정

IAM 역할에 role-ec2-codedeploy 지정하고 인스턴스를 재부팅한다.

EC2에 AWS CodeDeploy 에이전트 설치

CodeDeploy를 이용하려면 배포하는 환경에 CodeDeploy Agent를 설치해야한다.

# 설치
sudo yum -y update
sudo yum install -y ruby
aws s3 cp s3://aws-codedeploy-ap-northeast-2/latest/install . --region ap-northeast-2
chmod +x ./install
sudo ./install auto

# 확인
sudo service codedeploy-agent status

The AWS CodeDeploy agent is running as PID ~ 가 나오면 정상적으로 작동하는 것이다.

CodeDeploy 설정

애플리케이션 생성

CodeDeploy > 애플리케이션 > 애플리케이션 생성

애플리케이션 이름을 입력하고 컴퓨팅 플랫폼에 EC2를 선택하여 애플리케이션 생성을 완료한다.

배포 그룹 생성

CodeDeploy > 애플리케이션 > spring-deploy > 배포 그룹 > 배포 그룹 생성

배포 그룹 이름을 입력하고 서비스 역할에 만들어 둔 role-codedeploy를 지정한다.
배포 유형은 배포할 서비스가 1대이기 때문에 현재 위치를 선택한다.

환경 구성에 Amazon EC2 인스턴스를 선택하고 태그를 추가한다. 이 태그는 배포하려는 EC2 인스턴스의 태그와 동일하여야 한다.

배포 구성은 다음을 참고한다. 한번 배포할 때, 모든 인스턴스에 배포하도록 AllAtOnce를 선택한다.
로드 밸런싱 활성화는 해제한다.

EC2 설정

디렉토리 생성

mkdir ~/app

S3에 있는 소스를 내려받을 app 폴더를 생성한다.

도커 설치

# 도커 설치
sudo yum install docker -y
docker -v

# 도커 시작
sudo service docker start

# 확인
systemctl status docker.service

도커 실행을 위해 도커를 설치한다.

appspec.yml 작성

CodeDeploy에서 배포가 실행되면 이 appspec.yml파일에 따라 동작하게 된다.

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

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

hooks:
  ApplicationStart:
    - location: deploy.sh
      timeout: 60
      runas: ec2-user
  • files-destination: s3 zip 파일이 저장될 위치이다.
  • hooks-ApplicationStart: ApplicationStart단계에서 deploy.sh를 실행한다.

deploy.sh 작성

#!/bin/bash

echo "> 현재 실행 중인 Docker 컨테이너 pid 확인" >> /home/ec2-user/deploy.log
CURRENT_PID=$(sudo docker container ls -q)

if [ -z $CURRENT_PID ]
then
  echo "> 현재 구동중인 Docker 컨테이너가 없으므로 종료하지 않습니다." >> /home/ec2-user/deploy.log
else
  echo "> sudo docker stop $CURRENT_PID"   # 현재 구동중인 Docker 컨테이너가 있다면 모두 중지
  sudo docker stop $CURRENT_PID
  sleep 5
fi

cd /home/ec2-user/app
sudo docker build -t tcat-api-spring-boot-docker .
sudo docker run -d -p 8080:8080 tcat-api-spring-boot-docker

실행 중인 도커 컨테이너가 있다면 중지하고, 도커 이미지를 빌드하고 컨테이너를 실행하게 된다.

Dockerfile 작성

FROM openjdk:11

WORKDIR /tcat

COPY tcat-0.0.1-SNAPSHOT.jar app.jar

ENTRYPOINT ["java","-jar","app.jar"]

jar 파일을 실행하는 간단한 Dockerfile을 작성한다.

Github Actions Workflow에 CodeDeploy 설정 추가

main-deploy.yml에 CodeDeploy 설정을 추가한다.

name: Deploy to Production

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source code
        uses: actions/checkout@master

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

      - name: Gradle Caching
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-
      - name: Make application.yml
        run: |
          cd ./src/main
          mkdir resources
          cd ./resources
          touch ./application.yml
          echo "${{ secrets.APPLICATION }}" > ./application.yml
        shell: bash
        
      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Build with Gradle
        run: ./gradlew build
        
	  # 수정
      - name: Make zip file
        run: |
          mkdir deploy
          cp ./appspec.yml ./deploy/
          cp ./Dockerfile ./deploy/
          cp ./scripts/*.sh ./deploy/
          cp ./build/libs/*.jar ./deploy/
          zip -r -qq -j ./spring-build.zip ./deploy
          
      - 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: Upload to S3
        run: |
          aws s3 cp \
            --region ap-northeast-2 \
            ./spring-build.zip s3://tcat-spring
      
	  # 추가
      - name: Code Deploy
        run: aws deploy create-deployment --application-name spring-deploy
          --deployment-config-name CodeDeployDefault.OneAtATime
          --deployment-group-name spring-deploy-group
          --s3-location bucket=tcat-spring,bundleType=zip,key=spring-build.zip
  • Make zip file: appspec.yml, Dockerfile, scripts/deploy.sh을 zip 파일에 추가한다.
  • Code Deploy: CodeDeploy를 실행한다.

Application Load Balancer(ALB) 설정

서브 도메인 추가

Route5e > 호스팅 영역 > my-tcat.com > 레코드 생성

값에는 EC2의 IP 주소를 넣어두었다. ALB 설정을 마치면 변경할 것이다.

Certificate Manager SSL 인증서 발급

AWS Certificate Manager > 인증서 > 인증서 요청

인증서를 발급받고 'Route 53에서 레코드 생성' 버튼을 눌러 레코드를 생성한다.

로드 밸런서 생성

EC2 > 로드 밸런싱 > 로드 밸런서 생성

Application Load Balancer를 선택한다.

Network mapping에서는 가용영역을 선택한다.
Security groupts에서는 EC2와 같은 보안그룹을 선택해준다.

Create target group으로 타켓 그룹을 생성한다.

이름을 지정하고 프로토콜에 HTTP, Port에 80을 입력한다.

include as pending below를 누르고, Create target group으로 타겟 그룹 생성을 완료한다.

생성한 타겟그룹으로 forward 해주고 Create load balancer를 눌러 로드 밸런서 생성을 완료한다.

로드 밸런서 리스너 설정

로드 밸런서 > 리스너
HTTP 리스너의 규칙 편집을 선택하여 규칙을 편집한다.

HTTP 리스너 규칙 편집

규칙 THEN 부분에 리다렉션을 설정하고 HTTPS, 443 을 입력하고 업데이트를 완료한다.

HTTPS 리스너 추가


HTTPS, 443을 입력하고 forward to에 생성한 타겟 그룹을 지정한다.

생성한 SSL인증서를 지정하고 리스너 추가를 완료한다.

Route53 ALB 연결

Route5e > 호스팅 영역 > my-tcat.com > 레코드 > 레코드 편집

별칭을 선택하고 Application/Classic Load Balancer에 대한 별칭을 선택하고 등록한다

Nginx 설정

Nginx 설치

# 설치
sudo amazon-linux-extras install nginx1

# 동작
sudo service nginx start

EC2에 Nginx를 설치하고 동작시킨다.

프록시 설정 변경

sudo vim /etc/nginx/nginx.conf
server {
        listen       80;
        listen       [::]:80;
        server_name  _;
        root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        if ($http_x_forwarded_proto != 'https') {
                return 301 https://$host$request_uri;
        }

        location / {
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header HOST $http_host;
                proxy_set_header X-NginX-Proxy true;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_pass http://127.0.0.1:8080;
                proxy_redirect off;
        }

}

server 부분을 위와 같이 변경한다.
80포트로 들어오면 8080으로 전달해주는 설정이다.

sudo service nginx restart

Nginx를 재시작하면 정상적으로 작동되는 것을 확인할 수 있다.

413 오류 해결

payload가 너무 큰 경우 nginx에서 오류가 발생하기도 한다.

http {
    client_max_body_size 50M;

    ...
}

다음을 추가하여 준다.

무중단 배포 설정

기존의 배포 방식은 도커 컨테이너를 중단하고 새로운 컨테이너를 만들어 실행하기까지 서비스 중단이 일어나게 된다. 유저가 서비스 중단을 느끼지 못하도록 Blue-Green 배포 방식을 사용하여 무중단 배포를 진행하려 한다.

Blue-Green 배포 방식

배포 전

배포 후

Blue-Green 배포 방식이란 기존 버전을 운영하면서 신규 버전을 준비하여 신규 버전으로 전환하고 기존 버전을 중단하는 방식이다.

Docker Compose 파일 작성

docker-compose.blue.yml

#blue
version: '3'
services:
  tcat-api:
    build: .
    ports:
      - "8081:8080"
    container_name: spring-blue

blue 컨테이너는 port가 8081번으로 설정된다.

docker-compose.green.yml

#green
version: '3'
services:
  tcat-api:
    build: .
    ports:
      - "8082:8080"
    container_name: spring-green

green 컨테이너는 port가 8082번으로 설정된다.

deploy.sh 수정

#!/bin/bash

cd /home/ec2-user/app

DOCKER_APP_NAME=spring

# 실행중인 blue가 있는지
EXIST_BLUE=$(docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml ps | grep running)

# green이 실행중이면 blue up
if [ -z "$EXIST_BLUE" ]; then
	echo "blue up"
	docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml up -d --build

	sleep 30

	docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml down
	docker image prune -af # 사용하지 않는 이미지 삭제

# blue가 실행중이면 green up
else
	echo "green up"
	docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml up -d --build

	sleep 30

	docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml down
	docker image prune -af
fi

Github Actions Workflow 수정

main-deploy.yml 수정

      - name: Make zip file
        run: |
          mkdir deploy
          cp ./docker-compose.blue.yml ./deploy/
          cp ./docker-compose.green.yml ./deploy/
          cp ./appspec.yml ./deploy/
          cp ./Dockerfile ./deploy/
          cp ./scripts/*.sh ./deploy/
          cp ./build/libs/*.jar ./deploy/
          zip -r -qq -j ./spring-build.zip ./deploy
  • docker-compose.blue.yml, docker-compose.green.yml 파일을 복사하는 과정을 추가한다.

Nginx 설정 변경

sudo vim /etc/nginx/nginx.conf
# 추가
upstream tcat-api-server {
        least_conn;
        server 127.0.0.1:8081 max_fails=3 fail_timeout=10s;
        server 127.0.0.1:8082 max_fails=3 fail_timeout=10s;
    }

server {
        listen       80;
        listen       [::]:80;
        server_name  _;
        root         /usr/share/nginx/html;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        if ($http_x_forwarded_proto != 'https') {
                return 301 https://$host$request_uri;
        }

        location / {
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header HOST $http_host;
                proxy_set_header X-NginX-Proxy true;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                # 수정
                proxy_pass http://tcat-api-server;
                proxy_redirect off;
        }
 }
  • proxy_pass: 요청이 오면 upstrem tcat-api-server로 전달한다.
  • max_fails: 3만큼 요청이 fail한 경우 다른 서버에게 요청이 넘어간다.
  • fail_timeout: 30동안 서버가 응답하지 않으면 fail로 생각한다.
sudo service nginx restart

Nginx를 재시작한다.

확인

배포를 할 때마다 다음과 같이 port가 변경된다. 또한, 배포 중에 웹사이트 새로고침을 계속 하여도 접속에 이상이 없음을 확인할 수 있다.

참고

https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/welcome.html
https://loosie.tistory.com/424
https://loosie.tistory.com/425
https://galid1.tistory.com/745
https://devlog-wjdrbs96.tistory.com/291
https://medium.com/@vdongbin/aws-elb와-nginx로-https-서버-구축하기-736b8c5ee76
https://saksin.tistory.com/1388
https://webisfree.com/2018-03-29/nginx-413-request-entity-too-large-에러-해결하기-파일-업로드-사이즈
https://subicura.com/2016/06/07/zero-downtime-docker-deployment.html
https://velog.io/@jeff0720/Travis-CI-AWS-CodeDeploy-Docker-로-배포-자동화-및-무중단-배포-환경-구축하기-2

1개의 댓글

comment-user-thumbnail
2024년 3월 15일

안녕하세요. 글 정말 잘 읽었습니다!
스프링부트를 docker로 실행하고 nginx는 서버에 직접 설치한 상황에서 스프링주트 redirect 주소를 127.0.0.1로 설정해도 되는 건가요?
docker는 127.0.0.1이 아닌 고유의 ip 주소가 있는 걸로 알고 있는데 제가 잘못 알고 있는건지 헷갈려서 여쭤봅니다..

답글 달기