테스트 코드와 AWS를 통한 배포환경이 준비되었으니 이제 CI/CD를 구축할 수 있다.
CI/CD의 간단한 정의는 다음과 같다.
CI (Continuous Integration): 레포지트로의 코드가 변경될 때, 자동으로 빌드와 테스트를 해준다.
CD (Continuous Deployment): CI를 통과한 변경사항을 적용하여 자동으로 배포한다.
Github Actions에서는 push, pull request 등 다양한 변경사항과 cron 등의 스케줄에 대해 테스트, 빌드 배포 등의 workflow 자동화를 지원한다. 이를 사용해 CI/CD를 적용했고, 이에 대해 정리해보려 한다.
전체 workflow 파일은 Github에서 확인할 수 있다.
Backend CI 적용
백엔드의 경우 gradle build만 실행하도록 구성하면 된다. gradle build 실행 시 빌드 및 테스트가 함께 진행된다.
전체 코드는 다음과 같고, 작성한 workflow 코드를 하나씩 설명하도록 하겠다.
name: CI Test Backend
on:
pull_request:
branches: ["main"]
permissions:
contents: read
jobs:
build-and-test:
# main에 push하거나, Backend branch에서 pull_request를 요청했을 경우
if: ${{ !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[skip backend]') && (github.event_name == 'push' || (github.event_name == 'pull_request' && contains(github.head_ref, '/be/'))) }}
runs-on: ubuntu-22.04
services:
redis:
image: redis:latest
ports:
- 6379:6379
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: "17"
distribution: "corretto"
- name: Add executable permission to gradlew
run: chmod +x ./backend/gradlew
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: build
build-root-directory: ./backend
name: CI Test Backend
on:
pull_request:
branches: ["main"]
name: Actions에 보여지게 될 workflow의 이름을 설정할 수 있다.
on: workflow가 언제 실행될지 정의한다. 위와 같이 구성하는 경우, main branch에 pull request가 들어왔을 때 workflow가 실행된다. pull request 이외에도 push, fork, issue 등 다양한 이벤트를 설정할 수 있다.
자세한 내용은 공식 문서를 참고하면 좋을것 같다.
https://docs.github.com/ko/actions/using-workflows/workflow-syntax-for-github-actions#on
...
jobs:
build-and-test:
if: ${{ !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[skip backend]') && (github.event_name == 'push' || (github.event_name == 'pull_request' && contains(github.head_ref, '/be/'))) }}
runs-on: ubuntu-22.04
services:
redis:
image: redis:latest
ports:
- 6379:6379
jobs: workflow에서 수행할 job들을 정의한다. job의 이름은 자유롭게 지을 수 있고, 이후 workflow 실행 시 이름이 출력된다.
if: if문을 수행한 결과가 true일때만 job을 실행한다. backend 브랜치로부터 PR이 오거나, main에 push가 됐을 경우 job을 실행하고, 커밋 메세지에 [skip ci] 또는 [skip backend]가 포함되어있을 경우 실행하지 않도록 구성했다.
runs-on: job을 실행할 docker 이미지를 정의한다. ubuntu-22.04 컨테이너에서 수행하도록 했다. 처음엔 latest로 했지만, 이후 latest에 새로운 이미지가 할당될 때마다 CI 환경이 달라지기 때문에 버전을 명시해줬다.
services: 세션을 테스트하기 위해 Redis 서비스가 필요하여, redis 도커 이미지를 실행시키도록 설정했다.
...
jobs:
build-and-test:
...
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: "17"
distribution: "corretto"
- name: Add executable permission to gradlew
run: chmod +x ./backend/gradlew
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: build
build-root-directory: ./backend
steps: job 내에서 실행할 작업을 나열한다. 위에서 아래 방향으로 작업들을 실행하고, 모든 작업 완료 시 실행된 작업의 역순으로 Post 작업이 실행된다.
name: 작업의 이름을 지정한다. 이후 Job이 실행될 때 name에 지정한 이름으로 작업의 이름이 표시된다.
uses: 다른 사람이 만들어두거나 내가 직접 만든 action을 불러와 사용한다. checkout의 경우 현재 레포지토리를 도커 환경에 복사해오는 역할을 하고, setup-java의 경우 도커 환경에 Java JDK 환경을 구성해주는 역할을 한다.
run: 도커 환경에 명령어를 실행한다.
따라서 위 작업의 진행 순서는 다음과 같다.
- 코드를 도커 환경에 복사
- Java 17 (Corretto) 환경 구성
- .gradlew 파일에 execute 권한 추가 (원래는 execute 권한이 없어 실행 시 오류가 난다.)
- backend 디렉토리에서 gradle build 실행 (빌드 시 테스트도 함께 진행된다.)
작업 실행이 완료되고 로그를 확인하면 아래와 같이 테스트 / 빌드 모두 성공적으로 끝난것을 확인할 수 있다.
Starting a Gradle Daemon (subsequent builds will be faster)
> Task :initQuerydslSourcesDir
> Task :compileQuerydsl
> Task :compileJava
> Task :processResources
> Task :classes
> Task :bootJarMainClassName
> Task :bootJar
> Task :jar
> Task :assemble
> Task :compileTestJava
> Task :processTestResources
> Task :testClasses
> Task :test
2023-02-06 11:31:26.893 INFO 2187 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2023-02-06 11:31:26.902 INFO 2187 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2023-02-06 11:31:26.911 INFO 2187 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
2023-02-06 11:31:26.913 INFO 2187 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2023-02-06 11:31:26.930 INFO 2187 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2023-02-06 11:31:27.033 INFO 2187 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-2 - Shutdown initiated...
2023-02-06 11:31:27.034 INFO 2187 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-2 - Shutdown completed.
> Task :check
> Task :build
Frontend CI 적용
프론트엔드의 경우 ESLint 체크만 하도록 구성하면 된다.
name: CI Test Frontend
on:
pull_request:
branches: ["main"]
jobs:
build-and-test:
# main에 push하거나, Frontend branch에서 pull_request를 요청했을 경우
if: ${{ !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[skip frontend]') && (github.event_name == 'push' || (github.event_name == 'pull_request' && contains(github.head_ref, '/fe/'))) }}
runs-on: ubuntu-22.04
defaults:
run:
working-directory: ./frontend
strategy:
matrix:
node-version: [18.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Cache node modules
id: node-cache
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: "**/node_modules"
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-modules-
- name: Install NPM Modules
if: steps.node-cache.outputs.cache-hit != 'true'
run: npm install
- name: Run ESLint
run: npx eslint src --ext .js,.jsx,.ts,.tsx
frontend 브랜치로부터 PR이 오거나, main에 push가 됐을 경우 job을 실행하고, 커밋 메세지에 [skip ci] 또는 [skip frontend]가 포함되어있을 경우 실행하지 않도록 구성했다.
또한 npm install을 매번 수행하기엔 시간이 너무 오래걸려, node_modules 폴더를 캐시에 저장하고 불러오도록 설정했다.
캐시를 적용하면 package-lock.json에 변화가 생기지 않는 이상 이전에 설치한 node_modules 폴더를 그대로 재사용하기 때문에 시간을 훨씬 단축할 수 있다.
아래 작업 로그를 보면 Install NPM modules 작업이 skip된것을 확인할 수 있다.
Backend CD 적용
CI를 통해 테스트를 통과한 코드만 통합되도록 설정했고, 이에 대한 배포 workflow를 작성해야 한다.
코드가 좀 긴데, 중복되는 내용은 빼고 정리하도록 하겠다.
name: Deploy development server backend
on:
push:
branches: ["main"]
workflow_dispatch:
inputs:
logLevel:
description: "Log level"
required: true
default: "warning"
tags:
description: "Tags"
jobs:
build-and-push:
if: ${{ !contains(github.event.head_commit.message, '[skip cd]') && !contains(github.event.head_commit.message, '[skip backend]') }}
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: Add executable permission to gradlew
run: chmod +x ./backend/gradlew
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: build -x test
build-root-directory: ./backend
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
push: true
context: ./backend
build-args: SPRING_PROFILES_ACTIVE="prod"
tags: devjaewoo/openroadmaps-develop:latest
deploy:
runs-on: ubuntu-latest
needs: [build-and-push]
steps:
- name: Get Github action IP
id: ip
uses: haythem/public-ip@v1.2
- name: Add Github Actions IP to Security group
run: |
aws ec2 authorize-security-group-ingress --group-name ${{ secrets.AWS_SG_NAME_DEVELOP }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_DEVELOP }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_SECRET_DEVELOP }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
- name: Deploy to AWS
uses: appleboy/ssh-action@master
with:
username: ${{ secrets.AWS_SSH_USERNAME_DEVELOP }}
host: ${{ secrets.AWS_SSH_HOST_DEVELOP }}
key: ${{ secrets.AWS_SSH_KEY_DEVELOP }}
script: |
sudo docker compose pull
sudo docker compose up --force-recreate --build -d
sudo docker image prune -f
- name: Remove Github Actions IP from security group
run: |
aws ec2 revoke-security-group-ingress --group-name ${{ secrets.AWS_SG_NAME_DEVELOP }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_DEVELOP }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_SECRET_DEVELOP }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
if: always()
on:
push:
branches: ["main"]
workflow_dispatch:
inputs:
logLevel:
description: "Log level"
required: true
default: "warning"
tags:
description: "Tags"
on push: pull_request와 달리 push로 바뀌었다. main 브랜치에 push 하거나, main으로 온 PR을 merge할 때도 작동한다.
workflow_dispatch: 이벤트 없이 원할 때 workflow를 바로 실행할 수 있도록 해준다. 이 트리거가 적용된 workflow는 아래와 같이 Run workflow 버튼이 생기고, 이를 통해 수동으로 실행할 수 있다.
jobs:
build-and-push:
if: ${{ !contains(github.event.head_commit.message, '[skip cd]') && !contains(github.event.head_commit.message, '[skip backend]') }}
runs-on: ubuntu-latest
steps:
...
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: build -x test
build-root-directory: ./backend
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
push: true
context: ./backend
build-args: SPRING_PROFILES_ACTIVE="prod"
tags: devjaewoo/openroadmaps-develop:latest
Build with gradle: argument에 "-x test"가 추가됐다. CI 단계에서 이미 테스트를 진행했기 때문에, 배포 빌드 시 테스트는 제외하도록 했다.
Set up QEMU ~ Build and push: Docker Hub에 이미지를 빌드하고 push 한다. 아래 공식 문서를 참고하여 작성했다.
https://github.com/marketplace/actions/build-and-push-docker-images#git-context
코드 중 "${{ secrets.DOCKERHUB_USERNAME }}"와 같은 변수가 있다.
[Settings] - [Secrets and variables] - [Actions]에 Github Actions에서 사용할 변수를 등록할 수 있는데, 이렇게 등록된 변수는 타인에게 노출되지 않아 더 안전하게 workflow를 돌릴 수 있다.
workflow를 돌리면 Docker Hub에 이미지가 배포된다.
https://hub.docker.com/repository/docker/devjaewoo/openroadmaps-develop/general
이후 deploy job에서 AWS CLI에 연결하고, Docker Hub에 업데이트된 이미지를 받아와 배포하도록 구성했다.
jobs:
deploy:
runs-on: ubuntu-latest
needs: [build-and-push]
steps:
- name: Get Github action IP
id: ip
uses: haythem/public-ip@v1.2
- name: Add Github Actions IP to Security group
run: |
aws ec2 authorize-security-group-ingress --group-name ${{ secrets.AWS_SG_NAME_DEVELOP }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_DEVELOP }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_SECRET_DEVELOP }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
- name: Deploy to AWS
uses: appleboy/ssh-action@master
with:
username: ${{ secrets.AWS_SSH_USERNAME_DEVELOP }}
host: ${{ secrets.AWS_SSH_HOST_DEVELOP }}
key: ${{ secrets.AWS_SSH_KEY_DEVELOP }}
script: |
sudo docker compose pull
sudo docker compose up --force-recreate --build -d
sudo docker image prune -f
- name: Remove Github Actions IP from security group
run: |
aws ec2 revoke-security-group-ingress --group-name ${{ secrets.AWS_SG_NAME_DEVELOP }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_DEVELOP }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_SECRET_DEVELOP }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
if: always()
AWS에는 허용된 IP만 접근할 수 있게 해주는 보안 그룹 설정이 있기 때문에, Github Actions를 통해 접근하려 할 경우 보안그룹에 막혀 접근이 되지 않는다. 때문에 현재 IP를 받아와 AWS의 보안그룹에 추가해줘야 정상적으로 명령을 실행할 수 있다.
Get Github action IP: 보안그룹에 현재 IP를 추가하기 위해 IP를 받아온다.
Add / Remove Github Actions IP to Security group: 보안 그룹에 현재 IP에 대해 SSH 연결을 허용 / 불허한다.
Deploy to AWS: Docker Hub의 업데이트된 이미지를 받아와 다시 배포한다.
위 작업들을 수행해 AWS 환경에 자동으로 배포시킬 수 있었다.
Frontend CD 적용
Frontend CD는 CI와 크게 다르지 않다. ESLint 체크가 제거되고, Build와 배포가 추가되었다.
name: Deploy development server frontend
on:
push:
branches: ["main"]
workflow_dispatch:
inputs:
logLevel:
description: "Log level"
required: true
default: "warning"
tags:
description: "Tags"
jobs:
build-and-deploy:
if: ${{ !contains(github.event.head_commit.message, '[skip cd]') && !contains(github.event.head_commit.message, '[skip frontend]') }}
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./frontend
strategy:
matrix:
node-version: [18.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Cache node modules
id: node-cache
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: "**/node_modules"
key: ${{ runner.os }}-node-modules-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-modules-
- name: Install NPM Modules
if: steps.node-cache.outputs.cache-hit != 'true'
run: npm install
- name: Build
run: npm run build
- name: Deploy
env:
AWS_EC2_METADATA_DISABLED: true
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_DEVELOP }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_SECRET_DEVELOP }}
run: |
aws s3 sync --region ${{ secrets.AWS_DEFAULT_REGION }} ./build s3://openroadmaps-develop-s3
jobs:
build-and-deploy:
defaults:
run:
working-directory: ./frontend
steps:
...
- name: Build
run: npm run build
- name: Deploy
env:
AWS_EC2_METADATA_DISABLED: true
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID_DEVELOP }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_SECRET_DEVELOP }}
run: |
aws s3 sync --region ${{ secrets.AWS_DEFAULT_REGION }} ./build s3://openroadmaps-develop-s3
defaults - run - working-directory: 모든 step들의 run에 대해 ./frontend 디렉토리에서 명령어를 수행하도록 한다.
Deploy: aws s3 sync 명령어를 통해 빌드 결과물을 s3 버킷에 업로드한다.
이렇게 Frontend, Backend 모두 CI/CD를 적용시켰다.
아마 이번 글이 이 프로젝트의 마지막 글이 될 것 같다.
결과적으로 졸업작품은 A+를 받았다.
'Projects > OpenRoadmaps' 카테고리의 다른 글
[OpenRoadmaps] 10. SSL 인증 + NginX Proxy 적용기 (0) | 2023.03.01 |
---|---|
[OpenRoadmaps] 9. Docker 빌드 + AWS 배포하기 (0) | 2023.02.13 |
[OpenRoadmaps] 블로그 카테고리 Unique Constraint 오류 해결 (0) | 2023.02.06 |
[OpenRoadmaps] 8. 블로그 글 작성, 글 뷰어 기능 개발 (2) | 2023.02.05 |
[OpenRoadmaps] 7. 로드맵 뷰어 기능 개발 (0) | 2023.02.05 |