[배포] dev 서버 CI/CD 정리

김태훈·2023년 8월 29일
0
post-thumbnail

소프트웨어 마에스트로 프로젝트 배포 및 dev 서버 운영 방식에 대해 정리한 포스트이다.

팀원 한명이 이를 도맡아 하였기 때문에, 궁금한 부분은 물어가면서, 그리고 직접 확인해보면서 정리하였다.

지금은 "dev"용 서버만 다루고 있음을 명시한다.
추후 "prod" 인프라도 정리할 예정이다.

지금의 배포 상황이다.
현 배포 상황에서 깊게 다룰 부분은 두가지 이다.
Proxy역할을 해주는 CI/CD, Nginx + public/private subnet을 깊게 다루겠다.

1) CI/CD

github action으로 CI/CD 과정을 거친다.
이를 담은 workflow yml 파일을 첨부한다.
yml 을 보면서 이야기해보자.

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle

name: Java CI/CD with Gradle In Develope Branch

on:
  push:
    branches: [ "develope" ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'corretto'
      
    - name: Remove previous snapshot.jar
      run: rm -rf ./build/libs
      
    - name: Add properties
      run: echo "${{ secrets.APPLICATION_DEV }}" > ./src/main/resources/application.yml
      
    - name: init with Gradle
      uses: gradle/gradle-build-action@v2
      
    - run: gradle init
      
    - name: Build with Gradle
      uses: gradle/gradle-build-action@v2
      with:
        gradle-version: 7.5.1
        arguments: build -x test
        
    - name: Docker build
      run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKER_DEV_REPO }} .
      
    - name: Login to Docker Hub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}
        
    - name: Docker push
      run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKER_DEV_REPO }}
      
    - name: Deploy to dev
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.HOST_DEV }}
        username: ec2-user
        key: ${{ secrets.PRIVATE_KEY }}
        script: |
            ssh spring sudo docker rm -f $(ssh spring sudo docker ps -qa)
            ssh spring sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKER_DEV_REPO }}
            ssh spring sudo docker run -dp 8080:8080 ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKER_DEV_REPO }}
            
    - name: Notify Slack
      uses: rtCamp/action-slack-notify@v2
      env:
        SLACK_COLOR: '#00FF00'
        SLACK_TITLE: 'Build and Deploy'
        SLACK_TEXT: 'Backend repository build and deployment process has been completed successfully.'
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}

1. trigger

현재 배포 branch 는 다음과 같다.
1. develope (dev 서버)
2. main (prod - 실 배포)

현재 포스트는 dev용 서버이므로, 위 github action workflow는
develope 브랜치에 push가 일어나면 자동으로 CI/CD의 과정을 거친다.
github action의 여러가지 스크립트 설명은 제외하고 jobs의 step 순서대로 그 과정을 설명하겠다.

2. Set up JDK 17

우리 레포지토리 branch 코드를 가져온다.
본 프로젝트는 JAVA 17을 사용하므로 이를 맞춰준다.

3. Remove previous snapshot.jar

처음에 이 과정이 왜 필요할까에 의문을 가졌다. 매번 새로운 VM에서 Github Actions가 작동하는게 아닐까 싶었다.
하지만 GitHub Actions는 컨테이너 기반으로 작동하기 때문에, 매번 새로운 가상 VM에서 작업하는 것이 아니다. 따라서 매 작업마다 완전히 새로운 VM을 생성하는 것이 아니라, 작업 컨테이너를 사용하여 작업을 수행한다. 그렇기에 여러 정보가 cache 되고, 빌드 파일이 남아있을 수 있다. 따라서 해당 작업을 필수로 진행한다.

4. Add properties

Github Actions Secret 변수들을 build 전에 미리 저장한다.

5. init with Gradle

우리 프로젝트는 gradle을 기반이므로 gradle을 활용하여 init 하는 과정을 거친다.

6. Build with Gradle

build 하고,

7. Docker build

Docker image를 만든다.
이때, Docker Hub에 해당 image파일을 push하는 과정을 거치기 위한 과정이 필요하다.

8. Login to Docker Hub

Docker Hub에 push하기 위해서 login 하는 과정이다.

9. Docker push

만들어진 docker image를 push 한다.

10. Deploy to dev

dev용 서버에 배포하는 과정이다.
AWS remote 서버 dev 서버에 배포과정을 거쳐야 하므로, ssh설정이 필요했다.
여기서 script파일이 나는 이해가 되지 않았었다.

