GitHub ActionsでLaravelプロジェクトをCI/CDする
前回の記事でdeployer
を使ってLaravel
アプリをローカルからEC2
へデプロイしたので、今回はGitHub Actions
にのせてCI/CD
を組んでいきます。
今回の検証環境は以下の手順ですぐに作れます。
環境
- 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 で公開しています。
構成はこんな感じ
GitHub Actions で CI/CD
最終的なワークフローはこんな感じになりました。
流れとしてはjob
がunit-test
とdeploy
に分かれていて、deploy
の方でneeds: unit-test
と指定することで、先にunit-test
を実行させています。
どちらもubuntu-latest
で動作させています。
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
してると時間がかかるので、キャッシュを使うようにしています。
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
をつけました。
ssh のセットアップ
次に ssh のセットアップを行います。ssh の鍵は GitHub のシークレットに保存しておいて、読み込みます。
- 対象リポジトリの
Settings -> Actions -> New repository secret
をクリックします。
-
Name
をSSH_DEPLOY_KEY
に設定し、鍵の中身をコピーしてValue
に貼り付けAdd secret
をクリックします。
これで{{ 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_SECRET_ACCESS_KEY
も設定しておきます。
AWS_DEFAULT_REGION
とAWS_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
の状態となり、一時的にサービスダウンしてしまうことになります。
これを防ぐために、deploy.php
にターゲットの状態がhealthy
になるまで待機するタスクを追加しました。
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
に設定しておきます。
あとは簡単で、./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 することができました 🎉
まとめ
4 記事かけて検証しましたが、インフラもIaC
で管理してCI/CD
で自動化するとかなり気持ちよく開発をすすめることができると思います。
もっと開発者体験を高めるには例えばカバレッジを可視化して Slack に自動通知したりすると、更に開発意欲がまして良さそうだなと思いました。
参考
- Found preexisting AWS CLI installation: /usr/local/aws-cli/v2/current. の対処
- GitHub Actions × Laravel × Deployer で自動デプロイ - Qiita
- CloudFormation と Ansible で ALB+EC2+RDS の Laravel 環境を構築する(手順編)
- CloudFormation と Ansible で ALB+EC2+RDS の Laravel 環境を構築する(解説編)
- 【AWS】ALB からの切り離しと再登録手順
- Laravel プロジェクトを deployer を使って EC2 にデプロイする
Discussion