AI를 활용해 맞춤형 건강 조언을 제공하는 과민대장증후군 관리 어플리케이션 Bellywelly의 백엔드 개발을 담당했다.
무료로 CI/CD 파이프라인을 구축하고 HTTPS 적용하여 배포한 과정을 정리해보았다.
사용한 기술 스택
- Java 17, Springboot 3.2.0
- AWS EC2(OS: Ubuntu 22.04 / 아키텍처: Arm64), RDS(MySQL Community), S3
- Github Actions, Docker 26.0.0
- Nginx 1.18.0, Certbot 2.10.0
시스템 아키텍처
무료로 EC2를 사용하기 위해서는 프리티어 계정을 만들고, 프리티어 서비스만 사용해야 한다.
(EC2 프리티어 참고: https://aws.amazon.com/ko/ec2/pricing/?loc=ft#Free_tier)
원래 EC2 인스턴스 중 t2.micro만 프리티어가 지원되는데, 올해는 그보다 메모리 등이 더 좋은 t4g.small도 프리티어 적용이 된다.
(https://aws.amazon.com/ko/ec2/instance-types/t4)
EC2 AMI는 Ubuntu 22.04로 선택하였고, 아키텍처는 t4g.small을 사용하기 위해서 Arm으로 설정하였다.
키 페어는 pem 파일로 생성한다.
키 페어 파일을 저장한 폴더를 기억해두자.
보안 그룹을 생성하고, 모든 체크박스를 누른다.
프리티어는 최대 30GB의 스토리지를 사용할 수 있다. 따라서 30으로 설정하였다.
인스턴스 생성을 완료한 후, 인스턴스의 보안그룹 인바운드 규칙 편집에서 8080번 포트를 열어준다.
인스턴스 상세 페이지에서 연결
버튼을 누르고 SSH 클라이언트
탭에 나온 설명을 따라하면 된다.
cd downloads //키 페어 파일 저장한 폴더로 이동
chmod 400 "bellywelly-ec2.pem" //키 페어 파일 권한 변경
ssh -i "bellywelly-ec2.pem" ubuntu@{퍼블릭 DNS} //인스턴스 연결
접속에 성공했으면 대한민국 시간으로 timezone을 변경하자.
sudo timedatectl set-timezone Asia/Seoul
date
명령어로 timezone 확인
date
내도메인.한국에서 로그인하고 원하는 도메인 등록하면 된다.
백엔드 서버는 서브 도메인으로 api를 붙여주었다.
A 레코드에 EC2 인스턴스의 IP 주소(ex. 1.1.1.1)를 넣으면 설정 끝!
sudo apt update
sudo apt install nginx
Nginx가 잘 설치되었는지 확인해보자.
sudo systemctl status nginx
active(running) 이면 성공!
sudo vi /etc/nginx/conf.d/default.conf
default.conf 파일에 다음 내용을 작성한다.
{domain_name}에는 4번에서 EC2 IPv4와 연결되어있는 도메인을 넣어주면 된다.
나는 앞에 api를 붙였기 때문에 api.도메인.kr 을 넣어주었다.
server {
listen 80;
server_name {domain_name};
location / {
proxy_pass http://localhost:8080;
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를 재시작한다.
sudo service nginx restart
curl -fsSL https://get.docker.com/ | sudo sh
코드를 업로드한 레포지토리의 Settings 탭에 들어간다.
Security > Secrets and variables > Actions 탭에서 Repository secrets를 등록한다.
내가 등록한 것들
프로젝트 루트에 Dockerfile
이름의 파일을 작성한다.
# 사용할 base 이미지 선택
# ec2 아키텍처가 arm64v8이라서 적절한 이미지 가져옴
FROM arm64v8/eclipse-temurin:17-jdk-focal
# build/libs/ 에 있는 jar 파일을 JAR_FILE 변수에 저장
ARG JAR_FILE=build/libs/*.jar
# JAR_FILE을 app.jar로 복사
COPY ${JAR_FILE} app.jar
# Docker 컨테이너가 시작될 때 /app.jar 실행
# 애플리케이션 timezone을 대한민국으로 설정
ENTRYPOINT ["java","-jar","-Duser.timezone=Asia/Seoul","/app.jar"]
프로젝트 루트에 .github/workflows 폴더를 만들고, 그 아래에 yml 파일을 작성한다.
name: CI/CD
# main, deploy 브랜치에 push하면 워크플로우 실행
on:
push:
branches: [ "main", "deploy" ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: 17
distribution: 'zulu'
# repository secrets에 올린 application.yml을 빌드 시 생성
- name: Make application.yml
run: |
mkdir ./src/main/resources
cd ./src/main/resources
touch ./application.yml
echo "${{ secrets.APPLICATION }}" > ./application.yml
- 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: Build with Gradle
run: |
chmod +x ./gradlew
./gradlew build -x test
# ID, PW를 이용해 Docker hub에 로그인
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# Docker 이미지 빌드 후 푸시
- name: Docker build & push
uses: docker/build-push-action@v2
with:
# 빌드 컨텍스트 지정: 지정한 디렉토리 안에 Dockerfile이 있어야 함
context: .
# 빌드에 사용할 Dockerfile의 경로 지정
file: ./Dockerfile
# 빌드할 이미지의 플랫폼 지정
platforms: linux/arm64/v8
# 빌드 후 Docker 레지스트리에 푸시할지 여부 지정
push: true
# 이미지 태그 지정
tags: ${{ secrets.DOCKER_REPO }}:latest
# SSH를 사용하여 EC2에 명령을 전달
- name: Deploy to Server
uses: appleboy/ssh-action@master
with:
# 원격 서버의 호스트 주소 지정
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY }}
envs: GITHUB_SHA
# 아래 명령들을 실행
script: |
sudo docker rm -f $(sudo docker ps -qa)
sudo docker pull ${{ secrets.DOCKER_REPO }}:latest
sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_REPO }}:latest
sudo docker image prune -f
마지막 scripts 설명
sudo docker rm -f $(sudo docker ps -qa)
원격 서버에서 실행 중인 모든 Docker 컨테이너를 강제로 중지하고 제거
sudo docker pull ${{ secrets.DOCKER_REPO }}:latest
Docker 이미지 레지스트리에서 latest 태그의 이미지 pull
sudo docker run -d -p 8080:8080 ${{ secrets.DOCKER_REPO }}:latest
Docker 컨테이너 실행
-d: 컨테이너를 백그라운드에서 실행하도록 지정
-p: 이미지를 실행할 포트 지정 (왼쪽 포트: 호스트의 포트 번호, 오른쪽 포트: 컨테이너 내부의 포트 번호 / 호스트의 8080 포트로 들어오는 요청은 컨테이너의 8080 포트로 전달)
sudo docker image prune -f
사용하지 않는 이미지 정리
문제가 없다면 정상적으로 EC2 서버에서 도커가 실행된다.
pull된 이미지 확인
sudo docker images
실행 중인 컨테이너 확인
sudo docker ps
플랫폼 불일치 문제
ec2 생성 시 아키텍처를 arm으로 설정해놓아서, 도커 이미지도 arm64 플랫폼에서 동작하도록 빌드해야 했다.
처음에는 그걸 몰라서 Dockerfile에 베이스 이미지를 openjdk:17-alpine로 적어두었는데, 나중에 지금의 베이스 이미지로 변경하였다.
포트 문제
워크플로우 파일에서 도커 실행 명령문 작성 시 포트를 따로 지정해두지 않았다가 오류가 발생해서, -p 8080:8080을 추가하였다.
spring boot 어플리케이션이 8080번 포트에서 실행 중이고, nginx를 이용해 80번 포트로 들어오는 요청을 8080 포트로 전달하도록 설정했으므로
http://domain_name
으로 접속해보자.
로컬에서 spring boot 어플리케이션을 실행하고 localhost:8080
으로 접속했을 때와 같은 화면이 나오면 성공이다!
나는 인덱스 페이지에 아무것도 설정해두지 않았으므로 404 페이지가 뜨는 게 정상이다.
(참고: https://velog.io/@haru/certbot)
certbot 설치
sudo apt update
sudo apt install snapd
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
certbot 버전 확인
certbot --version
인증서 발급
sudo certbot --nginx -d domain_name
domain_name에는 6번과 마찬가지로 EC2 서버와 연결된 도메인을 적어주면 된다.
이후 이메일을 입력하라고 할 때는 본인의 이메일을 입력하고,
letsencrypt의 약관에 동의하는지 물을 때는 Y를 입력하고,
이메일 수신 여부를 물을 때는 원하는 대로 입력하면 된다.
(참고: https://velog.io/@haru/certbot)
인증서 발급이 완료되었다면, 이전에 작성했던 default.conf 파일에서 80번 포트 접속 시 443번 포트로 리다이렉트하는 작업을 해주면 된다.
sudo vi /etc/nginx/conf.d/default.conf
server {
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/{domain_name}/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/{domain_name}/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
server_name {domain_name};
location / {
proxy_pass http://localhost:8080;
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;
}
}
server {
if ($host = {domain_name}) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name {domain_name};
return 301 https://$host$request_uri; # managed by Certbot
}
# managed by Certbot
이 붙어있는 줄은 내용을 변경하지 않아도 된다.
{domain_name}에 도메인을 넣어준다.
Nginx를 재시작한다.
sudo service nginx restart
브라우저에 domain_name
만 입력해보자.
자동으로 https://domain_name
으로 접속되고, 12번에서 나왔던 화면과 같은 화면이 나오면 성공이다!