CI/CD 파이프라인과 함께 전체 배포될 인프라의 아키텍처는 위와 같이 설계하였다.
React는 현재 FE팀에서 별도로 Firebase를 통해 빌드 후 배포까지 진행될 수 있도록 구현하였고
BE의 경우 프레임워크 배포 이외에도 별도의 FFmpeg, OLLAMA 등 요구되는 사항이 많기 때문에 위와 같이 별도로 Gihub Action, Docker, Jenkins를 통해 CI/CD 파이프라인을 구축하는 것으로 계획하였다.
또한 FE Dev는 개발 도중 Postman Mock Server와 Test Express BE를 통해 더욱 자유롭게 테스트할 수 있도록 하였고 BE Dev는 인공지능 관련 기능 개발 시 Test Vue 앱을 통해 더 원활하게 개발을 진행할 수 있도록 하였다.
이전에 FE 팀원분께서 실시간 음성인식을 위한 Socket과 관련한 스레드르르 남겨주셔서
위와 같이 기존에 개발된 Express BE를 수정하여 테스트할 수 있도록 하였고
BE Dev 로컬에서 Test Vue 앱을 통해 테스팅을 완료하였다.
최종적으로는 AWS RDB를 사용하여 서비스 전체의 가용성과 안정성을 높일 예정이지만
현재는 1차 배포 단계이므로 가격이 더 저렴하거나 무료인 클라우드 RDB 서비스를 활용할 예정이다.
또한 이후 AWS RDB를 사용할 때 비용을 절약하기 위해서 아래와 같은 체크 리스트를 점검해야 한다.
- Multi-AZ(Multi-Availability Zone, 멀티 가용성 존) deployment -> NO
특히, Multi-AZ deployment 는 dafault가 Yes로 되어있기때문에 주의해야한다.
- Storage type(스토리지 타입) -> General Purpose(SSD)
- Backup retention period(백업 보존 기간) -> 0days
이러한 이유로 litegix
라는 서비스에서 위와 같이 hearus-db
를 생성하고 이를 호스팅하여준다.
정상적으로 연결할 수 있는지 테스트하기 위하여 로컬 HeidiSQL
에서 연결을 진행하였고, DB에 정상적으로 접속하는 것을 확인할 수 있었다.
# MariaDB Properties
spring.jpa.hibernate.ddl-auto=update
spring.datasource.driverClassName=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://....onlitegix.com:31927/hearus_test
spring.datasource.username=...
spring.datasource.password=...
또한 Spring Boot BE에서도 위와 같이 application-private.properties
를 수정하여
litegix
에 호스팅된 DB에 정상적으로 연결하는 것을 확인할 수 있었다.
FROM openjdk:17-jdk-slim
# Install FFmpeg
RUN apt-get update && apt-get install -y ffmpeg
WORKDIR /app
COPY build.gradle .
COPY gradlew .
COPY gradlew.bat .
COPY gradle gradle
COPY src ./src
RUN ./gradlew build --no-daemon
COPY build/libs/*.jar app.jar
EXPOSE 8080 9094
ENTRYPOINT ["java", "-jar", "app.jar"]
Spring CI Pipeline를 구축하기 위하여 먼저 위와 같이 Dockerfile
을 작성한다.
해당 Dockerfile
에서는 실시간 음성인식에 필요한 FFmpeg
라이브러리를 설치한다.
package com.hearus.hearusspring.common.ffmpeg;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class FFmpegConfig {
@Value("${spring.profiles.active:}")
private String activeProfile;
public String getFFmpegPath() {
if ("production".equals(activeProfile)) {
// Docker 환경에서는 시스템 경로의 FFmpeg 사용
return "ffmpeg";
} else {
// 로컬 개발 환경에서는 프로젝트에 포함된 FFmpeg 사용
return "src/main/resources/ffmpeg/bin/ffmpeg";
}
}
}
이후 FFmpegConfig.java
를 정의하고 만약 로컬 개발 환경이라면 현재 진행하는 것처럼 프로젝트에 포함된 FFmpeg
를 사용하고 Docker 환경에서는 시스템 경로의 FFmpeg
를 사용할 수 있도록 설정하였고
...
// Use FFmpeg to convert the audio data
FFmpeg ffmpeg = new FFmpeg(ffmpegConfig.getFFmpegPath());
FFmpegBuilder builder = new FFmpegBuilder()
.setInput(tempInputFile.getAbsolutePath())
.overrideOutputFiles(true)
.addOutput(tempOutputFile.getAbsolutePath())
.setAudioCodec("pcm_s16le")
.setAudioChannels(1)
.setAudioSampleRate(16000)
.setFormat("s16le")
.addExtraArgs("-loglevel", "quiet")
.done();
AudioConverter.java
에서 new FFmpeg(ffmpegConfig.getFFmpegPath())
을 통해 FFmpeg의 path를 받아와 Audio Format을 변경할 수 있게 하였다.
docker build --tag judemin/hearus-spring:lastest .
docker run -dit -p 8080:8080 -p 9094:9094 judemin/hearus-spring:lastest
이후 위와 같이 Dockerfile
을 빌드하여주고 run을 통해 정상적으로 실행되는지 확인한다.
docker login
docker push judemin/hearus-spring:lastest
또한 이후 위와 같이 Dockerhub에 정상적으로 Push되는지 확인한다.
이후 Github Action이 해당 Repo를 빌드하는 과정에서 application-private.properties
를 자동적으로 추가하여줄 수 있도록 Repositoty Secret에 위와 같이 application-private.properties
의 내용을 담은 Secret을 생성해준다.
name: Spring Boot CI with Gradle
on:
push:
branches: [ "main" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
## create application-private.properties
- name: make application-private.properties
run: |
## create application-private.properties
cd ./src/main/resources
# application-private.properties 파일 생성
touch ./application-private.properties
# GitHub-Actions 에서 설정한 값을 application-private.properties 파일에 쓰기
echo "${{ secrets.SPRING_BE_APPLICATION_PRIVATE }}" >> ./application-private.properties
shell: bash
# gradle build
- name: Build with Gradle
run: |
chmod +x ./gradlew
./gradlew bootJar
shell: bash
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.ORG_JUDEMIN_DOCKERHUB_USERNAME }}
password: ${{ secrets.ORG_JUDEMIN_DOCKERHUB_PASSWORD }}
- name: Install FFmpeg and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: judemin/hearus-spring:lastest
# GitHub Action Cache
cache-from: type=gha
cache-to: type=gha,mode=max
이후 Github Action의 Workflow를 위와 같이 작성하여 make application-private.properties
단계에서는 Secret 값을 프로젝트 내에 추가해줄 수 있도록 하였고 Build with Gradle
단계에서는 Gradle 빌드를 실행해준 뒤 Set up Docker Buildx
단계 이후로는 빌드된 jar 파일을 통해 Docker 빌드를 수행하고 DockerHub에 PUSH할 수 있도록 하였다.
/home/runner/work/_temp/fa30f693-5dd6-4bc0-92e0-8a6437feb104.sh: line 2: ./gradlew: Permission denied
아지만 위와 같이 gradlew
를 실행하는 도중 Permission 문제가 발생하였고
....
## gradle build
- name: Build with Gradle
run: |
chmod +x ./gradlew
./gradlew bootJar
shell: bash
Workflow 내에서 chmod
를 통해 gradlew
의 권한을 변경해주었다.
Run docker login -u -p ...
Error: Cannot perform an interactive login from a non TTY device
또한 Dockerhub에 로그인하는 과정에서 위와 같은 오류가 발생하여
...
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.ORG_JUDEMIN_DOCKERHUB_USERNAME }}
password: ${{ secrets.ORG_JUDEMIN_DOCKERHUB_PASSWORD }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: judemin/hearus-spring:lastest
# GitHub Action Cache
cache-from: type=gha
cache-to: type=gha,mode=max
Workflow를 docker/login-action@v1
를 통해 로그인하는 것으로 변경하였다.
buildx failed with: ERROR: failed to solve: lstat /tmp/buildkit-mount3977780021/build/libs: no such file or directory
이후 정상적으로 Gradle 빌드까지 수행하는 것을 확인하였지만 위와 같이 /build/libs
경로 문제가 발생하였고
FROM openjdk:17-jdk-slim
# Install FFmpeg
RUN apt-get update && apt-get install -y ffmpeg
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
EXPOSE 8080 9094
ENTRYPOINT ["java", "-jar", "app.jar"]
Github Action과 Docker에서 모두 Gradle 빌드를 하는 것이 비효율적이라고 판단하여 Docker에서는 Github Action에서 빌드한 jar 파일을 COPY할 수 있도록 변경하였으며
...
- name: Install FFmpeg and push
id: docker_build
uses: docker/build-push-action@v2
with:
# Dockerfile이 위치한 디렉토리를 지정
context: .
push: true
tags: judemin/hearus-spring:lastest
# GitHub Action Cache
cache-from: type=gha
cache-to: type=gha,mode=max
Dockerfile이 위치한 디렉토리를 지정하여 해당 문제를 해결하였다.
Github Action에서 Gradle 빌드가 정상적으로 수행되는 것을 확인하였고
최종적으로 Dockerhub에도 이미지가 잘 Push된 것을 확인하였다.
EC2 인스턴스를 생성하기 이전 위와 같이 VPC를 생성해주었다.
이후 HEARUS-SPRING
을 위한 보안 그룹을 생성하고
HEARUS-SPRING
의 인스턴스를 위와 같이 생성해주었다.
또한 퍼블릭 IP를 자동으로 할당해주도록 하여, 최종 배포 이전 테스트를 가능하게 하였다.
인바운드 규칙의 경우 기본 API 포트인 8080
, 추후 Socket 연결을 수행할 9094
포트에 대한 요청을 모두 허용하였고, SSH 연결의 경우 현재 내 IP만 허용해주었다.
이후 미리 생성한 pem
키를 통해 SSH 연결을 시도하였고
정상적으로 SSH 연결이 완료된 것을 볼 수 있었다.
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
EC2 인스턴스의 OS를 ubuntu로 설정하였기 때문에 Docker 공식 Document의 설명에 따라 위와 같이 apt repository
를 설정하였다.
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
이후 apt-get install
을 통해 해당 인스턴스에 Docker를 설치하였으며
~$ sudo systemctl | grep docker
sys-devices-virtual-net-docker0.device loaded active plugged /sys/devices/virtual/net/docker0
sys-subsystem-net-devices-docker0.device loaded active plugged /sys/subsystem/net/devices/docker0
docker.service loaded active running Docker Application Container Engine
docker.socket loaded active running Docker Socket for the API
systemctl
을 통해 Docker 엔진이 정상적으로 구동되고 있는지 확인하였다.
docker login
최종적으로 docker에 login한 이후
sudo docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
위와 같이 hello-world
를 실행해 Docker를 HEARUS-SPRING
EC2 인스턴스에 설치하였다.
이후 CD 파이프라인을 위한 Jenkins EC2 인스턴스를 위와 같이 생성한다.
sudo wget -O /usr/share/keyrings/jenkins-keyring.asc \
https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key
echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc]" \
https://pkg.jenkins.io/debian-stable binary/ | sudo tee \
/etc/apt/sources.list.d/jenkins.list > /dev/null
sudo apt-get update
sudo apt-get install jenkins
LTS 버전을 설치하기 위해 Jenkins 공식 Document를 참고하여 위와 같이 apt-get install jenkins
를 통해 Jenkins를 설치한다.
sudo apt update
sudo apt install fontconfig openjdk-17-jre
java -version
또한 Jenkins는 Java가 필요하기 때문에 openjdk-17-jre
를 위위 같이 설피하고.
sudo -i
$ systemctl | grep jenkins
● jenkins.service loaded failed failed Jenkins Continuous Integration Server
systemctl
를 통해 jenkins가 구동되는 것을 확인한다.
$ systemctl status jenkins.service
× jenkins.service - Jenkins Continuous Integration Server
Loaded: loaded (/usr/lib/systemd/system/jenkins.service; enabled; preset: enabled)
Active: failed (Result: exit-code) since Mon 2024-07-15 07:06:50 UTC; 4min 5s ago
Main PID: 1942 (code=exited, status=1/FAILURE)
CPU: 10ms
하지만 위와 같이 jenkins의 실행 상태가 FAILURE
라면
systemctl edit jenkins
systemctl edit jenkins
를 통해 jenkins conf 파일로 접근하여
별도로 Jenkins가 실행될 포트 번호를 환경변수로 설정한다.
systemctl restart jenkins
이후 jenkins 서비스를 재시작하면
# systemctl status jenkins
● jenkins.service - Jenkins Continuous Integration Server
Loaded: loaded (/usr/lib/systemd/system/jenkins.service; enabled; preset: enabled)
Drop-In: /etc/systemd/system/jenkins.service.d
└─override.conf
Active: active (running) since Mon 2024-07-15 07:17:48 UTC; 22s ago
위와 같이 active
함을 확인할 수 있다.
이후 Web UI 환경의 Jenkins에 접근하기 위하여 Jenkins conf에서 설정한 8081
포트를 EC2 인스턴스의 보안그룹의 인바운드 규칙 수정을 통해 허용해주고
위와 같이 정상적으로 접속이 되는 것을 확인할 수 있었다.
sudo cat /var/lib/jenkins/secrets/initialAdminPassword
이후 초기 비밀번호를 위와 같이 확인하여 Jenkins를 Unlock하고
suggested plugin들을 설치하면
위와 같이 EC2 환경에서 Jenkins를 정상적으로 실행할 수 있다.
$ ssh-keygen
$ ls -al
total 36
drwx------ 5 root root 4096 Jul 15 06:44 .
drwxr-xr-x 22 root root 4096 Jul 15 06:27 ..
-rw------- 1 root root 884 Jul 15 06:44 .bash_history
-rw-r--r-- 1 root root 3106 Apr 22 13:04 .bashrc
drwx------ 2 root root 4096 Jul 15 06:37 .docker
-rw------- 1 root root 20 Jul 15 06:36 .lesshst
-rw-r--r-- 1 root root 161 Apr 22 13:04 .profile
drwx------ 2 root root 4096 Jul 15 08:11 .ssh
drwx------ 3 root root 4096 Jul 15 06:27 snap
이후 Integration 이후 Deployment는 Jenkins에서 Spring-BE
EC2 인스턴스로 Docker command 요청을 보내 Github Action을 통해 빌드된 Docker 이미지를 pull 한 이후 run 할 수 있도록 하기 위하여 위와 같이 ssh-keygen
명령을 통해 key를 생성한다.
$ cat id_ed25519pub
ssh-... ... root@...
vi hearus-spring-be-key-pair.pem
chmod 600 hearus-spring-be-key-pair.pem
ssh -i hearus-spring-be-key-pair.pem ...@...
하지만 ssh-keygen
를 사용하는 방식에 문제가 있다고 판단되어 위와 같이 EC2 인스턴스 생성시 생성된 hearus-spring-be-key-pair.pem
를 통해 접속을 시도하였다.
하지만 이러한 방식에 문제가 있다고 판단하여 Jenkins의 Publish Over SSH
플러그인을 통해 해당 기능을 구현할 수 있도록 하였다.
cp hearus-spring-be-key-pair.pem /var/lib/jenkins
먼저 기존에 생성한 pem 키를 jenkins의 home dir로 복사하고
위와 같이 Jenkins 관리
-> System
에서 새로운 SSH Server
를 추가해주었다.
이때 Hostname
은 Remote Host IPv4 Address이다.
sudo docker pull judemin/hearus-spring:lastest
sudo docker run -p 8080:8080 -p 9094:9094 --name hearus-spring judemin/hearus-spring:lastest
이후 위와 같은 docker 명령어들을
Jenkins의 Item의 Transfer Set에 추가하였고
위와 같이 성공적으로 docker 명령어들이 이미지를 받아와서 실행하는 것을 볼 수 있었다.
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1d5328075184 judemin/hearus-spring:lastest "java -jar app.jar" About a minute ago Up About a minute 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp, 0.0.0.0:9094->9094/tcp, :::9094->9094/tcp hearus-spring
또한 SPRING-BE
인스턴스에서도 docker ps
를 통해 Jenkins에서 구동한 docker 컨테이너를 확인할 수 있었다.
하지만 첫번째 배포 이후에서는 이미 동일한 이름의 컨테이너가 구동되고 있기 때문에
sudo docker stop hearus-spring
sudo docker rm hearus-spring
sudo docker pull judemin/hearus-spring:lastest
sudo docker run -p 8080:8080 -p 9094:9094 --name hearus-spring -d judemin/hearus-spring:lastest
위와 같이 이전에 구동한 hearus-spring
컨테이너를 중지시키고 삭제한 뒤 다시 새로운 이미지를 통해 배포하는 형태로 명령어를 재구성하였고 -d
옵션을 통해 명령어 실행 후 detach하였다.
최종적으로 Jenkins에서 위와 같이 정상적으로 배포가 수행되는 것을 확인할 수 있었다.
이후 Webhook을 통해 Github Action에서 Jenkins의 Job을 Trigger할 수 있도록 위와 같이 Repo를 명시해주었다.
또한 EC2 인스턴스의 HTTP
에 대한 모든 인바운드 규칙을 활성화하고
Jenkins의 프로필(admin)
->설정
에서 API Token을 발급한 뒤
Github Orgatization에 http://[Jenkins 서버주소]:[포트번호]
값을 가지는 Secret을 추가해 주었다.
...
# Dockerhub로 PUSH된 이후 Jenkins의 Job Trigger
- name: Trigger Jenkins Job
uses: appleboy/jenkins-action@master
with:
url: ${{ secrets.ORG_JENKINS_WEBHOOK_ENDPOINT }}
user: "admin"
token: ${{ secrets.ORG_JENKINS_API_TOKEN }}
job: "SPRING-BE-SSH-CD"
또한 현재 Jenkins의 Deploy가 CI 이후에 이루어진다는 보장이 없기 때문에 Dockerhub로 PUSH된 이후 Jenkins의 Job을 Trigger할 수 있도록 Github Action Workflow에서 appleboy/jenkins-action@master
를 사용하여
해당 Repo의 main branch에 push되기만 하면 Github Action의 Workflow가 완료되어 Dockerhub로 push된 이후
해당 이미지가 Jenkins를 통해 EC2 인스턴스에 자동적으로 배포되는 CI/CD 파이프라인을 구축하였고
배포, FE 활용, 덤프 데이터 등의 내용을 팀원분들께 전달하였다.
References
https://gist.github.com/bmaupin/0ce79806467804fdbbf8761970511b8c
https://velog.io/@arara90/AWS-Free-tier%EB%A1%9C-RDS-%EC%82%AC%EC%9A%A9-%EC%A4%91-%EC%9A%94%EA%B8%88%EC%9D%84-%EC%A7%80%EB%B6%88%ED%96%88%EC%96%B4%EC%9A%94
https://gong-story.tistory.com/m/40
응원합니다~~!!
소규모 프로젝트인데 소통 채널로 slack 선택한 이유가 궁금합니다