[CI/CD] Github Action & Docker & Springboot & Redis & Slack 빌드/배포 자동화

HeavyJ·2023년 4월 26일
1

자바/스프링부트

목록 보기
5/17
post-custom-banner

기존에 CI / CD 자동화는 Github Action & S3 & Code Deploy를 사용했습니다.

S3 & Code Deploy 사용해서 CI/CD 자동화하기

하지만, 이번에 Docker를 사용하는 방식으로 자동화 방법을 변경했습니다.

아래 3가지 이유 때문에 자동화 방법을 변경했습니다.

  • Docker를 사용해보고 싶었습니다.
    채용공고에도 Docker의 역량을 요구하는 기업들을 종종 볼 수 있어서 Docker를 사용해서 배포하는 경험이 필요하다고 생각했습니다.

  • Docker가 유연성과 확장성이 더 뛰어나다고 생각했습니다.
    S3 & Code Deploy 방식은 aws에 너무 종속적인 느낌이 듭니다. S3에 코드가 압축된 zip 파일을 올리고 code deploy가 ec2에 배포를 하는것이 유연성과 확장성에 불리하다고 생각합니다. 추가적으로 S3 & Code Deploy는 프로젝트의 모든 파일을 ec2에 배포하기 때문에 용량 효율성에도 안 좋다고 생각했습니다.

  • 협엽에 더 유리하다고 생각했습니다.
    현재는 1인 백엔드 개발을 하기 때문에 배포를 어떻게 하느냐는 크게 상관이 없을 수 있습니다. 하지만 추가로 서버 개발자와 협업을 하게 된다면 Docker가 협업에 더 유리하다고 생각했습니다. 일단 Docker가 실행되는 모든 곳에서 사용할 수 있고 동일한 환경과 조건에서 개발이 가능하다는 점 때문에 이식성이 좋다고 생각합니다.

Docker를 활용하여 배포하는 과정을 정리해보도록 하겠습니다.(로컬과 EC2에 도커를 설치하는 과정은 패스하겠습니다.. )

Docker CI/CD 프로세스

Docker를 빌드하고 배포하는 프로세스를 간략하게 설명하면 다음과 같습니다.

  1. Dockerfile을 빌드해서 docker image 파일(프로젝트 image 파일)을 생성
  2. docker image 파일을 dockerhub에 push
  3. EC2에서 dockerhub에 존재하는 docker image 파일을 pull해서 받아옵니다.
  4. docker-compose up 명령어로 프로젝트 image 파일과 redis image 파일을 컨테이너로 실행

Docker 파일 생성 / 네트워크 설정

본격적으로 Github Action을 사용하기 전에 선행되어야 할 것들이 3가지 있습니다.

  • Intellij에 Dockerfile 파일 만들기
  • EC2에 docker-compose.yml 파일 만들기
  • EC2에 docker network 만들기
  • redis image pull 받기

Dockefile 만들기

Dockerfile을 intellij 프로젝트 구조 최상단에 만들어줍니다.
./Dockerfile

