기존의 프로젝트 Repo에서는 Github Actions를 이용하여 빌드파일을 압축하여 S3로 전송한 뒤, CodeDeploy를 통하여 EC2 서버 내에서 Nginx를 통해 배포하였다.
하지만, CI/CD가 복잡한 점이 아쉬워서 도커 컴포즈를 이용하여 여러개의 컨테이너(Nginx, React, Spring, Redis)를 실행시켜 서버를 배포(리버스 프록시)하도록 도전했다.😊
백엔드, 프론트엔드 깃허브 레포지토리에서 develop 브랜치로 푸쉬하면, CI/CD가 이루어져 EC2 내의 도커 이미지가 실행되어 자동으로 배포된다!😊
version: '3'
services:
web:
container_name: web
image: {docker ID}/moamoa-web
expose:
- 8080
volumes:
- ./secret:/secret
ports:
- 8080:8080
react:
container_name: front
image: {docker ID}/moamoa-front
expose:
- 3000
nginx:
container_name: nginx
image: nginx:latest
restart: always
volumes:
- ./conf/:/etc/nginx/conf.d
- /data/certbot/conf:/etc/letsencrypt
- /data/certbot/www:/var/www/certbot
ports:
- 80:80
- 443:443
depends_on:
- web
- react
redis:
image: redis:alpine
command: redis-server --port 6379
container_name: redis_boot
hostname: redis_boot
labels:
- "name=redis"
- "mode=standalone"
ports:
- 6379:6379
{docker ID}는 자신의 도커 ID로 수정해주세요!
docker-compose 파일을 EC2 내에서 생성하여 실행하였다. web 컨테이너에서는 로컬 서버에 있는 ./secret/application.yml 파일을 도커에 마운트하여 저장하였다.
nginx에서는 로컬 파일에 ./conf/ 내 nginx.conf (nginx 구성파일)를 통해 리버스 프록시를 설정하였다. /data/certbot/ 내의 인증서를 마운트하여, nginx 내에 구성하였다. depends_on은 web 컨테이너와 react 컨테이너를 먼저 실행하도록 설정했다.
redis 이미지를 불러와 컨테이너화 (port 6379)하여 구성했다.
EC2 내에서 /api 내로 들어오는 요청이면 8080포트(스프링)으로, / 내로 들어오는 요청이면 3000포트(리액트)로 리버스 프록시를 구성하도록 nginx.conf를 수정했다.
# conf/nginx.conf 생성
server {
listen 80;
client_max_body_size 20M;
server_name moamoadev.shop;
return 301 https://moamoadev.shop$request_uri; # http로 들어오면 https로 redirect 해주는 부분
}
server {
listen 443 ssl;
client_max_body_size 20M;
server_name moamoadev.shop;
# Certificate
ssl_certificate /etc/letsencrypt/live/moamoadev.shop/fullchain.pem;
# Private Key
ssl_certificate_key /etc/letsencrypt/live/moamoadev.shop/privkey.pem;
location /api {
proxy_pass http://web:8080; # 자신의 springboot app이사용하는 포트
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://front:3000; # 자신의 springboot app이사용하는 포>트
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
nginx.conf를 살펴보자.
우선 SSL 적용을 위해 80포트로 들어오면, 443 포트로 리다이렉트 하였다.
location /api는 web 컨테이너(스프링):8080로 리다이렉트하였고, location /는 front 컨테이너(리액트):3000로 리다이렉트 하였다.
# Dockerfile
FROM node:alpine as builder
WORKDIR /usr/src/app
COPY package.json .
RUN npm install
COPY ./ ./
RUN npm run build
FROM nginx
EXPOSE 3000
COPY ./default.conf /etc/nginx/conf.d/default.conf
COPY --from=builder usr/src/app/build /usr/share/nginx/html
node:-alpine 이미지를 베이스로 한다. WORKDIR을 usr/src/app으로 설정한다.
워킹 디렉토리 내부로 package.json 파일을 복사한 뒤, 그 안에서 npm install & build 한다.
Nginx 이미지를 베이스로 하여 프로젝트 내부의 default.conf 파일을 복사하고, 빌드 파일을 도커 내에 복사한다. 3000 포트를 열어준다.
# .dockerignore
.git
node_modules
.gitignore
server {
listen 3000;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}
#npm-publish.yml
name: Deploy
on:
push:
branches:
- develop
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: web docker build and push
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -t ${{ secrets.DOCKER_REPO }}/moamoa-front .
docker push ${{ secrets.DOCKER_REPO }}/moamoa-front
- name: executing remote ssh commands using password
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST_ID }}
username: ec2-user
key: ${{ secrets.PRIVATE_KEY }}
script: |
sudo docker rm -f $(docker ps -qa)
sudo docker pull ${{ secrets.DOCKER_REPO }}/moamoa-front
docker-compose up -d
docker image prune -f
[백엔드]
FROM openjdk:11
ARG JAR_FILE=build/libs/moamoa-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-Dspring.config.location=/secret/application.yml","-jar","/app.jar"]
openjdk 11 버전을 기반으로, 빌드파일을 도커내의 ./app.jar로 복사한다.
ENTRYPOINT를 통해 빌드된 app.jar 파일을 /secret/application.yml을 외부 주입하여 jar 파일을 실행한다.
이전 시도에서 application.yml을 깃허브 secret을 통해 내용을 넣은 후, 깃허브 액션에서 application.yml을 만드는 과정을 시도했다.
하지만 깃허브 액션내에 컴파일 과정(테스트 과정)에서 오류가 나서 외부 주입하는 방식으로 변경하였다.
# This workflow will build a Java project with Gradle
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
# Repo Action 페이지에 나타날 이름
name: Spring Boot & Gradle CI/CD
# Event Trigger
# master branch에 push 또는 pull request가 발생할 경우 동작
# branch 단위 외에도, tag나 cron 식 등을 사용할 수 있음
on:
push:
branches: [ develop ]
jobs:
build:
# 실행 환경 지정
runs-on: ubuntu-18.04
# Task의 sequence를 명시한다.
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
- name: Grant execute permission for gradlew
run: chmod +x gradlew
# Build
- name: Build with Gradle
run: ./gradlew clean build
# 웹 이미지 빌드 및 도커허브에 push
- name: web docker build and push
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -t ${{ secrets.DOCKER_REPO }}/moamoa-web .
docker push ${{ secrets.DOCKER_REPO }}/moamoa-web
# docker compose up
- name: executing remote ssh commands using password
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST_ID }}
username: ec2-user
key: ${{ secrets.PRIVATE_KEY }}
script: |
sudo docker rm -f $(docker ps -qa)
sudo docker pull ${{ secrets.DOCKER_REPO }}/moamoa-web
docker-compose up -d
docker image prune -f
EC2내에 docker-compose.yaml을 생성한 디렉터리 내에서 secret/application.yml을 따로 주입했다.
시크릿은 repository settings에 Secrets>Actions에 등록해주면 된다. 위의 DOCKER_USERNAME, DOCKER_REPO에는 각각 도커 유저 네임, 도커 레포지토리 이름을 시크릿으로 등록해주자.
✔️ 터미널에서 작업을 자동화 할 수 있다.
ssh 키를 우선 세팅한다. 내 컴퓨터에서 키를 만들기 위해 CMD 터미널을 띄운다.
배포할 서버에 public 키를 넣어두고 private 키로 접근한다.
ssh-keygen -t rsa -b 4096 -C "[내메일]" -f {이름}
공개키({이름})의 내용은 EC2 서버의 ~/.ssh 경로의 authorized_keys 에다가 붙여 넣어주자.
spring-cicd 파일에 담긴 개인키의 내용은 GITHUB SECRET의 PRIVATE_KEY로 등록한다.