본문 바로가기

카테고리 없음

Github actions, AWS Codedeploy, Docker blue/green 무중단 배포

name: Spring Boot CI/CD with Gradle

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

jobs:
  build:

    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
    - uses: actions/checkout@v4
    - name: Set up JDK 21
      uses: actions/setup-java@v3
      with:
        java-version: '21'
        distribution: 'temurin'

    # Gradle Build를 위한 권한 부여
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew

    # Gradle Build (test 제외)
    - name: Build with Gradle
      run: ./gradlew clean build --exclude-task test
      
    # DockerHub 로그인
    - name: DockerHub Login
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_PASSWORD }}
    # Docker 이미지 빌드
    - name: Docker Image Build
      run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.PROJECT_NAME }} .

    # DockerHub Push
    - name: DockerHub Push
      run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.PROJECT_NAME }}

    # EC2 인스턴스 접속 및 애플리케이션 실행
    - name: Application Run
      uses: appleboy/ssh-action@v0.1.6
      with:
        host: ${{ secrets.EC2_HOST }}
        username: ${{ secrets.EC2_USERNAME }}
        key: ${{ secrets.EC2_KEY }}

        script: |
          sudo docker kill ${{ secrets.PROJECT_NAME }}
          sudo docker rm -f ${{ secrets.PROJECT_NAME }}
          sudo docker rmi ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.PROJECT_NAME }}
          sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.PROJECT_NAME }}

          sudo docker run -p 8088:8088 \
          --name ${{ secrets.PROJECT_NAME }} \
          -e SPRING_DATASOURCE_URL=${{ secrets.POSTGRESQL_URL }} \
          -e SPRING_DATASOURCE_USERNAME=${{ secrets.POSTGRESQL_USERNAME }} \
          -e SPRING_DATASOURCE_PASSWORD=${{ secrets.POSTGRESQL_PASSWORD }} \
          -e SPRING_REDIS_HOST=${{ secrets.REDIS_HOST }} \
          -e SPRING_REDIS_PORT=${{ secrets.REDIS_PORT }} \
          -e JWT_ACCESS_KEY=${{ secrets.JWT_ACCESS_KEY }} \
          -e JWT_REFRESH_KEY=${{ secrets.JWT_REFRESH_KEY }} \
          -e KAKAO_TOKEN_URL=${{ secrets.KAKAO_TOKEN_URL }} \
          -e KAKAO_PROFILE_URL=${{ secrets.KAKAO_PROFILE_URL }} \
          -e KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }} \
          -e KAKAO_REDIRECT_URL=${{ secrets.KAKAO_REDIRECT_URL }} \
          -e KAKAO_CLIENT_SECRET=${{ secrets.KAKAO_CLIENT_SECRET }} \
          -e NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }} \
          -e NAVER_SECRET=${{ secrets.NAVER_SECRET }} \
          -d ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.PROJECT_NAME }}

 

현재 배포 프로세스 입니다.

 

기존 배포 프로세스 https://velog.io/@tilsong/%EC%BD%94%EB%93%9C-Push%EB%A1%9C-%EB%B0%B0%ED%8F%AC%EA%B9%8C%EC%A7%80-Github-Actions-Docker

 

 

환경변수 yml 부분을 파일로 따로 빼고 Load Balancing Blue / Green 무중단 배포로

 

아래와 같은 배포 프로세스로 리팩토링 진행해보겠습니다. 

 

 

 

 

1. Github Actions으로 S3에 업로드

 

 

S3 버킷 생성

 

IAM 사용자 생성

 

name: Deploy to Production

on:
  push:
    branches:
      - master

jobs:
  deploy:
    runs-on: ubuntu-latest

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

      - name: Set up JDK 21
        uses: actions/setup-java@v3
        with:
          java-version: '21'
          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/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 clean build --exclude-task test
        
      - 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://spring-book-bucket

 

github actions 를  push 하게 되면 Upload to S3 job 에 따라

 

방금 생성한 spring-book-bucket 버킷에 zip 한 파일이 업로드 됩니다.

 

 

 

2. AWS CodeDeploy 설정

 

 

권한 추가

 

 

EC2 IAM Role 생성

 

EC2에서 CodeDeploy를 사용하기 위해 IAM Role을 생성해줍니다.

 

IAM Role 은 주로 AWS 서비스들이 직접 다른 AWS 서비스를 제어하기 위해 사용됩니다.

 

 

IAM > 역할 > 역할 생성

 

AmazonEC2RoleforAWS-CodeDeploy 권한을 추가 -> 이름은 role-ec2-codedeploy로 지정 !

 

CodeDeploy IAM Role 생성

 

 

 

 

이름을 role-codedeploy 로 지정하고 생성 완료

 

 

