사이드 프로젝트 기술 기록 (15) - Github Action을 통한 AWS EC2 Docker 자동화 파이프라인 구축

서승·2025년 5월 11일

테스형

목록 보기
12/25

AWS EC2 인스턴스 구축EC2 Docker 설치까지 마쳤다면, 이제 배포를 할 때가 왔다.

트러블 슈팅

GitHub Action 코드 작성

GitHub Action 을 이용하여 Push 시 Master brach에 있는 프로젝트를 Docker 컨테이너를 이용해 AWS EC2 인스턴스에서 구동하게끔하는 자동화 파이프라인을 구축 할 예정이다.

1. 프론트 엔드 서버

우선 프로젝트 최상단에 Dockerfile을 생성해준다.
Next.js 애플리케이션을 Docker 컨테이너 안에서 실행하기 위한 설정.

# Node.js 20 버전 사용 (Next.js 지원 버전)
FROM node:20-alpine

# 명시적으로 /app 디렉토리 생성
RUN mkdir -p /app
WORKDIR /app

# package.json과 package-lock.json을 복사
COPY package*.json ./

# 의존성 설치
RUN npm install

# .env.local 파일 복사
COPY .env.local /app/.env.local

# .env.local 파일 확인
RUN ls -la /app && cat /app/.env.local

# 애플리케이션 파일 복사
COPY . .

# 애플리케이션 빌드
RUN npm run build

# 프로덕션 환경에서 서버 실행
CMD ["npm", "run", "start"]

# 컨테이너가 3000 포트를 리스닝하도록 설정
EXPOSE 3000

같은 위치에 .github/workflows/deploy.yml 생성해준다.
deploy.yml 은 코드가 merge 되었을 때 이를 감지하고 2차 동작을 하도록 하는 파일이다.

우선 기입해야할 정보를 체크하자.

  • Docker Hub에 업로드 할 이미지 이름 : 자동으로 업로드하니 정하기만 하면 된다.
    - 나의 경우 tst-fe-image 로 설정할 예정이다.
    - seuo/tst-fe-image

  • 환경 변수 : .env.local에 사용하는 환경 변수를 설정한다.
    - NEXT_PUBLIC_SPRINGBOOT_URL=${{ secrets.NEXT_PUBLIC_SPRINGBOOT_URL }}
    - NEXT_PUBLIC_S3_URL=${{ secrets.NEXT_PUBLIC_S3_URL }}

  • Docker Hub 로그인 정보
    - secrets.DOCKER_USERNAME : Docker 계정
    - secrets.DOCKER_PASSWORD : Docker 암호

  • 서버 접속 정보 수정
    - secrets.SERVER_HOST : FE 서버 IP 주소
    - secrets.SERVER_USER : 서버 로그인 사용자 명
    - secrets.PEM_KEY: 서버 pem 키 값

Git Seceret으로 값들을 관리한다.

이후에 .github/workflows/deploy.yml 생성 후

name: Deploy with Docker

on:
  push:
    branches:
      - master

