ec2 Gitlab Docker Jenkins https Vue Springboot CI/CD 배포

GYUBA·2024년 12월 3일
post-thumbnail

상태 : RDS, S3등은 db와 직접 연결되어있음.

이번엔 docker hub없이 진행하려 함. 편리했지만. 1개의 이미지까지만 무료로 만들 수 있기 때문에, 이번엔 과금하고 싶지 않아 배제했다.

따라할 땐 트러블슈팅까지 읽고 따라하기

인스턴스 생성

프리티어 방식보고 만듦

보안그룹 편집. https와 http 인스턴스 생성 시 허용하였고.
로컬에서 개발중인 프론트엔드의 접근을 위해 8080을 aniwhere ipv4로 열어주었음, 9090은 젠킨스 포트로 쓰기위해 열었음.

이후 할일:
1. 탄력적 IP 설정하기.
2. putty로 콘솔 접근하게 하기(선택)

한번 sudo apt update

필요한 거 설치

  1. java 설치 (springboot프로젝트 버전)
# Java 17 설치
sudo apt update
sudo apt install openjdk-17-jdk -y

# 설치된 버전 확인
java -version
  1. 도커 설치

참고


$ sudo apt-get update

# 패키지 인덱스 업데이트
$ sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    software-properties-common

# Docker의 공식 GPG 키 추가
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

# Docker 저장소 추가
$ sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"
   
# 패키지 인덱스 업데이트
$ sudo apt-get update

# Docker CE 설치
$ sudo apt-get install docker-ce docker-ce-cli containerd.io

# Docker 서비스 시작 및 자동 시작 설정
$ sudo systemctl start docker
$ sudo systemctl enable docker

# Docker 그룹에 사용자 추가
$ sudo usermod -aG docker ${USER}

# 확인
$ docker --version

스왑 메모리

하지않고 진행을 했다가, springboot를 빌드하는 과정에서 cpu 100% 찍고 죽는 경우를 여러번 겪었다. 직접적인 해결책이 된 것은 스왑 메모리였기에, 먼저 진행하자.

스왑공간은 메모리의 2배를 추천하는 듯하다.

t2.micro의 경우 RAM이 1GB밖에 없어서 스왑 공간을 2GB로 설정했음.

# 2GB 스왑 파일 생성
sudo dd if=/dev/zero of=/swapfile bs=128M count=16

# 권한 설정
sudo chmod 600 /swapfile

# 스왑 파일 포맷
sudo mkswap /swapfile

# 스왑 활성화
sudo swapon /swapfile

# 상태 확인
free -h

# 재부팅 후에도 스왑 유지
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Jenkins 설치

sudo docker pull jenkins/jenkins:lts 를 사용해서 docker 이미지로 받는 것과 비교해서 사용하자! docker in docker 문제가 있다지만 간편할 수도 있다고 함!
전 일단 호스트에서 설치 직접했음

많이 참고한 글 1 2


$ curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io.key | sudo tee \
  /usr/share/keyrings/jenkins-keyring.asc > /dev/null

$ echo deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] \
  https://pkg.jenkins.io/debian-stable binary/ | sudo tee \
  /etc/apt/sources.list.d/jenkins.list > /dev/null

$ sudo apt-get update

$ sudo apt-get install jenkins

# 확인
$ sudo systemctl status jenkins

# jenkins 권한 주기
sudo usermod -aG docker jenkins

$ sudo systemctl start jenkins

# 초기 비밀번호 확인
$ sudo cat /var/lib/jenkins/secrets/initialAdminPassword

jenkins 포트번호 변경

8080은 스프링부트 줄거라서 jenkins 포트번호 변경함
참고

# 난 vim이 좋아서
$ sudo apt-get install vim

$ sudo vi /etc/sysconfig/jenkins

들어가서 원하는 포트로 변경

JENKINS_PORT="9090"
sudo vim /usr/lib/systemd/system/jenkins.service

들어가서 포트 변경

