Laravel メンテナンスモードをJenkinsで実装してみた

## 経緯

業務でLaravelをECSを利用して運用しています。

Laravelメンテナンスモードを利用したいと思った時に、ディレクトリをマウントさせるか、イメージに含めるパターンがあると思います。
`artisan`コマンドを打てるECSのタスク(コンテナ)も立ち上げていなかったこともあり、今回はイメージに含めるパターンで作成することにしました。

## 環境

- AWS ECS

## 構成

- ECSのタスク定義
    - nginx + php-fpm
- Jenkins

## 前提条件
以下のコードは、既にLaravelが動作するイメージが作成できているのが前提になります。

### Jenkins Jobファイル

```sh:LARAVEL_MAINTENANCE/Jenkinsfile
// Declarative pipeline
pipeline {
    agent { label 'built-in' }

    parameters {
        booleanParam(name: 'MAINTENANCE_BUILD', defaultValue: false, description: 'メンテナンス状態にする(※チェックなしの場合はメンテナンスを解除する)')
    }

    environment {
        // 実行環境
        ENV = "<環境 (production | staging)>"

        // ECR情報
        AWS_ACCOUNT_ID = '<AWS アカウントID>'
        AWS_REGION = '<リージョン>'
        AWS_ECR = "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"

        // 作成してあるイメージ名
        IMAGE_NAME = "php"

        // Dockerビルド作業フォルダ
        DOCKER_BUILD_WORKSPACE = 'build_workspace_laravel_maintenance'

        // ECS情報
        CLUSTER_NAME = 'cluster'
        SERVICE_NAME = 'frontend'
    }

    stages {
        stage('作業フォルダ 削除') {
            steps {
                echo "======== Start!! ========"
                deleteDir()
            }
        }

        stage("メンテナンス ON") {
            when {
                expression {
                    return params.MAINTENANCE_BUILD
                }
            }
            steps {
                // AWS ECRログイン
                sh '''#!/bin/bash -xe
                aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ECR}
                '''

                sh '''#!/bin/bash -xe
                docker pull ${AWS_ECR}/${IMAGE_NAME}:latest
                '''

                sh '''#!/bin/bash -xe
                docker run -e ENV=${ENV} --name ${IMAGE_NAME} -dit ${AWS_ECR}/${IMAGE_NAME}
                docker exec ${IMAGE_NAME} php artisan down --render="errors::503" --secret="xxxxxxxxxx"
                docker commit ${IMAGE_NAME} ${AWS_ECR}/${IMAGE_NAME}:latest
                '''
            }

            //ステップ終了処理
            post {
                always {
                    sh '''#!/bin/bash -xe
                    docker stop ${IMAGE_NAME}
                    docker rm ${IMAGE_NAME}
                    '''
                }
            }
        }

        stage("メンテナンス OFF") {
            when {
                expression {
                    return !params.MAINTENANCE_BUILD
                }
            }
            steps {
                // AWS ECRログイン
                sh '''#!/bin/bash -xe
                aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ECR}
                '''

                sh '''#!/bin/bash -xe
                docker pull ${AWS_ECR}/${IMAGE_NAME}:latest
                '''


                sh '''#!/bin/bash -xe
                docker run -e ENV=${ENV} --name ${IMAGE_NAME} -dit ${AWS_ECR}/${IMAGE_NAME}
                docker exec ${IMAGE_NAME} php artisan up
                docker commit ${IMAGE_NAME} ${AWS_ECR}/${IMAGE_NAME}:latest
                '''
            }

            //ステップ終了処理
            post {
                always {
                    sh '''#!/bin/bash -xe
                    docker stop ${IMAGE_NAME}
                    docker rm ${IMAGE_NAME}
                    '''
                }
            }
        }

        stage("Docker プッシュ") {
            steps {
                // AWS ECRログイン
                sh '''#!/bin/bash -xe
                aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ECR}
                '''

                // ECRにプッシュ
                sh '''#!/bin/bash -xe
                docker push ${AWS_ECR}/${IMAGE_NAME}:latest
                '''

                // 成功したので過去のイメージを消す
                sh '''#!/bin/bash -xe
                docker image prune -f
                '''
            }
        }

        stage("デプロイ frontend(Rolling Update)") {
            steps {
                // ローリングアップデート実行
                sh """
                aws ecs update-service \
                    --cluster $CLUSTER_NAME \
                    --service $SERVICE_NAME_FRONT \
                    --platform-version 1.4.0 \
                    --force-new-deployment
                """

                // アップデートが完了するまで待つ
                sh """
                aws ecs wait services-stable  \
                    --cluster $CLUSTER_NAME \
                    --service $SERVICE_NAME_FRONT
                """
            }
        }

        stage("HDD容量 ＆ビルドキャッシュクリア") {
            steps {
                // 容量表示
                sh """
                df -h
                docker system df
                """

                // ビルドキャッシュ クリア
                sh """
                docker builder prune -f
                """
            }
        }
    }

    post{
        always{
            echo "======== Finish!! ========"
        }
    }
}
```

### メンテナンスにしている部分だけ抜粋

```sh
docker run -e ENV=${ENV} --name ${IMAGE_NAME} -dit ${AWS_ECR}/${IMAGE_NAME}
```

- 環境を指定して、イメージを起動させます
    - ここで`artisan`コマンドを渡してしまうと、Webサービスとしては動作しないイメージが出来てしまいました
    - 使えるイメージにするためPHP-FPMとして起動だけさせます

```sh
docker exec ${IMAGE_NAME} php artisan down --render="errors::503" --secret="xxxxxxxxxx"
```

- 起動中のコンテナに対して`artisan`コマンドでメンテナンスモードにします

```sh
docker commit ${IMAGE_NAME} ${AWS_ECR}/${IMAGE_NAME}:latest
```

- 起動中のコンテナからイメージを作成して完了です

## 最後に

Dockerビルドが実行されないので、Job実行の殆どはECSのデプロイ時間だけになります
既にECS環境で構築済みの場合でも導入しやすいかな？と思っています

誰かの役になっていたら嬉しいです