배포관련/AWS

AWS EC2에서 Nginx + Docker Compose + Jenkins를 활용한 CI/CD 구축

솜사탕코튼 2025. 2. 23. 17:01

젠킨스와 nginx를 통해서 배포 자동화를 구현해보려고 하였습니다.

여기에 관련해서 시행착오가 많았는데 정리하려고 합니다.

 

일단 구조는 다음과 같습니다.

 

1. aws ec2 instance jenkins 설치

2. docker compose 로 nginx, postgresql, certbot, spring 이미지 빌드 후 실행

 

이상하다고 생각할 수 있는데, 맞습니다. 

 

1번 방법을 먼저 수행해봤고, 2번 방법으로 나중에 도입했기 때문에 

보통은 jenkins도 docker compose로 띄워야 네트워크 이슈도 적고, 관리도 편하다는 것을 나중에 깨달았습니다.

 

AWS EC2 프리티어 기준입니다.

 

1. SWAP 메모리 설정

2. Docker-compose, Docker 설치

  • 필수 패키지 설치
sudo yum update -y  # Amazon Linux 기준 (Ubuntu는 `apt update`)
sudo yum install -y docker git unzip
sudo systemctl enable docker && sudo systemctl start docker
sudo usermod -aG docker ec2-user  # Docker 권한 추가
  • Docker-compose 설치
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version  # 설치 확인

 

3. Jenkins 설치

sudo yum install -y java-11-amazon-corretto  # Jdk 설치 (저는 21이라 바꿔서)
wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io-2023.key
sudo yum install -y jenkins
sudo systemctl enable jenkins && sudo systemctl start jenkins

 

4. Jenkins 비밀번호 확인

sudo cat /var/lib/jenkins/secrets/initialAdminPassword

 

5. 젠킨스 접속 확인

  • 기본은 http://EC2-PUBLIC-IP:8080 
  • 8080 포트를 인바운드 규칙에서 열어주어야 합니다.

6. 도커 컴포즈 작성

version: '3.8'

services:
  db:
    image: postgres:latest
    container_name: postgres-db
    restart: always
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  backend:
    image: my-backend:latest
    container_name: backend
    restart: always
    depends_on:
      - db
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/${POSTGRES_DB}
      SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER}
      SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
    ports:
      - "8080:8080"

  nginx:
    image: nginx:latest
    container_name: nginx
    restart: always
    depends_on:
      - backend
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - "80:80"
      - "443:443"

volumes:
  postgres_data:
  • 설명
    • rds를 사용하지 않고, ec2 인스턴스 환경에 postgres db를 올렸습니다.
    • 실무에서는 인스턴스 하나에 애플리케이션과 디비를 같이 설정한다는 것은 지양한다는 점 유의하시길 바랍니다.
    • 각 컨테이너들은 aws ec2 인스턴스의 특정 디렉토리와 마운트를 해주었습니다.

7. Dockerfile 작성

FROM amazoncorretto:21.0.4-alpine3.18 AS builder
WORKDIR /app

COPY gradlew gradlew
COPY gradle gradle
COPY build.gradle build.gradle
COPY settings.gradle settings.gradle

RUN chmod +x gradlew && ./gradlew dependencies || echo "Ignore dependency error"
COPY . .
RUN ./gradlew build -x test

FROM amazoncorretto:21.0.4-alpine3.18
WORKDIR /app

COPY --from=builder /app/build/libs/Foody-0.0.1-SNAPSHOT.jar app.jar
CMD ["java", "-jar", "app.jar"]

 

1. 빌드 이미지 생성(builder 스테이지)

  • amazoncorretto:21.0.4-alpine3.18(경량 Java 21 버전) 기반으로 빌드 컨테이너 생성
  • WORKDIR /app으로 작업 디렉토리 설정
FROM amazoncorretto:21.0.4-alpine3.18 AS builder
WORKDIR /app

2. Gradle 관련 파일 복사

  • Gradle Wrapper(gradlew), gradle 폴더, build.gradle, settings.gradle을 복사
  • 이유: Gradle 캐시를 활용하여 빌드 속도를 높이기 위함
COPY gradlew gradlew
COPY gradle gradle
COPY build.gradle build.gradle
COPY settings.gradle settings.gradle

 

3. Gradle 의존성 다운로드 (캐싱 최적화)

  • chmod +x gradlew → Gradle Wrapper 실행 권한 부여
  • ./gradlew dependencies → 의존성만 미리 다운로드
  • 실패해도 echo "Ignore dependency error"로 무시
RUN chmod +x gradlew && ./gradlew dependencies || echo "Ignore dependency error"

 

