[Dev] GCP Cloud Build를 활용한 Spring boot 배포 자동화
GCP Cloud Build로 배포 자동화
Github 레포지토리의 main 브랜치에 push가 발생할 경우 GCP Cloud Build에서 빌드 및 배포까지 실행하는 파이프라인을 만들어보았습니다.
Cloud Build
- GCP Cloud Build > 트리거 > 트리거 만들기
- 이벤트: 브랜치로 푸시
- 소스: 2세대
- 브랜치:
^main$
- 유형: Cloud Build 구성 파일 (YAML 또는 JSON)
- 위치: 저장소
- cloudbuild.yaml 배포 과정
- Cloud Build가 트리거될 때 트리거된 소스 코드를
/workspace
에 압축 해제해두기 때문에 따로git pull
을 할 필요 없음 - GCS > {bucket_name}/credential 폴더에서 credentials.json 파일 복사
gradle clean build -Pprofile=prod -x test
빌드 실행- 빌드 파일 실행
- GCS > {bucket_name}/history 폴더에 빌드 파일을 저장하여 버전 관리
- 배포 스크립트 (deploy.sh) 실행
- 그냥 백그라운드로 실행하면 로그 출력 때문에 빌드가 끝나지 않음!
> $HOME_DIR/nohup.out 2>&1 &
을 추가하여 로그를 파일에서 관리하고 스크립트는 종료
- 그냥 백그라운드로 실행하면 로그 출력 때문에 빌드가 끝나지 않음!
- Cloud Build가 트리거될 때 트리거된 소스 코드를
YAML 파일
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
steps:
# 웹서비스에 필요한 credential 파일 다운로드 (GCP 서비스 계정 키 파일 등)
- name: 'gcr.io/cloud-builders/gsutil'
args:
- 'cp'
- 'gs://{gcs_bucket_name}/credential/*.json'
- '/workspace/server/mkt-solution/src/main/resources/'
- name: 'ubuntu'
entrypoint: 'bash'
args:
- '-c'
- |
echo "INFO: Listing JSON files in the directory:"
ls -l /workspace/server/mkt-solution/src/main/resources/
chmod -R 777 /workspace/server/mkt-solution/src/main/resources/*.json
# Build
- name: 'gradle:8.8-jdk17'
entrypoint: 'bash'
args:
- '-c'
- |
cd /workspace/server/mkt-solution
if gradle clean build -Pprofile=prod -x test; then
echo "Build succeeded."
else
echo "Build failed."
curl -X POST "{slack_webhook_url}" -H "Content-Type: application/json" -d "{\"text\": \"$_BUILD_FAIL\"}"
exit 1
fi
# GCS에 빌드된 jar 파일 적재 (히스토리 관리)
- name: 'gcr.io/cloud-builders/gsutil'
args:
- 'cp'
- '/workspace/server/mkt-solution/build/libs/mkt-solution-0.0.1-SNAPSHOT.jar'
- 'gs://{gcs_bucket_name}/history/$BUILD_ID-mkt-solution-0.0.1-SNAPSHOT.jar'
- name: 'gcr.io/cloud-builders/gsutil'
args:
- 'cp'
- '/workspace/server/mkt-solution/deploy.sh'
- 'gs://{gcs_bucket_name}/'
# VM에 접속하여 배포 스크립트 실행
- name: 'gcr.io/cloud-builders/gcloud'
entrypoint: 'bash'
args:
- '-c'
- |
echo "INFO: Deploying application"
curl -X POST "{slack_webhook_url}" -H "Content-Type: application/json" -d "{\"text\": \"$_START\"}"
# Secret Manager에서 SSH 키 가져오기
mkdir -p /root/.ssh
gcloud secrets versions access latest --secret="{secret_name}" > /root/.ssh/id_rsa
chmod 600 /root/.ssh/id_rsa
# 알려진 호스트 추가
cat > /root/.ssh/config << EOF
Host *
StrictHostKeyChecking no
UserKnownHostsFile=/dev/null
LogLevel=DEBUG3
EOF
# SSH 명령 실행
ssh -i /root/.ssh/id_rsa {vm_username}@{vm_internal_ip} "sudo gsutil cp gs://{gcs_bucket_name}/deploy.sh /home/{vm_username}/ && sudo chmod +x /home/{vm_username}/deploy.sh && sudo /home/{vm_username}/deploy.sh $BUILD_ID"
if [[ $? -eq 0 ]]; then
curl -X POST "{slack_webhook_url}" -H "Content-Type: application/json" -d "{\"text\": \"$_SUCCESS\"}"
else
curl -X POST "{slack_webhook_url}" -H "Content-Type: application/json" -d "{\"text\": \"$_FAIL\"}"
fi
# 슬랙 메시지 변수로 정의
substitutions:
_BUILD_FAIL: "❌ Ads Hub build failed. Please check the system and try again. - <https://github.com/{organization_name}/{repository_name}/commit/${COMMIT_SHA}|Github>, <https://console.cloud.google.com/cloud-build/builds;region=asia-east1/${BUILD_ID}?authuser=2&project={gcp_project_id}|CodeBuild>"
_START: "🚀 Ads Hub deployment started. Please monitor. - <https://github.com/{organization_name}/{repository_name}/commit/${COMMIT_SHA}|Github>, <https://console.cloud.google.com/cloud-build/builds;region=asia-east1/${BUILD_ID}?authuser=2&project={gcp_project_id}|CodeBuild>"
_SUCCESS: "✅ Ads Hub deployment completed. Thank you for your patience. - <https://github.com/{organization_name}/{repository_name}/commit/${COMMIT_SHA}|Github>, <https://console.cloud.google.com/cloud-build/builds;region=asia-east1/${BUILD_ID}?authuser=2&project={gcp_project_id}|CodeBuild>"
_FAIL: "❌ Ads Hub deployment failed. Please check the system and try again. - <https://github.com/{organization_name}/{repository_name}/commit/${COMMIT_SHA}|Github>, <https://console.cloud.google.com/cloud-build/builds;region=asia-east1/${BUILD_ID}?authuser=2&project={gcp_project_id}|CodeBuild>"
options:
logging: CLOUD_LOGGING_ONLY
# machineType: 'E2_HIGHCPU_8'
pool:
name: 'projects/{gcp_project_id}/locations/asia-northeast3/workerPools/{worker_pool_name}'
dynamicSubstitutions: true
name
: 실행할 컨테이너 이미지entrypoint
: 컨테이너가 시작될 때 실행할 명령어args
: entrypoint에 전달할 인자들 지정- 여러 명령어는
-c
플래그로 지정 |
파이프를 이용하여 여러 줄의 명령어 실행
- 여러 명령어는
substitutions
: 빌드 과정에서 사용할 변수 정의dynamicSubstitutions
: true로 지정하면 동적으로 변수 값 설정 가능
배포 스크립트
- application.yaml 에 필요한 보안 변수들은 GCP Secret Manager로 관리
- systemd를 사용하여 안정적으로 실행되도록 관리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#!/bin/bash
BUILD_ID=$1
HOME_DIR=/home/{vm_username}
echo "INFO: Current user: $(whoami)"
echo "INFO: Home directory: $HOME_DIR"
# Secret Manager에서 환경변수 가져오기
echo "INFO: Fetching secrets from Secret Manager..."
JWT_SECRET=$(gcloud secrets versions access latest --secret="jwt-secret")
ES_USERNAME=$(gcloud secrets versions access latest --secret="es-username")
ES_PASSWORD=$(gcloud secrets versions access latest --secret="es-password")
MAIL_USERNAME=$(gcloud secrets versions access latest --secret="mail-username")
MAIL_PASSWORD=$(gcloud secrets versions access latest --secret="mail-password")
GOOGLE_CLIENT_ID=$(gcloud secrets versions access latest --secret="google-client-id")
GOOGLE_CLIENT_SECRET=$(gcloud secrets versions access latest --secret="google-client-secret")
DB_USERNAME=$(gcloud secrets versions access latest --secret="db-username")
DB_PASSWORD=$(gcloud secrets versions access latest --secret="db-password")
CLOUD_SCHEDULER_API_KEY=$(gcloud secrets versions access latest --secret="cloud-scheduler-api-key")
DB_URL=$(gcloud secrets versions access latest --secret="db-url")
DB_URL_DEV=$(gcloud secrets versions access latest --secret="db-url-dev")
ES_URIS=$(gcloud secrets versions access latest --secret="es-uris")
ES_URIS_REPLICA=$(gcloud secrets versions access latest --secret="es-uris-replica")
ES_URIS_DEV=$(gcloud secrets versions access latest --secret="es-uris-dev")
CLOUDRUN_PROJECT_NUMBER=$(gcloud secrets versions access latest --secret="cloudrun-project-number")
# 기존 직접 실행 중인 jar 프로세스 중지
echo "INFO: Checking for existing jar processes..."
OLD_PID=$(ps -ef | grep "java -jar.*mkt-solution-0.0.1-SNAPSHOT.jar" | grep -v grep | awk '{print $2}')
if [ ! -z "$OLD_PID" ]; then
echo "INFO: Found old process with PID $OLD_PID, terminating..."
sudo kill -15 $OLD_PID
sleep 5
# 프로세스가 여전히 실행 중인지 확인
if ps -p $OLD_PID > /dev/null; then
echo "INFO: Process still running, force terminating..."
sudo kill -9 $OLD_PID
sleep 2
fi
echo "INFO: Old process terminated."
fi
echo "INFO: Downloading new JAR file from GCS"
gsutil cp gs://{gcs_bucket_name}/history/$BUILD_ID-mkt-solution-0.0.1-SNAPSHOT.jar $HOME_DIR/mkt-solution-0.0.1-SNAPSHOT.jar
# systemd 서비스 파일 생성
cat << EOF | sudo tee /etc/systemd/system/{service_name}.service
[Unit]
Description=
After=network.target
[Service]
User={vm_username}
WorkingDirectory=/home/{vm_username}
Environment="JWT_SECRET=${JWT_SECRET}"
Environment="MAIL_USERNAME=${MAIL_USERNAME}"
Environment="MAIL_PASSWORD=${MAIL_PASSWORD}"
Environment="GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}"
Environment="GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}"
Environment="DB_USERNAME=${DB_USERNAME}"
Environment="DB_PASSWORD=${DB_PASSWORD}"
Environment="DB_URL=${DB_URL}"
Environment="DB_URL_DEV=${DB_URL_DEV}"
Environment="ES_URIS=${ES_URIS}"
Environment="ES_URIS_REPLICA=${ES_URIS_REPLICA}"
Environment="ES_URIS_DEV=${ES_URIS_DEV}"
Environment="ES_USERNAME=${ES_USERNAME}"
Environment="ES_PASSWORD=${ES_PASSWORD}"
Environment="CLOUD_SCHEDULER_API_KEY=${CLOUD_SCHEDULER_API_KEY}"
Environment="CLOUDRUN_PROJECT_NUMBER=${CLOUDRUN_PROJECT_NUMBER}"
Environment="GOOGLE_CLOUD_PROJECT=innocean-mkt-datalake-01"
ExecStart=/usr/lib/jvm/java-17-openjdk-amd64/bin/java \
-Xms4g -Xmx4g \
-Xlog:gc*,gc+heap=debug:file=/home/{vm_username}/gc.log:time,uptime,level,tags:filecount=5,filesize=50m \
-XX:+UseG1GC \
-XX:+UnlockExperimentalVMOptions \
-XX:G1NewSizePercent=30 \
-XX:InitiatingHeapOccupancyPercent=30 \
-XX:G1MaxNewSizePercent=60 \
-XX:ParallelGCThreads=2 \
-XX:ConcGCThreads=2 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/home/{vm_username}/heapdump.hprof \
-Dspring.profiles.active=prod \
-Duser.timezone=Asia/Seoul \
-Dlogging.level.com.google.cloud.logging=DEBUG \
-Dlogging.level.com.google.auth=DEBUG \
-jar /home/{vm_username}/mkt-solution-0.0.1-SNAPSHOT.jar
SuccessExitStatus=143
StandardOutput=append:/home/{vm_username}/server.log
StandardError=append:/home/{vm_username}/server-error.log
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable {service_name}.service
# 서비스 재시작
sudo systemctl restart {service_name}.service
# 상태 확인
if sudo systemctl is-active --quiet {service_name}.service; then
echo "INFO: Service started successfully"
exit 0
else
echo "ERROR: Failed to start service"
sudo systemctl status {service_name}.service
exit 1
fi
리전 선택
본 문서에 따라 제한이 적은 asia-east1
리전을 선택했습니다.
Cloud Build > 트리거에서 수동으로 빌드를 트리거할 수 있고, 대시보드에서 로그 및 상태를 확인할 수 있습니다.
main 브랜치 protection
Github에서는 main 브랜치에 바로 push하는 것을 막고, develop 브랜치에 먼저 커밋을 모은 뒤 pull request를 통해서만 main에 push할 수 있도록 설정했습니다.
Settings > Branches > Add classic branch protection rule 에서 branch rule을 생성할 수 있습니다.
이렇게 yaml 파일 작성만으로 빌드와 배포를 쉽게 자동화할 수 있습니다. 👍
This post is licensed under CC BY 4.0 by the author.