목차
0. 포스팅 계기
1. CI/CD 개념
2. OverView : Github Action + Docker + Nginx + AWS EC2
3. 스프링 프로젝트 내부에 .yml 파일 작성
4. 스프링 프로젝트 내부에 Dokerfile 작성
5. Github Actions의 Secrets 값 설정
6. Github Actions의 workflow 작성
7. EC2에 Docker 설치 + docker-compose.yml 파일 작성
8. 방화벽 설정
9. Nginx로 reverse_proxy 설정
0. 포스팅 계기
CI/CD 어느순간부터 귀에 딱지 앉게 많이 들은 단어지만
정확한 개념도 어떻게 구현하는건지도 찾아본 적도 없었다.
그러다 프로젝트 배포를 늘 수동으로!!!!!!!!!! 한 끝에 드디어 CI/CD 구축을 마음 먹게 되었다. (프로젝트 끝날때쯤 먹은 마음...)
"몸이 고생하면 머리가 편하다"를 실천하며 살았지만
막상 몸이 너무 고생하니까 머리를 불편하게 만들고 싶더라고...
CI/CD 개념
CI
- 빌드/테스트 자동화 과정
- CI는 개발자를 위한 자동화 프로세스인 지속적인 통합 의미
- 커밋할때마다 빌드와 일련의 자동화 테스트가 이루어짐
→ 동작을 확인하고 변경으로 인해 문제가 생기는 부분이 없도록 보장 - ex) main 브랜치로 commit or pull request가 발생할 때마다 항상 검증
CD
- 지속적인 서비스 제공/배포 의미
- 코드 변경이 main에 커밋되면, 자동화된 빌드 및 테스트 프로세스를 거쳐 문제가 발견되지 않으면 최종적으로 배포
- ex) main 브랜치에 커밋 후 CI를 통과 > gradle.yml workflow에 따라 Docker Image를 생성 > Docker Image를 EC2에 자동 배포
CI/CD로 선택할 수 있는 옵션은 다양하지만 나는 프로젝트에 Github Action + Docker를 사용하는 방식을 선택했다.
이로써 EC2에 수동으로 빌드 후 배포하는 수고를 덜게 되는 것이다! 오예
2. Overview
시스템 아키텍쳐 사진은 다음과 같다.
3. 스프링 프로젝트 내부에 .yml 작성
스프링 프로젝트를 생성하면 기본으로 application.properties 파일이 생성되지만 나는 여러개의 profile을 적용하기 위해 yaml 파일을 만들었다. 로컬에서 작업할때는 local을 적용하고 실제로 배포할 때는 prod를 적용하며, 계정 관련 정보는 secret에 적어주었다.
yaml 파일 작성은 처음이기도 하고, 여러개의 profile을 적용하는건 더욱이 초면이라 과정을 나같은 초보들을 위해 조금 자세히 적어보려고 한다!
우선 내가 application-prod.yml에 적어주었던 내용은 다음과 같다.
server:
servlet:
encoding:
charset: UTF-8
enabled: true
force: true
# profile 이름으로 prod를 사용하겠다는 의미
spring:
config:
activate:
on-profile: prod
# DB 관련 설정
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url:
username:
password:
jpa:
hibernate:
naming:
# Entity Table 대소문자 구분
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
ddl-auto: none
properties:
hibernate:
format_sql: true
show-sql: true
database: mysql
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
# Initial Data config
defer-datasource-initialization: true
# 메일 전송을 위한 설정
mail:
host: smtp.gmail.com
port: 587
username:
password:
# file upload 최대 크기 지정
servlet:
multipart:
maxFileSize: 5MB
maxRequestSize: 10MB
jackson:
timeZone: Asia/Seoul
dateFormat: yyyy-MM-dd HH:mm:ss
cloud:
aws:
credentials:
accessKey:
secretKey:
# AWS S3 bucket 정보
s3:
bucket:
region:
static:
stack:
auto: false
url:
# 문자 인증을 위한 설정
naver-cloud-sms:
accessKey:
secretKey:
serviceId:
senderPhone:
.gitignore에 application-secret.yml과 application-prod.yml을 추가해주었고
application-local.yml에 application-secret.yml을 import하여 사용했다.
(application-prod.yml에는 secret 내용도 함께 넣어주었다.)
Q. 현재 profile에 다른 yml을 import + 현재 적용할 profile 지정하는 방법
A. 이건 버전에 따라 다르게 작성해주어야 한다. 나는 스프링부트 2.1x. 이상의 버전을 사용했는데 구버전에서 적용하는 방법을 사용해서 열심히 삽질을 해주었다 ^^
EX) 현재 Profile을 prod로 적용 + application-secret.yml을 import하고 싶을 때
- SpringBoot v.2.4 이전
spring:
profiles:
active: prod
include: secret
- SpringBoot v.2.4 이후
spring:
config:
import: application-secret.yml
activate:
on-profile: prod
그리고 스프링부트 프로젝트는 대부분 jar 파일을 활용해 배포하는 형식인데 스프링부트 2.5.0 버전 이후부터는 plain.jar를 생성하기 때문에 plain.jar를 생성하고 싶지 않은 경우엔 build.gradle에 다음과 같은 내용을 추가해주어야 한다.
jar {
enabled = false
}
잘 적용됐는지 확인하고 싶다면
gradle (코끼리 누르기) > build > bootJar를 더블클릭하면 .jar를 생성하는데 이때 생성된 파일 이름을 확인해보면 plain이 없는 것을 볼 수 있다.
EX) 프로젝트명-0.0.1-SNAPSHOT.jar 형식으로 생성
4. 스프링 프로젝트 내부에 Dockerfile 작성
Dockerfile은 Docker 컨테이너 이미지를 어떻게 구성할지 지정하는 텍스트 파일로, 프로젝트 최상위 경로에 작성해준다.
내용은 다음과 같다.
ARG JAR_FILE엔 jar 파일의 경로를 작성, ENTRYPOINT엔 적용할 profile을 지정해준다.
FROM openjdk:11-jdk
ARG JAR_FILE=./build/libs/TogeDutch-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT [ "java", "-jar", "-Dspring.profiles.active=prod", "/app.jar" ]
5. Github Actions의 Secrets 값 설정
Github Actions을 사용하면 main 브랜치에 trigger가 발생했을 때 이를 감지하고 gradle.yml에 적힌대로 프로젝트를 빌드, 테스트, 배포 등의 작업을 자동으로 수행한다.
gradle.yml을 통해 Docker를 거쳐 EC2에 배포까지 완료하려면 gradle.yml에 Docker나 EC2의 계정정보와 같은 개인정보들도 작성해야 하는데 이를 노출시키고 싶지 않을 때 Github Actions의 Secret을 활용하면 된다.
IntelliJ에서 환경변수를 통해 변수에 값을 할당해주듯이 Secrets에 값을 넣어 gradle.yml 변수에 값을 할당해주는 것이다!
Github 프로젝트 레포지토리에서 Settings > Secrets and variables > Actions를 선택하고,
New repository secret을 클릭해 필요한 secret들을 생성한다.
나는 총 6개의 Secret을 생성했고 각각 들어가야 하는 내용은 다음과 같다.
Repository Secrets | Contents |
DOCKER_PASSWORD | Docker 계정 비밀번호 |
DOCKER_USERNAME | Docker 계정 아이디 |
EC2_USERNAME | EC2 인스턴스 ID (ubuntu를 사용하는 경우 : ubuntu) |
EC2_PRIVATE_KEY | EC2 인스턴스 .pem (.ppk는 .pem으로 변환) |
EC2_HOST_PROD | Prod 환경의 EC2 인스턴스 IP (퍼블릭 Ipv4 domain 값) |
FIREBASE_KEY | firebase-key.json 파일 내용 (base64 인코딩한 값) |
FIREBASE_KEY 값은 json 형식인데 파일 내용을 그냥 넣어주었더니 자꾸 build에 실패했는데
json 파일의 경우 base64로 인코딩 한 값을 넣어주어야 한다는 것을 알게 되었다! ㅎㅎ
6. Github Actions의 workflow 작성
대망의 workflow 작성이다!
Github 프로젝트 레포지토리에서 Actions 탭을 선택 후 Java with Gradle을 선택한다.
그러면 자동으로 .github/workflows 디렉토리 밑에 gradle.yml 파일이 생성된다.
내가 작성한 gradle.yml 파일과 각각의 프로세스에 대한 과정은 다음과 같다.
Process Name | Process Content |
Event Trigger | main 브랜치에 pull request가 발생하면 gradle.yml 실행 |
Set up JDK 11 | java version 11에 맞게 jdk를 설정 |
Gradle Caching | 빌드 시 캐싱을 활용해 빌드 속도 향상 |
Grant execute permission for gradlew | gradlew에 스크립트에 실행 권한 부여 |
Create application-prod.yml | Secret에 설정해둔 내용으로 application-prod.yml 생성 |
Create firebase-key.json | Secret에 설정해둔 내용으로 firebase-service-key.json 생성 |
Build with Gradle | gradlew 스크립트를 실행하여 Gradle을 사용 |
Docker build & Push to prod | DockerFile을 통해 jar파일을 build 후 DockerHub에 push |
Deploy to prod | Docker Image를 가져와 EC2에 컨테이너를 배포 |
# github repository Actions 페이지에 나타낼 이름
name: CI/CD
# event trigger
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-20.04
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-
- name: Grant execute permission for gradlew
run:
chmod +x gradlew
- name: Build with Gradle
run: ./gradlew build -x test
shell: bash
## create application-prod.yml
- name: create application-prod.yml
if: contains(github.ref, 'main')
run: |
cd ./src/main/resources
touch ./application-prod.yml
echo "${{ secrets.PROPERTIES_PROD }}" > ./application-prod.yml
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
- name: Build With Gradle
if: contains(github.ref, 'main')
run: ./gradlew build -x test
## docker build & push to production
- name: Docker build & push to prod
if: contains(github.ref, 'main')
run: |
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
docker build -f Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/togedutch .
docker push ${{ secrets.DOCKER_USERNAME }}/togedutch
## deploy to production
- name: Deploy to prod
uses: appleboy/ssh-action@master
id: deploy-prod
if: contains(github.ref, 'main')
with:
host: ${{ secrets.EC2_HOST_PROD }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
envs: GITHUB_SHA
script: |
sudo docker rm -f $(docker ps -qa)
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/togedutch
docker-compose up -d
docker image prune -f
7. EC2에 Docker 설치 + docker-compose.yml 파일 작성
이제 EC2에 docker와 docker-compose를 설치한다.
나는 Ubuntu 20.04 버전을 사용하며, 버전마다 도커 설치 방법에 차이가 있으니 유의해야 한다.
// 1. 우분투 시스템 패키지 업데이트
$ sudo apt-get update
// 2. 필요한 패키지 설치
$ sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
// 3. Docker 공식 GPG키 추가
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
// 4. Docker 공식 apt 저장소 추가
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
// 5. 시스템 패키지 업데이트
$ sudo apt-get update
// 6. Docker 설치
$ sudo apt-get install docker-ce docker-ce-cli containerd.io
// 7. Dokcer 실행상태 확인
$ sudo systemctl status docker
// 8. Docker 관련 권한 추가
$ sudo chmod 666 /var/run/docker.sock
$ docker ps
// 9. 도커 컴포즈 설치
$ 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
// 10. 권한 추가
$ sudo chmod +x /usr/local/bin/docker-compose
// 11. 버전 확인
$ docker-compose --version
docker와 docker-compose가 성공적으로 설치되었다면, docker-compose.yml 파일을 작성해야 한다.
나는 docker-compose.yml 파일을 어디에 작성해야 하는지
정말 X 100000 많이 헤맸다. 나만 모르는 숨겨진 위치가 있는 줄 알고 며칠을 헤맸는데 그냥 홈디렉토리에 만들면 된다는 걸 알았을 땐 세상이 날 억까하는 줄 알았다;;
💥아무튼 docker-compose.yml 그냥 홈디렉토리에 만드십쇼!!!!!!!!💥
홈디렉토리 이꼬르 /home/ubuntu 입니다요 ^.^
아 파일 내용은 다음과 같다.
앞에서 봤던 어마무시한 길이의 gradle.yml에 비하면 귀여운 수준이다.
version: '3'
services:
서비스 이름:
image: 도커이미지(= 도커 계정명/레포지토리)
container_name: 컨테이너 이름
restart: always
ports:
- 8080:8080
마지막 줄의 ports는 호스트의 8080 포트와 컨테이너의 8080 매핑한다는 의미로 http://EC2퍼블릭IP:8080으로 접속하면 컨테이너 내부의 애플리케이션에 접근할 수 있다.
8. 방화벽 설정
나는 8080 포트를 사용할 예정이므로 사용하려는 포트의 방화벽과 EC2 인바운드 규칙 편집이 필요하다.
1. EC2에서 8080 포트에 대한 방화벽 설정
# 8080 포트 열려있는지 확인
$ sudo ufw status
# Status: inactive 라면
$ sudo ufw allow 8080
$ sudo ufw allow OpenSSH
$ sudo ufw enable
$ sudo ufw status
2. EC2 인바운드 규칙 편집
보안 > 보안그룹 > 인바운드 규칙 편집 클릭
나는 8080 포트에 대한 모든 요청을 허용해주었고, 필요에 따라 규칙을 수정하면 된다.
9. Nginx 설정
나는 Nginx를 배포를 위한 프록시 서버로 사용하려고 한다.
그러면 80 포트로 요청이 들어왔을 때 자동으로 8080 포트로 연결해주게 된다.
우선 EC2에 nginx를 설치해야 한다. docker와 마찬가지로 Ubuntu 20.04 기준으로 작성했다.
$ sudo apt update
$ sudo apt install nginx
# Nginx 실행 확인
$ sudo systemctl start nginx
$ sudo systemctl status nginx
그리고 conf 파일을 포함하여 몇 가지 파일의 수정이 필요하다.
$ sudo vi /etc/nginx/sites-enabled/default
위의 파일을 열고 아래와 같은 내용 2줄을 추가해준다. 위치를 확인하고 추가해야한다!!!
include /etc/nginx/conf.d/service-url.inc;
proxy_pass $service_url;
+ proxy_pass 위에 try files는 주석 처리!!
다음으로 service-url.inc 파일을 추가해야한다.
이 파일이 nginx가 자동으로 8080 포트로 포워딩할 수 있도록 해준다고 보면 될 것 같다.
# 1. 파일 열기
$ sudo vi /etc/nginx/conf.d/service-url.inc
# 2번은 밑에 코드 참고
# 3. 아래의 파일 내용 추가 후
$ sudo service nginx restart
# 2. 파일 내용 추가
set $service_url http://127.0.0.1:8080;
마지막으로 80포트도 8080 포트처럼 방화벽 규칙을 등록해주면 reverse_proxy까지 잘 동작하는
Github Actions + Docker + Nginx + AWS EC2를 활용한 CI/CD를 구축해낸 것이다!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
테스트용으로 급하게 만들었지만 결과도 잘 나오는 것을 확인할 수 있었다!!! ㅎㅎ
+ EC2 생성 시 프로젝트 볼륨에 맞게 메모리 크기를 설정하고, Swapfile을 설정해주어야 한다. 그렇지 않으면 배포 직후 CPU 사용률이 급증하면서 EC2 상태 검사에 실패하고 서버가 죽는다... 내가 CI/CD를 담당하지 않아서 원인을 몰랐는데 알고보니 Swapfile이 문제였다 ㅎㅎ
'Server > CI&CD' 카테고리의 다른 글
[CI/CD] Github Actions + Elastic Beanstalk를 활용한 Node.js CI/CD 구축 (2) (0) | 2024.03.11 |
---|---|
[CI/CD] Github Actions + Elastic Beanstalk를 활용한 Node.js CI/CD 구축 (1) (2) | 2024.02.18 |
[CI/CD] GitLab + Jenkins를 활용한 SpringBoot CI/CD 구축 (3) - 完 (2) | 2024.02.13 |
[CI/CD] GitLab + Jenkins를 활용한 SpringBoot CI/CD 구축 (2) (0) | 2023.10.20 |
[CI/CD] GitLab + Jenkins를 활용한 SpringBoot CI/CD 구축 (1) (0) | 2023.08.18 |