GitHub-Actions๋ก CI/CD ๊ตฌ์ถํ๊ธฐ(AWS, Docker, SpringBoot)
์๋ ํ์ธ์, ์ด๋ฒ ์๊ฐ์๋ GitHub-Actions๋ก CI/CD๋ฅผ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์๋ณด๊ฒ ์ต๋๋ค.
ํด๋น ํฌ์คํ ์ด CI/CD๋ฅผ ์ ์ฒด์ ์ผ๋ก ํฌํจํ๊ณ ์๊ธฐ๋ ํ์ง๋ง, ๊ณผ๊ฑฐ Maven ๊ธฐ๋ฐ์ผ๋ก CI๋ฅผ ๊ตฌ์ถํ ํฌ์คํ ์ด ์๋๋ฐ์, ๊ถ๊ธํ์๋ฉด ์ฐธ๊ณ ํ์ ๋ ์ข์ ๊ฒ ๊ฐ์ต๋๋ค ๐
์ ๋ ๋ค์๊ณผ ๊ฐ์ ํ๊ฒฝ์ ์ฌ์ฉํ์ฌ ๊ตฌ์ถํ ์์ ์ ๋๋ค.
- GitHub-Actions
- AWS EC2
- Docker, Docker-Compose
- Spring Boot
- Spring profiles(prod, dev)
- Gradle
GitHub-Actions์ Docker, Docker-Compose๋ฅผ ํตํด Spring Boot์ ํ๊ฒฝ๋ณ(prod, dev) ํ์ผ์ AWS EC2 ์ธ์คํด์ค์ ์๋๋ฐฐํฌ ํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์ดํด๋ณด๊ฒ ์ต๋๋ค.
(์ฝ๋๋ ๊นํ๋ธ ์์ ํ์ธํ ์ ์์ต๋๋ค.)
๊ธฐ๋ณธ์ ์ผ๋ก AWS ๊ณ์ ์ ์์ฑํ๋ ๋ฐฉ๋ฒ์ด๋ ์คํ๋ง ๋ถํธ ํ๋ก์ ํธ ์์ฑ, Spring profiles์ ๋ํด ๋ฐ๋ก ์ค๋ช ์ ์๋ตํ๊ฒ ์ต๋๋ค.
Spring profiles์ ๊ฒฝ์ฐ Maven ๊ธฐ๋ฐ์ด๊ธด ํ์ง๋ง ๊ณผ๊ฑฐ ํฌ์คํ ์ ์ฐธ๊ณ ํด์ฃผ์ธ์ :)
๐ AWS EC2 ์ธ์คํด์ค ์์ฑํ๊ธฐ
๋จผ์ AWS ์ฌ์ดํธ์์ EC2 ์ธ์คํด์ค๋ฅผ ์์ฑํฉ๋๋ค.
๊ณผ๊ฑฐ EC2 ์ธ์คํด์ค ์์ฑํ๋ ๋ฐฉ๋ฒ์ ๋ํด ํฌ์คํ ์ ํ ์ ์ด ์์ง๋ง, ํด๋น ํฌ์คํ ์์๋ ๊ณผ๊ฑฐ ๋ฒ์ ์ ๊ธฐ์ค์ผ๋ก ์์ฑ๋์๊ธฐ ๋๋ฌธ์ ํ์ฌ UI์๋ ๋ค๋ฆ ๋๋ค. ํ์ง๋ง ํ์ฌ์ UI๋ ๋งค์ฐ ๋จ์ํด์ก๊ธฐ ๋๋ฌธ์ ์์ฝ๊ฒ ์์ฑ์ด ๊ฐ๋ฅํฉ๋๋ค.
์ด๋ฆ๊ณผ ํค ํ์ด๋ฅผ ์์ฑํ ํ ์ธ์คํด์ค๋ฅผ ์์ฑํ์๋ฉด ๋ฉ๋๋ค.
ํค ํ์ด๋ ์ค์ํ๊ธฐ ๋๋ฌธ์ ์๋ก์ด ํค ํ์ด๋ฅผ ์์ฑํ ํ ๋ฐ๋์ ๋ณด๊ดํ๊ณ , ๋ถ์ค๋์ง ์๋๋ก ์ ์ํฉ๋๋ค.
๊ทธ ์ธ ๋ฐ๋ก ์๋๋ถ๋ถ์ ์์ผ๋ฉฐ, ์ถํ ์ฉ๋์ด ๋ถ์กฑํ ๊ฒฝ์ฐ๋ ์๊ธฐ ๋๋ฌธ์ ํ๋ฉด ์๋์์ ์คํ ๋ฆฌ์ง ๊ตฌ์ฑ์์ ํ๋ฆฌํฐ์ด ๊ธฐ์ค ์ต๋ ์ฉ๋(30GB)์ ๊ธฐ์ค์ผ๋ก ์ ๋นํ ๋๋ ค์ฃผ๋๋ก ํฉ๋๋ค.
๋ํ ๋ณธ์ธ์ ์ํฉ์ ๋ง๊ฒ ํ์ํ ๋ณด์๊ทธ๋ฃน(์ธ๋ฐ์ด๋ ๊ท์น)์ ์ ์ ํ ์ค์ ํด์ฃผ์ธ์.
์ ๋ 8080(Tomcat), 27017(MongoDB), 5431(PostgreSQL) ๋ฑ๋ฑ์ ๋ํด ์ธ๋ฐ์ด๋ ๊ท์น์ ํ์ฉํด์ฃผ์์ต๋๋ค.
๐ AWS EC2 ์ธ์คํด์ค ๋์ปค ์ค์นํ๊ธฐ
EC2 ์ธ์คํด์ค์์ ์์ ์ ํ์ํ Docker์ Docker-Compose๋ฅผ ์ค์นํด์ฃผ๋๋ก ํฉ๋๋ค.
๊ด๋ จํ์ฌ ์๋ฃ๊ฐ ๋ง๊ธฐ ๋๋ฌธ์ ์์ธํ ์ค๋ช ์ ์๋ตํ๊ณ , ์๋์ ๊ฐ์ด ๊ฐ๋จํ ๊ตฌ์ถํ๊ฒ ์ต๋๋ค.
// ๋์ปค ์ค์น
sudo yum install docker -y
// ๋์ปค ์คํ
sudo service docker start
// ๋์ปค ์ํ ํ์ธ
systemctl status docker.service
// Docker ๊ด๋ จ ๊ถํ ์ถ๊ฐ
sudo chmod 666 /var/run/docker.sock
docker ps
// ๋์ปค ์ปดํฌ์ฆ ์ค์น
sudo curl \
-L "https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$(uname -s)-$(uname -m)" \
-o /usr/local/bin/docker-compose
// ๊ถํ ์ถ๊ฐ
sudo chmod +x /usr/local/bin/docker-compose
// ๋ฒ์ ํ์ธ
docker-compose --version
๐ GitHub-Actions ์คํฌ๋ฆฝํธ ํ์ผ ์์ฑํ๊ธฐ
๋ค์์ผ๋ก ๋ฐฐํฌ์ ์ฌ์ฉํ GitHub-Actions ์คํฌ๋ฆฝํธ ํ์ผ์ ์์ฑํด๋ณด๊ฒ ์ต๋๋ค.
GitHub ๋ ํฌ์งํ ๋ฆฌ - Actions - Continuous integration์์ Java with Gradle์ Configure๋ฅผ ํด๋ฆญํฉ๋๋ค.
๊ทธ๋ผ ์๋ ์ฌ์ง๊ณผ ๊ฐ์ด ~/.github/workflow/gradle.yml ์ด๋ผ๋ ํ์ผ์ด ์์ฑ์ด ๋ ํ ๋ฐ์, ์๋์ฒ๋ผ ์์ฑํด์ฃผ๋๋ก ํฉ๋๋ค.
gradle.yml ๋ผ๋ ํ์ผ์ ํ์ฌ gradle build ์ ์ฉ ์คํฌ๋ฆฝํธ ํ์ผ์ด๊ณ , ํ์ผ๋ช ์ ์์ ๋กญ๊ฒ ๋ณ๊ฒฝํ์ ๋ ๋ฉ๋๋ค.
(์ ๋ github-actions.yml ๋ก ๋ณ๊ฒฝํ์ต๋๋ค ๐)
๐ github-actions.yml
github-actions.yml(gradle.yml) ํ์ผ์ ์ ์ฒด ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ์๋ฐ์, ํ๋์ฉ ์ค๋ช ๋๋ฆฌ๊ฒ ์ต๋๋ค.
์ด ๋ถ๋ถ์ ์ ์ ์ํฉ ๋ฐ ํ๊ฒฝ์ ๋ง๊ฒ ์ปค์คํ ํ ๊ฒฐ๊ณผ์ด๊ธฐ ๋๋ฌธ์, ๋ณธ์ธ์ ํ๊ฒฝ์ ๋ง๊ฒ ์์ ์ด ํ์ํฉ๋๋ค.
๋ํ ์ ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๊ฐ๋ฐํ๊ฒฝ(dev)์ ๊ธฐ์ค์ผ๋ก ํฌ์คํ ์ ํ ์์ ์ ๋๋ค.
# github repository Actions ํ์ด์ง์ ๋ํ๋ผ ์ด๋ฆ
name: CI/CD
# event trigger
on:
push:
branches:
- main
- develop
permissions:
contents: read
jobs:
CI-CD:
runs-on: ubuntu-latest
steps:
## jdk setting
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin' # https://github.com/actions/setup-java
## gradle caching
- 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-
## create application-dev.properties
- name: make application-dev.properties
if: contains(github.ref, 'develop')
run: |
cd ./src/main/resources
touch ./application-dev.properties
echo "${{ secrets.PROPERTIES_DEV }}" > ./application-dev.properties
shell: bash
## create application-prod.properties
- name: make application-prod.properties
if: contains(github.ref, 'main')
run: |
cd ./src/main/resources
touch ./application-prod.properties
echo "${{ secrets.PROPERTIES_PROD }}" > ./application-prod.properties
shell: bash
## create firebase-key.json
- name: create firebase key
run: |
cd ./src/main/resources
ls -a .
touch ./firebase-service-key.json
echo "${{ secrets.FIREBASE_KEY }}" > ./firebase-service-key.json
shell: bash
## gradle build
- name: Build with Gradle
run: ./gradlew build -x test -x ktlintCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck -x ktlintKotlinScriptCheck
## docker build & push to production
- name: Docker build & push to prod
if: contains(github.ref, 'main')
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -f Dockerfile-prod -t ${{ secrets.DOCKER_REPO }}/dotoriham-prod .
docker push ${{ secrets.DOCKER_REPO }}/dotoriham-prod
## docker build & push to develop
- name: Docker build & push to dev
if: contains(github.ref, 'develop')
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -f Dockerfile-dev -t ${{ secrets.DOCKER_REPO }}/dotoriham-dev .
docker push ${{ secrets.DOCKER_REPO }}/dotoriham-dev
## deploy to production
- name: Deploy to prod
uses: appleboy/ssh-action@master
id: deploy-prod
if: contains(github.ref, 'main')
with:
host: ${{ secrets.HOST_PROD }}
username: ec2-user
key: ${{ secrets.PRIVATE_KEY }}
envs: GITHUB_SHA
script: |
sudo docker rm -f $(docker ps -qa)
sudo docker pull ${{ secrets.DOCKER_REPO }}/dotoriham-prod
docker-compose up -d
docker image prune -f
## deploy to develop
- name: Deploy to dev
uses: appleboy/ssh-action@master
id: deploy-dev
if: contains(github.ref, 'develop')
with:
host: ${{ secrets.HOST_DEV }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
port: 22
#key: ${{ secrets.PRIVATE_KEY }}
script: |
sudo docker rm -f $(docker ps -qa)
sudo docker pull ${{ secrets.DOCKER_REPO }}/dotoriham-dev
docker-compose up -d
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.
์คํฌ๋ฆฝํธ ํ์ผ์ด ํ ๋์ ๋ค์ด์ค์ง ์์ผ๋ฏ๋ก ๋ถ๋ถ์ ์ผ๋ก ๋๋์ด์ ์ค๋ช ๋๋ฆฌ๊ฒ ์ต๋๋ค.
๋จผ์ ์ ์ฒด ํ๋ก์ธ์ค๋ GitHub ๋ ํฌ์งํ ๋ฆฌ์ main ๋ธ๋์น(prod) ๋ฐ develop ๋ธ๋์น(dev)๋ก push๊ฐ ๋ ๋ ๊ฐ ํ๊ฒฝ๋ณ๋ก ๋ค์๊ณผ ๊ฐ์ ๊ณผ์ ์ ๊ฑฐ์ณ AWS EC2 ์ธ์คํด์ค์ ๋ฐฐํฌ๊ฐ ๋๋ ๊ณผ์ ์ ๋๋ค.
- Gradle Build - ๊ฐ ํ์ผ ์์ฑ - Docker build & push - AWS EC2 Deploy(ssh) - Slack ์๋ฆผ
๋ค๋ง ๋ ธ์ถ๋์ด์๋ ์๋๋ properties๊ณผ ๊ฐ์ ํ์ผ์ ๊นํ๋ธ์ ์ฌ๋ ค๋๋ฉด ์๋๊ธฐ ๋๋ฌธ์ GitHub-Actions ์คํฌ๋ฆฝํธ์์ ์ง์ ์์ฑํด์ฃผ์ด์ผ ํ๊ณ , ํ๊ฒฝ๋ณ ๋ถ๋ฆฌ๊ฐ ํ์ํ๊ธฐ ๋๋ฌธ์ ์ฝ๋์ ๊ธธ์ด๊ฐ 2๋ฐฐ๊ฐ ๋ ๊ฒ์ ๋๋ค. ๐
๊ทธ๋ผ ์ ์คํฌ๋ฆฝํธ ํ์ผ์ ๋ํด ํ๋์ฉ ์ค๋ช ๋๋ฆฌ๊ฒ ์ต๋๋ค.
(์ฐธ๊ณ ๋ก ๊ณต์๋ฌธ์์ ์์ธํ ์ค๋ช ๋์ด ์์ผ๋, ๋ ๊ถ๊ธํ์ ๋ถ๋ถ์ ์ฐพ์๋ณด์๋ฉด ๋ฉ๋๋ค.)
๐ on: push: branches:
- ์๋ ์ฝ๋๋ main, develop ๋ธ๋์น์ push๊ฐ ๋์์ ๋ Workflow๋ฅผ Trigger๋ฅผ ์คํํ๋ค๋ ์๋ฏธ์ ๋๋ค.
# github repository Actions ํ์ด์ง์ ๋ํ๋ผ ์ด๋ฆ
name: Dotoriham CI/CD
# event trigger
on:
push:
branches:
- main
- develop
๐ Jdk Setting
๋ค์์ผ๋ก jobs ๋ถ๋ถ์ธ๋ฐ์, GitHub-Actions์ Workflow๋ ๋ค์ํ Job์ผ๋ก ๊ตฌ์ฑ๋๋ฉฐ Job๋ ๋ค์ Steps๋ก ๊ตฌ์ฑ์ด ๋ฉ๋๋ค.
- GitHub-Actions์์ ์ฌ์ฉ๋ JDK๋ฅผ ์ธํ ํฉ๋๋ค. (ํ๋ก์ ํธ๋ AWS์ Java ๋ฒ์ ๊ณผ๋ ๋ฌ๋ผ๋ ๋ฌด๋ฐฉํฉ๋๋ค.)
- Java-Version์ผ๋ก 11์ ์ฌ์ฉํ๊ณ , distribution์ผ๋ก 'temurin'์ ์ฌ์ฉํฉ๋๋ค.
- ๊ณผ๊ฑฐ์๋ adopt๋ฅผ ์ฌ์ฉ์ ํ์๋๋ฐ, ํ์ฌ ๊ณต์๋ฌธ์์ ๋ฐ๋ฅด๋ฉด adopt ๋์ temurin์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ฐ๋ ฅํ ๊ถ์ฅํ๊ณ ์์ต๋๋ค. ๋ํ adopt๋ ๋์ด์ ์ ๋ฐ์ดํธ ๋์ง์๋๋ค๊ณ ํ๊ธฐ์ ํน๋ณํ ์ด์ ๊ฐ ์๋ค๋ฉด temurin์ ์ฌ์ฉํฉ๋๋ค.
jobs:
CI-CD:
runs-on: ubuntu-latest
steps:
## jdk setting
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin' # https://github.com/actions/setup-java
๐ Gradle Caching
๋ค์์ผ๋ก Gradle์ ์บ์ฑํ๋ ์ฝ๋์ ๋๋ค.
์ด ๋ถ๋ถ์ ์ถ๊ฐํ์ง ์์๋ ๋ฌด๋ฐฉํ๋ ๊ฐ์ธ์ ์ผ๋ก ์ ์ฉํ์ ๋ ๋น๋ ์๊ฐ์ด 20~30% ๊ฐ๋ ํฅ์๋์๊ธฐ ๋๋ฌธ์ ์ถ๊ฐํด์ฃผ์์ต๋๋ค.
๋ ์์ธํ ์ค๋ช ์ ์๋ ๊ณต์๋ฌธ์๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์ :)
## gradle caching
- 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-
๐ application-<dev, prod>.properties
๋ค์์ผ๋ก ํ๊ฒฝ ๋ณ properties ํ์ผ์ ์์ฑํ๋ ๋ถ๋ถ์ ๋๋ค.
์ด ํ์ผ์ ๊ฐ์ธ ์ ๋ณด(DB ์ ๋ณด, AWS ์ ๋ณด ๋ฑ๋ฑ)๊ฐ ํฌํจ๋์ด ์๊ธฐ ๋๋ฌธ์ ๊นํ๋ธ์๋ ์ฌ๋ ค๋์ง ์๊ณ , GitHub-Actions์์ ์ง์ ํ์ผ์ ์์ฑํฉ๋๋ค.
์ ๋ ./src/main/resources๊ฐ ์๋ ./src/main/resource ๋ก ๊ฒฝ๋ก๋ฅผ ์๋ชป์ก์์ค์ ๊ฝค๋ ์ฝ์ง์ ํ๋๋ฐ์.. ์ ์ ๊ฐ์ ์ค์๋ฅผ ๋ฒํ์ง ๋ง์ธ์. ๐ญ๐ญ
## create application-dev.properties
- name: make application-dev.properties
if: contains(github.ref, 'develop') # branch๊ฐ develop์ผ ๋
run: |
# spring์ resources ๊ฒฝ๋ก๋ก ์ด๋
cd ./src/main/resources
# application-dev.properties ํ์ผ ์์ฑ
touch ./application-dev.properties
# GitHub-Actions์์ ์ค์ ํ ๊ฐ์ application-dev.properties ํ์ผ์ ์ฐ๊ธฐ
echo "${{ secrets.PROPERTIES_DEV }}" > ./application-dev.properties
shell: bash
## create application-prod.properties
- name: make application-prod.properties
if: contains(github.ref, 'main') # branch๊ฐ main ์ผ ๋, ๋๋จธ์ง๋ ์์ ๋์ผ
run: |
cd ./src/main/resources
touch ./application-prod.properties
echo "${{ secrets.PROPERTIES_PROD }}" > ./application-prod.properties
shell: bash
์ ๊ณผ์ ์ ๋ํ ๋ด์ฉ์ ์ฃผ์์ผ๋ก ์ค๋ช ํด๋์์ต๋๋ค.
ํ์ผ ๋ด์ฉ์ ์ฐ๊ธฐ ์ํด์๋ GitHub-Actions์ secrets์ ๊ฐ์ ์ค์ ํด์ผ ํฉ๋๋ค.
ํด๋น ๋ด์ฉ์ ๊นํ๋ธ ๋ ํฌ์งํ ๋ฆฌ์ Settings > Secrets > Actions > New repository secret ๋ฒํผ์ ํตํด ๋ง๋ค ์ ์์ต๋๋ค.
โป secrets ๋ด์ฉ๋ค์ ์ํธํ๋์ด ์๊ธฐ ๋๋ฌธ์ ํ ๋ฒ ๊ฐ์ ์ค์ ํ๋ฉด ๋ค์ ํ์ธํ ์ ์์ด ๋ณ๊ฒฝ๋ง ๊ฐ๋ฅํฉ๋๋ค(ํจ์ค์๋์ฒ๋ผ)
- DOCKER_PASSWORD: ๋์ปค ๊ณ์ ํจ์ค์๋
- DOCKER_REPO: ๋์ปค ๋ ํฌ์งํ ๋ฆฌ
- DOCKER_USERNAME: ๋์ปค ID
- FIREBASE_KEY: ํ์ด์ด๋ฒ ์ด์ค key
- HOST_DEV: dev ํ๊ฒฝ์ EC2 ์ธ์คํด์ค ip
- HOST_PROD: prod ํ๊ฒฝ์ EC2 ์ธ์คํด์ค ip
- PASSWORD: EC2 ์ธ์คํด์ค ํจ์ค์๋
PRIVATE_KEY:- PROPERTIES_DEV: application-dev.properties ํ์ผ ๋ด์ฉ
- PROPERTIES_PROD: application-prod.properties ํ์ผ ๋ด์ฉ
- SLACK_WEBHOOK_URL: ์ฌ๋ ์ฐ๋์ ์ํ ์นํ URL
- USERNAME: EC2 ์ธ์คํด์ค ๊ณ์ ID(ec2-user)
์ ๋ ์์ ๊ฐ์ ์ ๋ณด๋ค์ด ํ์ํด์ ์ค์ ์ ํด ์ฃผ์๋๋ฐ์, ๋ณธ์ธ์ ์ํฉ์ ๋ฐ๋ผ ํ์ํ ๊ฐ๋ค๋ง ์ค์ ํด์ฃผ์๋ฉด ๋ฉ๋๋ค ๐คฏ
Name์ ๋ณ์ ๋ช ์, Value์ ๊ฐ์ ๋ณต์ฌํด์ ์ค์ ํด์ฃผ์๋ฉด ๋ฉ๋๋ค.
๐ firebase-key.json
๋ค์์ ํ์ด์ด๋ฒ ์ด์ค์ ํค ํ์ผ์ ์ค์ ํ๋ ๋ถ๋ถ์ธ๋ฐ์, ์ ๋ FCM์ ์ฌ์ฉํ๊ธฐ ์ด ํ์ผ๋ ๋ง๋ค์ด ์ฃผ์์ต๋๋ค.
## create firebase-key.json
- name: create firebase key
run: |
cd ./src/main/resources
touch ./firebase-service-key.json
echo "${{ secrets.FIREBASE_KEY }}" > ./firebase-service-key.json
shell: bash
์ฃผ์ํ์ค ๋ถ๋ถ์, GitHub-Actions์์๋ double-quote(์๋ฐ์ดํ ")๋ฅผ ์ธ์ํ๋๋ก ํ๊ธฐ ์ํด์๋ escape ๋ฌธ์(๋ฐฑ์ฌ๋์ \)๋ฅผ ์ถ๊ฐํด์ฃผ์ด์ผ ํฉ๋๋ค. ์ด๋ถ๋ถ ๋๋ฌธ์๋ ์ฝ๊ฐ ์ฝ์ง์ ํ๋ค์ ^^;
## ๊ธฐ์กด firebase-key.json ๋ด์ฉ
{
type: service_account,
project_id: dotoriham-dfee3,
private_key_id: 85796c2b2645b94eb354eedf718e81ae2fa514dd,
...
}
## GitHub-Actions์์ ์ธ์ํ๊ธฐ ์ํ escape character \
{
\"type\": \"service_account\",
\"project_id\": \"dotoriham-dfee3\",
\"private_key_id\": \"85796c2b2645b94eb354eedf718e81ae2fa514dd\",
...
}
๐ Gradle Build - Docker Build & Push
๋ค์์ผ๋ก ๊ทธ๋ ์ด๋ค ๋น๋ ๋ฐ ๋์ปค ๋น๋, ํธ์ฌ ๊ณผ์ ์ ๋๋ค.
์์ ๋ง์ฐฌ๊ฐ์ง๋ก secrets. ๋ณ์๋ค์ ๋ชจ๋ ๊นํ๋ธ์ ๋ฏธ๋ฆฌ ์ค์ ์ ํด์ฃผ์์ต๋๋ค.
(๋์ปค์ ๊ณผ์ ์ ๋ํด์๋ ๋ฐ๋ก ์ค๋ช ์ ์๋ตํ๊ฒ ์ต๋๋ค. ๋์ปค์ ์คํ๋ง๋ถํธ์ ๋ฐฐํฌ ๊ณผ์ ์ ๋ํด ๊ถ๊ธํ์ ๋ถ์ ํฌ์คํ ์ ์ฐธ๊ณ ํด์ฃผ์ธ์.)
## gradle build
- name: Build with Gradle
run: ./gradlew build -x test -x ktlintCheck -x ktlintTestSourceSetCheck -x ktlintMainSourceSetCheck -x ktlintKotlinScriptCheck
## docker build & push to production
- name: Docker build & push to prod
if: contains(github.ref, 'main')
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -f Dockerfile-prod -t ${{ secrets.DOCKER_REPO }}/dotoriham-prod .
docker push ${{ secrets.DOCKER_REPO }}/dotoriham-prod
## docker build & push to develop
- name: Docker build & push to dev
if: contains(github.ref, 'develop')
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -f Dockerfile-dev -t ${{ secrets.DOCKER_REPO }}/dotoriham-dev .
docker push ${{ secrets.DOCKER_REPO }}/dotoriham-dev
prod, dev๋ณ๋ก spring profiles๋ฅผ ์ ์ฉํด์ผ ํ๊ธฐ ๋๋ฌธ์ Dockerfile์ ๋ถ๋ฆฌํด์ฃผ์๋๋ฐ์, ๋์ปค๋ฅผ ๋น๋ ํ ๋ ํน์ ํ์ผ์ ๊ฐ์ ์ ์ผ๋ก ์ง์ ํ๊ณ ์ ํ๋ฉด -f ์ต์ ์ ์ง์ ํด์ฃผ์๋ฉด ๋ฉ๋๋ค.
(๋ํดํธ๋ ์ต์์ ๋๋ ํ ๋ฆฌ์ Dockerfile์ ํ ๋๋ก ๋น๋๋ฅผ ์งํํฉ๋๋ค)
Dockerfile- ํ์ผ์ ๊ฒฝ๋ก ๋ฐ ๋ด์ฉ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.(ํ๋ก์ ํธ ์ต์์ ๋๋ ํ ๋ฆฌ์ ๋๋ค.)
## Dockerfile-dev
FROM openjdk:14-jdk-slim
EXPOSE 8080
ARG JAR_FILE=/build/libs/Web-Team-2-Backend-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","-Dspring.profiles.active=dev","/app.jar"]
## Dockerfile-prod
FROM openjdk:14-jdk-slim
EXPOSE 8080
ARG JAR_FILE=/build/libs/Web-Team-2-Backend-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","-Dspring.profiles.active=prod","/app.jar"]
๐ Deploy to AWS EC2(using ssh)
๋ง์ง๋ง์ผ๋ก SSH ์ฐ๊ฒฐ์ ํตํด EC2 ์ธ์คํด์ค์ ๋์ปค์ ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ ธ์ ๋ฐฐํฌํ๋ ๊ณผ์ ์ ๋๋ค.
ssh-action์ ๋ค์ ์ฌ์ดํธ๋ฅผ ์ฐธ๊ณ ํ์๋ฉด ๋ฉ๋๋ค.
## deploy to production
- name: Deploy to prod
uses: appleboy/ssh-action@master
id: deploy-prod
if: contains(github.ref, 'main')
with:
host: ${{ secrets.HOST_PROD }}
username: ec2-user
key: ${{ secrets.PRIVATE_KEY }}
envs: GITHUB_SHA
script: |
sudo docker rm -f $(docker ps -qa)
sudo docker pull ${{ secrets.DOCKER_REPO }}/dotoriham-prod
docker-compose up -d
docker image prune -f
## deploy to develop
- name: Deploy to dev
uses: appleboy/ssh-action@master
id: deploy-dev
if: contains(github.ref, 'develop')
with:
host: ${{ secrets.HOST_DEV }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
port: 22
#key: ${{ secrets.PRIVATE_KEY }}
script: |
sudo docker rm -f $(docker ps -qa)
sudo docker pull ${{ secrets.DOCKER_REPO }}/dotoriham-dev
docker-compose up -d
docker image prune -f
์์์ HOST_PROD ๋ฐ HOST_DEV์ ๊ฐ์ EC2 ์ธ์คํด์ค์ IP๋ฅผ ์ค์ ํ์๋ฉด ๋ฉ๋๋ค.
์ ๊ฐ์ ๊ฒฝ์ฐ๋ ์๋ ํ๋์ ๋ค๋ชจ์น ๊ฐ์ ๋ณต์ฌํด์ ๋ถ์ฌ๋ฃ์ด์ฃผ์์ต๋๋ค.
- ~~~.ap-northeast-2.compute.amazonaws.com
secrets.USERNAME, secrets.PASSWORD ์ ๊ฒฝ์ฐ EC2์์ ๋ฐ๋ก ์ค์ ์ด ํ์ํฉ๋๋ค.
// ec2 ๊ณ์ ๋น๋ฐ๋ฒํธ ์ค์
sudo passwd [ec2 ์ฌ์ฉ์ ๊ณ์ ]
// ssh config ์์ -> PasswordAuthentication๋ฅผ yes๋ก ๋ณ๊ฒฝ
sudo vi /etc/ssh/sshd_config
PasswordAuthentication yes
// ์ฌ๊ธฐ๋
sudo service sshd restart
// ec2 ์ธ์คํด์ค ๋ก๊ทธ์ธ
ssh ec2-user@[EC2 ์ธ์คํด์ค ip]
๐ time ์ถ๋ ฅ ๋ฐ slack ์ฐ๋
๋ง์ง๋ง์ ํ์ฌ ์๊ฐ ์ถ๋ ฅ ๋ฐ ์ฌ๋ ์ฐ๋์ ํตํด GitHub-Actions์ Workflow์ ๊ฒฐ๊ณผ์ ๋ํด ์๋ฆผ์ ๋ฐ๊ธฐ ์ํ ์์ ์ ๋๋ค.
ํ์์ ์ธ ๋ถ๋ถ์ ์๋๋ฉฐ, ์ถํ ์ฌ๋์ ์ฐ๋ํ๋ ๋ฐฉ๋ฒ์ ๋ํด์๋ ๋ฐ๋ก ํฌ์คํ ์ผ๋ก ์์ฑํด๋ณด๊ฒ ์ต๋๋ค..๐
## 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.
์ ๊ณผ์ ์ด ๋๋ ํ GitHub-Actions์ ํ ์คํธ๋ฅผ ํด๋ณด๋ฉด EC2 ์ธ์คํด์ค์ ์กด์ฌํ๋ docker-compose.yaml ํ์ผ์ ์ฝ์ด์ ๋ฐฐํฌ๊ฐ ๋๋๋ฐ์, ์ฐธ๊ณ ๋ก ์ ์ docker-compose.yaml ํ์ผ์ ๋ด์ฉ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
version: "3"
services:
mongodb:
image: mongo
restart: always
container_name: yapp-mongo
ports:
- 27017:27017
environment:
- MONGO_INITDB_ROOT_USERNAME=yapp
- MONGO_INITDB_ROOT_PASSWORD=yapp
- MONGO_INITDB_DATABASE=yapp
volumes:
- mongodb:/var/lib/mongo
postgresdb:
image: postgres
restart: always
container_name: yapp-postgres
ports:
- 5432:5432
environment:
- POSTGRES_USER=yapp
- POSTGRES_PASSWORD=yapp
volumes:
- postgresdb:/var/lib/postgres
redisdb:
image: redis
restart: always
ports:
- 6379:6379
container_name: yapp-redis
volumes:
- redisdb:/var/lib/redis
dotoriham:
image: juhyun419/dotoriham-dev
restart: always
container_name: dotoriham-dev
ports:
- 8080:8080
depends_on:
- mongodb
- postgresdb
- redisdb
volumes:
mongodb:
postgresdb:
redisdb:
์ ๋ MongoDB, PostgreSQL, Redis๋ฅผ ์๋ฒ์์ ์ฌ์ฉํ๊ณ ์๊ธฐ ๋๋ฌธ์ dev์ ๊ฒฝ์ฐ docker-compose๋ฅผ ํ์ฉํด ๋ชจ๋ ๋์ปค๋ก ๋์์ฃผ์์ต๋๋ค.
์์ ๊ฐ์ด ์ ์์ ์ผ๋ก ๋ฐฐํฌ๊ฐ ๋๋๋ฐ์, ํน์ ๋ฐฐํฌ์ ์คํจํ๋ค๋ฉด ๋์ปค ๋ก๊ทธ๋ฅผ ํตํด ์๋ฌ ๋ก๊ทธ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.
docker logs -f <CONTAINER ID>
๐ Spring Boot Logback์ ํตํด ๋ก๊ทธ ํ์ผ ๋จ๊ธฐ๊ธฐ
์ถ๊ฐ์ ์ผ๋ก Spring Boot, docker-compose์์ logback์ ํ์ฉํ์ฌ AWS EC2 ์๋ฒ์ ๋ก๊ทธ ํ์ผ์ ๋จ๊ธฐ๋ ๋ฐฉ๋ฒ์ ๋ํด์๋ ๊ฐ๋ตํ ์ดํด๋ณด๊ฒ ์ต๋๋ค. (๊ฐ์ ํ ๋ถ๋ถ์ด ๊ต์ฅํ ๋ง๊ธด ํ์ง๋ง.. ์ ์์ ์ผ๋ก ๋ก๊ทธ ํ์ผ์ด ๊ธฐ๋ก๋๋ ๊ฒ์๋ง ํฌ์ปค์ฑ์ ๋์์ต๋๋ค. ๐)
๋จผ์ , Dockerfile์ VOLUME ํค์๋๋ก ์ปจํ ์ด๋์์ ๋ก๊ทธ๋ก ๋จ๊ธธ ๋๋ ํ ๋ฆฌ๋ฅผ ์ค์ ํฉ๋๋ค.
์ ๋ ํ๊ฒฝ๋ณ prod, dev๋ก ๋ถ๋ฆฌํ์์ผ๋ฏ๋ก ๋ ํ์ผ ๋ชจ๋ ์ค์ ์ ํ์ต๋๋ค.
// Dockerfile-prod
FROM openjdk:14-jdk-slim
EXPOSE 8080
ARG JAR_FILE=/build/libs/Web-Team-2-Backend-0.0.1-SNAPSHOT.jar
VOLUME ["/var/log"]
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","-Dspring.profiles.active=prod","/app.jar"]
// Dockerfile-dev
FROM openjdk:11-jdk-slim
EXPOSE 8080
ARG JAR_FILE=/build/libs/Web-Team-2-Backend-0.0.1-SNAPSHOT.jar
VOLUME ["/var/log"]
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","-Dspring.profiles.active=dev","/app.jar"]
- VOLUME์ ๋์ปค ์ปจํ ์ด๋์์ ์์ฑํ๊ณ ์ฌ์ฉ๋๋ ๋ฐ์ดํฐ๋ฅผ ์ ์งํ๊ธฐ ์ํด ์ฌ์ฉํ๋ ๋ฉ์ปค๋์ฆ์ ๋๋ค. ๋์ปค ์ปจํ ์ด๋์ ์ฐ์ฌ์ง ๋ฐ์ดํฐ๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ปจํ ์ด๋๊ฐ ์ญ์ ๋ ๋ ํจ๊ป ์์ด์ง๋๋ฐ์, ์ด๋ฌํ ๋์ปค ์ปจํ ์ด๋์ ์๋ช ์ฃผ๊ธฐ์ ๊ด๊ณ์์ด ๋ฐ์ดํฐ๋ฅผ ์์์ ์ผ๋ก ์ ์ฅํ๊ณ ์ ํ ๋ ๋ณผ๋ฅจ(volume)์ ๋ฐ์ธ๋ ๋ง์ดํธ(bind mount)๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
- ์ค์ ์ ์์ ๊ฐ์ด VOLUME ["/var/log"]๋ VOLUME /var/log์ ๊ฐ์ด ์ฌ์ฉํ ์ ์์ต๋๋ค.
https://docs.docker.com/storage/volumes/
https://docs.docker.com/storage/bind-mounts/
๋ค์์ผ๋ก, logback ์ค์ ํ์ผ์์ ๋ก๊ทธ๋ฅผ ๋จ๊ธธ ํ์ผ์ ๊ฒฝ๋ก๋ฅผ ๋์ปค ์ปจํ ์ด๋ ๊ธฐ์ค์ผ๋ก ์ค์ ํฉ๋๋ค.
(logback์ ๋ํ ์ค๋ช ์ ๋ฐ๋ก ์งํํ์ง ์๊ฒ ์ต๋๋ค.)
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- ๋ณ์ ์ค์ -->
<property name="LOG_DIR_SERVER" value="/var/log"/>
<property name="LOG_FILE" value="logfile.log"/>
<property name="LOG_FILE_PROD" value="${LOG_DIR_SERVER}/${LOG_FILE}"/>
<property name="LOG_FILE_DEV" value="${LOG_DIR_SERVER}/${LOG_FILE}"/>
<property name="LOG_FILE_LOCAL" value="./log/${LOG_FILE}"/>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>[%d{yyyy-MM-dd HH:mm:ss}][%thread] %highlight(%-5level) %logger{36} - %msg%n</Pattern>
</layout>
</appender>
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<springProfile name="prod">
<file>${LOG_FILE_PROD}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_DIR_SERVER}/${LOG_FILE}.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>180</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
</springProfile>
<springProfile name="dev">
<file>${LOG_FILE_DEV}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_DIR_SERVER}/${LOG_FILE}.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>180</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
</springProfile>
<springProfile name="local">
<file>${LOG_FILE_LOCAL}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>./log/%d{yyyy-MM}/${LOG_FILE}.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>365</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
</springProfile>
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss}:%-4relative][%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<!-- Logging level - TRACE > DEBUG > INFO > WARN > ERROR > FATAL -->
<!-- ๋ชจ๋ ๋ก๊ทธ ๋ ๋ฒจ์ INFO๋ก ์ค์ : INFO, WARN, ERROR, FATAL ๋ ๋ฒจ ์ถ๋ ฅ -->
<root level="INFO">
<appender-ref ref="console"/>
<appender-ref ref="file"/>
</root>
</configuration>
- ์ ์ค์ ํ์ผ์ ๊ฐ์ ์ด ๋ง์ด ํ์ํ logback ํ์ผ์ด๋ ์ฐธ๊ณ ๋ง ํด์ฃผ์ธ์..
- ์์์ ๋ก๊ทธ ํ์ผ์ ๊ฒฝ๋ก๋ ์๋ ์ฝ๋ ๋ถ๋ถ์
๋๋ค.
- <property name="LOG_DIR_SERVER" value="/var/log"/>
๋ง์ง๋ง์ผ๋ก, docker-compose.yaml ํ์ผ์์ volumes ์ค์ ์ ์งํํฉ๋๋ค.
// ์๋ต
...
dotoriham:
image: juhyun419/dotoriham-dev
restart: always
container_name: dotoriham-dev
ports:
- 8080:8080
depends_on:
- mongodb
- postgresdb
- redisdb
environment:
- TZ=Asia/Seoul
volumes:
- /home/ec2-user/log:/var/log // ์ถ๊ฐ๋ ๋ถ๋ถ, ํธ์คํธ ๊ฒฝ๋ก:์ปจํ
์ด๋ ๊ฒฝ๋ก
volumes:
mongodb:
postgresdb:
redisdb:
- volumes: ํ์์ /home/ec2-user/log:/var/log ์ ๋๋ค.
์์ ๊ฐ์ด ์ค์ ํ๋ฉด ์๋์ ๊ฐ์ด /home/ec2-user/log ๊ฒฝ๋ก์ ๋ก๊ทธ ํ์ผ์ด ๊ธฐ๋ก์ด ๋ฉ๋๋ค.
๋ค๋ง, ๋ง์๋๋ฆฐ ๋๋ก ์๋ด์ผ ํ ๋ถ๋ถ์ด ๋ง๊ธฐ ๋๋ฌธ์ ๋ณธ์ธ์ ํ๊ฒฝ์ ๋ง๊ฒ ์ ์ ํ ์์ ํ์ฌ ์ฌ์ฉํ์๋ ๊ฒ์ ์ถ์ฒ๋๋ฆฝ๋๋ค.
๐ ์ ๋ฆฌ
์ด์์ผ๋ก GitHub-Actions๋ฅผ ํ์ฉํ์ฌ CI/CD๋ฅผ ๊ตฌ์ถํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์๋ณด์์ต๋๋ค.
๊ฐ์ธ์ ์ผ๋ก๋ ์คํ, ๊ณต๋ฐฑ, SSH ๋ฑ ๊ต์ฅํ ๋ง์ ์ฝ์ง์ ํ๋๋ฐ์..๐ญ ๋ง์ ์ฝ์ง์ด ์์์ง๋ง, ๋ง์ด ๋ฐฐ์ธ ์ ์๋ ์ข์ ๊ฒฝํ์ด์์ต๋๋ค.
๐ ์ฐธ๊ณ ๋ฌธ์
- https://docs.github.com/en/actions
- https://stackoverflow.com/questions/60916931/github-action-does-the-if-have-an-else
- https://itcoin.tistory.com/685?category=769089
- https://github.com/appleboy/ssh-action
- https://github.com/actions/cache/blob/main/examples.md#java---gradle
- https://bgpark.tistory.com/85
'DevOps' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Docker] Spring Boot + Docker + AWS EC2 ๋ฐฐํฌํ๊ธฐ(MacOS M1) (1) | 2021.07.19 |
---|---|
GitHub Action์ ํตํ CI ๊ตฌ์ถํ๊ธฐ (SpringBoot + Maven) (2) | 2021.05.14 |
[Apache Kafka] ์นดํ์นด Window ์ค์น & ํ ์คํธ (0) | 2021.02.06 |
๋๊ธ