# base image
FROM amazoncorretto:17
# 빌드 파일의 경로
ARG JAR_FILE=build/libs/*.jar
# 빌드 파일을 app.jar 컨테이너로 복사
COPY ${JAR_FILE} app.jar
# jar 파일 실행
ENTRYPOINT ["java","-jar","/app.jar"]

docker-compose.yml 만들기

이제 EC2에 docker-compose.yml을 만들어줘야 합니다.
docker-compose.yml은 컨테이너들을 관리해주는 역할을 합니다.
제가 docker-compose.yml을 만든 이유는 redis 컨테이너를 함께 사용하기 위함입니다.
(만약 프로젝트 컨테이너만 사용한다면? 굳이 docker-compose.yml은 필요없을 수 있습니다.)

version: "3"
services:
  redis:
    image: redis
    container_name: my_redis
    ports:
      - "6379:6379"
    networks: 
      - calendar-net

  concert_calendar:
    image: joonghyun/concert_calendar
    container_name: my_calendar
    ports:
      - "8080:8080"
    depends_on:
      - redis
    networks: 
      - calendar-net
networks:
        calendar-net:
                external: true

하나하나 살펴보면

  • version은 3버전으로 지정합니다.
  • services의 redis와 concert_calendar는 서비스 이름입니다.
  • image는 이미지 이름을 지정하는 것입니다. 이미지를 사용하기 위해서는 sudo docker images를 했을 때 해당 이미지의 이름이 있어야 합니다.
  • container_name은 컨테이너 이름입니다.
    (참고로 레디스 컨테이너 이름과 application.ymlredis host는 일치해야 합니다.)
    이 부분 때문에 삽질을 많이 해서 저처럼 삽질하는 사람이 없기를 바라면서 포스트에 추가합니다...😂
  redis:
    host: my_redis
    port: 6379
  • ports는 port 번호를 지정해줍니다.
  • networks는 해당 network에서 컨테이너를 실행하겠다는 것입니다.
  • depends_on은 redis 서비스와 의존관계를 설정하는 것입니다.
  • networks: 이름: external: true를 한 이유는 새 네트워크가 만들어지지 않고 기존 network에 연결되게 하는 옵션입니다.

networks를 새로 만들지 않는 옵션을 사용했으니까 docker network를 하나 만들어줘야겠죠?

docker network 만들기

docker network create 만들고싶은네트워크이름 커맨드로 새로운 Docker 네트워크를 생성할 수 있습니다.

추가된 네트워크는 docker network ls로 확인할 수 있습니다.
network를 만들때 -d 같은 옵션을 붙이지 않으면 bridge 네트워크가 생성이 되는데 bridge 네트워크는 하나의 호스트 컴퓨터 내에서 여러 컨테이너들이 서로 소통할 수 있게 해줍니다. 저는 제 프로젝트 컨테이너와 redis 컨테이너가 소통해야 하므로 bridge 네트워크를 사용했습니다.

sudo docker network inspect 만든네트워크이름 을 하면 네트워크의 정보를 확인할 수 있는데 만약 컨테이너들이 네트워크에서 잘 동작하고 있으면

"Containers": {
            "sdafgioerqqoierhgoiqewrhgoierghqoiwehgqhwehioh": {
                "Name": "my_calendar",
                "EndpointID": "f5e54d99f3de6d68e2e0366a6572dh3jkh134k2j5hk234hkk231h5",
                "MacAddress": "01:41:ac:10:00:01",
                "IPv4Address": "172.18.0.1/16",
                "IPv6Address": ""
            },
            "f77e098ffeccc879b70ecbd95031erqjpeorqjqopej79477ded8916c19401a7da24": {
                "Name": "my_redis",
                "EndpointID": "ac2cd436eba062615a252b3270e43eacjiqjegioqrejgioej461b24b804a14",
                "MacAddress": "02:45:ac:11:00:01",
                "IPv4Address": "172.18.0.1/16",
                "IPv6Address": ""
            }
        },

이런식으로 컨테이너 정보들이 네트워크 정보에 들어가게 됩니다.

redis image pull 받기

이제 redis 컨테이너를 만들 redis 이미지를 pull 받으면 됩니다.
명령어는 sudo docker pull redis로 image를 받으면 됩니다.

Github Action gradle.yml 파일 만들기

.github/workflows/gradle.yml 파일을 만들어서 해당 내용을 입력해줍니다.

name: ConcertCalendar(SpringBoot & Gradle) CI/CD

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

permissions:
  contents: read

jobs:
  CI-CD:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          # 사용하는 자바 버전 17
          java-version: '17'
          distribution: 'temurin'
  		
        # 암호파일을 위해서 암호파일 만들어주기
      - run: touch ./src/main/resources/application-secret.properties
      - run: echo "${{ secrets.APPLICATION_SECRET }}" > ./src/main/resources/application-secret.properties
      - run: cat ./src/main/resources/application-secret.properties
 
      # gradlew에 권한 부여
      - name: Grant execute permission for gradlew
        run: chmod +x gradlew
      
      # gradlew 빌드
      - name: Build with Gradle
        run: ./gradlew clean build --exclude-task test


      # docker build & push
      - name: Docker build & push
        run: |
            docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
            docker build -t ${{ secrets.DOCKER_REPO }}/concert_calendar .
            docker push ${{ secrets.DOCKER_REPO }}/concert_calendar
            
      # docker deploy
      - name: Docker Deploy executing remote ssh commands using ssh_key
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.HOST }}
          username: ubuntu
          key: ${{ secrets.SSH_KEY }}
          script: |
            cd app
            sudo rm concal.log
            sudo docker rm - f $(docker ps -qa)
            sudo docker pull ${{ secrets.DOCKER_REPO }}/concert_calendar
            docker-compose up -d
            sudo docker logs -f my_calendar &> concal.log &
            docker image prune -f
  # time
  current-time:
    needs: CI-CD
    runs-on: ubuntu-latest
    steps:
      - name: Get Current Time
        uses: 1466587594/get-current-time@v2
        id: current-time
        with:
          format: YYYY-MM-DDTHH:mm:ss
          utcOffset: "+09:00" # 기준이 UTC이기 때문에 한국시간인 KST를 맞추기 위해 +9시간 추가

      - name: Print Current Time
        run: echo "Current Time=${{steps.current-time.outputs.formattedTime}}" # current-time 에서 지정한 포맷대로 현재 시간 출력
        shell: bash

  ## slack
  action-slack:
    needs: CI-CD
    runs-on: ubuntu-latest
    steps:
        - name: Slack Alarm
          uses: 8398a7/action-slack@v3
          with:
              status: ${{ job.status }}
              author_name: GitHub-Actions CI/CD
              fields: repo,message,commit,author,ref,job,took
          env:
              SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required
          if: always() # Pick up events even if the job fails or is canceled.

gradle.yml 내용이 어떻게 작동하는지 파트별로 알아보겠습니다.

github action 실행 시점

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

on 속성으로 해당 워크플로우가 언제 실행되는지 정의합니다.
master 브랜치로 push 되거나 pull리퀘 될 경우 github action이 작동합니다.

Job 개념과 자바 버전 세팅

jobs:
  CI-CD:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          # 사용하는 자바 버전 17
          java-version: '17'
          distribution: 'temurin'

job은 하나의 처리 단위이며 step 안에 있는 작업 단위들을 단계별로 입력할 수 있습니다.
자바 버전을 17로 세팅해줍니다.

보안을 위해 암호파일 새로 만들기

      - run: touch ./src/main/resources/application-secret.properties
      - run: echo "${{ secrets.APPLICATION_SECRET }}" > ./src/main/resources/application-secret.properties
      - run: cat ./src/main/resources/application-secret.properties

그리고 프로젝트의 보안을 위해 암호만 모아놓은 application-secret.properties를 Github 내 SECRET과 touch, echo 명령어를 통해 새로 만들어주는 작업을 진행해줍니다.
(보안이 필요한 yml, properties 파일이 없는 경우 없어도 됩니다)
저는 여기에 jasypt encryptor password를 넣어놨습니다.

      # gradlew에 권한 부여
      - name: Grant execute permission for gradlew
        run: chmod +x gradlew
      
      # gradlew 빌드
      - name: Build with Gradle
        run: ./gradlew clean build --exclude-task test

gradlew에 권한을 부여하고 gradlew로 빌드를 해줍니다.

docker를 빌드하고 dockerhub에 push

      # docker build & push
      - name: Docker build & push
        run: |
            docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
            docker build -t ${{ secrets.DOCKER_REPO }}/concert_calendar .
            docker push ${{ secrets.DOCKER_REPO }}/concert_calendar

docker를 빌드하고 푸시하는 과정입니다.
이 과정은 dockerhub를 사용해야 하기 때문에 login -> build -> push 과정을 거칩니다.
run 명령어로
docker login을 합니다. login 할 때 주의사항은 DOCKER_USERNAME은 dockerhub의 이메일 계정이 아니라 프로필 이름으로 해야합니다.

Dockerfile을 활용한 docker build를 합니다.
docker build -t [새로 생성할 이미지 이름] [Dockerfile 디렉토리 경로]

docker build -t 도커 레포지터리/concert_calendar(이미지 이름) .(Dockerfile 위치)

docker push로 dockerhub에 이미지를 저장합니다.

docker로 EC2에 배포하기

      # docker deploy
      - name: Docker Deploy executing remote ssh commands using ssh_key
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.HOST }}
          username: ubuntu
          key: ${{ secrets.SSH_KEY }}
          script: |
            cd app
            sudo rm concal.log
            sudo docker rm - f $(docker ps -qa)
            sudo docker pull ${{ secrets.DOCKER_REPO }}/concert_calendar
            docker-compose up -d
            sudo docker logs -f my_calendar &> concal.log &
            docker image prune -f

저는 ssh로 접속해서 커맨드를 작성하는 방식으로 CD를 진행할거라 외부 서버에 SSH로 접속해서 커맨드를 실행하는 방법인 appleboy/ssh-action@master를 use 했습니다.
with를 입력하고
host에는 EC2 인스턴스의 퍼블릭 IPv4 DNS를 입력해주면 됩니다.
username은 저는 ubuntu로 설정해서 ubuntu로 입력했습니다.
key는 인스턴스 만들 때 발급받은 .pem 파일 내에 있는 key 값을 입력해주면 됩니다.
맥북에서 .pem 내용 확인이 안돼서 저는 terminal cat 명령어로 확인했습니다.
pem 은 ~begin 에서 ~end까지 다 복붙해서 입력해야 하며 %앞까지 복붙하시면 됩니다.

script: 는 터미널에 커맨드를 입력해주는 역할을 합니다.

  • 저는 docker-compose.yml이 app 폴더에 있기 때문에 cd app 했습니다.
    (docker-compose 명령어는 docker-compose.yml이 있는 위치에서 실행해야 합니다.)

  • 로그 파일을 초기화 하기 위해 concal.log를 삭제합니다.

  • 현재 실행중인 컨테이너들을 모두 삭제해줍니다. 컨테이너 초기화

  • dockerhub에 push 했던 이미지를 다시 받아옵니다.

  • docker-compose up -d(백그라운드 실행) 로 어플리케이션을 실행해줍니다.

  • docker logs 명령어로 실행중인 로그를 concal.log에 입력합니다.

  • docker image prune -f로 필요없는 이미지들이 쌓이지 않도록 사용하지 않는 이미지는 삭제합니다.

한국 시간으로 설정

  # time
  current-time:
    needs: CI-CD
    runs-on: ubuntu-latest
    steps:
      - name: Get Current Time
        uses: 1466587594/get-current-time@v2
        id: current-time
        with:
          format: YYYY-MM-DDTHH:mm:ss
          utcOffset: "+09:00" # 기준이 UTC이기 때문에 한국시간인 KST를 맞추기 위해 +9시간 추가

      - name: Print Current Time
        run: echo "Current Time=${{steps.current-time.outputs.formattedTime}}" # current-time 에서 지정한 포맷대로 현재 시간 출력
        shell: bash

현재 시간을 세팅/출력하는 job 입니다. 별다른 기능은 아니고 EC2의 시간을 한국 시간으로 설정하고 현재 시간을 출력해줍니다.

슬랙 알림 모니터링

  ## slack
  action-slack:
    needs: CI-CD
    runs-on: ubuntu-latest
    steps:
        - name: Slack Alarm
          uses: 8398a7/action-slack@v3
          with:
              status: ${{ job.status }}
              author_name: GitHub-Actions CI/CD
              fields: repo,message,commit,author,ref,job,took
          env:
              SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required
          if: always() # Pick up events even if the job fails or is canceled.

github action의 과정이 완료되면 slack에 알림을 띄워줍니다.

이 과정으로 Github Action을 진행하면 아래와 같이 job이 실행됩니다.

slack을 이용해서 모니터링 알림을 구축하니까 일일이 github action이 끝났는지 확인하지 않아도 돼서 굉장히 편리했습니다.

후기

Docker가 처음에 접할때는 너무 어려워서 포기하려고 했지만 삽질 많이 하고 숙련도가 조금 쌓이니까 생각보다 직관적이고 편리한 것 같습니다. (오히려 S3랑 Code Deploy 쓴다고 설정 엄청할 때 보다 간단한거 같기도 하고요..)
컨테이너가 더 늘어나도 docker-compose.yml에 입력만 잘하면 편리하게 컨테이너 관리도 가능할 것 같네요.

조금 더 효과 좋은 CI/CD 방식을 찾으면 또 발전시켜 보겠습니다!! 😃

profile
There are no two words in the English language more harmful than “good job”.
post-custom-banner

4개의 댓글

comment-user-thumbnail
2023년 5월 1일

s3 deploy 방식에 프로젝트 전체가 올라간다고 하셨습니다. 그런데, 압축해서 올릴때 build하고 jar 파일만 압축해서 보낼 수가 있어요.
두번째는.. docker기반에서 무중단 배포를 구현하기가 좀 까다롭게 됩니다..
aws codedeploy를 거치게 되면, 무중단 배포 전략이 좀 더 수월해지는 장점이 있습니다..
그외 나머지는 잘 구현하신듯합니다 !

1개의 답글
comment-user-thumbnail
2023년 5월 1일

고민을 많이 하신 게 느껴집니다.
글 재밌게 읽고 갑니다. 👏👏

1개의 답글