Springboot + EC2 + Github Actions + Docker 이용한 CI/CD, Nginx + Certbot 이용한 HTTPS 무료 배포

xyzw·2024년 4월 9일
1

Bellywelly

목록 보기
2/2
post-thumbnail

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

시스템 아키텍처

1. EC2 생성

무료로 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)

AMI 및 인스턴스 유형 설정

EC2 AMI는 Ubuntu 22.04로 선택하였고, 아키텍처는 t4g.small을 사용하기 위해서 Arm으로 설정하였다.

키 페어 생성

키 페어는 pem 파일로 생성한다.
키 페어 파일을 저장한 폴더를 기억해두자.

네트워크 설정

보안 그룹을 생성하고, 모든 체크박스를 누른다.

스토리지 구성

프리티어는 최대 30GB의 스토리지를 사용할 수 있다. 따라서 30으로 설정하였다.

보안 그룹 설정

인스턴스 생성을 완료한 후, 인스턴스의 보안그룹 인바운드 규칙 편집에서 8080번 포트를 열어준다.

2. EC2 timezone 설정

터미널에서 EC2에 접속

인스턴스 상세 페이지에서 연결 버튼을 누르고 SSH 클라이언트 탭에 나온 설명을 따라하면 된다.

cd downloads  //키 페어 파일 저장한 폴더로 이동
chmod 400 "bellywelly-ec2.pem"  //키 페어 파일 권한 변경
ssh -i "bellywelly-ec2.pem" ubuntu@{퍼블릭 DNS}  //인스턴스 연결

timezone 변경

접속에 성공했으면 대한민국 시간으로 timezone을 변경하자.

sudo timedatectl set-timezone Asia/Seoul

date 명령어로 timezone 확인

date

3. 도커 허브 가입 후 레포지토리 생성

https://hub.docker.com/

4. 무료 도메인 발급

내도메인.한국에서 로그인하고 원하는 도메인 등록하면 된다.

백엔드 서버는 서브 도메인으로 api를 붙여주었다.

A 레코드에 EC2 인스턴스의 IP 주소(ex. 1.1.1.1)를 넣으면 설정 끝!

5. Nginx 설치

(참고: https://velog.io/@junho5336/AWS-Http-Https-%EB%A6%AC%EB%8B%A4%EC%9D%B4%EB%A0%89%ED%8A%B8-%EB%AC%B4%EB%A3%8C#nginx-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0)

sudo apt update
sudo apt install nginx

Nginx가 잘 설치되었는지 확인해보자.

sudo systemctl status nginx

active(running) 이면 성공!

6. Nginx 설정

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

7. EC2에 Docker 설치

curl -fsSL https://get.docker.com/ | sudo sh

8. Github Repository Secrets 등록

코드를 업로드한 레포지토리의 Settings 탭에 들어간다.

Security > Secrets and variables > Actions 탭에서 Repository secrets를 등록한다.

내가 등록한 것들

  • APPLICATION: application.yml
  • DOCKER_USERNAME: Docker 아이디
  • DOCKER_PASSWORD: Docker 패스워드
  • DOCKER_REPO: Docker 리포지토리(아이디/레포지토리명 형식)
  • HOST: EC2의 IPv4
  • KEY: EC2 pem key
  • USERNAME: EC2 username(ec2-user, ubuntu, admin, root 등등)

9. Dockerfile 작성

프로젝트 루트에 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"]

10. Github Actions Workflow 파일 작성

프로젝트 루트에 .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
    사용하지 않는 이미지 정리

11. GitHub actions 실행

문제가 없다면 정상적으로 EC2 서버에서 도커가 실행된다.

pull된 이미지 확인

sudo docker images

실행 중인 컨테이너 확인

sudo docker ps

겪었던 문제

  1. 플랫폼 불일치 문제
    ec2 생성 시 아키텍처를 arm으로 설정해놓아서, 도커 이미지도 arm64 플랫폼에서 동작하도록 빌드해야 했다.
    처음에는 그걸 몰라서 Dockerfile에 베이스 이미지를 openjdk:17-alpine로 적어두었는데, 나중에 지금의 베이스 이미지로 변경하였다.

  2. 포트 문제
    워크플로우 파일에서 도커 실행 명령문 작성 시 포트를 따로 지정해두지 않았다가 오류가 발생해서, -p 8080:8080을 추가하였다.

12. 브라우저에서 접속 테스트

spring boot 어플리케이션이 8080번 포트에서 실행 중이고, nginx를 이용해 80번 포트로 들어오는 요청을 8080 포트로 전달하도록 설정했으므로
http://domain_name 으로 접속해보자.

로컬에서 spring boot 어플리케이션을 실행하고 localhost:8080으로 접속했을 때와 같은 화면이 나오면 성공이다!

나는 인덱스 페이지에 아무것도 설정해두지 않았으므로 404 페이지가 뜨는 게 정상이다.

13. Certbot으로 SSL 인증서 발급

(참고: 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를 입력하고,
이메일 수신 여부를 물을 때는 원하는 대로 입력하면 된다.

14. Https 설정

(참고: 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

15. 브라우저에서 접속 테스트

브라우저에 domain_name 만 입력해보자.

자동으로 https://domain_name으로 접속되고, 12번에서 나왔던 화면과 같은 화면이 나오면 성공이다!

0개의 댓글