-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathJenkinsfile
More file actions
399 lines (365 loc) · 21 KB
/
Jenkinsfile
File metadata and controls
399 lines (365 loc) · 21 KB
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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
pipeline {
agent any
parameters {
booleanParam(name: 'FORCE_BUILD_BACKEND', defaultValue: false, description: '무조건 Backend 빌드')
booleanParam(name: 'FORCE_BUILD_MOBILE', defaultValue: false, description: '무조건 Mobile App 빌드')
booleanParam(name: 'FORCE_BUILD_WEAR', defaultValue: false, description: '무조건 Wear 빌드')
booleanParam(name: 'FORCE_BUILD_AI', defaultValue: false, description: '무조건 AI 빌드')
}
environment {
// GitLab
GITLAB_CREDENTIALS = 'gitlab-token'
GITLAB_URL = 'https://lab.ssafy.com/s13-final/S13P31A106.git'
// Docker Registry
REGISTRY_LOCAL = "localhost:${env.REGISTRY_PORT}"
REGISTRY_PRIVATE = "${env.REGISTRY_PRIVATE_IP}:${env.REGISTRY_PORT}"
REGISTRY_PUBLIC = "${env.REGISTRY_PUBLIC_IP}:${env.REGISTRY_PORT}"
// Backend
BACKEND_IMAGE = 'hand-backend'
BACKEND_SERVER = "${env.BACKEND_SERVER_IP}"
// AI
AI_IMAGE = 'hand-ai'
AI_SERVER = "${env.AI_SERVER_IP}"
// Credentials
SSH_CREDENTIALS = 'server-ssh-key'
}
options {
gitLabConnection('GitLab')
buildDiscarder(logRotator(numToKeepStr: '10'))
timestamps()
timeout(time: 30, unit: 'MINUTES')
disableConcurrentBuilds()
skipDefaultCheckout(true)
}
triggers {
gitlab(
triggerOnPush: true,
triggerOnMergeRequest: false,
triggerOnAcceptedMergeRequest: true,
branchFilterType: 'NameBasedFilter',
includeBranchesSpec: 'dev',
targetBranchRegex: 'dev',
secretToken: "${env.GITLAB_WEBHOOK_TOKEN}"
)
}
stages {
stage('Checkout') {
steps {
echo '📥 Checking out code...'
checkout([
$class: 'GitSCM',
branches: [[name: '*/dev']],
userRemoteConfigs: [[
url: "${GITLAB_URL}",
credentialsId: "${GITLAB_CREDENTIALS}"
]]
])
sh 'git log -1 --oneline'
}
}
stage('Parallel Build & Deploy') {
parallel {
stage('Backend CI/CD') {
when {
beforeAgent true
anyOf {
changeset pattern: "backend/**", caseSensitive: true
expression { return params.FORCE_BUILD_BACKEND }
}
}
stages {
stage('Backend Docker Build & Push') {
steps {
dir('backend') {
echo '🐳 Building and Pushing Docker Image to Registry...'
withCredentials([file(credentialsId: 'fcm-service-account', variable: 'FCM_KEY_FILE')]) {
sh """
# FCM 키 파일을 resources 폴더에 복사
echo "📋 Copying FCM service account key..."
cp \${FCM_KEY_FILE} src/main/resources/firebase-service-account.json
chmod 600 src/main/resources/firebase-service-account.json
# Docker Multi-stage build로 Gradle 빌드 포함 (cache-from으로 캐시 재사용)
docker pull ${REGISTRY_LOCAL}/${BACKEND_IMAGE}:latest || true
docker build --cache-from ${REGISTRY_LOCAL}/${BACKEND_IMAGE}:latest -t ${REGISTRY_LOCAL}/${BACKEND_IMAGE}:latest .
# Registry에 Push (latest만)
docker push ${REGISTRY_LOCAL}/${BACKEND_IMAGE}:latest
echo "✅ Pushed to Registry: ${REGISTRY_LOCAL}/${BACKEND_IMAGE}:latest"
"""
}
}
}
}
stage('Backend Deploy to Server 2') {
steps {
echo '🚀 Deploying Backend to Server 2...'
withCredentials([
file(credentialsId: 'backend-env', variable: 'ENV_FILE')
]) {
sshagent([SSH_CREDENTIALS]) {
sh """
# .env 파일 전송
echo "📤 Transferring .env file..."
scp -o StrictHostKeyChecking=no \${ENV_FILE} ubuntu@${BACKEND_SERVER}:/home/ubuntu/.env
# 서버2에서 배포 실행
ssh -o StrictHostKeyChecking=no ubuntu@${BACKEND_SERVER} '
# Registry에서 이미지 Pull
echo "📥 Pulling image from Registry..."
docker pull ${REGISTRY_PUBLIC}/${BACKEND_IMAGE}:latest
# 기존 컨테이너 중지 및 제거
echo "🛑 Stopping old container..."
docker stop hand-backend 2>/dev/null || true
docker rm hand-backend 2>/dev/null || true
# 새 컨테이너 실행
echo "🚀 Starting new container..."
docker run -d \
--name hand-backend \
-p 8080:8080 \
--env-file /home/ubuntu/.env \
--restart unless-stopped \
-e TZ=Asia/Seoul \
-v /etc/localtime:/etc/localtime:ro \
${REGISTRY_PUBLIC}/${BACKEND_IMAGE}:latest
# 컨테이너 실행 확인
echo "⏳ Waiting for container to start..."
sleep 10
if docker ps | grep -q hand-backend; then
echo "✅ Container is running!"
docker ps | grep hand-backend
else
echo "❌ Container failed to start!"
docker logs hand-backend
exit 1
fi
# 오래된 이미지 정리
echo "🧹 Cleaning old images..."
docker images | grep ${REGISTRY_PUBLIC}/${BACKEND_IMAGE} | grep -v latest | awk "{print \$3}" | xargs -r docker rmi -f || true
docker image prune -f || true
'
"""
}
}
}
}
}
}
// stage('Mobile App CI/CD') {
// when {
// beforeAgent true
// anyOf {
// changeset pattern: "frontend/app/**", caseSensitive: true
// expression { return params.FORCE_BUILD_MOBILE }
// }
// }
// stages {
// stage('Prepare Firebase Credentials') {
// steps {
// dir('frontend') {
// echo '🔧 Preparing Firebase and Google Services credentials...'
// withCredentials([
// file(credentialsId: 'firebase_sa_json', variable: 'FIREBASE_SA_FILE'),
// file(credentialsId: 'google-services-json', variable: 'GOOGLE_SERVICES_FILE')
// ]) {
// sh """
// # Firebase 서비스 계정 JSON 파일 복사
// echo "📋 Copying Firebase service account..."
// cp \${FIREBASE_SA_FILE} firebase-service-account.json
// chmod 600 firebase-service-account.json
//
// # google-services.json 복사
// echo "📋 Copying google-services.json..."
// cp \${GOOGLE_SERVICES_FILE} app/google-services.json
// chmod 600 app/google-services.json
//
// # 파일 존재 확인
// if [ ! -f "app/google-services.json" ]; then
// echo "❌ google-services.json not found!"
// exit 1
// fi
// echo "✅ google-services.json prepared successfully!"
// """
// }
// }
// }
// }
//
// stage('Build & Upload APK') {
// agent {
// docker {
// image 'mingc/android-build-box:latest'
// args '-v /var/jenkins_home/.gradle:/root/.gradle -u root'
// reuseNode true
// }
// }
// steps {
// dir('frontend') {
// withCredentials([
// string(credentialsId: 'firebase_app_id_text', variable: 'FIREBASE_APP_ID_VALUE'),
// string(credentialsId: 'gms-base-url', variable: 'GMS_BASE_URL_VALUE'),
// string(credentialsId: 'gms-api-key', variable: 'GMS_API_KEY_VALUE')
// ]) {
// script {
// def gitCommit = sh(returnStdout: true, script: 'git log -1 --oneline').trim()
// def releaseNotes = """
// 빌드 번호: ${BUILD_NUMBER}
// 빌드 시간: ${new Date().format('yyyy-MM-dd HH:mm:ss')}
// 커밋: ${gitCommit}
// 배포자: Jenkins CI/CD
// """.trim()
//
// echo '📦 Building Debug APK and Uploading to Firebase...'
// sh """
// chmod +x gradlew
// ./gradlew --version
//
// # GMS API 환경변수 설정
// export GMS_BASE_URL='${GMS_BASE_URL_VALUE}'
// export GMS_API_KEY='${GMS_API_KEY_VALUE}'
//
// echo "🔨 Building app module APK..."
// ./gradlew :app:assembleDebug
//
// # APK 확인
// if [ -f "app/build/outputs/apk/debug/app-debug.apk" ]; then
// echo "✅ APK built successfully!"
// ls -lh app/build/outputs/apk/debug/app-debug.apk
// else
// echo "❌ APK build failed!"
// exit 1
// fi
//
// # Firebase 환경변수
// export FIREBASE_SERVICE_ACCOUNT_JSON=\$(pwd)/firebase-service-account.json
// export FIREBASE_APP_ID=${FIREBASE_APP_ID_VALUE}
// export RELEASE_NOTES='${releaseNotes}'
//
// # Firebase 업로드
// echo "📤 Uploading to Firebase..."
// ./gradlew :app:appDistributionUploadDebug
//
// echo "✅ Uploaded to Firebase!"
// """
// }
// }
// }
// }
// post {
// always {
// dir('frontend') {
// echo '🧹 Cleaning up sensitive files...'
// sh '''
// rm -f firebase-service-account.json || true
// rm -f app/google-services.json || true
// '''
// }
// }
// }
// }
// }
// }
stage('AI CI/CD') {
when {
beforeAgent true
anyOf {
changeset pattern: "ai/**", caseSensitive: true
expression { return params.FORCE_BUILD_AI }
}
}
stages {
stage('AI Docker Build & Push') {
steps {
dir('ai') {
echo '🐳 Building AI Docker Image...'
sh """
# Docker 빌드 (cache-from으로 이전 이미지 레이어 재사용)
docker pull ${REGISTRY_LOCAL}/${AI_IMAGE}:latest || true
docker build --cache-from ${REGISTRY_LOCAL}/${AI_IMAGE}:latest -t ${REGISTRY_LOCAL}/${AI_IMAGE}:latest .
# Registry에 Push (latest만)
docker push ${REGISTRY_LOCAL}/${AI_IMAGE}:latest
echo "✅ Pushed to Registry: ${REGISTRY_LOCAL}/${AI_IMAGE}:latest"
"""
}
}
}
stage('AI Deploy to Server 3') {
steps {
echo '🚀 Deploying AI to Server 3...'
withCredentials([
file(credentialsId: 'ai-env', variable: 'ENV_FILE')
]) {
sshagent([SSH_CREDENTIALS]) {
sh """
# 기존 읽기 전용 .env 파일 제거 후 전송
echo "📤 Transferring .env file..."
ssh -o StrictHostKeyChecking=no ubuntu@${AI_SERVER} 'rm -f /home/ubuntu/ai/.env' || true
scp -o StrictHostKeyChecking=no \${ENV_FILE} ubuntu@${AI_SERVER}:/home/ubuntu/ai/.env
ssh -o StrictHostKeyChecking=no ubuntu@${AI_SERVER} 'chmod 644 /home/ubuntu/ai/.env'
# docker-compose.yml 파일 전송
echo "📤 Transferring docker-compose.yml..."
scp -o StrictHostKeyChecking=no ai/docker-compose.yml ubuntu@${AI_SERVER}:/home/ubuntu/ai/docker-compose.yml
# 서버3에서 배포 실행
ssh -o StrictHostKeyChecking=no ubuntu@${AI_SERVER} "
cd /home/ubuntu/ai
# Registry에서 이미지 Pull
echo '📥 Pulling image from Registry...'
docker pull ${REGISTRY_PRIVATE}/${AI_IMAGE}:latest
# 기존 컨테이너 중지 및 제거
echo '🛑 Stopping old containers...'
docker compose down 2>/dev/null || true
# docker compose로 서비스 시작
echo '🚀 Starting AI services...'
REGISTRY_URL=${REGISTRY_PRIVATE} docker compose up -d
# 컨테이너 실행 확인
echo '⏳ Waiting for containers to start...'
sleep 15
if docker ps | grep -q hand-ai && docker ps | grep -q hand-weaviate; then
echo '✅ AI containers are running!'
docker ps | grep hand-
else
echo '❌ AI containers failed to start!'
docker compose logs
exit 1
fi
# 오래된 이미지 정리
echo '🧹 Cleaning old images...'
docker images | grep ${REGISTRY_PRIVATE}/${AI_IMAGE} | grep -v latest | awk '{print \$3}' | xargs -r docker rmi -f || true
docker image prune -f || true
"
"""
}
}
}
}
}
}
}
}
}
post {
success {
echo '✅ 빌드/배포 성공!'
updateGitlabCommitStatus name: 'build', state: 'success'
}
failure {
echo '❌ 빌드/배포 실패!'
updateGitlabCommitStatus name: 'build', state: 'failed'
}
always {
echo '🧹 Cleaning up...'
sh '''
# Jenkins 서버 로컬 이미지 정리만 수행
echo "🧹 Cleaning local images on Jenkins server..."
docker images | grep ${BACKEND_IMAGE} | grep -v latest | awk '{print $3}' | xargs -r docker rmi -f || true
docker images | grep ${AI_IMAGE} | grep -v latest | awk '{print $3}' | xargs -r docker rmi -f || true
docker image prune -f || true
echo "✅ Local cleanup completed"
'''
cleanWs(
deleteDirs: true,
patterns: [
[pattern: '**', type: 'INCLUDE'], // 기본은 모두 삭제
[pattern: 'frontend/app/build/**', type: 'EXCLUDE'] // 여기만 보존
]
)
}
}
}