🎢

GitHub ActionsでLaravelプロジェクトをCI/CDする

2022/02/11に公開

前回の記事deployerを使ってLaravelアプリをローカルからEC2へデプロイしたので、今回はGitHub ActionsにのせてCI/CDを組んでいきます。

今回の検証環境は以下の手順ですぐに作れます。

https://zenn.dev/tokku5552/articles/create-php-env-with-cfn

環境

  • OS
    • macOS Monterey バージョン 12.1(Intel)
  • docker
% docker -v
Docker version 20.10.11, build dea9396
  • docker 内の php/composer
# php -v
PHP 7.3.33 (cli) (built: Dec 21 2021 22:11:19) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.33, Copyright (c) 1998-2018 Zend Technologies

# composer -v
   ______
  / ____/___  ____ ___  ____  ____  ________  _____
 / /   / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                    /_/
Composer version 2.1.14 2021-11-30 10:51:43

コードは GitHub で公開しています。

https://github.com/tokku5552/php-docker-nginx-postgresql

構成はこんな感じ
image

GitHub Actions で CI/CD

最終的なワークフローはこんな感じになりました。
流れとしてはjobunit-testdeployに分かれていて、deployの方でneeds: unit-testと指定することで、先にunit-testを実行させています。
どちらもubuntu-latestで動作させています。

.github/workflows/deploy.yml
name: deploy stg

on:
  push:
    branches:
      - main

jobs:
  unit-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Setup PHP 7.4
        run: sudo update-alternatives --set php /usr/bin/php7.4

      - name: cache vendor
        id: cache
        uses: actions/cache@v1
        with:
          ref: main
          path: ./vendor
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-composer-

      - name: composer install
        if: steps.cache.outputs.cache-hit != 'true'
        run: composer install
        working-directory: ./src

      - name: set laravel env
        run: echo "${{ secrets.LARAVEL_ENV }}" > .env
        working-directory: ./src

      - name: run unit test
        run: vendor/bin/phpunit tests/
        working-directory: ./src

  deploy:
    runs-on: ubuntu-latest
    needs: unit-test
    steps:
      - uses: actions/checkout@v2
      - name: Setup PHP 7.4
        run: sudo update-alternatives --set php /usr/bin/php7.4

      - name: cache vendor
        id: cache
        uses: actions/cache@v1
        with:
          ref: main
          path: ./vendor
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-composer-

      - name: composer install
        if: steps.cache.outputs.cache-hit != 'true'
        run: composer install
        working-directory: ./src

      - name: install awscli
        working-directory: ./src
        run: |
          # AWS CLIインストール
          curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
          unzip awscliv2.zip
          sudo ./aws/install --update
          aws --version

      - name: setup ssh
        working-directory: ./src
        run: |
          # sshキーをコピー
          mkdir -p /home/runner/.ssh
          touch /home/runner/.ssh/MyKeypair.pem
          echo "${{ secrets.SSH_DEPLOY_KEY }}" > /home/runner/.ssh/MyKeypair.pem
          chmod 600 /home/runner/.ssh/MyKeypair.pem
          # known_hostsに追加
          ssh-keyscan 13.112.197.49 >> ~/.ssh/known_hosts
          ssh-keyscan 18.181.224.249 >> ~/.ssh/known_hosts

      - name: deploy to EC2 with rolling updates
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: ap-northeast-1
          AWS_DEFAULT_OUTPUT: json
        working-directory: ./src
        run: |
          # デプロイ
          vendor/bin/dep deploy LaravelWeb1 -vvv
          vendor/bin/dep deploy LaravelWeb2 -vvv

共通部分 - php のセットアップと composer install

php7.4 を指定してセットアップしたあと、composer install を実行しています。
./vendorは毎回composer installしてると時間がかかるので、キャッシュを使うようにしています。

.github/workflows/deploy.yml
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Setup PHP 7.4
        run: sudo update-alternatives --set php /usr/bin/php7.4

      - name: cache vendor
        id: cache
        uses: actions/cache@v1
        with:
          ref: main
          path: ./vendor
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-composer-

      - name: composer install
        if: steps.cache.outputs.cache-hit != 'true'
        run: composer install
        working-directory: ./src

今回のプロジェクトではsrcの下にlaravelのコードがあるので、そちらで必要な動作を行うようにworking-directory: ./srcを指定しています。
例えば上記ならrun: cd ./src && composer installとやるのと同じ意味です。

deploy

次にデプロイ部分のステップについて説明します。
流れとしては、AWSCLI インストール -> ssh キーのセットアップ -> deployer でデプロイ
という感じになります。

awscli のインストール

AWSCLI をインストールします。
こちらは公式の v2 のインストール方法に従ってインストールしています。
後で環境変数に必要事項を設定するので、この時点でaws configureは不要です。

      - name: install awscli
        working-directory: ./src
        run: |
          # AWS CLIインストール
          curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
          unzip awscliv2.zip
          sudo ./aws/install --update
          aws --version

