일단 시작 전에, 게시물을 똑같이 따라해도 안되는 경우가 생길 수 있을 것이다.

여기에 적은 모든 내용이 100% 맞는다고 할 수는 없기에 혹시라도 잘못된 내용이 있다면 수정 요청을 하시길 바란다.

개발 환경은 다음과 같다.

  • Window 11
  • Spring Boot 2.7.7
  • java 11
  • Ubuntu Server.22.04 LTS
  • Intellij IDEA Ultimate

기존 프로젝트의 배포를 Github에서 Organization을 생성해서 코드가 Github에 push될 때, 자동으로 GitLab에 미러링되서 CI 되게끔 하고 CD는 crontab을 통해 진행하였다.

그런데 이렇게 하면 몇 가지 문제점이 있다.

🚫 첫째, application.yml에 들어있는 설정 정보들을 Xshell을 켜서 배포 스크립트에 -e 옵션을 통해 다 포함시켜서 진행해야 하고, 수정사항 또한 마찬가지 순서를 따른다.

또한, Github이든 GitLab이든 application.yml 파일은 올리지 않거나 가짜 정보를 넣어놔야 한다.

전체적으로 번거롭다…

🚫 둘째, 기존의 crontab은 코드의 변화가 있을 때만 체크하는 것이 아니라 변화가 있든 없든 항상 내가 정해둔 시간 간격만큼 돌아가고 있는 상황이었고 해당 작업이 자원을 낭비한다는 느낌을 주었다.

🚫 셋째, Github에 코드를 push하고, 굳이 GitLab에 미러링해서 CI/CD를 할 이유가 없다는 생각을 했다. Github든 GitLab이든 한 쪽에서 모든걸 다 진행하면 더 효율적이라고 생각했다.

GitLab 배포 스크립트는 비교적 쉬워서 간단히 작성할 줄은 알았지만, Github Actions 배포 스크립트 작성에는 두려움이 있었다.

또한, Github에서 Organization을 생성해서 진행하던 중이라 어쩔 수 없이 GitLab으로 미러링 하는 작업이 추가되었는데, 이참에 여러 문제점들을 해결하고자 Github Actions를 통해 배포를 진행해 보도록 하겠다.

Github Actions에서 CI/CD를 진행하면 위에서 말한 노출되면 안 되는 설정 정보들을 Git submodule을 적용해서 관리하도록 해보겠다.

이 도움을 준 나의 영원한 친구 Gilbert에게 무한한 찬사를 보낸다…


1. EC2 생성 및 Docker, MySQL 설치


EC2를 생성하고 서버에 Docker 및 MySQL을 설치하는 방법은 아래 링크를 통해 확인하자.

EC2 생성 및 프로젝트 연결


2. 프로젝트 Repository 생성 및 Git submodule 적용


다음과 같이, Organization 안에서 프로젝트 repo를 public으로 생성해서, 프로젝트와 연결시켜 보겠다.

image

나의 경우, 로컬 바탕화면에서 마우스 우클릭으로 Git Bash Here 해서 원격 repo를 clone해 생성된 폴더 안에 프로젝트를 넣어주었다.

프로젝트를 Github repo와 연결했으면 이제 Git submodule을 적용해보자.

우선, 마찬가지로 Organization 안에서 private respository를 생성해준다.

이 private repo에는 노출되면 안되는 application.yml에 들어있는 설정 정보 파일들이 작성될 예정이다.

image

다음과 같이, application-dev.yml의 파일에 yml 파일에 있는 내용들을 전부 작성한다. 여기엔 실제 환경변수들이 들어가야 한다.

작성을 마치면, commit 한다.

image

다음으로, 프로젝트 메인 repo에서 위의 submodule로 사용할 repo를 등록해준다.

git submodule add ${submodule_repository_url}

image

submodule을 등록하면 다음과 같이, 프로젝트 root 경로에 초록색 박스의 파일들이 생긴다.

image

.gitmodules 파일을 간단히 살펴보면, submodule repo의 경로와 url을 알려준다.

[submodule "myacademy-config"]
	path = myacademy-config
	url = https://github.com/mutsa-team6/myacademy-config.git

이제 submodule을 등록했으니 사진의 빨간색 박스인 yml 파일들을 지워도 된다.

그리고 로컬에서 서버를 run하면…? ❌ 다음과 같은 error가 발생한다.❌

image