Environment="JENKINS_PORT=9090"

재실행 하기

# 하라고 뜸
sudo systemctl daemon-reload 
# 재시작
sudo systemctl restart jenkins

이제 ip주소:9090로 들어가서 초기비밀번호 입력 후
suggested plugins를 설치한 후 admin 생성하고 URL 또한 ip주소:9090으로 설정

블로그에선

[설치 플러그인]

  • Generic Webhook Trigger
  • Gitlab
  • Gitlab API
  • Gitlab Authentication
  • Mattermost Notification
  • Docker pipeline

설치하라고 해서 설치했음. 그런데 Docker pipeline에서 실패 떴음.

Authentication Tokens API가 실패로 뜨고 Docker CommonsDocker Pipeline이 줄줄이 실패하였다.
앞에 두개는 Docker pipeline를 설치하려면 자동으로 오는듯한데 실패뜬다.

-> Authentication Tokens API 부터 하나씩 따로 설치하면 되는 듯하다.


Gitlab <-> Jenkins

(각 프로젝트마다, 각각 연결 필요함!)

Jenkins와 Gitlab 연동 참고한 블로그 글!

Gitlab Project

프로젝트 들어감 -> Settings -> Access Tokens에서 만들기

설정함. 그리고 생성된 토큰 바로 복사해두기

Jenkins에서 Gitlab 연결

Jenkins관리 -> System -> Gitlab Connections 탭에서\

+add 눌러서


설정함.

Webhook 설정

jenkins

  1. Jenkins에서 '새로운 Item' -> 'Pipeline'
  2. 'Build Triggers'에서 'Build when a change is push to Gitlab~'
  3. 뒤에 URL 기록해두고
  4. 고급 버튼 에서 Secret token에서 Generate해서 토큰 저장

    그 밑에 더 내려서 이렇게함


아마 근데 branch 특정은 여기서 안하고 gitlab에서 설정해도 충분함 근데 그냥햇음

gitlab

  1. webhook탭에서 기록한 url과 secret toekn 넣음
  2. branch는 정규식 브랜치 선택해서 ^master$ 넣음 (배포 대상이 될 branch 넣기)

백엔드(스프링부트) 연결

gitlab credential 설정

숨기려는 파일 업로드

Jenkins 관리 -> Credentials -> Stores scoped to Jenkins의 (glabal) ->Add Credentails

Secret file선택 후 업로드

Jenkins file에 적용된 부분:

        stage('Secrets Setup') {
            steps {
                withCredentials([
                    file(credentialsId: 'prod-yaml', variable: 'prodFile'),
                    file(credentialsId: 'secret-yaml', variable: 'secretFile')
                ]) {
                    sh '''
                        cp "$prodFile" src/main/resources/application-prod.yml
                        cp "$secretFile" src/main/resources/application-secret.yml
                        chmod 644 src/main/resources/application-*.yml
                    '''
                }
            }
        }

Docker file

FROM openjdk:17-jdk-slim