EC2 IAM 역할 수정

 

 

해당 역할을 인스턴스에 연결 후 꼭 인스턴스 재부팅을 해주어야 나중에 CodeDeploy 배포에 아래처럼 성공할 수 있습니다. (재부팅안해줘서 실패한 흔적들,,)

 

 

 

 

EC2에 AWS CodeDeploy 에이전트 설치

 

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

 

# 시스템 패키지 업데이트
sudo apt update
sudo apt upgrade -y

# Ruby 설치
sudo apt install -y ruby

# AWS CLI를 사용해 S3에서 설치 스크립트 다운로드
aws s3 cp s3://aws-codedeploy-ap-northeast-2/latest/install . --region ap-northeast-2

# 설치 스크립트에 실행 권한 부여
chmod +x ./install

# CodeDeploy 에이전트 설치
sudo ./install auto

# CodeDeploy 에이전트 상태 확인
sudo service codedeploy-agent status

 

 

status 확인후 active 가 나오면 정상적으로 작동하는 것입니다.

 

 

CodeDeploy 설정

 

애플리케이션 생성

 

 

배포 그룹 생성

 

EC2 설정

 

디렉토리 생성

mkdir ~/app

 

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

 

 

docker 실행 중임을 확인했습니다.

 

appspec.yml 작성

 

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

 

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

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

hooks:
  ApplicationStart:
    - location: deploy.sh
      timeout: 60
      runas: ubuntu

 

 

deploy.sh 스크립트 작성

 

#!/bin/bash

cd /home/ubuntu/app

DOCKER_APP_NAME=spring
LOG_FILE="deploy.log"

# 실행중인 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 "$(date): blue up" >> $LOG_FILE
        docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml up -d --build >> $LOG_FILE 2>&1

        sleep 30

        docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml down >> $LOG_FILE 2>&1
        docker image prune -af >> $LOG_FILE 2>&1

# blue가 실행중이면 green up
else
        echo "$(date): green up" >> $LOG_FILE
        docker-compose -p ${DOCKER_APP_NAME}-green -f docker-compose.green.yml up -d --build >> $LOG_FILE 2>&1

        sleep 30

        docker-compose -p ${DOCKER_APP_NAME}-blue -f docker-compose.blue.yml down >> $LOG_FILE 2>&1
        docker image prune -af >> $LOG_FILE 2>&1
fi

 

저는 로그를 확인해보고 싶어서 deploy.log 파일을 설정해줬지만 생략하셔도 됩니다.

 

 

Github Actions Workflow에 CodeDeploy 설정 추가

 

name: Deploy to Production

on:
  push:
    branches:
      - master

jobs:
  deploy:
    runs-on: ubuntu-latest

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

      - name: Set up JDK 21
        uses: actions/setup-java@v3
        with:
          java-version: '21'
          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/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 clean build --exclude-task test
        
      - 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://spring-book-bucket

      - 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=spring-book-bucket,bundleType=zip,key=spring-build.zip

 

 

github actions 가 잘 작업을 마쳤음을 확인할 수 있습니다.

 

 

3. Application Load Balancer(ALB) 설정

 

서브 도메인 추가

 

Route53 > 호스팅 영역 > my-book-note-spring.link > 레코드 생성

 

 

ec2 public ip 주소에 대한 레코드를 생성해주면 됩니다. 

 

 

Certificate Manager SSL 인증서 발급

 

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

 

 

인증서 발급 후 레코드를 추가해주면 클릭하시면 Route 53 을 통해 자동으로 CNAME 이 등록되며 DNS 검증이 진행됩니다.

 

 

로드 밸런서 생성

 

 

 

target group 을 추가해주고 Load balancer 생성을 눌러주면 됩니다.

 

 

HTTPS 리스너 추가

 

 

아까 생성해준 인증서를 적용해줍니다.

 

 

 

  1. HTTPS 요청 수신:
    • 클라이언트가 https://api.my-book-note-spring.link 로 요청을 보냅니다.
    • 이 요청은 EC2 로드 밸런서의 HTTPS 리스너 (포트 443) 에서 수신됩니다.
  2. SSL 인증 및 처리:
    • 로드 밸런서는 요청을 수신하고, 설정된 SSL 인증서를 사용하여 HTTPS 연결을 인증합니다.
    • SSL/TLS 연결이 종료되고 요청이 안전하게 수신됩니다.
  3. Target Group(book-target-group) 으로 요청 전달:
    • 인증이 완료된 후, 로드 밸런서는 요청을 book-target-group으로 전달합니다.
    • book-target-group 은 80 포트에 연결된 인스턴스들로 구성되어 있습니다. 즉, 로드 밸런서는 HTTPS 요청을 HTTP로 변환하여 80 포트를 통해 대상 인스턴스에 전달합니다.
  4. 레코드 설정:
    • DNS 레코드(Route 53이나 다른 DNS 서비스에서) 에서 api.my-book-note-spring.link 에 대한 요청이 로드 밸런서의 주소로 트래픽 라우터 대상으로 설정되어 있습니다. -> 바로 밑에 Route53 ALB 연결 참고 
    • 이를 통해 클라이언트의 요청이 EC2 로드 밸런서로 적절히 routing 되는 흐름입니다!

 

 