jobs:
  # Build Job: Docker 이미지 빌드 및 푸시
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: Create .env.local file from Secrets
        run: |
          echo "NEXT_PUBLIC_SPRINGBOOT_URL=${{ secrets.NEXT_PUBLIC_SPRINGBOOT_URL }}" >> .env.local
          echo "NEXT_PUBLIC_S3_URL=${{ secrets.NEXT_PUBLIC_S3_URL }}" >> .env.local          

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Log in to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and Push Docker Image
        run: |
          docker build --no-cache -t seuo/tst-fe-image:latest .
          docker push seuo/tst-fe-image:latest

  # Deploy Job: 빌드 후 배포
  deploy:
    needs: build
    runs-on: ubuntu-latest

    steps:
      - name: Deploy to Server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.PEM_KEY }}
          script: |
            cd ~/tst-fe-deploy || mkdir ~/tst-fe-deploy && cd ~/tst-fe-deploy

            # 🔥 기존 컨테이너 중복 방지용 정리 (tst-fe-image 형식에 맞게 수정)
            docker stop tst-fe || true
            docker rm tst-fe || true
            docker stop nginx || true
            docker rm nginx || true
            docker stop goaccess || true
            docker rm goaccess || true

            # 최신 프론트엔드 이미지 pull
            docker pull seuo/tst-fe-image:latest

            # 🧾 docker-compose.yml 생성
            cat <<EOF > docker-compose.yml
            version: "3.8"

            services:
              frontend:
                container_name: tst-fe
                image: seuo/tst-fe-image:latest
                restart: always
                expose:
                  - "3000"
                networks:
                  - frontend-net

              nginx:
                image: nginx:latest
                container_name: nginx
                ports:
                  - "80:80"
                volumes:
                  - ./nginx.conf:/etc/nginx/nginx.conf:ro
                  - ./nginx-logs:/var/log/nginx
                depends_on:
                  - frontend
                networks:
                  - frontend-net

              goaccess:
                image: allinurl/goaccess
                container_name: goaccess
                environment:
                  - GOACCESS_ACCESS_LOG=/var/log/nginx/access.log
                  - GOACCESS_OUTPUT_FILE=/usr/share/nginx/html/report.html
                ports:
                  - "7890:7890"  # GoAccess 대시보드 포트
                volumes:
                  - ./nginx-logs:/var/log/nginx
                  - ./goaccess-report:/usr/share/nginx/html
                networks:
                  - frontend-net
                depends_on:
                  - nginx

            networks:
              frontend-net:
                driver: bridge
            EOF

            # 🛠️ nginx.conf 생성 (Next.js 3000 포트로 프록시)
            cat <<EOF > nginx.conf
            events {}
            http {
              access_log /var/log/nginx/access.log;
              server {
                listen 80;
                location / {
                  proxy_pass http://frontend:3000;  # IP 또는 도메인 주소로 수정
                  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;
                }
              }
            }
            EOF

            docker compose down
            docker compose up -d

이후 master 브랜치로 merge하면 감지되어 동작한다.
Actions에서 확인할 수 있다.

2. 백 엔드(API), DB 서버

Dockerfile 생성

# 1단계: 빌드용 이미지
FROM gradle:7.6-jdk17 AS builder
WORKDIR /app
COPY . .
RUN gradle clean build -x test --no-daemon

# 2단계: 실행용 경량 이미지
FROM openjdk:17-alpine
WORKDIR /app
COPY --from=builder /app/build/libs/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-Xmx256m", "-Xms128m", "-XX:+UseSerialGC", "-jar", "app.jar"]

같은 위치에 docker-compose.yml 생성
1. API 서버
2. Redis
3. DB
를 하나의 EC2로 돌려야하기 때문에 Docker-composer로 순차적으로 구동되도록 설정 해준다.

version: '3.8'

volumes:
  mysql-data:

services:
  redis:
    image: redis:alpine
    container_name: redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    mem_limit: 128m
    cpus: 0.2
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  db:
    image: mysql:8
    container_name: mysql-db
    restart: on-failure  # 실패 시에만 재시작
    ports:
      - "3306:3306"
    environment:
      MYSQL_DATABASE: tstdb
      MYSQL_USER: tessbro
      MYSQL_PASSWORD: tess1234
      MYSQL_ROOT_PASSWORD: 1234
    volumes:
      - mysql-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      retries: 5

  app:
    image: seuo/tst-be-image
    container_name: tst-be
    restart: always
    ports:
      - "80:8080"
    depends_on:
      - db
      - redis
    environment:
      SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/tstdb
      SPRING_DATASOURCE_USERNAME: tessbro
      SPRING_DATASOURCE_PASSWORD: tess1234
      SPRING_REDIS_HOST: redis
      SPRING_REDIS_PORT: 6379

  nginx:
    image: nginx:latest
    container_name: nginx-proxy
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
    depends_on:
      - app

volumes:
  mysql-data:

기입해야할 정보 체크

  • DB 접속 정보
    secret.MYSQL_DATABASE : DB 이름
    secret.MYSQL_USER : DB 사용자 이름
    secret.MYSQL_PASSWORD : DB 비밀번호
    secret.MYSQL_ROOT_PASSWORD : Root 비밀번호

  • Docker 이미지 이름
    secret.DOCKER_USERNAME/[이미지 이름:나의 경우 tst-be-imgae]
    secret.DOCKER_USERNAME
    secret.DOCKER_PASSWORD

  • API -> DB 접속 정보
    SPRING_DATASOURCE_URL : DB의 접속 주소
    SPRING_DATASOURCE_USERNAME : DB에 접속할 때 사용할 사용자명
    SPRING_DATASOURCE_PASSWORD : 사용자 계정의 비밀번호