はじめsudo ./aws/installでインストールを実施したらFound preexisting AWS CLI installation: /usr/local/aws-cli/v2/current. Please rerun install script with --update flag.というエラーが出てしまったので、以下の記事を参考に--updateをつけました。
https://zenn.dev/hdmt/scraps/db91ecc16f3b10

ssh のセットアップ

次に ssh のセットアップを行います。ssh の鍵は GitHub のシークレットに保存しておいて、読み込みます。

  • 対象リポジトリのSettings -> Actions -> New repository secretをクリックします。

シークレットの作成方法

  • NameSSH_DEPLOY_KEYに設定し、鍵の中身をコピーしてValueに貼り付けAdd secretをクリックします。

SSH_DEPLOY_KEY

これで{{ secrets.SSH_DEPLOY_KEY}}で取得できるようになったので、以下のようにキーペアとして書き出してパーミッションを 600 にしてやります。

      - name: setup ssh
        working-directory: ./src
        run: |
          # sshキーをコピー
          mkdir -p /home/runner/.ssh
          touch /home/runner/.ssh/MyKeypair.pem
          echo "${{ secrets.SSH_DEPLOY_KEY }}" > /home/runner/.ssh/MyKeypair.pem
          chmod 600 /home/runner/.ssh/MyKeypair.pem
          # known_hostsに追加
          ssh-keyscan 13.112.197.49 >> ~/.ssh/known_hosts
          ssh-keyscan 18.181.224.249 >> ~/.ssh/known_hosts

deployer でのデプロイ

次はローリングアップデートの部分です。
今回のdeploy.phpでは AWSCLI を使って ALB の制御を行っているので、AWSCLI で必要なシークレットを設定しておきます。
AWS_ACCESS_KEY_ID
同様にしてAWS_SECRET_ACCESS_KEYも設定しておきます。
AWS_DEFAULT_REGIONAWS_DEFAULT_OUTPUTは特に秘匿する必要もないので直接書いていますが、秘匿したい場合は同様にシークレットに保存しておけば良いと思います。

      - name: deploy to EC2 with rolling updates
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: ap-northeast-1
          AWS_DEFAULT_OUTPUT: json
        working-directory: ./src
        run: |
          # デプロイ
          vendor/bin/dep deploy LaravelWeb1 -vvv
          vendor/bin/dep deploy LaravelWeb2 -vvv

デプロイ自体はvendor/bin/dep deploy LaravelWeb1コマンドのみで、ログに詳細出力させるために-vvvオプションを付与しています。

Rolling Update にする

前回の記事の状態のdeploy.phpで上記のように順番にデプロイすると、以下のように一方がinitialでもう一方がdrainingの状態となり、一時的にサービスダウンしてしまうことになります。
initial draining

これを防ぐために、deploy.phpにターゲットの状態がhealthyになるまで待機するタスクを追加しました。

deploy.php
after('register-targets', 'describe-target-health');
task('describe-target-health', function () {
  $retry_count = 10;
  $i = 0;
  while ($i <= $retry_count) {
    $result = runLocally('aws elbv2 describe-target-health --target-group-arn {{target_group_arn}}');
    $obj = json_decode($result);
    foreach ($obj->TargetHealthDescriptions as $val) {
      if ($val->Target->Id === get('instance_id')) {
        if ($val->TargetHealth->State != 'healthy') {
          if ($i == $retry_count) {
            writeln('The preparation was not completed. Please try later');
            exit(1);
          }
          writeln('waiting...');
          break;
        } else {
          writeln('{{instance_id}} is healthy');
          return;
        };
      } else {
        break;
      }
    }
    sleep(1);
    $i++;
  }
});

条件がいくつもあって分かりづらいですが、aws elbv2 describe-target-healthでターゲットグループの情報を取得し、デプロイ対象インスタンスの状態がhealthyかどうかを$retry_countだけ確かめて、回数オーバーしたら終了ステータス 1 で終了させるようにしました。
これをafter('register-targets', 'describe-target-health');としておくことで、ターゲットグループへの再登録後に実施するようになり、サービスダウンなくデプロイを行うことができるようになりました。

テスト

今回検証したリポジトリは Laravel のほうはほぼプロジェクト作成後いじっていないのであまり意味ないですが、CI/CDということでちゃんとテストも自動化しておきます。
Laravel でユニットテストを動かすために、.envファイルを配置してやる必要があったので、こちらもsecrtsに設定しておきます。
LARAVEL_ENV

あとは簡単で、./srcの直下に.envファイルを作成して、vendor/bin/phpunit tests/を実行することでユニットテストを実施します。

      - name: set laravel env
        run: echo "${{ secrets.LARAVEL_ENV }}" > .env
        working-directory: ./src

      - name: run unit test
        run: vendor/bin/phpunit tests/
        working-directory: ./src

ターゲットブランチにpushすると、無事 pass することができました 🎉
image

まとめ

4 記事かけて検証しましたが、インフラもIaCで管理してCI/CDで自動化するとかなり気持ちよく開発をすすめることができると思います。
もっと開発者体験を高めるには例えばカバレッジを可視化して Slack に自動通知したりすると、更に開発意欲がまして良さそうだなと思いました。

参考

Discussion