[GitHub ActionsでCI/CDパイプライン構築] Laravel AWS EC2 Docker Deployer 

今回は、GitHub Actionsとdeployerを使ったCI/CDパイプラインの構築手順に関してご紹介していきます。

## アプリの使用技術
フロントエンド：HTML5、CSS、bootstrap4.5.0、Tailwindcss2.2.15
バックエンド：PHP7.4.15、Lravel8.34.0、Jetstream1.0、Livewire
インフラ：Docker20.10.2、nginx1.18、MySQL8.0.23/phpMyAdmin、Apache2.4.38、AWS（EC2、ALB、Autoscaling、S3）

## CI/CDとは
簡単にまとめると、ローカルからのプッシュ後、CIサーバーがそれを検知して事前に作成していたテストコードを実行し、問題なければデプロイまでを自動で行なってくれる仕組みになります。
`CIはテストまでの自動化`、`CDはテスト後からデプロイまでの自動化`です。
つまり、`CIとCDの両方を構築することでプッシュ後からデプロイまでの一連の動作を自動化できます。`

メリットやデメリット等に関して詳しく知りたい方は下記記事を参考にしてください。

https://mintaku-blog.net/about-cicd/

## deployer構築
CI/CDの前に先ずは手動でデプロイを行えるように設定していきます。
今回はdeployerを使用します。
初期設定としてインストール等を実行する必要があるので、下記の参考記事を見て進めてみてください。

参考記事：

https://zenn.dev/tokku5552/articles/deployer-local

自分の方では、deploy.phpを以下のように実装しました。

```php
<?php

namespace Deployer;

require 'recipe/laravel.php';

// プロジェクト名（自分のプロジェクト名を設定）
set('application', 'laravel-zaikokanri');

// [Optional] gitcloneにttyを割り当てる。デフォルト値はfalse。
set('git_tty', false);

// デプロイ間の共有ファイル/ディレクトリ
add('shared_files', []);
add('shared_dirs', []);

// Webサーバーによる書き込み可能なディレクトリ
add('writable_dirs', []);
set('allow_anonymous_stats', false);

inventory('servers.yml');

// タスク
task('build', function () {
    ('cd {{release_path}} && build');
});

// [Optional] デプロイが失敗した場合、自動的にロックが解除される。
after('deploy:failed', 'deploy:unlock');

// シンボリックリンクの新しいリリースの前にデータベースを移行する。
before('deploy:symlink', 'artisan:migrate');

//release_pathに作業ディレクトリとして/backendを設定。（自身の作業ディレクトリを指定してください。）
after('deploy:update_code', 'set_release_path');
task('set_release_path', function () {
    $newReleasePath = get('release_path') . '/backend';
    set('release_path', $newReleasePath);
});

//deploy:infoの前にALBからインスタンスを外す。
before('deploy:info', 'deregister-targets');
task('deregister-targets', function () {
    runLocally('aws elbv2 deregister-targets --target-group-arn {{target_group_arn}} --targets Id={{instance_id}}');
});

//deploy:unlockの後に外したインスタンスをALBに戻す。
after('deploy:unlock', 'register-targets');
task('register-targets', function () {
    runLocally('aws elbv2 register-targets --target-group-arn {{target_group_arn}} --targets Id={{instance_id}},Port=80 ');
});

```

・AWSのアクセス情報等は別で用意したservers.ymlに記載して読み込むようにしました。

```
LaravelWeb1:
  hostname: ??.??.??.?? #デプロイするインスタンスのパブリックIPv4アドレス
  stage: LaravelWeb1　#自分で決めたステージ名
  user: ec2-user #デプロイを行うユーザー
  port: 22 #ssh接続でやりとりする場合は22
  identityFile: ~/.ssh/id_rsa #sshキーが保存されてる場所指定
  deploy_path: /var/www/ #アプリディレクトリをデプロイする場所指定
  branch: main #デプロイを実行するブランチ
  repository: https://github.com/????/?????.git #デプロイするアプリのリポジトリ
  target_group_arn: arn:aws:????? #ALBに登録してるターゲットグループ
  instance_id: i-????? #デプロイ先のインスタンス
  region: ap-northeast-1 #登録してるリージョン
```

