PHP 애플리케이션에 대한 CI/CD 파이프라인 설정 방법
환경
- Git 리포지토리 및 컨테이너 레지스트리: GitLab
- CI/CD: Jenkins
- PHP 버전: 8.3
- 파이프라인을 컨테이너에서 동작시키기 위한 Jenkins Agent 도커 설정
파이프라인 설정
- Git 리포지토리 연동
Definition: Pipeline script from SCM
- 파이프라인 구성에 필요한 스크립트를 SCM에서 가져오도록 설정
SCM: Git
Repository URL: Git 리포지토리 URL
Credentials: Git 리포지토리에 접근 가능하도록 저장한 Credential
Branch Specifier: */main

CI 파이프라인
- 컨테이너 이미지를 빌드하기 위한 Dockerfile도 Git 리포지토리에 있어야 함
- PHP는 빌드 과정이 필요 없는 인터프리터 언어이기 때문에 소스 빌드 없이 이미지 빌드 → 이미지 푸시 과정만 필요
- CI 완료 전 CD 파이프라인을 트리거
1. 환경
- agent any로 jenkins agent 컨테이너를 파이프라인으로 사용
1
2
3
4
5
6
7
8
9
| pipeline {
agent any
environment {
APP_NAME = '<애플리케이션 명>'
GITLAB_REGISTRY = '<컨테이너 레지스트리 URL>'
GITLAB_PROJECT = '<Git 리포지토리 명>'
GITLAB_CREDENTIAL_ID = '<저장한 Git PAT 토큰 ID>'
}
|
2. Git 체크아웃
- Git 리포지토리의 최신 커밋을 체크아웃
- 파이프라인 로그에 커밋 정보 출력
1
2
3
4
5
6
7
| stages {
stage('Checkout') {
steps {
checkout scm
sh 'git log -1 --oneline'
}
}
|
3. 컨테이너 이미지 빌드 및 레지스트리 푸시
- Git 리포지토리에 있는 Dockerfile을 기반으로 이미지 빌드
- Git 커밋 해시와 Jenkins 빌드 번호를 조합해서 태그 생성
- GitLab PAT 토큰으로 로그인 후 푸시
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
| stage('Docker Build & Push') {
steps {
script {
def gitCommit = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
def imageTag = "${env.BUILD_NUMBER}-${gitCommit}"
def fullImage = "${GITLAB_REGISTRY}/${GITLAB_PROJECT}/${APP_NAME}"
sh """
docker build --no-cache -t ${fullImage}:${imageTag} .
docker tag ${fullImage}:${imageTag} ${fullImage}:latest
"""
withCredentials([usernamePassword(credentialsId: "${GITLAB_CREDENTIAL_ID}",
usernameVariable: 'GITLAB_USER',
passwordVariable: 'GITLAB_TOKEN')]) {
sh """
echo "\$GITLAB_TOKEN" | docker login ${GITLAB_REGISTRY} -u "\$GITLAB_USER" --password-stdin
docker push ${fullImage}:${imageTag}
docker push ${fullImage}:latest
docker logout ${GITLAB_REGISTRY}
"""
}
echo "Docker image pushed: ${fullImage}:${imageTag}"
echo "Docker image pushed: ${fullImage}:latest"
}
}
|
4. CD 파이프라인 트리거
- CI 파이프라인이 완료 되기 전 CD 파이프라인을 동작하도록 트리거
1
2
3
4
5
6
7
8
9
10
11
12
| stage('Trigger CD') {
steps {
build job: '<CD 파이프라인 이름>',
parameters: [
string(name: 'IMAGE_TAG', value: 'latest'),
string(name: 'DEPLOY_TARGET', value: 'ALL')
],
wait: true, // CD 끝날 때까지 기다릴지
propagate: true // CD 실패하면 이 Job도 실패로 처리할지
}
}
}
|
5. 파이프라인 결과 체크
1
2
3
4
5
6
7
8
9
| post {
success {
echo "Build #${env.BUILD_NUMBER} succeeded for ${APP_NAME}"
}
failure {
echo "Build #${env.BUILD_NUMBER} failed - check console output"
}
}
}
|
CD 파이프라인
- Jenkins 서버에서 배포할 서버에 SSH로 접근 후 docker compose 파일을 기동하여 컨테이너 레지스트리의 이미지를 가져오는 방식으로 배포
- 배포할 서버가 여러 개일 경우를 고려해서 구성
환경
- agent any로 jenkins agent 컨테이너를 파이프라인으로 사용
- DB 접속정보 등 민감한 정보가 있는 .env 파일은 Jenkins Credential로 관리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| pipeline {
agent any
parameters {
string(name: 'IMAGE_TAG', defaultValue: 'latest', description: 'Docker image tag to deploy')
choice(name: 'DEPLOY_TARGET', choices: ['WAS1', 'ALL'], description: 'WAS1: <WAS 서버 IP> | ALL: 전체 WAS 배포')
}
environment {
WAS1_HOST = '<WAS1 서버 IP>'
// WAS2_HOST = '<WAS2 서버 IP>' // TODO: WAS2 추가 시 활성화
DEPLOY_PATH = '<배포 경로>'
GITLAB_REGISTRY = '<컨테이너 레지스트리 URL>'
GITLAB_PROJECT = '<Git 리포지토리명>'
IMAGE_NAME = '<컨테이너 이미지 명>'
DEPLOY_CREDENTIAL_ID = '<배포 서버 SSH 계정명>'
GITLAB_CREDENTIAL_ID = '<저장한 Git PAT 토큰 ID>'
TEACHER_ENV_FILE_CREDENTIAL_ID = '<Credential에 저장한 .env 파일 ID>'
COMPOSE_FILE_LOCAL = 'docker-compose.yaml'
}
|
1. Git 체크아웃
- 파이프라인이 “
Pipeline script from SCM” 으로 되어 있기 때문에 가능 docker-compose.yaml 파일을 가져오기 위함
1
2
3
4
5
6
7
| stage('Checkout') {
steps {
// CD Job이 Pipeline from SCM로 설정돼 있다는 전제
checkout scm
sh "ls -al ${COMPOSE_FILE_LOCAL} || echo 'compose 파일 경로 확인 필요'"
}
}
|
2. 준비
배포할 이미지 태그 / 배포 대상 서버를 선택할 수 있게 설정- deployHosts: 배포 대상 호스트 목록
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| stages {
stage('Prepare') {
steps {
script {
echo "Deploying image: ${GITLAB_REGISTRY}/${GITLAB_PROJECT}/${IMAGE_NAME}:${params.IMAGE_TAG}"
echo "Deploy target: ${params.DEPLOY_TARGET}"
def deployHosts = []
if (params.DEPLOY_TARGET == 'WAS1') {
deployHosts = [WAS1_HOST]
} else if (params.DEPLOY_TARGET == 'ALL') {
deployHosts = [WAS1_HOST]
// TODO: WAS2 추가 시 -> deployHosts = [WAS1_HOST, WAS2_HOST]
}
env.DEPLOY_HOSTS = deployHosts.join(',')
echo "Target servers: ${env.DEPLOY_HOSTS}"
}
}
}
|
3. 배포
- 컨테이너 레지스트리에서 이미지 풀링
.env 파일을 Credential에서 배포 서버로 복사- git으로 체크아웃한
docker-compose 파일을 배포 서버로 복사 - 배포 경로에서 docker compose 명령어를 통한 애플리케이션 재기동
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
| stage('Deploy') {
steps {
script {
def hosts = env.DEPLOY_HOSTS.split(',')
withCredentials([
usernamePassword(credentialsId: "${DEPLOY_CREDENTIAL_ID}",
usernameVariable: 'DEPLOY_USER',
passwordVariable: 'DEPLOY_PASS'),
usernamePassword(credentialsId: "${GITLAB_CREDENTIAL_ID}",
usernameVariable: 'GITLAB_USER',
passwordVariable: 'GITLAB_TOKEN'),
// .env 전체를 파일로 받음
file(credentialsId: "${TEACHER_ENV_FILE_CREDENTIAL_ID}",
variable: '<env 변수명>')
]) {
for (host in hosts) {
echo "Deploying to ${host}..."
sh """
# 0) 임시 파일 경로(충돌 방지용)
TMP_ENV=/tmp/env.\$(date +%s)
TMP_COMPOSE=/tmp/compose.\$(date +%s).yaml
# 1) .env / docker-compose.yaml 둘 다 /tmp 로 복사
sshpass -p "\$DEPLOY_PASS" scp -o StrictHostKeyChecking=no "\$<env 변수명>" "\$DEPLOY_USER"@${host}:"\$TMP_ENV"
sshpass -p "\$DEPLOY_PASS" scp -o StrictHostKeyChecking=no "${COMPOSE_FILE_LOCAL}" "\$DEPLOY_USER"@${host}:"\$TMP_COMPOSE"
# 2) 서버 안에서 sudo로 최종 위치로 이동 + 권한 조정 + 컨테이너 재기동
sshpass -p "\$DEPLOY_PASS" ssh -o StrictHostKeyChecking=no "\$DEPLOY_USER"@${host} "
echo 'Apply .env & docker-compose.yaml...'
# .env 교체
sudo mv \$TMP_ENV ${DEPLOY_PATH}/.env
sudo chown nginx:nginx ${DEPLOY_PATH}/.env
# docker-compose.yaml 교체
sudo mv \$TMP_COMPOSE ${DEPLOY_PATH}/docker-compose.yaml
sudo chown root:root ${DEPLOY_PATH}/docker-compose.yaml
echo 'Logging in to GitLab Registry...'
echo '\$GITLAB_TOKEN' | sudo docker login ${GITLAB_REGISTRY} -u '\$GITLAB_USER' --password-stdin
echo 'Pulling image: ${GITLAB_REGISTRY}/${GITLAB_PROJECT}/${IMAGE_NAME}:${params.IMAGE_TAG}'
sudo docker pull ${GITLAB_REGISTRY}/${GITLAB_PROJECT}/${IMAGE_NAME}:${params.IMAGE_TAG}
echo 'Redeploying containers...'
export IMAGE_TAG=${params.IMAGE_TAG}
sudo docker compose -f ${DEPLOY_PATH}/docker-compose.yaml down
sudo docker compose -f ${DEPLOY_PATH}/docker-compose.yaml up -d
sudo docker logout ${GITLAB_REGISTRY}
"
"""
echo "Deployment to ${host} completed."
}
}
}
}
}
|
4. 헬스 체크
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
| stage('Health Check') {
steps {
script {
echo "Waiting for application to start..."
sleep 10
def hosts = env.DEPLOY_HOSTS.split(',')
withCredentials([
usernamePassword(credentialsId: "${DEPLOY_CREDENTIAL_ID}",
usernameVariable: 'DEPLOY_USER',
passwordVariable: 'DEPLOY_PASS')
]) {
for (host in hosts) {
echo "Health checking ${host}..."
sh """
sshpass -p "\$DEPLOY_PASS" ssh -o StrictHostKeyChecking=no "\$DEPLOY_USER"@${host} "
# 컨테이너 상태 확인
sudo docker ps | grep pj-1 || exit 1
"
"""
echo "${host} health check passed."
}
}
}
}
}
}
|
5. 파이프라인 결과 체크
1
2
3
4
5
6
7
8
9
| post {
success {
echo "Deployment succeeded - ${IMAGE_NAME}:${params.IMAGE_TAG} deployed to ${env.DEPLOY_HOSTS}"
}
failure {
echo "Deployment failed - check console output"
}
}
}
|