미소를뿌리는감자의 코딩

[Spring] 프로젝트 CI/CD 흐름 파악 본문

강의수강/[Spring]

[Spring] 프로젝트 CI/CD 흐름 파악

미뿌감 2024. 4. 3. 22:19
728x90

https://velog.io/@mminjg/Github-Actions-CodeDeploy%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-EC2-%EB%B0%B0%ED%8F%AC-%EC%9E%90%EB%8F%99%ED%99%94

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

Github main 브랜치에 PushGithub Actions에서 AWS S3에 빌드 파일 및 Dockerfile, deploy.sh 등 업로드Github Actions이 AWS CodeDeploy에 배포 요청CodeDeploy가 배포 실행도커 빌드 및 실행소스코드를

velog.io

 
주된 프로젝트 흐름은 이 분의 글을 통해 적용시켜나갔다.
 
github에서 push가 되게 되면 github actions가 발동되게 된다. 
github actions는 main에서 .github 폴더 --> workflows 폴더 --> main-deploy.yml 파일을 실행시키게 된다.
그 이유는 github repository > actions > set up a workflo 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 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          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
          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 ./docker-compose.blue.yml ./deploy/
          cp ./docker-compose.green.yml ./deploy/
          cp ./appspec.yml ./deploy/
          cp ./Dockerfile ./deploy/
          cp ./deploy.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://quokkax2

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

 
main에 push 가 되면 실행되게 되는 코드이다.

on:
  push:
    branches:
      - main

 
java 17버전으로 세팅을 해놓는다.

      - 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-

위 코드는 캐싱을 하는 과정이다. 
~/.gradle/caches 와 ~/.gradle/wrapper은 gradle의존성과 설정 파일을 저장하는 곳이라고 한다. 
이를 캐싱하므로서 다음번엔 더 빠르게 접근할 수 있도록 한다.
 
파일의 내용이 변했는지를 확인하기 위해 캐시 키를 생성해 준다.
 

      - name: Make application.yml
        run: |
          cd ./src/main/resources
          echo "${{ secrets.APPLICATION }}" > ./application.yml
        shell: bash

다음으로는 application.yml 파일을 동적으로 생성해 주는 과정이다.
github secrets 에 넣어주었던 application.yml 파일을 동적으로 넣어주는 과정이다.
왜냐하면 해당 파일은 보안상의 이유로 secrets에 저장해 두었기 때문이다.
 

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

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

다음으로 gradlew에 권한을 주어 실행 가능한 상태로 만들어 준다. 이후 gradlew을 build 한다.
gradlew 와 gradlew.bat은 프로젝트의 root directory에서 확인이 가능하다.
gradlew와 gradlew.bat의 차이점은 POSIX 호환 시스템(linux, macOS) gradlew.bat은 windows 시스템에서 사용된다는 점이 다르다.
해당 코드를 통해서 github actions workflow에서 POSIX 호환 시스템을 대상으로 gradle 프로젝트를 빌드하였음을 알 수 있다.
 