### ポイント
・初めにrequireでlaravel.phpを読み込んでいますが、laravel.phpの内容は以下になります。
※requireは自身の環境のファイルだけでなく、外部のファイルも読み込めます。

https://github.com/deployphp/deployer/blob/6.x/recipe/laravel.php

・laravel.phpでは更にcommon.phpを読み込んでいて、common.phpの内容は以下になります。

https://github.com/deployphp/deployer/blob/6.x/recipe/deploy/prepare.php

・ALBの脱着を実行するようにしていますが、これについては下記の記事に詳しく説明が書かれているので見て見てください。
脱着は、本来はインスタンスを二台構成にしていて、かつAWSでALBを使って負荷分散している状況で、片方ずつデプロイやメンテナンスを行いたい場合にAWSCLIからターゲットグループを外すということをしています。
今回紹介している環境ではインスタンスは一台構成なので、必ず脱着を行う必要はありません。
好みで付け加えてもらえれば大丈夫です。

https://zenn.dev/tokku5552/articles/aws-target-group-desc

### 鍵の配置
デプロイ時にインスタンスにssh接続する為にはsshキーが必要になります。
インスタンス接続で使用しているsshキーをコンテナ上にも配置します。
以下のように手動で登録してもいいですが、これだとコンテナをビルドやらなんやらする度に一々手動でまた作成し直さないといけなくなってしまうので、Dockerfileにコピーコマンドを書き込んで、ビルドしても勝手に鍵が配置されるようにしておくのが一番適切な手段かなと思います。

```
root@a2b065cfb628:~/.ssh# touch id_rsa #鍵ファイル作成
root@a2b065cfb628:~/.ssh# vi id_rsa #鍵の中身を書き込み
root@a2b065cfb628:~/.ssh# cat id_rsa #登録できてるか確認
```

・ Dockerfile　でコピーさせる

```
RUN mkdir /root/.ssh #元々ある/root下に/.sshディレクトリを作成
COPY .ssh/ /root/.ssh/ #ローカルでDockerfileと場所に配置されてる鍵をコンテナ上の/.sshディレクトリにコピー
```

docker-compose buildでDockerfileの変更点を反映させようとしたのですが、ただビルドしただけでは鍵がコピーされなかったので、デスクトップのGUIのdockerアプリで一度コンテナをstopしたあとにゴミ箱ボタンでコンテナを削除してから再構築させました。

```
アプリディレクトリ $ docker-compose build #構築
アプリディレクトリ $ docker compose up -d　#起動
```

### 手動デプロイ
設定が完了したらコンテナから手動でデプロイしてみます。
`Successfully deployed!`と出ればデプロイ完了です！

```
#LaravelWeb1はservers.ymlで指定したstageになります。
root@a2b065cfb628:/work# ./vendor/bin/dep deploy LaravelWeb1 -vvv 
・
・
・
Successfully deployed!
```

・以下のようなエラーが出た場合。
 1.　`< aws: not found`でAWS CLIが使えない
 →　AWS CLIインストールで解決
docker-compose.ymlにAWS CLIlコンテナを作成するよう書き込み、docker-compose upで変更点を反映させます。

```
aws-cli-container:
    build: ./docker/php
    container_name: awscli-container
    volumes:
      - .:/workdir
    env_file:
      - ./backend/.env #.envが配置されてる場所
    environment:
      AWS_DEFAULT_REGION: ap-northeast-1
      AWS_DEFAULT_OUTPUT: json
```

docker-compose upしたら作成できてるかコンテナ上でバージョン確認してみます。
バージョンが表示されれば完了です。

 ```
root@e8563224ecd0:/work# aws --version
aws-cli/2.4.19 Python/3.8.8 Linux/4.19.121-linuxkit exe/x86_64.debian.10 prompt/off
```

2.AWSのリージョンが指定されてない

```
You must specify a region. You can also configure your region by running "aws confi  
  gure".
```

下記コマンドで設定すれば解決できます。
→これも先ほど同様Dockerfileに環境変数を書き込めば永続化できます。

