본문 바로가기
[AWS]

nginx, github action을 활용한 ci/cd 구축

by 프롯 2025. 9. 20.

개발 프로세스에서 가장 중요하면서도 번거로운 작업 중 하나는 바로 '배포'이다. 코드를 수정한 후 매번 서버에 접속해 빌드하고, 기존 프로세스를 중지한 뒤 새 파일을 옮기고, 다시 실행하는 과정은 실수를 유발하기 쉽고 상당한 시간을 소모한다.

이러한 문제를 해결하기 위해 CI/CD(Continuous Integration/Continuous Deployment) 파이프라인을 구축하여 배포 과정을 자동화하고자 한다. 이 글에서는 Spring Boot 애플리케이션을 기준으로, main 브랜치에 코드가 푸시(Push)될 때마다 자동으로 AWS EC2 프로덕션 환경에 배포되는 전체 과정을 상세히 다루고자 한다. GitHub Actions, systemd, Nginx, 그리고 Let's Encrypt를 활용하여 안정적이고 효율적인 무중단 배포 환경을 구축하는 방법을 단계별로 설명한다.


1단계: Production 환경 GitHub Actions 설정

가장 먼저, GitHub Actions 워크플로우가 EC2 인스턴스에 안전하게 접근하여 배포를 진행할 수 있도록 GitHub Secrets에 민감한 정보를 등록해야 한다.

  • AWS_PROD_EC2_HOST: Production EC2 인스턴스의 퍼블릭 IP 또는 연결된 도메인 주소를 입력한다.
  • AWS_PROD_EC2_USERNAME: EC2 인스턴스에 접속할 사용자 이름(예: ubuntu)을 입력한다.
  • AWS_PROD_EC2_SECRET: EC2 접속에 필요한 SSH 개인키(.pem 키)의 내용을 복사하여 붙여넣는다.

이어서, 배포 파이프라인의 실질적인 동작을 정의하는 .github/workflows/deploy-prod.yml 파일을 생성한다.

name: Deploy to PROD Environment with systemd