해당 문제는 프로젝트에서 submodule로 지정한 repo의 yml 파일을 불러오지 못한다는 뜻이다.

분명 나는, submodule을 등록해서 프로젝트 root 경로에 존재하는 것을 확인했는데 왜 안되는 것일까?

✅ 그 이유는, submodule이 존재하는 것처럼 보이나 실제로는 아니고, 추가적으로 복사를 해줘야 한다. ✅

build.gradle에 다음과 같이 복사하는 코드를 작성한다.

코드를 간단히 살펴보면, gradle이 copyPrivate task를 수행할 때 from 경로의 repo에 있는 application-dev.yml 파일을 메인 프로젝트의 src/main/resources 경로로 복사하라는 의미이다.

task copyPrivate(type: Copy) {
	copy {
		from './myacademy-config'
		include "application-dev.yml"
		into 'src/main/resources'
	}
}

마지막으로, 작성한 copyPrivate task를 실행시켜주면, 실제로 존재하게 되고, push해주고, project run하면 정상적으로 실행된다.

image

image

만약, submodule repo를 변경하게 된다면 submodule 변경 사항 메인 프로젝트 반영 을 참고하도록 하자.


3. Docker Hub & docker compose


4번 과정에서 docker image를 생성하기 앞서, image를 저장할 저장소 개념의 Docker Hub에 가입해준다.

다음으로, docker compose 설치를 해야한다.

설치 명령어는 다음과 같다.

현재 기준 도커 컴포트 v2를 설치하려고 한다.

sudo curl -sSL "https://github.com/docker/compose/releases/download/v2.15.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

[리눅스] 도커 컴포즈 설치(docker-compose install)

curl: (92) HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)

도커 컴포즈 최신 버전 확인

설치 후에, chmod +x 명령어를 통해 docker compose 실행 권한을 주도록 한다. 여기서 +x는 executable, 즉 실행 이라는 뜻이다.

chmod +x /usr/local/bin/docker-compose

docker-compose 명령이 제대로 먹히는지 확인해준다.

docker-compose -v

image

🚨 여기서 끝이 아니다. 🚨

docker compose는 여러 컨테이너를 가지는 애플리케이션을 통합적으로 Docker 이미지를 만들고, 만들어진 각각의 컨테이너를 시작 및 중지하는 등의 작업을 더 쉽게 수행할 수 있도록 도와주는 도구이다.

즉, docker compose는 컨테이너로 올릴 image들을 docker hub로부터 pull 받아서 한꺼번에 올려주는 작업을 한다.

EC2 인스턴스 내에서 해당 작업이 이루어지기 때문에 docker compose 파일도 EC2 인스턴스 내에 만들어주면 된다.

Xshell을 열고, EC2 서버에 접속한 뒤, sudo su - 의 관리자 권한으로 들어간다.

다음으로, /home/ubuntu 경로에 compose경로를 만들고, 그 안에 docker-compose.yml 파일을 만들어준다.

mkdir compose
vim docker-compose.yml

파일 내용은 다음과 같다.

version: '3'
services:
  web:
    container_name: myacademy
    image: percyfrank/myacademy
    ports:
      - '8080:8080'

container_name은 내가 이름을 지정하면 되고, image의 경우 {docker hub ID}/{image를 담을 docker hub repo 이름} 을 작성하면 된다.

나의 경우 id는 percyfrank, image 저장소 repo 이름은 container_name과 같은 myacademy로 했다.

image

제일 중요한 port속성은 호스트가 접근할 수 있는 포트인 8080:8080 으로 작성해준다.

추후에 Nginx를 사용하게 되면 Nginx를 거쳐 오기 때문에 port 속성이 조금 바뀔 수도 있다.

현재는 Nginx를 적용하지 않았으므로 이대로 작성을 마무리한다.

esc 후, Shift+zz 를 눌러 저장한다.


4. DockerFile 작성 및 Docker image 생성


다음으로, Github Actions에서 CI/CD 하기 위해 Docker image 생성을 위한 DockerFile 작성을 해보겠다.

스크립트는 다음과 같다.

설정 파일을 분리해서 사용할 땐 -Dspring.profiles.active 속성에 원하는 설정 파일을 지정해주면 된다. 나의 경우는 dev.

FROM openjdk:11
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-Dspring.profiles.active=dev", "-jar", "/app.jar"]

스크립트에 대한 이해가 필요하면 baeldung - spring boot docker start with profile을 참고하자.


