컨테이너 환경에서 Jenkins의 PHP CICD 파이프라인 구축하기

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

      JenkinsPHPCICD01.png

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"
        }
    }
}