```
$ aws configure
AWS Access Key ID [None]: xxxxxxxxxx
AWS Secret Access Key [None]: xxxxxxxxxx
Default region name [None]: ap-northeast-1
Default output format [None]: json
```

3.ディレクトリが存在していなく、アクセス権限が無い
デプロ先にあるはずのディレクトリが存在してないと発生します。

```
[LaravelWeb1] < mkdir: cannot create directory '/var/www/': Permission denied
```

この場合は、 エラーが出た存在してないディレクトリをEC2上に作成し、デプロイに使うユーザーがアクセスできるようにパーミッションを変更してあげれば解決できます。

```
[ec2-user@ip-????? var]$　chmod 777　www
```

4.gitが無い

```
fatal: could not read Username for 'https://github.com': No such device or address 
```

自分の場合はリモートリポジトリがprivateになっていたことが原因だったのでpublicに変更して解決しました。
privateのままデプロイさせたい場合は、https:// ではなくgit@ から始まるssh用のリモートリポジトリURLを使ってEC2上からクローンできるように設定する必要があります。

5.DBエラー

```
production.ERROR: Database (/var/www/releases/1/backend/database/data  
  base.sqlite) does not exist.
```

今回はMySQLを使っているのにsqliteのエラーが出ました。
この場合は、先ず.envとdatabase.phpでMySQLの設定ができているか確認してください。
設定できているのにこのエラーが出る場合は、/var/www/shared/下に.envを配置してあげれば解決できます。

### ドキュメントルート変更
deployerでは、deploy_pathの下にcurrent,releases,sharedというディレクトリが作成され、最新版は常にcurrentディレクトリに置かれることになります。なので、ドキュメントルートもcurrentの下を指すように修正する必要があります。

nginxの設定ファイルを以下のように書き換えます。
nginx設定ファイルのパスはこちらになります。

```
[ec2-user@ip-??????] $ /etc/nginx/nginx.conf
```

```
nginx.conf
    server {
        listen       80;
        listen       [::]:80;
        server_name  _;
        root          /var/www/current/自身のアプリディレクトリ/public;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;
```

以下コマンドで再起動と起動確認。

```
$ sudo service nginx restart #再起動
$ sudo systemctl status nginx.service #起動確認
```

補足になりますが、sharedディレクトリにはデプロイを行っても常に共有されるファイル(先程の.envファイルや、ログファイルなど)が置かれ、releasesディレクトリにはデプロイされたフォルダが順番に置かれ一定個数保持されます。(デフォルト5つ)

## CI/CD構築手順
では、手動デプロイができるようになったところで、次はいよいよCI/CD構築を進めていきます！

### 事前準備
EC2の環境設定等の為にターミナルからEC2上のアプリディレクトリを編集等されていた方は、変更点をプッシュし、ローカルのアプリディレクトリでプルを実行しておきましょう。
※プルしておかないとEC2上のアプリディレクトリとローカル上のアプリディレクトリとで異なる箇所が出てしまうので、リモートリポジトリとローカル上のアプリディレクトリを最新の状態にしておく必要があります。

### ワークフローの作成
GitHub Actionsでは、YMLファイルで定義されたワークフローを元に自動化された動作が実行されます。
なので、先ずはYMLファイルを作成してワークフローを定義していきます。

1.YMLファイルの作成と書き込み
YMLファイルにワークフローを定義することでCI/CDを動作させることができ、そのワークフローの雛形がGitHubのActions欄から作成できるので下記記事を参考に進めてみてください。
※`作成した.ymlファイルはリポジトリ直下の.github/workflowsに配置することでワークするようになります。`

https://techracho.bpsinc.jp/wingdoor/2020_07_03/93868

自分は今回以下のような内容でワークフローを定義させました。

