GitHub + GCP＝CI/CDとかいれたサービス（の構成）

# はじめに
株式会社トラストバンクでサービス開発部に所属してます湊（みなと）（@karura618）です。
この記事は [トラストバンクAdvent Calender 2023](https://qiita.com/advent-calendar/2023/trustbank) の記事です。

今回はGitHub + GCPのCloudBuild、ArtifactRegistry、CloudRunを使用してCI/CDを導入したミニマムなサービスを作ってみました。
各サービスの設定等はほんとにシンプルなので今回は割愛し、実際に作成した構成ファイルの解説をしたいと思います。

# 各サービス概要
長いのでトグルに閉じてます。
<details>
<summary>
GitHub
</summary>
ご存じプログラムのソースコードの共有、管理するサービス。
後述のCloudBuildと連携させてブランチ名によってユニットテストだけを走らせたり、デプロイまで実施したりなどを制御します。

[https://github.co.jp/](https://github.co.jp/)
</details>

<details>
<summary>
CloudBuild
</summary>
CI/CDを簡単にできるGCPのサービスです。
トリガー作成時に上記GitHubのリポジトリと連携することでCI/CDを簡単に導入できます。
連携したGitHubのリポジトリ内に「cloudbuild.yaml」というCI/CDの構成ファイルを配置しておくことでプッシュ時等にて「cloudbuild.yaml」の処理を実行します。

また、トリガー上で「代入変数」の定義が可能。
定義した代入変数は「cloudbuild.yaml」内に受け渡しが可能なのでイメージやサービス名、リージョン等、公開するサービスによって調整したい値などはこちらを使うと便利。

ちなみに、CloudBuildからGitHubのリポジトリに連携する場合、トリガー作成時のGitHubのアカウントと紐づくので個人アカウントで作成する場合は注意が必要。
（うっかりアカウントを削除すると動かなくなる。）
また、連携時のGitHubアカウントは対象リポジトリに対してAdmin権限が付与されている必要があります。

なお、今回は使用してませんが「cloudbuild.yaml」にて [シークレットマネージャー](https://cloud.google.com/secret-manager?hl=ja) を参照することも可能なのでパスワードや証明書などセキュアな情報はそちらで管理することをお勧めします。

 [https://cloud.google.com/build?hl=ja](https://cloud.google.com/build?hl=ja)

</details>

<details>
<summary>
ArtifactRegistry
</summary>
AWSでいうECR。
コンテナイメージの保存とアクセスに使用されるリポジトリ (またはリポジトリのコレクション) です。

2年くらい前まではContainerRegistryが主流でしたが現在は非推奨となっておりContainerRegistryで作成したイメージもArtifactRegistryへの移行が推奨されています。

ContainerRegistryの時は事前にリポジトリを用意する必要がなかったのですが、ArtifactRegistryの場合はどうもCloudBuildを走らせる前に事前に保管先のリポジトリを用意しておく必要があるらしく、個人的にちょっと手間が増えた印象。。。

[https://cloud.google.com/artifact-registry?hl=ja](https://cloud.google.com/artifact-registry?hl=ja)
</details>

<details>
<summary>
CloudRun
</summary>
コンテナを自動的にスケールするフルマネージド型のコンピューティング プラットフォーム。
サーバレスなためデプロイするだけでインフラ知識なしでサービスを構築できます。

~~ただし、Dockerfileの用意が必要なのと、一つのコンテナの中でアプリ、Webサーバを構成する必要がある
（例：PHPとNginxを一つのコンテナの中にインストール必要がある。）
そのためDockerfileの構成が若干面倒というかdocker composeとかでアプリとWEBを明確に分けて構成しているのに慣れているとやや違和感はあるけどもなれるとそこまで気にならない。~~
と思ったら [マルチコンテナにも対応した様子。](https://cloud.google.com/blog/ja/products/serverless/cloud-run-now-supports-multi-container-deployments) 

オートスケールも対応しており [CPU 使用率が 60% を超過した場合にのみスケールアウトします。](https://cloud.google.com/run/docs/configuring/cpu-allocation?hl=ja#choosing-scaling)

[https://cloud.google.com/run?hl=ja](https://cloud.google.com/run?hl=ja)
</details>

# 構成イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308142/5fb150fd-3c24-c28f-e434-b2e69d3a7a72.png)




# ファイル構成
PHP（Laravel）を使用した構成。

```
C:
│  artisan
│  cloudbuild.yaml // CloudBuild構成ファイル（CloudRunへのデプロイ用）
│  cloudbuildtest.yaml // CloudBuild構成ファイル（作業ブランチへのプッシュ時にユニットテストを実施）
│  composer.json
│  composer.lock
│  docker-compose.yml
│  phpstan.neon // 静的解析ツールの設定ファイル（今回は説明割愛）
│  phpstan_ergebnis.neon // 静的解析ツールの拡張設定ファイル（今回は説明割愛）
│  phpunit.xml
│  ruleset.xml // lint、フォーマッターの設定ファイル（今回は説明割愛）
│  
├─app
│          
├─docker
│  │  docker-php-ext-xdebug.ini
│  │  Dockerfile // cloudbuild.yamlを同じ階層に置かない場合、ちょっと対応が必要（後述）
│  │  entrypoint.sh
│  │  php-fpm.conf
│  │  php.ini
│  │  startup.sh
│  │  
│  ├─nginx
│  │      nginx.conf
│  │      php.conf
│  │      
│  └─php-fpm.d
│        www.conf
│
│          
├─public
│          
├─resources
│                  
├─routes
│  │  api.php
│  │  channels.php
│  │  console.php
│  └─web.php
│          
├─tests
│  │  CreatesApplication.php
│  │  TestCase.php
│  │  
│  ├─Feature
│  │      ExampleTest.php
│  │      
│  └─Unit
│          ExampleTest.php
│          
└─vendor

```

## cloudbuild.yaml

```yaml
steps:
  # Build the container image
  # 以下はCloudBuildの代入変数への登録が必須な項目です。（先頭の「$」は除外して登録）
  # $_IMAGE_NAME : ArtifactRegistryに登録されるイメージ名及び、CloudRunのサービス名称
  # $_REGION_NAME : ArtifactRegistryの保管場所となるリージョン指定。デフォルトは「asia-northeast1」にしているので特に理由がなければ設定不要
  # $_DOCKERFILE_MULTI_STAGE_NAME : ビルド時に使用するDockerfile内のマルチステージ名を指定。テスト環境であれば「development」。本番は「production」
  # CloudBuildで使用するDockerfileはデフォルトだとcloudbuild.yamlと同一階層に配置する必要がある。
  # 管理上の理由等によりサブディレクトリ内にDockerfileを配置したい場合は「-f」オプションを使用してファイルパスを記述する必要がある。
  # CloudBuild内のビルド時に使用するディレクトリは「/workspace」となるため以下のように「/workspace/docker/Dockerfile」を指定することで対応可能。

  # （１）docker/Dockerfileを使用してビルドを実施。「-t」オプションなしの場合はcloudbuild.yamlと同じ階層にあるDockerfileを使用
  # Docker 17.05からマルチステージビルドにも対応しており「--target」でステージの指定が可能。
  - name: 'gcr.io/cloud-builders/docker'
    args:
      - 'build'
      - '-t'
      - '${_REGION_NAME}-docker.pkg.dev/$PROJECT_ID/$REPO_NAME/$_IMAGE_NAME:latest'
      - '-f'
      - '/workspace/docker/Dockerfile'
      - '--network=cloudbuild'
      - '--target=$_DOCKERFILE_MULTI_STAGE_NAME'
      - '.'
    id: build-container-image

  # （２）（１）でビルドしたイメージをArtifactRegistryへ格納（プッシュ）する。
  # 実施前にArtifactRegistry側で対象のリポジトリを作成する必要がある。
  - name: 'gcr.io/cloud-builders/docker'
    args: ['push', '${_REGION_NAME}-docker.pkg.dev/$PROJECT_ID/$REPO_NAME/$_IMAGE_NAME:latest']

  # （３）（２）でプッシュしたイメージをもとにCloudRunへのデプロイを実施する。
  # イメージ名をそのままCloudRunのサービス名として使用。
  - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
    entrypoint: gcloud
    args:
      - 'run'
      - 'deploy'
      - '$_IMAGE_NAME'
      - '--project'
      - $PROJECT_ID
      - '--image'
      - '${_REGION_NAME}-docker.pkg.dev/$PROJECT_ID/$REPO_NAME/$_IMAGE_NAME:latest'
      - '--region'
      - '$_REGION_NAME'
      - '--platform'
      - 'managed'
images:
  - '${_REGION_NAME}-docker.pkg.dev/$PROJECT_ID/$REPO_NAME/$_IMAGE_NAME:latest'
substitutions:
  # ArtifactRegistryに作成したリポジトリのリージョンを指定。
  # 特に理由がなければ東京リージョンを使用するように初期値をセット
  # CloudBuildのトリガーで初期値がセットされていれば指定したリージョンを使用。
  _REGION_NAME: "asia-northeast1"
  # ビルド時のマルチステージを指定。デフォルトは「development」
  _DOCKERFILE_MULTI_STAGE_NAME: "development"
# ビルド時のタイムアウト値をセット。（デフォルトは10分）
timeout: 1800s
# 今回は実装を省いていますがトリガーの履歴に対して以下のような独自タグをつけておくとPUB/SUBを使用したSlack通知とかも実装しやすいかもです。
tags: ['slack-notifier', $TRIGGER_NAME, $SHORT_SHA]

```

### ポイント
* 汎用的な設定ファイルを構築したかったので[CloudBuildでデフォルトで用意されている変数](https://cloud.google.com/build/docs/configuring-builds/substitute-variable-values?hl=ja)を使用しています。
* デフォルトの変数で足りない箇所やデフォルトの変数が使えない箇所（※）については極力「substitutions」でデフォルト値を指定した上で個別に定義。
* ArtifactRegistryのイメージ名とCloudRunのサービス名はサービスごとに個別設定が必要になるのでデフォルト値は用意せずトリガーの代入変数で登録。
特に名称を分ける必要もなかったので「ArtifactRegistryのイメージ名」＝「CloudRunのサービス名」としています。
* Dockerfileを含むDocker周りの設定ファイルは管理の都合上すべて「/dokcer」以下に配置したかったのでビルド時にDockerfileの場所を明示的に指定するよう「-f」オプションを追加。

※CloudBuildでは「LOCATION」というデフォルトの変数があるので一見すると「_REGION_NAME」の代わりに利用できそうですが、CloudBuildのデフォルトのリージョンはArtifactRegistryには存在しない「global」となるため今回は使用していません。

## cloudbuildtest.yaml

```yaml
steps:
  # Build the container image
  - name: 'gcr.io/cloud-builders/docker'
    args:
      - 'build'
      - '-t'
      - '${_REGION_NAME}-docker.pkg.dev/$PROJECT_ID/$REPO_NAME/unittest:$COMMIT_SHA'
      - '--build-arg=_UNIT_TEST_ENV=$_UNIT_TEST_ENV'
      - '-f'
      - '/workspace/docker/Dockerfile'
      - '--network=cloudbuild'
      - '--target=$_DOCKERFILE_MULTI_STAGE_NAME'
      - '.'
  # MySQL（CloudSQL）を使用したユニットテストを実施する場合、CloudSQL用のProxyを用意しておく。
  # CloudSql Proxy Standby
  #   - name: gcr.io/cloud-builders/docker
  #     args:
  #       - '-c'
  #       - >-
  #         docker run --network=cloudbuild
  #         -d -v /cloudsql:/cloudsql
  #         gcr.io/cloudsql-docker/gce-proxy:1.16 /cloud_sql_proxy -dir=/cloudsql
  #         -instances=$PROJECT_ID:${_REGION_NAME}:${_CLOUD_SQL_INSTANCE_NAME}
  #     id: proxy-cloud-sql
  #     entrypoint: bash

  # Run phpstan
  - name: gcr.io/cloud-builders/docker
    args:
      - '-c'
      - >-
        docker run --network=cloudbuild
        ${_REGION_NAME}-docker.pkg.dev/$PROJECT_ID/$REPO_NAME/unittest:$COMMIT_SHA sh -c "cd /var/www/html&&composer phpstan"
    id: run-phpstan
    entrypoint: bash


  # Run phpstan
  - name: gcr.io/cloud-builders/docker
    args:
      - '-c'
      - >-
        docker run --network=cloudbuild
        ${_REGION_NAME}-docker.pkg.dev/$PROJECT_ID/$REPO_NAME/unittest:$COMMIT_SHA sh -c "cd /var/www/html&&composer lint ./"
    id: run-lint
    entrypoint: bash

  # Run UnitTest
  - name: gcr.io/cloud-builders/docker
    args:
      - '-c'
      - >-
        docker run --network=cloudbuild
        ${_REGION_NAME}-docker.pkg.dev/$PROJECT_ID/$REPO_NAME/unittest:$COMMIT_SHA sh -c "cd /var/www/html&&php artisan test"
  #     MySQL（CloudSQL）を使用したユニットテストを実行する場合、以下のようにあらかじめ作成したProxyをコンテナ内にマウントしてテストを実施する。
  #     docker run -v /cloudsql:/cloudsql --network=cloudbuild
  #     ${_REGION_NAME}-docker.pkg.dev/$PROJECT_ID/$REPO_NAME/unittest:$COMMIT_SHA sh -c "cd /var/www/html&&php artisan test"
    id: run-unittest
  #   waitFor:
  #     - proxy-cloud-sql
    entrypoint: bash

images:
  - '${_REGION_NAME}-docker.pkg.dev/$PROJECT_ID/$REPO_NAME/unittest:$COMMIT_SHA'

substitutions:
  # ArtifactRegistryに作成したリポジトリのリージョンを指定。
  # 特に理由がなければ東京リージョンを使用するように初期値をセット
  # CloudBuildのトリガーで初期値がセットされていれば指定したリージョンを使用。
  _REGION_NAME: "asia-northeast1"
  # ユニットテスト実行時に必要な環境変数を指定（改行で複数指定可能）
  _UNIT_TEST_ENV: ""
  # ビルド時のマルチステージを指定。デフォルトは「development」
  _DOCKERFILE_MULTI_STAGE_NAME: "development"
  # _CLOUD_SQL_INSTANCE_NAME: $_CLOUD_SQL_INSTANCE
timeout: 1800s
# 今回は実装を省いていますがトリガーの履歴に対して以下のような独自タグをつけておくとPUB/SUBを使用したSlack通知とかも実装しやすいかもです。
tags: ['slack-notifier', $TRIGGER_NAME, $SHORT_SHA]

```

### ポイント
* コンテナビルド時に「--build-arg」を使用してCloudBuildで設定した「_UNIT_TEST_ENV」の値をDockerfile内に渡しています。後述のDockerfile内でこの値を.env.testingファイルとしてコンテナ内に出力することでユニットテストに必要な環境変数をセットしています。
（ユニットテスト用の環境変数なのでこの手法取っていますが本番環境で必要な環境変数をこの手法で配置するのはセキュアじゃないのでやめましょう；）
* 今回はコメントアウトしていますがユニットテスト時にDB（CloudSQL）を使用する場合は[Proxyを使用した接続が必要です](https://cloud.google.com/sql/docs/mysql/connect-build?hl=ja#connect_to)。
そのため、まずProxy用のコンテナを立ち上げた後、ユニットテストを実施するコンテナにマウントをしています。
* phpstan、lint、ユニットテストを実行するため各ステップで「-c」オプションを使用してコンテナ起動⇒composer、artisanを実施しています。また、コンテナ内の初期ディレクトリは「workspace」となるため「cd」でcomposer、artisanがある階層に移動しています。

## Dockerfile
```dockerfile
FROM php:fpm-alpine3.15 as base

WORKDIR /usr/src/php/ext
RUN set -eux; \
	mkdir -p "$PHP_INI_DIR/conf.d"

# apkを最新に更新（セキュリティーアップデート含む）
RUN apk -U upgrade
RUN apk add autoconf
RUN apk add build-base

RUN set -x \
&& apk update \
&& docker-php-source extract \
&& apk add --no-cache git \
                    vim \
                    wget \
                    bash \
                    perl \
                    automake \
                    cmake \
                    build-base \
                    nginx \
                    libmcrypt-dev \
                    libpng-dev \
                    libxml2-dev \
                    freetype-dev \
                    libjpeg-turbo-dev \
                    unifont \
                    freetype \
                    harfbuzz \
                    ca-certificates \
                    ttf-freefont \
                    pcre-dev \
                    curl \
                    lsof \
                    zip \
                    rsync \
                    sudo

# timezone
RUN apk add --no-cache tzdata\
    && cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \
    && echo "Asia/Tokyo" > /etc/timezone

RUN echo "www-data ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers \
  && echo 'www-data:www-data' | chpasswd

RUN git clone https://github.com/phpredis/phpredis.git redis \
&& docker-php-ext-install redis \
&& docker-php-ext-install mysqli \
&& docker-php-ext-install opcache \
&& docker-php-ext-install pdo \
&& docker-php-ext-install pdo_mysql \
&& docker-php-ext-install bcmath \
&& docker-php-ext-configure gd --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd

# DockerHubにあるcomposerの公式イメージからcomposerをコピー
# https://hub.docker.com/_/composer
# 公式且つ、常に最新のcomposerを使用できるため、運用管理が楽。
# （以下issueでも推奨されている）
# https://github.com/docker-library/php/issues/344#issuecomment-364843883
COPY --from=composer /usr/bin/composer /usr/bin/composer

# PHPのライブラリリポジトリ（https://packagist.org）の日本用ミラーサイト（https://packagist.jp。さくらインターネット上にあるとのこと）をcomposerで使用するように設定。
# 本家がフランスにあるためそのままcomposerを走らせると通信に時間がかかるため設定。
RUN composer config -g repos.packagist composer https://packagist.jp
ENV COMPOSER_ALLOW_SUPERUSER 1
ENV COMPOSER_HOME /composer
ENV PATH $PATH:/composer/vendor/bin

COPY --chown=www-data:www-data ./ /var/www/html
RUN chmod 777 -R /var/www/html \
&& chmod 777 -R /var/www/html/storage \
&& chown -R www-data:www-data /var/www/html

COPY docker/php.ini /usr/local/etc/php/php.ini
COPY docker/php-fpm.conf /usr/local/etc/php-fpm.conf
COPY docker/php-fpm.d/www.conf /usr/local/etc/php-fpm.d/www.conf
RUN chmod 644 /usr/local/etc/php/php.ini \
&& chmod 644 /usr/local/etc/php-fpm.conf \
&& chmod 644 /usr/local/etc/php-fpm.d/www.conf \
&& mkdir -p /var/log/laravel \
&& chmod 777 -R /var/log/laravel \
&& chown -R www-data:www-data /var/log/laravel

ENV HOSTNAME 0.0.0.0

COPY docker/nginx/php.conf /etc/nginx/conf.d/php.conf
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
RUN mkdir -p /app/docker
RUN mkdir -p /var/log/php-fpm
COPY docker/startup.sh /app/docker/startup.sh
RUN chmod 777 -R /app/docker \
&& chmod 777 -R /var/log/php-fpm \
&& chown www-data:www-data /app/docker \
&& chown www-data:www-data /app/docker

RUN chown -R www-data:www-data /var/lib/nginx

FROM base as local
EXPOSE 8080 9003
ENV PHP_IDE_CONFIG "serverName=host.docker.internal"
ENV XDEBUG_CONFIG "idekey=PHPSTORM"

RUN pecl install xdebug-3.1.6 \
 && docker-php-ext-enable xdebug
COPY docker/docker-php-ext-xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
RUN chmod 644 /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
RUN touch /tmp/xdebug.log
RUN chmod 0777 /tmp/xdebug.log

# requwie-devのcomposerをインストール
WORKDIR /var/www/html
RUN composer install --dev
RUN composer dumpautoload --dev
RUN php artisan config:clear
RUN php artisan route:clear
#RUN php artisan test

CMD ["sh", "/app/docker/startup.sh"]


FROM base as development
EXPOSE 8080
## ユニットテスト実施に必要なenvファイルを配置。envファイルの中身はCloudBuildのトリガー内で定義
RUN echo "${_UNIT_TEST_ENV}" >> /var/www/html/.env.testing

# requwie-devのcomposerをインストール
WORKDIR /var/www/html
RUN composer install --dev
RUN composer dumpautoload --dev
RUN php artisan config:clear
RUN php artisan route:clear
#RUN php artisan test

CMD ["sh", "/app/docker/startup.sh"]

FROM base as production
EXPOSE 8080
WORKDIR /var/www/html
RUN composer install --optimize-autoloader --no-dev
RUN composer dumpautoload --no-dev
RUN php artisan config:clear
RUN php artisan route:clear

RUN set -ex; \
  { \
    echo "; Cloud Run enforces memory & timeouts"; \
    echo "memory_limit = -1"; \
    echo "max_execution_time = 0"; \
    echo "; File upload at Cloud Run network limit"; \
    echo "upload_max_filesize = 500M"; \
    echo "post_max_size = 500M"; \
    echo "; Configure Opcache for Containers"; \
    echo "opcache.enable = On"; \
    echo "opcache.validate_timestamps = Off"; \
    echo "; Configure Opcache Memory (Application-specific)"; \
    echo "opcache.memory_consumption = 32"; \
  } > "$PHP_INI_DIR/conf.d/cloud-run.ini"

CMD ["sh", "/app/docker/startup.sh"]

```

### ポイント
* 「development」でcloudbuildtest.yamlにて設定したユニットテスト用の引数「_UNIT_TEST_ENV」を「/var/www/html/.env.testing」に出力しています。こうすることでartisanでユニットテストを走らせるときに必要な環境変数を用意しています。
(.gitignoreで.env.testingを除外していないならそもそも不要な対応です。)
* 「production」で「opcache」を使用することでレスポンス速度を向上。ローカル環境で有効にするとソースの反映がされないので本番のみ有効化しています。

# おわりに
久々にGCPでマイクロサービス作ってみましたがマルチステージが使えるようになって大分使いやすくなりましたね！
（以前はテスト用と本番用でDockerfileを分けていた。。。）
今回は省いちゃいましたがGCPの各サービスのUIもシンプルで非常に使いやすいので是非一度お試しいただければ幸いです！

# エンジニア募集
弊社では絶賛エンジニア募集中ですので、気になった方は是非ご連絡ください！

https://www.wantedly.com/companies/trustbank/projects