on:
  push:
    branches:
      - main  # 또는 'production' 등 배포를 트리거할 브랜치

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Github Repository 파일 불러오기
        uses: actions/checkout@v4

      - name: JDK 17버전 설치
        uses: actions/setup-java@v4
        with:
          distribution: corretto
          java-version: 17

      - name: 테스트 및 빌드하기
        run: ./gradlew clean build -x test

      - name: 빌드된 JAR 파일을 EC2로 전송하기
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.AWS_PROD_EC2_HOST }}
          username: ${{ secrets.AWS_PROD_EC2_USERNAME }}
          key: ${{ secrets.AWS_PROD_EC2_SECRET }}
          port: 22
          source: "build/libs/*.jar"
          target: "/tmp/"
          strip_components: 2

      - name: SSH로 EC2에 접속하여 배포하기
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.AWS_PROD_EC2_HOST }}
          username: ${{ secrets.AWS_PROD_EC2_USERNAME }}
          key: ${{ secrets.AWS_PROD_EC2_SECRET }}
          port: 22
          script_stop: true
          script: |
            echo "=== Stopping service ==="
            sudo systemctl stop [서비스명]

            echo "=== Copying new JAR file ==="
            JAR_FILE=$(ls /tmp/*.jar | grep -v plain | head -1)
            sudo cp "$JAR_FILE" /opt/[서비스명]/[서비스명].jar
            sudo chmod 755 /opt/[서비스명]/[서비스명].jar
            echo "JAR file updated: $JAR_FILE"

            echo "=== Starting service ==="
            sudo systemctl start [서비스명]

            echo "=== Checking service status ==="
            sleep 5
            sudo systemctl status [서비스명] --no-pager

이 워크플로우는 main 브랜치에 푸시가 발생하면 자동으로 실행된다. 소스 코드를 체크아웃하고, JDK 17 환경에서 Gradle로 프로젝트를 빌드한다. 빌드가 완료되면 SCP를 통해 생성된 JAR 파일을 EC2의 /tmp/ 디렉토리로 전송하고, SSH로 접속하여 미리 설정해 둔 systemd 서비스를 재시작하는 방식으로 배포를 완료한다.


2단계: Production EC2에 systemd 서비스 설정

애플리케이션을 안정적으로 백그라운드에서 실행하고 관리하기 위해 systemd 서비스를 등록한다. SSH를 통해 Production EC2 인스턴스에 접속하여 다음 명령어를 순서대로 실행한다.

# 애플리케이션 JAR 파일이 위치할 디렉토리 생성
sudo mkdir -p /opt/[서비스명]
sudo chown ubuntu:ubuntu /opt/[서비스명]

# systemd 서비스 파일 생성
sudo tee /etc/systemd/system/[서비스명].service > /dev/null <<EOF
[Unit]
Description=[서비스명] Spring Boot Application
After=network.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/opt/[서비스명]
ExecStart=/usr/bin/java -jar /opt/[서비스명]/[서비스명].jar
Restart=always
RestartSec=10
Environment=SPRING_PROFILES_ACTIVE=prod

[Install]
WantedBy=multi-user.target
EOF

# systemd 데몬 리로드 및 서비스 활성화 (부팅 시 자동 시작)
sudo systemctl daemon-reload
sudo systemctl enable [서비스명]

위 설정은 [서비스명].service라는 이름의 새로운 서비스를 정의한다. 이 서비스는 /opt/[서비스명]/[서비스명].jar 파일을 prod 프로파일로 실행하며, 예기치 않게 종료될 경우 10초 후에 자동으로 재시작한다. 이제 sudo systemctl [start|stop|status] [서비스명] 명령어로 애플리케이션을 손쉽게 관리할 수 있다.


3단계: Nginx 설치 및 리버스 프록시 설정

외부 도메인 요청을 내부에서 실행 중인 Spring Boot 애플리케이션(기본 포트 8080)으로 전달하기 위해 Nginx를 리버스 프록시로 설정한다.

 
# Nginx 설치
sudo apt update
sudo apt install nginx -y

# Nginx 서비스 시작 및 활성화
sudo systemctl start nginx
sudo systemctl enable nginx

# 기본 설정 파일 백업
sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak

# API 서버를 위한 Nginx 설정 파일 생성
sudo tee /etc/nginx/sites-available/[서비스명]-api > /dev/null <<EOF
server {
    listen 80;
    server_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;

        # 타임아웃 설정
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}
EOF

# 생성한 설정을 활성화하고 기본 설정은 비활성화
sudo ln -s /etc/nginx/sites-available/[서비스명]-api /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default

# Nginx 설정 문법 테스트 및 재시작
sudo nginx -t
sudo systemctl restart nginx

이제 [도메인]의 80번 포트로 들어오는 모든 HTTP 요청은 Nginx를 통해 http://localhost:8080에서 실행 중인 Spring Boot 애플리케이션으로 전달된다.


4단계: SSL 인증서 설정 (Let's Encrypt)

안전한 통신을 위해 Let's Encrypt를 사용하여 무료 SSL 인증서를 발급받고 HTTPS를 적용한다.

# Certbot 설치 (snapd 사용)
sudo apt install snapd -y
sudo snap install core; sudo snap refresh core
sudo snap install --classic certbot

# certbot 명령어 링크 생성
sudo ln -s /snap/bin/certbot /usr/bin/certbot

# SSL 인증서 발급 (도메인이 현재 EC2 IP를 가리키고 있어야 함)
sudo certbot --nginx -d [도메인]

Certbot을 실행하면 몇 가지 질문(이메일 주소, 약관 동의 등)이 나타난다. 모든 과정을 마치면 Certbot이 자동으로 Nginx 설정 파일(/etc/nginx/sites-available/[서비스명]-api)을 수정하여 HTTPS를 적용하고, HTTP 요청을 HTTPS로 리디렉션하는 설정을 추가해 준다.

수정된 Nginx 설정은 다음과 같은 형태가 된다.

server {
    server_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;

        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/[도메인]/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/[도메인]/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 {
    if ($host = [도메인]) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    listen 80;
    server_name [도메인];
    return 404; # managed by Certbot
}

5단계: 방화벽 설정

서버 보안을 위해 UFW(Uncomplicated Firewall)를 사용하여 필요한 포트만 허용하도록 설정한다.

# UFW 방화벽 규칙 설정
sudo ufw allow OpenSSH         # SSH 접속 (포트 22) 허용
sudo ufw allow 'Nginx Full'   # HTTP(80), HTTPS(443) 허용
sudo ufw --force enable         # 방화벽 활성화

# 방화벽 상태 확인
sudo ufw status

 

이제 SSH 접속과 웹 서비스에 필요한 포트를 제외한 모든 외부 접근이 차단된다.


6단계: 도메인 DNS 설정

사용 중인 도메인 등록 기관(예: 가비아, Route 53 등)의 DNS 관리 콘솔로 이동하여, 서브도메인이 Production EC2 인스턴스의 퍼블릭 IP를 가리키도록 A 레코드를 추가한다.

  • Type(유형): A
  • Name(이름/호스트): @, api 등 원하는 서브도메인
  • Value(값/주소): [Production EC2의 퍼블릭 IP]
  • TTL(Time To Live): 300 (5분) 또는 기본값

7단계: 최종 테스트

모든 설정이 올바르게 적용되었는지 확인한다.

# 1. DNS 설정 확인
nslookup [도메인]

# 2. HTTP -> HTTPS 리디렉션 테스트
# 응답 헤더에 '301 Moved Permanently'와 'Location: https://...'가 포함되어야 한다.
curl -I http://[도메인]

# 3. HTTPS 연결 테스트
# 응답 헤더에 'HTTP/2 200' 또는 정상 응답 코드가 포함되어야 한다.
curl -I https://[도메인]

8단계: 인증서 자동 갱신 설정

Let's Encrypt 인증서는 유효 기간이 90일로 짧기 때문에 주기적인 갱신이 필요하다. Certbot은 설치 과정에서 systemd 타이머나 cron 작업을 통해 자동 갱신을 설정하므로, 실제로 잘 동작하는지 테스트만 해보면 된다.

# 자동 갱신 시뮬레이션 (실제 갱신은 하지 않음)
sudo certbot renew --dry-run

# cron 작업에 등록되었는지 확인 (선택 사항)
sudo crontab -l | grep -q certbot || echo "0 12 * * * /usr/bin/certbot renew --quiet" | sudo crontab -

 

--dry-run 테스트가 성공적으로 완료되면 인증서가 만료되기 전에 자동으로 갱신되므로 더 이상 신경 쓸 필요가 없다.


결론

이제 main 브랜치에 코드를 푸시하기만 하면 GitHub Actions가 자동으로 애플리케이션을 빌드하고, EC2 서버에 배포한 후 서비스를 재시작하는 완벽한 CI/CD 파이프라인이 구축되었다. 이 자동화된 프로세스를 통해 개발자는 코드 자체에 더 집중할 수 있게 되었고, 수동 배포로 인한 실수를 원천적으로 차단하여 서비스의 안정성을 크게 향상시킬 수 있다. 본문에서 다룬 과정들을 차근차근 따라 하면 누구나 자신만의 강력한 배포 자동화 시스템을 구축할 수 있을 것이다.