AppEngine の外向き通信 IP アドレスを固定化したかっただけなのに

社内ツールを新たに作る際、認証機構などを省略したり気軽にホスティングしたい、NuxtJS を使いたいなどの理由に加え、NuxtJS から IP アドレスで制限された API サービスに通信するという要件があり、何らかのコンピューティングリソースからの外向き通信の IP アドレスを固定化させる必要が発生しました。そのため、AppEngine を採用することとし、Cloud NAT を使った外向き IP の固定化を試みました。

結果、対応できたのですが、GCP のドキュメントの通りに進めてもうまくいかない箇所が多々あり、ここに供養しようと思います。

# 構成図

```mermaid
flowchart LR
    subgraph VPC["VPC - 標準環境"]
        AE["App Engine"]
        NAT["Cloud NAT"]
        EIP["予約した静的IP"]
        
        AE --> NAT
        EIP --> NAT
    end
    
    Client["Client"] --> AE
    NAT --> Server["外部サーバー"]
```

App Engine から出ていく通信については Cloud NAT を経由し、予約した静的 IP からのリクエストが外部サーバーに飛ぶ、というイメージです。そのために、 VPC とコネクターを用意して、 App Engine はそのコネクターを利用する、という構成です。

早速やっていきます。

# App Engine（最大の難関）

まずは、雑に App Engine のインスタンスを建てようとします。
チュートリアルもあるので **簡単だと思っていました** 。

https://cloud.google.com/appengine/docs/standard/nodejs/building-app?hl=ja

プロジェクトはすでに構築済みなので、 express で固定の文字列を返すサーバーを立てるところから始めます。

https://cloud.google.com/appengine/docs/standard/nodejs/building-app/writing-web-service?hl=ja

これを参考に、雑にアプリを組みます。

app.yaml は、

```yaml
runtime: nodejs20
```

だけ書いて、 `gcloud deploy` を実行します。

すると、こんなエラーが発生してしまいました。

```
Updating service [default]...failed.                                    


ERROR: (gcloud.app.deploy)
Error Response: [13] Failed to create cloud build:
com.google.net.rpc3.client.RpcClientException:
<eye3 title='/ArgoAdminNoCloudAudit.CreateBuild, FAILED_PRECONDITION'/> APPLICATION_ERROR;
google.devtools.cloudbuild.v1/ArgoAdminNoCloudAudit.CreateBuild;
invalid bucket "staging.[PROJECT_ID].appspot.com";
service account [PROJECT_ID]@appspot.gserviceaccount.com does not have access to the bucket;
AppErrorCode=9;
StartTimeMs=1739500964301;
unknown;
ResFormat=uncompressed;
ServerTimeSec=0.715281377;
LogBytes=256;
Non-FailFast;
EndUserCredsRequested;
EffSecLevel=privacy_and_integrity;
ReqFormat=uncompressed;
ReqID=xxxxxxxx;
GlobalID=0;
Server=[xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]:4001.
```

※長いので適宜改行しています。

公式の手順に則っているのですが、どうも権限が足りないようです。

ということで、ググったところ、以下の GCP コミュニティへの投稿が目に止まりました。

https://www.googlecloudcommunity.com/gc/Serverless/Error-during-gcloud-app-deploy-for-GAE-app-quot-Failed-to-create/m-p/778088

ということで、どうもデフォルトで生成されるサービスアカウントの権限が不足しているようです。なんてこったい。

根本的な原因は、先日書いたコチラと同様の事象ですね。。

https://qiita.com/akagire/items/6f82058fe7aabe9c6876

ということで、デフォルトサービスアカウントに

- Artifact Registry Create-on-Push Writer
- Storage Admin
- Logs Writer

の権限を付与してあげれば良さそうですが、試したところ

- Storage Object Admin

の権限も必要でした…

これで、ようやく App Engine のデプロイができるようになります。
（一度デプロイできてしまえば、もう少し権限を絞れそうです）

### サービスアカウントを指定したデプロイはできないのか

gcloud コマンドには権限借用の機能があるので、 App Engine 向けのサービスアカウントをあらかじめ用意して、こちらを使ってデプロイしたほうが GCP のベストプラクティスに従った運用ができそうです。

https://cloud.google.com/sdk/docs/authorizing?hl=ja

# Cloud NAT の設定（ウィニングラン）

あとは、こちらの記事を参考に進めることができそうですが、暗示的に作成されるリソースがあるとトレースが厄介なので、もう少し精緻なリソース作成を行いました。

https://cloud.google.com/appengine/docs/standard/outbound-ip-addresses

といっても、手順としては、以下のようになります。

- VPC のセットアップ & サブネットの作成（最小構成、IPv4、レンジも狭め `/24` で、プライベートGoogle アクセスは不要、その他はファイアウォールルール含めてデフォルトで OK）
- Cloud ルーターの作成（ネットワークを上記で作った VPC を指定、リージョンも合わせる）
- Cloud NAT の作成（予約 IP 以外は上記で作ったものを指定）

注意点としては、リージョンを揃える（今回は `asia-northeast1` ）、  Cloud NAT IP を手動で構成する、IP アドレスは Cloud NAT 作成時に予約する（パフォーマンスが重要でないのならスタンダードティアで OK）くらいは気をつけています。IP も事前に予約しておいても良かったかもです。

# 余談

GCP、ここ最近は IAM 周りの混迷を極めており、やや不穏ですね（自分のような IaaS 管理がそこまで得意ではない人間にとっては、ググったり AI に聞くことでトラブルシュートできなくもないですが、ドキュメント通りチュートリアルが進められないのは結構辛いです。。）

コミュニティフォーラムへの投稿から半年以上ドキュメントが直っていないことを見ると、特別な理由がない場合、今後は Cloud Run を使うほうが良さそうです。
IAP を Cloud Run に指す場合は LB が必要になりますが、 NAT に加えて LB も使うとコスト的にも余計なので、今回は App Engine を利用しています。