Google CloudとCloud Runを使って効率的にデモ環境を作る

この記事は [一休.com Advent Calendar 2024](https://qiita.com/advent-calendar/2024/ikyu) の 24 日目の記事です。

一休の [RESZAIKOチーム](https://qiita.com/hamabek/items/f16e035d95b06982812a) で働いているmatsumuranです。
[昨年の記事](https://user-first.ikyu.co.jp/entry/2023/12/16/112705) ではCloud Runを利用したデモ環境の紹介をしましたが、デモ環境を増やすためには手動でGoogle Cloudやリポジトリ内の設定を操作する必要がありました。今年は昨年の心残りを解決しようと思います。

## デモ環境を簡単に作りたい

開発中のデモ環境は簡単に作成、更新、破棄できると嬉しいものです。

[Advent Calendar 6 日目の記事](https://zenn.dev/s_tokutake/articles/8b0512921d4b20) では Kubernetes を使ってデモ環境を作る方法を紹介しましたが、
一休では Google Cloud の [Cloud Run](https://cloud.google.com/run?hl=ja) を利用して提供しているサービスもあります。

Cloud Run は完全にゼロの状態からスケールするので使用していない時間は課金されず、常に使用している訳ではないデモ環境と相性がいいです。
本記事では、[Cloud Load Balancing](https://cloud.google.com/load-balancing?hl=ja) と Cloud Run を使用したデモ環境の作成方法について紹介します。

## やりたいこと
1. IDを指定すると、新規デモ環境が自動で作成される
1. 不要になったデモ環境のIDを指定すると、自動で削除される

## 各環境の ID

デモ環境を並列にいくつも作りたいので、自由な ID を設定できるようにします。
ID 名は `demo-xxxxx` のような形式にして、 `https://demo-xxxxx.ikyu-demo.example.com/` のように、ドメインで環境を分ける事にします。

## GitHub Actions によるデプロイ

通常のデプロイを GitHub Actions で行っているプロジェクトであれば、デモ環境の構築も GitHub Actions でやるようにすれば構成が楽になります。
という訳で、Google Cloud を操作できる `gcloud` のコマンドを駆使して環境の構築、破棄をどんどん行うスクリプトを作成します。
一度 action を定義すれば、特定の PR のタイミングで実行する事も可能ですし、外部から API で呼び出すこともできます。
以下のような `workflow_call` を定義した Workflow を定義します。

```yaml:api-server.demo.deploy.yaml
name: api-server.demo.deploy

on:
  workflow_call:
    demo-id:
      required: true
      type: string

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        
      - name: Dockerの設定
        run: gcloud auth configure-docker asia-northeast1-docker.pkg.dev --quiet
        
      - name: DockerをビルドしてArtifact RegistryにPush
        uses: docker/build-push-action@v6
        with:
          file: ./Dockerfile
          push: true
          
      - name: Cloud Runにデプロイ
        run: |
          gcloud run services replace settings/api-server-demo.yaml
          
      - ...以下省略
```

## Cloud Run のデプロイ

今回は省略しますが、前提として、 Docker でビルドしたイメージを [Artifact Registry](https://cloud.google.com/artifact-registry/docs?hl=ja) に push できているものとします。
Cloud Run は構成を yaml で記述できるので、以下のようにデモ環境用の設定を作成します。

```yaml:api-server-demo.yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: api-server-demo-<demo-id>
  labels:
    cloud.googleapis.com/location: asia-northeast1
spec:
  template:
    metadata:
      name: api-server-demo-<demo-id>-v<Version>
      annotations:
        run.googleapis.com/vpc-access-egress: all-traffic
        autoscaling.knative.dev/minScale: "0"
    spec:
      containers:
        - image: asia-northeast1-docker.pkg.dev/demo/test/api-server-demo-<demo-id>:V<Version>
          name: api-server
          args:
            - "yarn"
            - "server:demo"
          ports:
            - name: http1
              containerPort: 80
```

`api-server-demo-<demo-id>:V<Version>` というイメージを使って、 `api-server-demo-<demo-id>` という名前のサービスを作る事になります。
`<demo-id>` や `<Version>` 等のデプロイ毎に変わる値については、デプロイ時に sed で強制的に書き換えています。

以下のコマンドでサービスを作成できます。

```shell
gcloud run services replace api-server-demo.yaml
```

`replace` を使う事で、サービスの作成と更新の両方に対応します。

新規作成時にはサービスはデフォルトで認証ユーザしかアクセスできないようになっているので、認証を入れない場合は以下のコマンドで未認証の呼び出しを許可するようにしてください。

```shell
gcloud run services add-iam-policy-binding api-server-demo-${demo-id} \
    --member="allUsers" \
    --role="roles/run.invoker"
```

## Cloud Load Balancing によるルーティング

1 つのサーバで全てをやりくりするならこれで大丈夫ですが、
`demo-xxxxx.demo-ikyu.example.com` 等のドメインと各デモ環境の対応や、パスによって接続先を振り分けするために CLB のグローバル外部アプリケーション ロードバランサを使ってルーティングを行います。
ロードバランサは大まかにいって以下の 3 層に分かれます。

- 外部からのアクセスを受け付けるターゲットプロキシ
- Cloud Run 等のサービスへ繋がるバックエンド
- ドメインやパスなどから接続先のバックエンドを振り分けるルーティングルールとなる URL マップ

ここでは、作成済みのロードバランサにデモ環境毎の要素を追加する方法について説明します。

### ターゲットプロキシ

[ターゲットプロキシ](https://cloud.google.com/load-balancing/docs/https?hl=ja#target-proxies) を作成すると IP を割り振ってくれて、外部からの HTTPS リクエストを受け付けます。
DNSで `*.demo-ikyu.example.com` がこの IP を指すようにすれば、以降はデモ環境毎の設定をする必要なくターゲットプロキシが利用できます。
[Google マネージドの証明書](https://cloud.google.com/load-balancing/docs/ssl-certificates/google-managed-certs?hl=ja) を利用している場合は自動で証明書を作成してくれますが、ワイルドカードの証明書が利用できませんし、発行まで数分の時間がかかります。
ロードバランサで証明書を設定する場合は、[Certificate Manager](https://cloud.google.com/blog/ja/products/identity-security/simplify-saas-scale-tls-certificate-management) を使えばワイルドカードの証明書を設定できるのでそちらを利用してください。

### バックエンド

LB から Cloud Run に接続するためには、Cloud Run のサービス毎に以下の 2 つのリソースを作成する必要があります。
[公式の手順](https://cloud.google.com/load-balancing/docs/https/setting-up-reg-ext-https-serverless?hl=ja#configure_permissions) に従って作成していきます。
VPC ネットワークについては既にある物を利用する事にします。

1. サーバレス NEG の作成
2. バックエンドサービスの作成

#### NEG の作成

作成した `api-server-demo-xxxxx` という名前の Cloud Run サービスに接続する NEG を作成するためのコマンドです。
NEG の名前は `neg-api-server-demo-xxxxx` にします。

```bash
gcloud compute network-endpoint-groups create neg-api-server-demo-xxxxx \
        --region=asia-northeast1 \
        --network-endpoint-type=serverless  \
        --cloud-run-service=api-server-demo-xxxxx
```

#### バックエンドサービスの作成

次に LB のバックエンドサービスを作成します。
`backend-api-server-demo-xxxxx` という名前にします。

```bash
gcloud compute backend-services create backend-api-server-demo-xxxxx \
        --load-balancing-scheme=EXTERNAL_MANAGED \
        --protocol=HTTP \
        --region=asia-northeast1
```

先程作成した NEG を紐付けます。

```bash
gcloud compute backend-services add-backend backend-api-server-demo-xxxxx  \
        --region=northeast1 \
        --network-endpoint-group=neg-api-server-demo-xxxxx \
        --network-endpoint-group-region=northeast1
```

### URL マップの作成

HTTP リクエストを条件に応じてバックエンドサービスに振り分けるために、[URL マップ](https://cloud.google.com/load-balancing/docs/url-map?hl=ja#configure_url_maps) を作成します。

```shell
gcloud compute url-maps create demo-url-map \
   --default-service=demo-frontend
```

コンソールから LB を作成した場合は自動で紐つきますが、gcloud で作成した場合はターゲットプロキシと紐付ける必要があります。

```shell
gcloud compute target-https-proxies update target-proxy-demo \
   --url-map=demo-url-map
```

URL マップはホストルールとパスマッチャーの 2 種類の要素があります。

今回は、以下のようなケースを想定してパスマッチャーを作ります。

1. APIサーバへのリクエストは `/api/*` に集約して、バックエンドサーバに振り分ける
1. それ以外のパスへのリクエストはフロントエンドサーバに振り分ける

`https://demo-xxxxx.ikyu-demo.example.com/` に来たアクセスの内、 `/api/*` を `api-server-demo-xxxxx` に、それ以外を `react-server-demo-xxxxx` に振り分けるようにします。

#### パスマッチャーの作成

パスマッチャーを作成します。
デフォルトでは `backend-react-server-demo-xxxxx` というバックエンドサービス、 `/api/*` にアクセスが来たときは `backend-api-server-demo-xxxxx` というバックエンドサービスにリクエストを振り分けます。

```shell
gcloud compute url-maps add-path-matcher demo-url-map \
   --path-matcher-name demo-xxxxx \
   --default-service=backend-react-server-demo-xxxxx
   --path-rules="/api/*=backend-api-server-demo-xxxxx"
```

#### ホストルールの作成

アクセスが来たときのホスト名( `demo-xxxxx.ikyu-demo.example.com` ) に応じて処理を行います。
今回は、先程作成した `demo-url-map` を使うように設定します。

```shell
gcloud compute url-maps add-host-rule demo-xxxxx \
    --hosts=[demo-xxxxx.ikyu-demo.example.com] --path-matcher-name=demo-url-map
```

## アクセス元の制限等

以上でデモ環境が作成でき、指定した URL でアクセスできるようになりました。
ただし、特に制御を入れていないので全世界からアクセスできてしまいます。
接続元 IP 等を制限したい場合は、 Cloud Armor 等で適宜制御を追加してください。

## 削除について

環境を削除する場合、今回追加したそれぞれの要素を削除していけば大丈夫です。

```shell
# ホストルールの削除
gcloud compute url-maps remove-host-rule demo-xxxxx \
    --host=demo-xxxxx.ikyu-demo.example.com

# バックエンドサービスの削除
gcloud compute backend-services delete backend-api-server-demo-xxxxx \
   --region=asia-northeast1

# NEGの削除
gcloud compute network-endpoint-groups delete neg-api-server-demo-xxxxx \
   --region=asia-northeast1

# Cloud Runの削除
gcloud run services delete api-server-demo-xxxxx \
   --region=asia-northeast1
```

## 感想

EKSのようにファイル1つで全ての設定が完了せずコマンドでいろいろ操作する必要はあるものの、環境IDを決めておけば一括で操作できるようにはなっています。

現在のプロジェクトでは monorepo を採用しているので、フロントエンドサーバとバックエンドサーバのバージョンが異なる事などを気にしなくていいですし、
DB を [Prisma](https://www.prisma.io/) で構成していてスキーマを開発リポジトリ内で管理しているので、デモ用の DB の構築が非常に楽でした。
monorepo 自体には色々メリットデメリットがあると思いますが、今回のケースでは上手くかみ合って非常に良い開発体験でした。

## おわりに

一休では、エンジニアを募集しています。

https://www.ikyu.co.jp/recruit/engineer/

カジュアル面談も実施しているので、お気軽にご応募ください。

https://hrmos.co/pages/ikyu/jobs/1745000651779629061