4. 전체 프로젝트 복사 및 빌드

  • COPY . . → 프로젝트 전체 복사
  • ./gradlew build -x test → 테스트 제외하고 빌드 수행 (build/libs/*.jar 생성됨)
FROM amazoncorretto:21.0.4-alpine3.18
WORKDIR /app

 

5. 2단계 (런타임 이미지 생성)

FROM amazoncorretto:21.0.4-alpine3.18
WORKDIR /app
  • 빌드에 사용한 이미지를 그대로 사용하지 않고, 새 컨테이너를 만들고 실행 환경만 유지

6. 빌드된 Jar 파일 복사

COPY --from=builder /app/build/libs/Foody-0.0.1-SNAPSHOT.jar app.jar
  • --from=builder → 이전 빌드 단계에서 생성된 JAR 파일을 가져옴
  • /app/build/libs/Foody-0.0.1-SNAPSHOT.jar → app.jar로 복사

7. 컨테이너 실행 시 JAR 파일 실행

CMD ["java", "-jar", "app.jar"]

 

멀티스테이지 빌드란?

더보기

멀티스테이지 빌드는 빌드 과정과 실행 환경을 분리해서 컨테이너의 크기를 줄이고, 실행 환경을 최적화하는 방식입니다.

 

1. 첫 번째 FROM amazoncorretto:21.0.4-alpine3.18 AS builder

  • 이 단계에서는 Gradle을 사용하여 애플리케이션을 빌드하는 역할을 합니다.
  • Gradle을 실행하려면 JDK(Java Development Kit)가 필요합니다.
  • 하지만 실행 환경에서는 JDK가 필요 없고, JRE(Java Runtime Environment)만 필요합니다.
  • 즉, 빌드 전용 컨테이너라고 보면 됩니다.

 

2. 두 번째 FROM amazoncorretto:21.0.4-alpine3.18

  • 실행을 위한 경량 컨테이너로, 불필요한 빌드 도구(Gradle 등)는 포함하지 않습니다.
  • 오직 애플리케이션 실행을 위한 JAR 파일만 복사해서 실행합니다.
  • 덕분에 컨테이너 크기가 작아지고, 실행 속도도 빨라집니다.

 

9. jenkins pipeline script 작성

왼쪽 상단의 + 새로운 Item을 클릭합니다.

  • item name을 입력하고, pipeline을 선택합니다.

  • github project는 다음과 같이 작성합니다.
  • github에 있는 repository (public 기준)

Triggers 방법으로는 pipeline script 방식을 사용할 것이기 때문에 다음과 같이 체크해줍니다.(github webhook 연동)

  • github의 repository에 변경사항이 있을 시 polling 당겨와서 trigger (빌드 수행)

 

jenkins pipeline script 작성

 

  • 일반적으로 Pipeline script from SCM 으로 쉽게 사용할 수 있는데
  • 저의 상황에서는 github repository에 jenkins file을 업로드를 안 하고 진행하였습니다. 
  • 즉, jenkinsfile을 업로드하지 않으면 저와 같이 pipeline script를 작성해야합니다. (기본적으로 jenkins file과 동일)
pipeline {
    agent any
    
    triggers { 
        githubPush()   // 📌 GitHub Webhook을 통한 자동 빌드 트리거
    }

    stages {
        stage('Checkout') {
            steps {
                git branch: 'main', url: 'https://github.com/beginner0107/Foody.git'
            }
        }  

        stage('Build') {
            steps {
                sh '''
                    chmod +x ./gradlew
                    ./gradlew clean build -x test || exit 1
                '''
            }
        }

       stage('Docker Build & Deploy') {
            steps {
                sh '''
                    echo '[INFO] Backend 컨테이너만 재시작'
                    cd /home/ec2-user/ci-cd-project
                    docker-compose up -d --build backend
                '''
            }
        }
    }
}

 

해상 스크립트를 작성하면서 생긴 이슈들이 있는데 이후 설명하겠습니다.

  • pipeline script는 다음과 같이 작성되어 있습니다.
  • webhook 요청이 오면, 발생할 이벤트들이 stages 밑에 있는 작업들이라고 생각하면 됩니다.
  • githubPush(): 빌드 트리거

  • jenkins가 요청을 받게 되면 stages 별로 작업이 수행된 것을 확인할 수 있습니다.

10. Jenkins에서 빌드 수행

  • 지금 빌드를 클릭합니다.
  • 빨간색 X 표시 : 실패
  • 초록색 체크 표시 : 성공

  • 콘솔 출력에서는 어떤 부분에서 실패했는지 로그를 확인할 수 있습니다.

 

11. Webhook 설정

  • 본인의 project > Settings > Code and automation > Webhooks
  • Add webhook 선택

 

기입해야할 설정

  • Payload URL * : http://본인도메인 or 포트:포트/gihub-webhook/
    • 저는 nginx로 https 설정을 해두어서 https로 설정해두었습니다.
  • Content type : application/json
  • SSL verification: GitHub Webhook에서 SSL verification(SSL 검증) 설정은 Webhook이 데이터를 전송할 때 HTTPS 인증서를 검증할지 여부를 결정하는 옵션이라서, https 설정이 되어 있지 않으면 Disable을 체크합니다.
  • Which events would you like to trigger this webhook?
    • "Just the push event" (push 이벤트만 트리거)  
    • "Send me everything" (모든 이벤트 전송)
    • "Let me select individual events" (개별 이벤트 선택)
  • 저는 1번 push event가 있을 때를 선택했습니다.
  • Add webhook 클릭
  • Recent Delivers 를 누르면 다음과 같이 확인할 수 있습니다.

 

12. 404나 다른 오류 메시지가 발생되었을 때

  • AWS의 보안그룹과 nginx 설정을 살펴봅니다.
  • 8080포트로 젠킨스가 돌고 있으면 8080포트 규칙을 추가해야하고
  • 80포트 or 443 포트로 nginx설정을 추가해놓았을 때는
  • nginx 설정 스크립트를 수정해야 합니다.
  • 요청이 왔을 때, jenkins로 리버스 프록시 되게 설정을 추가해야 합니다.
location /jenkins/ {
    proxy_pass http://IP:PORT/jenkins/;
    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 https;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Port 443;

    # Jenkins WebSocket 지원
    proxy_http_version 1.1;
    proxy_request_buffering off;
    proxy_buffering off;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_redirect off;
}
  • https 설정을 하고 https://도메인/jenkins로 요청이 왔을 때 내부적으로 띄어져있는 jenkins 서비스로 포워딩해주는 설정을 추가하였습니다.
  • 여기서 docker로 띄운 jenkins랑, ec2 instance 로 띄운 젠킨스는 설정이 다릅니다.
  • 도커 호스트에서 Nginx를 실행하고, Jenkins도 직접 설치한 경우
    • proxy_pass http://127.0.0.1:포트/jenkins/;
  • 도커에서 Jenkins를 실행하고, Nginx도 도커 컨테이너로 실행한 경우
    • proxy_pass http://컨테이너이름:포트/jenkins/;
  • 도커에서 Jenkins를 실행하고, Nginx는 호스트(EC2)에서 실행한 경우
    • Jenkins 컨테이너의 IP 확인
      • docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' jenkins
    • Nginx 설정 변경
      • proxy_pass http://172.18.0.2:8080/jenkins/; (가상 IP, 포트 예시)

 

다른 에러 메시지나 트러블 슈팅에 대해서는 따로 정리하겠습니다.

80번이나 시도할 만큼 실패를 너무 많이 해서..

 

참고 사이트

https://1minute-before6pm.tistory.com/52

 

Github WebHooks 연동하여 Jenkins 빌드 자동화

Jenkins로 배우는 CI/CD 파이프라인 구축 1. Install and Run Jenkins With Docker Compose 2. Jenkins Pipeline 개념 및 예제 3. Github WebHooks 연동하여 Jenkins 빌드 자동화 4. Jenkins Multibranch Pipeline: 효과적인 브랜치 관리

1minute-before6pm.tistory.com

https://potato-yong.tistory.com/134

 

[Jenkins] Jenkins에 Github를 연동하고 빌드 자동화하기

Jenkins를 이용하여 Github repository에 push 했을 때, 자동으로 빌드가 되도록 만들어보자. Jenkins와 Github를 연동하는 방법은 ID/PW를 인증하는 방식과 ssh 연동 방식이 있다. 하지만, ID/PW 인증으로 연동하

potato-yong.tistory.com

https://zuminternet.github.io/JENKINS-BUILD/

 

젠킨스 사용하여 자동 배포환경 만들기!

젠킨스 사용하여 자동 배포환경 만들기!

zuminternet.github.io

https://as-i-am-programing.tistory.com/33

 

[Jenkins] GitLab 푸시 이벤트 훅 테스트 방법 (push event hook test)

안녕하세요Jenkins로 GitLab Branch에 push되면 자동으로 빌드가 일어나도록 설정하기 위해서 가장 중요한건 hook 설정인데요. 이 방법에 대해 자세히 알아보겠습니다.# VersionJenkins 2.346.1GitLab 11.5.1-ee# Git

as-i-am-programing.tistory.com