5. Github Actions CI/CD 스크립트


프로젝트 root 경로에 ./github/workflows 디렉토리를 만들고 cicd.yml 파일을 생성한다.

image

여기서부턴, 스크립트의 내용을 순서대로 간략히 설명하겠다.

5-1. on

main 브랜치에 push 가 일어나면 해당 event가 실행된다.

# github repository Actions 페이지에 나타낼 이름
name: CI/CD

# event trigger
on:
  push:
    branches: [main]

pull request 가 일어나도 event가 실행되게끔 하고 싶다면, 다음과 같이 추가하면 된다.

  pull_request:
    branches : [main]

5-2. jobs

Github Actions의 workflow는 다양한 job으로 구성되며 job은 steps로 구성이 된다.

job에선 수행할 작업을 작성하면 된다.

token은 repo를 가져오는데 사용될 액세스 토큰이고, submodules: true 옵션은 하위 모듈을 체크하겠다는 의미이다.

checkout 공식문서

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          token: $
          submodules: true


다음으로, Github Actions에서 사용될 JDK를 세팅하는 부분이다.

JDK 11을 사용하고, distribution으로 temurin을 사용한다.

Gradle Wrapper Validation 공식 문서 Setup Java 공식문서

- name: Validate Gradle Wrapper
  uses: gradle/wrapper-validation-action@v1
- name: Set up JDK11
  uses: actions/setup-java@v3
  with:
    distribution: 'temurin'
    java-version: '11'
    cache: 'gradle'


gradlew에 실행권한을 부여하고, gradle build를 진행하는 작업이다.

- name: Grant execute permission for gradlew
  run: chmod +x gradlew
- name: Execute Gradle build
  run: ./gradlew clean build -x test -Pprofile=dev


Docker를 세팅하고, 로그인하는 부분이다.

Docker Setup Buildx 공식문서 Docker Login 공식문서

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v2
- name: Docker Login
  uses: docker/login-action@v2
  with:
    username: $
    password: $


docker image를 생성하고 docker hub에 push하는 작업이다.

APP 위치에는 docker-compose 파일에 작성한 이름을 넣으면 된다.

나의 경우 image: percyfrank/myacademy 였기 때문에 myacademy 를 넣어주었다.

- name: build and release to DockerHub
  env:
    NAME: $
    APP: myacademy
  run: |
    docker build -t $NAME/$APP -f ./DockerFile .
    docker push $NAME/$APP:latest


마지막으로 docker-compose를 통해 EC2 인스턴스에 APP 컨테이너를 올리는 작업이다.

SSH Remote Commands 공식문서

- name: EC2 Docker Run
  uses: appleboy/ssh-action@master
  env:
    APP: "myacademy"
    COMPOSE: "/home/ubuntu/compose/docker-compose.yml"
  with:
    username: ubuntu
    host: $
    key: $
    envs: APP, COMPOSE
    script_stop: true
    script: |
      sudo docker-compose -f $COMPOSE down --rmi all
      sudo docker pull $/$APP:latest
      sudo docker-compose -f $COMPOSE up -d


✅ 추가적으로, gradle build 시 springboot 2.5 버전 이후로는 jar 파일이 2개가 생겨 Github Actions 작업이 제대로 작동하지 않는다.

해결방법은 build.gradle 에 다음과 같이 추가하면 된다.

jar {
  enabled = false
}


✅ jobs 중간중간에 보면 스크립트에 작성할 환경 변수들을 $ 처리해서 작성했다.

해당 변수들을 실제 보관해둘 곳은 Main 프로젝트 Settings - Secrets and variables - Actions - New repository secret에서 진행하면 된다.

주의할점은 한 번 작성해서 넣고 수정하기 위해 누르면 이전에 작성했던 값들이 전혀 보이지 않게 된다. 즉, 특정 부분만 드래그해서 바꿀 수 없으니, 통쨰로 입력하거나 아니면 삭제하고 다시 만들면 될 것 같다.

image

  • CHECKOUT_TOKEN : 액세스 토큰
  • DOCKER_USER : Docker Hub ID
  • DOCKER_PASSWORD : Docker Hub Password
  • EC2_HOST : 퍼블릭 IPv4 DNS 주소
  • EC2_KEY : EC2 pem 파일에 있는 내용 전부

코드를 push 후, Github Actions 작업이 성공하면 docker ps -a로 컨테이너가 올라갔는지 확인한다.

image


References

Leave a comment