ssh spring 이 뭐지?

script: |
   ssh spring sudo docker rm -f $(ssh spring sudo docker ps -qa)
   ssh spring sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKER_DEV_REPO }}
   ssh spring sudo docker run -dp 8080:8080 ${{secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKER_DEV_REPO }}

2) Nginx Proxy Server + Private/Public Subnet

ssh spring이 무엇을 의미하는지 알아보기 전에,
Nginx 구성 이유와, 설정 정보들을 알아야만 한다.

아무리 dev용 서버여도, 보안은 중요한 부분이다.
이를 위해, nginx용 컨테이너를 띄우고 있는 EC2 인스턴스를 구성하였다.

(참고)
아래 ecs-agent는 ecs optimized용 ami를 사용하였기 때문에 자동으로 띄워진 container이므로 무시해도 좋다.

위에 ~/nginx가 우리가 daemon으로 띄운 nginx용 컨테이너이다.
80으로 포트 매핑이 되어있다.

그러면

docker exec -it 컨테이너ID /bin/bash

해당 컨테이너ID로 bash로 접속해보자.

그 안에 nginx config 파일을 살펴보자.
이는 /etc/nginx 디렉토리에 존재한다.

user  nginx;
worker_processes  1;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    upstream next {
        server 10.~~;
    }
    upstream spring {
        server 10.~~;
    }
    server {
        listen 80;
        server_name localhost;

        location / {
            proxy_pass         http://next;
            proxy_http_version 1.1;
        }
        location /api {
            proxy_pass         http://spring;
            proxy_http_version 1.1;
        }
    }
    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;
    keepalive_timeout  65;
    include /etc/nginx/conf.d/*.conf;

spring 서버, next 서버 두가지 서버로 같은 vpc내에 존재하는 서버로 proxy역할을 한다.
기본적으로 nginx에서 여러 보안사항 및 cache역할을 담당하므로 (리버스 프록시), 실질적인 WAS서버 앞단에 이를 붙인다. 그래서 사람들이 우리 서비스를 이용하기 위해서는 인터넷 gateway와 연결되어 있는, nginx서버를 통해서 우리 서버와 통신하게끔 해야한다.
그렇기에, public subnet에 nginx용 ec2를, private subnet에 프론트, 백엔드 서버, RDS를 둔것이다.

그리고, server 블록에서는 80포트의 Nginx로 들어오는 요청 경로마다 다른 proxy를 거치도록 설정하였다. (아까 설정한 upstream 경로)
루트는 next를,
/api는 spring의 api서버를 거치게 한다.

그렇다면 private subnet에 존재하는 spring 서버에 자동배포를 하기 위해선 어떻게 해야할까?

3) private subnet에 접근하는 방법

답은 간단하다.
proxy 서버를 거치는 것이다.
서버에 deploy를 하기 위해서는, Nginx의 ec2에 접속하는 과정을 거쳐야 한다.
같은 VPC내에 존재하고, private subnet에 존재하는 서버의 인바운드 규칙이 nginx ec2와 통신이 가능케만 한다면, 문제 없을 것이다.

그렇다면 ssh spring 이 뜻하는 것은,
spring 서버를 접속하는 방식과 연관되있을 것이다.

Nginx용 ec2에 루트경로에 ssh폴더를 생성하고, config 파일을 생성하여,

다음과 같이 설정하였다.
위 private ip로 ssh접속을 ssh spring, ssh next로 할 수 있게 된다.
그리고, 해당 디렉토리 내에는 pem키의 키값들이 존재하는 것은 당연하겠다.
private ip는 그냥 찝찝해서 지웠다.

사실 public subnet에 존재하는 nginx에 접속만하면 털릴 수 있긴 한데, 결국 서버 key 값이 털리지 않게 하는 것이 중요하지 않을까?

dev서버 설정을 좀더 발전시켰습니다.
발전된 내용은 아래 포스트를 참고해주세요.
https://velog.io/@goat_hoon/%EB%B0%B0%ED%8F%AC-dev-%EC%84%9C%EB%B2%84-%EB%B3%80%EA%B2%BD-%EC%82%AC%ED%95%AD-%EC%A0%95%EB%A6%AC

profile
기록하고, 공유합시다

2개의 댓글

comment-user-thumbnail
2023년 8월 30일

많은 도움이 되었어요..!

1개의 답글