```
#初めにCI（自動テスト）のワークフローを定義させます。

name: UnitTest

on:
  push:
    branches:
      - main #mainブランチにpushするとワークフローが作動

jobs:
  laravel-tests:

    name: phpunit test
    runs-on: ubuntu-latest
    
    #DB側（MySQLコンテナ）設定
    services:
      mysql: 
        image: mysql:8.0.19
        ports:
          - 3306:3306
        env:  
          MYSQL_ROOT_PASSWORD: ???????
          MYSQL_DATABASE: test_db
          MYSQL_USER: user
          MYSQL_PASSWORD: ?????
        options: --health-cmd "mysqladmin ping -h localhost" --health-interval 20s --health-timeout 10s --health-retries 10 #optionsでMySQLが起動したかヘルスチェックするようにします。
   
    #アプリ側（テスト用DB）設定
    env:
      DB_CONNECTION: mysql
      DB_HOST: db
      DB_PORT: 3306
      DB_DATABASE: test_db
      DB_USERNAME: user
      DB_PASSWORD: "password"
    
    steps:
      - uses: actions/checkout@v2
      - name: Setup PHP 7.4 　#phpのバージョン指定（しておかないと後々バージョンエラーになる）
        run: sudo update-alternatives --set php /usr/bin/php7.4
        
      - name: cache vendor 
        id: cache
        uses: actions/cache@v1
        with:
          path: ./vendor
          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-composer-
            
      - name: composer install
        shell: bash -xe {0} # custom shell
        if: steps.cache.outputs.cache-hit != 'true'
        run: cd backend && composer install -n --prefer-dist
        
      - name: composer dump autoload　#requireを使用せず、どこからでも対象のソースやクラスを呼び出せるようにする。
        shell: bash -xe {0} 
        run: cd backend && composer dump-autoload
        
      - name: generate key #アプリケーションキー作成
        shell: bash -xe {0} 
        run: cd backend && php artisan key:generate --env=testing
        
      - name: migrate #DB作成
        shell: bash -xe {0} 
        env:
          DB_HOST: 127.0.0.1
        run: cd backend && php artisan migrate --force
        
      - name: unit test #テスト実行
        shell: bash -xe {0} 
        env:
          DB_HOST: 127.0.0.1
        run: cd backend && ./vendor/bin/phpunit

  #ここから自動デプロイのワークフローを定義しています。
  deploy:
    name: deploy
    runs-on: ubuntu-latest
    needs: laravel-tests #自動テストが成功したらデプロイを実行するように。
    if: github.ref == 'refs/heads/main'

    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:
          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: cd backend && composer install -n --prefer-dist
        
      - name: install awscli #ALBの脱着を行うようにする為、AWS CLIを使います。
        run: | # AWS CLIインストール
          cd backend &&
          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 # ssh接続の為、sshキーをコピー
        run: | 
          cd backend &&
          mkdir -p /home/runner/.ssh
          touch /home/runner/.ssh/id_rsa
          echo "${{ secrets.SSH_DEPLOY_KEY }}" > /home/runner/.ssh/id_rsa
          chmod 600 /home/runner/.ssh/id_rsa     
          
      - name: add known hosts # known_hostsに追加
        run: ssh-keyscan 34.32.12.66 >> ~/.ssh/known_hosts
        
      - name: composer require deployer/deployer #depコマンドを叩けるようにする。
        run: cd backend && composer require deployer/deployer --dev
        
      - name: deploy #デプロイ実行
        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
        run: cd backend && ./vendor/bin/dep deploy LaravelWeb1 -vvv #LaravelWeb1はdeploy.phpで指定したstage。
```

### ポイント
・./vendorは毎回composer installしてると時間がかかるので、`name: cache vendor`部分でキャッシュを使うようにしています。

・`cd backend`で/backendディレクトリでコマンドを実行するように作業ディレクトリを指定しています。
これは、`working-directory: ./backend`としても指定可能です。

・AWS CLIを使うのにキーの設定が必要です。
こちらの記事が参考になるので進めてみてください。

https://zenn.dev/tokku5552/articles/laravel-github-actions

### デプロイ実行
今の設定だとmainブランチにプッシュ、又はマージされると自動テスト＆デプロイが実行されます。
GitHubで確認して下記のように表示されればCI/CDパイプラインの構築成功です！

![スクリーンショット 2022-03-10 21.42.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/601015/642c8f4f-cbaf-3d3c-e441-b9e806aff1d9.png)

## 参考記事

https://zenn.dev/tokku5552/articles/deployer-local

https://zenn.dev/tokku5552/articles/laravel-github-actions