Route53 ALB 연결

 

 

 

별칭을 활성화하고 Application/Classic Load Balancer에 대한 별칭을 선택하고 등록하면 됩니다.

 

4. Nginx 설치

 

sudo apt install nginx -y

sudo systemctl start nginx

 

nginx 설치하려고 보니 disk 용량 꽉차서 루트 볼륨을 늘려줍니다.

 

 

sudo growpart /dev/xvda 1

lsblk

 

파티션이 잘 적용되었고

 

 

sudo resize2fs /dev/xvda1

df -hT

 

root 가 확장되어 잘 적용됨을 확인할 수 있네요.

 

 

nginx 프록시 설정을 변경해줍니다.

 

sudo vim /etc/nginx/sites-available/my_site.conf

server {
        listen       80;
        listen       [::]:80;
        server_name  *.my-book-note-spring.link my-book-note-spring.link;
        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;
        }

}

sudo ln -s /etc/nginx/sites-available/my_site.conf /etc/nginx/sites-enabled/

sudo systemctl restart nginx

 

 

restart nginx 해주면 my-book-note-spring.link가

 

루트 도메인과 서브도메인 모두에 대한 트래픽을 nginx 가 처리하고 있음을 확인할 수 있습니다.

 

 

 

 

 

 

5. 무중단 배포 설정

기존의 배포 방식은 도커 컨테이너를 중단하고 새로운 컨테이너를 만들어 실행하기까지 서비스 중단이 일어나게 되므로 


두 개의 동일한 프로덕션 환경(Blue와 Green)을 구성하여, Green 환경에서 새 버전을 배포하고 트래픽을 전환하는 Blue-Green 배포 전략을 사용해보겠습니다.

 

 

Blue-Green 배포 방식

 

Docker Compose 파일 작성

 

docker-compose.blue.yml

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

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

 

 

docker-compose.green.yml

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

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

 

deploy.sh 수정

#!/bin/bash

cd /home/ubuntu/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/sites-available/my_site.conf
# 추가
upstream app-api {
        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  *.my-book-note-spring.link my-book-note-spring.link;
        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://app-api;
                proxy_redirect off;
        }
 }

 

 

SSL 인증 흐름

  1. 클라이언트 요청:
    • 클라이언트가 https://api.my-book-note-spring.link 에 요청을 보냅니다.
  2. EC2 로드 밸런서:
    • EC2 로드 밸런서는 443 포트에서 HTTPS 요청을 수신합니다.
    • 로드 밸런서는 미리 구성된 SSL 인증서를 사용하여 클라이언트와의 SSL 핸드셰이크를 수행합니다. 이 과정에서 로드 밸런서는 클라이언트의 요청을 인증하고, SSL 연결을 설정합니다.
  3. HTTP로 Nginx에 요청 전달:
    • SSL 핸드셰이크가 완료되면, EC2 로드 밸런서는 클라이언트의 요청을 Nginx로 전달합니다. 이 때, 로드 밸런서와 Nginx 간의 연결은 HTTP로 이루어집니다.
    • 따라서 Nginx는 SSL 인증서 없이 요청을 받습니다.
  4. Nginx의 역할:
    • Nginx는 클라이언트 요청을 받아 proxy_pass 설정에 따라 Upstream 서버(Spring 애플리케이션)로 요청을 전달합니다.
    • Nginx는 SSL 인증서를 가지지 않기 때문에, SSL 관련 처리나 인증을 수행하지 않고, 단순히 클라이언트의 요청을 업스트림으로 routing 해줍니다.

 

 

upstream 블록의 이름을 app-api 로 Docker Compose에서 사용한 서비스 이름과 일치시키면 됩니다.

proxy_pass 지시어에서도 docker compose service 명과 일치시켜  http://app-api 이런식으로 사용하시면 됩니다.

 

 

sudo service nginx restart

 

Nginx를 재시작합니다.

 

 

 

Swagger 엔드포인트로 접속하니 Swagger UI 가 성공적으로 나타났고 API 요청도 잘 전달됩니다.

 

확인

 

배포 중에 웹사이트 새로고침을 계속 하여도 접속에 이상이 없음을 확인할 수 있습니다.