./github/workflows/deploy.yml 생성

  • Redis
  • MySQL
  • API app
    를 순차적으로 컨테이너에 띄워준다.
# github repository actions 페이지에 나타날 이름
name: Deploy with Docker

# event trigger
on:
  push:
    branches: [ "master" ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
       # JDK setting - github actions에서 사용할 JDK 설정 (프로젝트나 AWS의 java 버전과 달라도 무방)
      - uses: actions/checkout@v4
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          
      # gradle caching - 빌드 시간 향상
      - name: Gradle Cache
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle
      # 환경별 application.properties 파일 생성(1) - application.properties
      - name: Make application.properties
        run: |
          mkdir -p src/main/resources
          echo "${{ secrets.APP_PRO }}" > src/main/resources/application.properties
          
      # 환경별 yml 파일 생성(1) - application-aws.yml
      # 서버 <-> S3 기능 추가 시 활성화
      # - name: Make application-aws.yml
      #   run: |
      #     mkdir -p src/main/resources
      #     echo "${{ secrets.YML_AWS }}" > src/main/resources/application-aws.yml

      - name: Build project
        run: ./gradlew build -x test

      - name: Log in to DockerHub
        run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin

      - name: Build & Push Docker image
        run: |
          docker build -t seuo/tst-be-image .
          docker push seuo/tst-be-image:latest

      - name: Deploy to EC2
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.PEM_KEY }}
          script: |
            mkdir -p ~/tst-deploy/nginx/conf.d && cd ~/tst-deploy


            # 최신 이미지 가져오기
            docker pull seuo/tst-be-image

            # Nginx default.conf 생성
            cat <<EONGINX > ~/tst-deploy/nginx/conf.d/default.conf
            
            server {
              listen 80;
            
              location / {
                proxy_pass http://app:8080;
                proxy_set_header Host \$host;
                proxy_set_header X-Real-IP \$remote_addr;
                #proxy_set_header Origin http_origin;                
                proxy_connect_timeout       1200;
                proxy_send_timeout          1200;
                proxy_read_timeout          1200;
                send_timeout                1200;
              }
    
            }
            EONGINX


            # docker-compose.yml 생성
            cat <<EOF > docker-compose.yml
            version: '3'
            services:
              redis:
                image: redis:alpine
                container_name: redis
                restart: unless-stopped
                ports:
                  - "6379:6379"
                mem_limit: 128m
                cpus: 0.2

              db:
                image: mysql:8
                container_name: mysql-db
                restart: unless-stopped
                ports:
                  - "3306:3306"
                environment:
                  MYSQL_DATABASE: tstdb
                  MYSQL_USER: tessbro
                  MYSQL_PASSWORD: tess1234
                  MYSQL_ROOT_PASSWORD: 1234
                volumes:
                  - mysql-data:/var/lib/mysql
                mem_limit: 512m
                cpus: 0.3

              app:
                image: seuo/tst-be-image
                container_name: tst-be
                restart: unless-stopped
                expose:
                  - "8080"
                depends_on:
                  - db
                  - redis
                environment:
                  SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/tstdb
                  SPRING_DATASOURCE_USERNAME: tessbro
                  SPRING_DATASOURCE_PASSWORD: tess1234
                  SPRING_REDIS_HOST: redis
                  SPRING_REDIS_PORT: 6379
                mem_limit: 768m
                cpus: 0.4

              nginx:
                image: nginx:latest
                container_name: nginx-proxy
                restart: unless-stopped
                ports:
                  - "80:80"
                volumes:
                  - ./nginx/conf.d:/etc/nginx/conf.d:ro
                depends_on:
                  - app

            volumes:
              mysql-data:
            EOF

            docker compose down
            docker compose up -d

기입해야할 정보 체크

  • application.properies
    secrets.APP_PRO

  • Docker 계정
    secrets.DOCKER_USERNAME
    secrets.DOCKER_PASSWORD

  • EC2 서버 정보
    secrets.SERVER_HOST : BE EC2 IP 주소
    secrets.USERNAME : EC2 사용자 이름 보통 ubuntu
    secrets.PEM_KEY : .pem키의 내용을 모두 복사한다.

기분 좋은 녹색 뱃지 ~

잘 부탁해 ~

내 프론트 FE2 주소로 접속하면

서비스가 구동되는 모습을 볼 수 있다.


다음 작업

Route53 도메인 연결 및 HTTPS 적용

profile
정진 또 정진

0개의 댓글