그렇다면 gradlew.bat은 필요가 없는 것인가?
아니다. window 개발자가 참여하게 된다면 gradlew.bat을 이용해서 gradle을 빌드해주어야 한다.
 

      - 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 ./deploy.sh ./deploy/
          cp ./build/libs/*.jar ./deploy/
          zip -r -qq -j ./spring-build.zip ./deploy

deploy 폴더를 만들고, 
1. docker-compose.blue.yml
2. docker-compose.green.yml
3. appspec.yml
4. Dokerfile
5. deploy.sh
를 압축해서 저장한다. 이 이외의 다른 파일들은 .jar파일을 생성하고, 이를 build/libs에 저장한다.
5개의 파일들은 여기에서 설명할 경우 흐름이 지저분해 질 수 있기 때문에 추후에 가서 다시 언급하도록 할 것이다.
 
압축한 zip 파일의 이름은 spring-build.zip이 되게 된다.
 

      - 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

IAM을 설정하면서 aws에서 얻은 secret key id 와 key를 secrets에 저장해 두었다.
 

      - name: Upload to S3
        run: |
          aws s3 cp \
            --region ap-northeast-2 \
            ./spring-build.zip s3://quokkax2

이후 S3에 spring-build.zip 파일을 업로드 하게 된다.
 

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

이후 이 명령어를 실행함으로서 github actions는 aws codeDeploy에게 배포를 요청하게 된다.
 
codeDeploy는 지정된 s3 버킷에서 spring-build.zip 파일을 가져와서 application 배포 그룹에 지정된 대상에 배포한다.
 

이렇게 aws의 codeDeploy를 확인해보면 애플리케이션에 spring-deploy가 들어가 있는 것을 확인할 수 있다.
즉, spring-deploy가 지정된 대상에 해당되며 여기에 배포를 진행하게 되는 것이다.
 
 + codeDeploy를 사용하기 위해 배포하는 환경에 codeDeploy Agent를 설치해야하고, 정의된 appspec.yml에 따라 동작하게 된다.
 
spring deploy라는 application에 spring-deploy-group이라는 배포 그룹을 만들어준다.
이후 배포 그룹에 IAM에서 만들어 두었던 role-codedeploy 를 서비스 역할로 입력해준다.

인스턴스의 태그는 위와 같다.

위는 spring-deploy application에서 spring-deploy-group이라는 배포 그룹에서 사용하고 있는 EC2 태그이다.
 
흐름에서 조금 벗어났는데, 다시 돌아와서 이야기 해보면, spring-deploy-group이라는 배포 그룹에 사용할 EC2 환경을 알려주기 위해 태그로서 알려준다.
해당 프로젝트의 경우 quokkax2라는 EC2의 태그를 적어주었다.
 
이제 ec2환경에서, 위에 조금 적어두었듯이 codeDeploy가 appspec.yml 파일을 실행시키게 된다.
appspec.yml 파일 내용은 아래와 같다.
( codeDeploy -> appspec.yml -> deploy.sh )

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]
이제 ec2에서 /home/ec2-user/app으로 이동해서 overwrite를 진행하게 된다.
files 섹션에서 spring-build.zip 파일을 다운로드하고 압축을 해제한다. 즉 위에서 github actions에서 압축해놓았던 것들을 풀어서 /app 폴더에다가 넣어주는 과정이다.
 
[permissions]
다음으로 permissions는 권한 설정과 관련된 상목들의 리스트를 의미한다.
/는 root디렉토리를 의미하며 최상위 디렉토리를 의미하기 때문에, 모든 파일과 디렉토리에 권한 설정을 적용하겠다는 것을 의미한다.
 
[hooks]
다음으로 hooks는 특정 시점에 실행되어야 하는 명령이나 스크립트에 대한 설정을 포함하는데, 
application이 시작되면, deploy.sh를 실행할 것을 명시하고 있다.
 
여기서 deploy.sh에 대해서 알아보기 전에, 
spring-build.zip파일에서 압축했었던 파일들을 하나씩 알아보도록 하자.
1. docker-compose.blue.yml
2. docker-compose.green.yml
3. appspec.yml
4. Dokerfile
5. deploy.sh
 
1. docker-compose.blue.yml

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

이는 docker compose를 사용한 컨테이너화된 애플리케이션의 설정 예시이다.
즉, 컨테이너를 정의하고 실행하기 위한 docker compose file이라고 할 수 있을 것 같다.
build: . 인데 즉 현재 디렉토리에 위치하고 있는 Dockerfile을 이용하여 빌드함을 나타내고 있다.
8081:8080이 의미하는 바는 8080포트로 서비스하고 있는 application에 8081 포트를 통해 접근이 가능함을 나타낸다.
즉, 외부의 8081 포트를 컨테이너 내부의 8080포트로 연결함을 나타낸다. 이를 통해 외부에서 내부 애플리케이션에 access 할 수 있도록 한다.
 
2. docker-compose.green.yml

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

Docker-compose.blue와 동일한 반면 다른 부분은 외부의 8082 포트를 내부의 8080 포트로 연결함을 나타낸다는 점이 다르다.
 
3. appspec.yml
위에서 언급 하였다. 
이는 codeDeploy가 가장 먼저 찾아서 ec2에서 실행하게 되는 .yml 파일이다.
 
4. Dockerfile
docker-compose.blue와 docker-compose.green의 컨테이너를 정의하고 실행하기 위한 Dockerfile에 대해서 알아보자.

FROM openjdk:17

WORKDIR /classmate

COPY classmate-0.0.1-SNAPSHOT.jar app.jar

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

 
jdk 17을 사용할 것을 명시하고 있으며, 컨테이너 이미지를 빌드할 때, java application이 포함된 컨테이너 이미지를 생성하게 된다.
즉, dockerfile은 컨테이너 이미지를 정의하는 script이고 이를 이용해서 컨테이너 이미지를 빌드하고 생성한다.
 
5. deploy.sh
애플리케이션이 실행되면 실행되는 파일인 deploy.sh에 대해서 알아보자.

#!/bin/bash

cd /home/ec2-user/app

DOCKER_APP_NAME=spring

# 실행중인 blue가 있는지
EXIST_BLUE=$(docker ps --filter "name=spring-blue" --format "{{.ID}}")

# 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

해당 파일은 실행 중인 블루가 있는지 확인하고 이를 EXIST_BLUE에다가 유무를 저장해 둔다.
 
만약 green이 실행 중이면 blue up을 echo 하고, docker-compose.blue.yml을 빌드한다.
이후 green은 종료시킨 후, 사용하지 않는 이미지는 삭제해준다.
else는 그 반대로 동일하다.
 
이제 nginx에 대해서 알아볼 것이다.
nginx를 ec2에 설치하고, nginx.conf로 프록시 설정 변경을 해준다.

nignx.conf 파일은 다음과 같다.

# For more information on configuration, see:
#   * Official English Documentation: http://nginx.org/en/docs/
#   * Official Russian Documentation: http://nginx.org/ru/docs/

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
    worker_connections 1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    keepalive_timeout   65;
    types_hash_max_size 4096;

    include             /etc/nginx/mime.types;
    default_type        application/octet-stream;

    # Load modular configuration files from the /etc/nginx/conf.d directory.
    # See http://nginx.org/en/docs/ngx_core_module.html#include
    # for more information.
    include /etc/nginx/conf.d/*.conf;

    upstream quokkax2-server {
        least_conn;
        server localhost:8081 max_fails=3 fail_timeout=30s;
        server localhost:8082 max_fails=3 fail_timeout=30s;
    }

    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;

        error_page 404 /404.html;
        location = /404.html {
        }

        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
        }

	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://quokkax2-server;
                proxy_redirect off;

		proxy_buffer_size          128k;
      		proxy_buffers              4 256k;
      		proxy_busy_buffers_size    256k;
        }

    }

# Settings for a TLS enabled server.
#
#    server {
#        listen       443 ssl http2;
#        listen       [::]:443 ssl http2;
#        server_name  _;
#        root         /usr/share/nginx/html;
#
#        ssl_certificate "/etc/pki/nginx/server.crt";
#        ssl_certificate_key "/etc/pki/nginx/private/server.key";
#        ssl_session_cache shared:SSL:1m;
#        ssl_session_timeout  10m;
#        ssl_ciphers PROFILE=SYSTEM;
#        ssl_prefer_server_ciphers on;
#
#        # Load configuration files for the default server block.
#        include /etc/nginx/default.d/*.conf;
#
#        error_page 404 /404.html;
#        location = /404.html {
#        }
#
#        error_page 500 502 503 504 /50x.html;
#        location = /50x.html {
#        }
#    }

}

 
[server {} ]
이는 80 포트로 들어오면, https가 아닐 경우엔 https로 redirection 되도록 한다
root의 경우 정적 파일에 대한 루트 디렉토리를 의미한다.
 

include /etc/nginx/conf.d/*.conf;

conf.d 폴더에 있는 모든 *.conf 파일을 포함하도록 지정. 현재 서버에는 추가적인 conf 파일은 해당 디렉토리에 없다.
 
[location /] 
location 부분의 다른 부분들은 공부가 필요할 것 같고, 
proxy_pass http://quokkax2-server; 이 부분은 upstream에서 결정한다.
 

    upstream quokkax2-server {
        least_conn;
        server localhost:8081 max_fails=3 fail_timeout=30s;
        server localhost:8082 max_fails=3 fail_timeout=30s;
    }

upstream에서 8081이 살아있다면 8081로, 8082가 살아있다면 8082로 보내준다.

728x90

'강의수강 > [Spring]' 카테고리의 다른 글

Class vs. Instance  (0) 2024.04.11
JPA의 필요성  (0) 2024.04.11
[Spring] 도커를 이용한 배포  (0) 2024.04.03
[Spring] S3 이용 - 이미지 등록, 조회 하기  (1) 2024.03.22
[Spring] JPA n+1 문제  (0) 2024.03.20