ARG JAR_FILE=build/libs/*.jar
# jar 파일 복제
COPY ${JAR_FILE} app.jar

# 실행 명령어
ENTRYPOINT ["java", "-Dspring.profiles.active=prod,secret", "-jar", "app.jar"]

Jenkins file

  • 메모리 스왑을 적용하기 이전(메모리 스왑을 몰랐음), 서버가 죽는 문제를 해결하고자 걸어놓은 것들이 있다.
    - Gradle 빌드 메모리 제한 (256MB)
    - 작업자 수 제한 (max-workers 2)
    - Docker 빌드와 실행 시 메모리 제한 (512MB)
    - 빌드 전 Docker 캐시 정리

적용하고 효과는 없었지만, 굳이 메모리 스왑을 적용하고 위 사항을 제거하진 않았음.

Jenkinsfile:

pipeline {
    agent any

    environment {
        APP_NAME = 'jenkins-test'
        DOCKER_IMAGE = 'jenkins-test:latest'
        GRADLE_OPTS = '-Dorg.gradle.jvmargs="-Xmx256m"'  // Gradle 메모리 제한
    }

    stages {
        stage('Prepare Environment') {
            steps {
                sh '''
                    rm -rf src/main/resources
                    mkdir -p src/main/resources
                    chmod 777 src/main/resources

                    # Docker 캐시 정리
                    docker system prune -f
                '''
            }
        }

        stage('Secrets Setup') {
            steps {
                withCredentials([
                    file(credentialsId: 'prod-yaml', variable: 'prodFile'),
                    file(credentialsId: 'secret-yaml', variable: 'secretFile')
                ]) {
                    sh '''
                        cp "$prodFile" src/main/resources/application-prod.yml
                        cp "$secretFile" src/main/resources/application-secret.yml
                        chmod 644 src/main/resources/application-*.yml
                    '''
                }
            }
        }

        stage('Build') {
            steps {
                sh '''
                    chmod +x gradlew
                    # gradle 빌드 시 테스트 스킵 (-x test)
                    # 메모리 제한 (256m)
                    # 데몬 비활성화 (--no-daemon)
                    ./gradlew clean build -x test --no-daemon --max-workers 2 -Dorg.gradle.jvmargs="-Xmx256m"
                '''
            }
        }

        stage('Docker Build & Deploy') {
            steps {
                script {
                    sh 'docker rm -f ${APP_NAME} || true'
                    sh 'docker rmi ${DOCKER_IMAGE} || true'

                    sh 'docker build --memory=512m --memory-swap=512m -t ${DOCKER_IMAGE} .'

                    sh '''
                        docker run -d \
                            --name ${APP_NAME} \
                            --restart unless-stopped \
                            --memory=512m \
                            --memory-swap=512m \
                            -p 8080:8080 \
                            ${DOCKER_IMAGE}
                    '''
                }
            }
        }
    }
}

TroubleShooting

docker 권한 없음

ubuntu@ip-172-31-14-202:~$ docker ps
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.47/containers/json": dial unix /var/run/docker.sock: connect: permission denied
  • Dokcer에 권한을 주었음
# docker 그룹에 ubuntu 사용자 추가
sudo usermod -aG docker ubuntu

# 변경사항 적용을 위해 다음 중 하나 실행:
# 1. 새로운 그룹 적용
newgrp docker

# 또는
# 2. EC2에 재접속

# 그 후 다시 시도
docker ps

파일 복사 권한 없음 (상기 Jenkinsfile에 적용되어 있음

+ cp **** src/main/resources/application-prod.yml
cp: cannot create regular file 'src/main/resources/application-prod.yml': Permission denied

Jenkinsfile에서 Jenkins가 resources 접근하도록 Prepare Environment 작성

        stage('Prepare Environment') {
            steps {
                sh '''
                    rm -rf src/main/resources
                    mkdir -p src/main/resources
                    chmod 777 src/main/resources
                '''
            }
        }

방법
1. 테스트 스킵 (테스트가 없다면..?)

stage('Build') {
    steps {
        sh '''
            chmod +x gradlew
            ./gradlew clean build -x test --no-daemon --max-workers 2 -Dorg.gradle.jvmargs="-Xmx256m"
        '''
    }
}
  1. src/test/resources에 테스트용 application.yml추가
spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:testdb;MODE=MySQL
    username: sa
    password:

테스트에 application-secret.yml이 인식 안됨

TripggApplicationTests > contextLoads() FAILED
    java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
        Caused by: org.springframework.beans.factory.BeanCreationException at AbstractBeanFactory.java:326
            Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:795
                Caused by: org.springframework.beans.factory.BeanCreationException at ConstructorResolver.java:648
                    Caused by: org.springframework.beans.BeanInstantiationException at SimpleInstantiationStrategy.java:178
                        Caused by: org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException at DataSourceProperties.java:186

그냥 secret file로 관리하고 싶어서, 민감변수를 application-prod.yml에 담아 credentail로 관리하였음.


프론트엔드(Vue) 연결

  • 본 프로젝트에서는 민감변수를 따로 처리하지 않았으나, 처리한다면 상단과 같은 방식으로 env파일 을 Jenkins Credential에서 관리하면 됨

  • Vue를 빌드한 결과물을 Nginx의 정적파일로 마운트하여 사용. Nginx에 https 적용, /api uri는 8080포트로 프록시하도록 적용

순서
nginx 설치 -> nginx 설정 수정 -> Https 발급(Let's Encrypt) -> nginx 중지 -> Docker로 nginx 생성 후 실행(기존 nginx설정, https 파일 마운트)

Nginx 설치

# Nginx 설치
sudo apt update
sudo apt install nginx

# Nginx 상태 확인
sudo systemctl status nginx

nginx 설정 수정
sudo vi /etc/nginx/sites-available/default
(vim 설치 안되어있으면 설치하고!)

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    
    server_name 도메인(ex.tripggukgguk.site);

    root /var/www/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

nginx 설정 문법 체크
sudo nginx -t

nginx 시작

sudo systemctl start nginx
sudo systemctl enable nginx

https 적용

자동 갱신은 적용하지 않았음

# certbot 설치
sudo apt install certbot python3-certbot-nginx

# 인증서 발급 (자동으로 Nginx 설정도 수정됨)
sudo certbot --nginx -d 도메인(ex.tripggukgguk.site)

자동갱신 방법(난 적용하지 않아서 되는진 모름)

# 갱신 스크립트 생성
sudo vim /home/ubuntu/nginx/renew-cert.sh

# 아래 적용
#!/bin/bash
docker stop nginx
certbot renew --quiet
docker start nginx

# 스크립트 권한 설정
chmod +x /home/ubuntu/nginx/renew-cert.sh

# crontab 설정
sudo crontab -e

# 아래 라인 추가 (매월 1일 새벽 3시에 실행)
0 3 1 * * /home/ubuntu/nginx/renew-cert.sh

nginx 파일 수정

이전에 만들었던 사진 첨부

/api 로 들어오는 요청 프록시. 다른 경우 바꿀것

Docker로 전환

# 디렉토리 생성
mkdir -p /home/ubuntu/nginx/html
mkdir -p /home/ubuntu/nginx/conf

# 소유권 설정
sudo chown -R ubuntu:ubuntu /home/ubuntu/nginx

# 호스트의 nginx 중지
sudo systemctl stop nginx
sudo systemctl disable nginx

nginx 컨테이너 실행

# 이미지 받기
docker pull nginx

# Nginx 컨테이너 실행
docker run -d --name nginx \
  --network host \
  -v /home/ubuntu/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro \
  -v /etc/letsencrypt:/etc/letsencrypt:ro \
  -v /home/ubuntu/nginx/html:/usr/share/nginx/html \
  nginx:stable
  
# 테스트
echo "<h1>HTTPS Test</h1>" > /home/ubuntu/nginx/html/index.html
  • -v는 마운팅
    - nginx 설정 파일과, https파일
  • --network 해야지, localhost호출이 가능함(안하면 docker와 호스트의 network가 따로)

Jenkins설정

# Jenkins 사용자에게 필요한 권한 부여
sudo vim /etc/sudoers.d/jenkins

# 아래 내용 추가
jenkins ALL=(ALL) NOPASSWD: /bin/rm -rf /home/ubuntu/nginx/html/*
jenkins ALL=(ALL) NOPASSWD: /bin/cp -r /var/lib/jenkins/workspace/*/dist/* /home/ubuntu/nginx/html/
jenkins ALL=(ALL) NOPASSWD: /bin/chown -R ubuntu:ubuntu /home/ubuntu/nginx/html
jenkins ALL=(ALL) NOPASSWD: /bin/chmod -R 755 /home/ubuntu/nginx/html

Jenkinsfile

pipeline {
    agent any
    
    environment {
        // 배포 경로 설정
        DEPLOY_PATH = '/home/ubuntu/nginx/html'
    }
    
    tools {
        nodejs 'node18'  // Jenkins에서 설정한 NodeJS 이름과 일치해야 함
    }
    
    stages {
        stage('Prepare') {
            steps {
                // 버전 확인
                sh '''
                    node --version
                    npm --version
                '''
                
                // 기존 node_modules 제거 (캐시 클린)
                sh 'rm -rf node_modules'
            }
        }
        
        stage('Install Dependencies') {
            steps {
                // 의존성 설치
                sh 'npm install'
            }
        }
        
        stage('Build') {
            steps {
                // Vite 프로젝트 빌드
                sh 'npm run build'
            }
        }
        
        stage('Deploy') {
            steps {
                // 기존 파일 제거 및 새 파일 복사
                sh '''
                    # 기존 파일 제거
                    rm -rf ${DEPLOY_PATH}/*
                    
                    # 빌드 결과물 복사 (Vite는 기본적으로 dist 폴더에 생성)
                    cp -r dist/* ${DEPLOY_PATH}/
                '''
            }
        }
    }
    
    post {
        always {
            // 작업 디렉토리 정리
            cleanWs()
        }
    }
}

Jenkins에 Nodejs설치

jenkinsfile에 명시한 nodejs와 동일한 버전설치.
검색해보고 따라할것. 버전은 해당 버전의 LTS버전 설치하였음(nodejs-18중 lts)

trouble shooting

  • 권한 에러 발생
+ sudo rm -rf /home/ubuntu/nginx/html/*
+ sudo cp -r dist/assets dist/favicon.ico dist/index.html /home/ubuntu/nginx/html/
sudo: a terminal is required to read the password; either use the -S option to read from standard input or configure an askpass helper
sudo: a password is required
[Pipeline] }

Jenkins에게 권한을 주고 sudo를 쓰지말아야함(현재 jenkinsfile에서는 sudo 안씀) sudo 쓰면 jenkins가 아니라 root로 실행하는데, 권한을 jenkins에게 줘서 정상 동작 x
아래 명령어로 소유자 변경(생성/수정/삭제 위해서)

sudo chown -R jenkins:jenkins /home/ubuntu/nginx/html
sudo chmod -R 755 /home/ubuntu/nginx/html
  • 권한 에러 발생2
+ rm -rf /home/ubuntu/nginx/html/*
rm: cannot remove '/home/ubuntu/nginx/html/*': Permission denied

해결: 상위 디렉토리부터 jenkins에게 권한을 주도록 했음

sudo chmod 755 /home/ubuntu
sudo chmod 755 /home/ubuntu/nginx
sudo chmod -R 755 /home/ubuntu/nginx/html

# conf 디렉토리는 ubuntu 소유 유지
sudo chown -R ubuntu:ubuntu /home/ubuntu/nginx/conf
sudo chmod -R 755 /home/ubuntu/nginx/conf

sudo -u jenkins ls -la /home/ubuntu/nginx/html 이 명령어로 접근 가능한지 확인 가능
-R태그는 하위 디렉토리까지 적용


참고:

https://moon1z10.github.io/development/AWS-Ubuntu-Docker-Installation/

https://z1-colab.tistory.com/entry/Jenkins-EC2%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-Springboot%EC%9D%98-CICD-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95

https://velog.io/@carrot1st/Jenkins-%ED%8F%AC%ED%8A%B8%EB%B2%88%ED%98%B8-%EB%B3%80%EA%B2%BD

https://erinh.tistory.com/entry/CICD-Spring-Jenkins-Nginx-EC2-Docker%EB%A1%9C-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC-%EA%B5%AC%ED%98%84-2-Jenkins-Gitlab-%EC%97%B0%EB%8F%99-%EB%B0%8F-CI-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95

profile
ship

1개의 댓글

comment-user-thumbnail
2025년 1월 16일

규바는 신이다.

답글 달기