Terraform 検証:手元から GCP GAE のNext.jsアプリをデプロイする
TL;DR
- tfstate、アセット管理用 バケットだけは コマンドラインないし GCP コンソールで作成する
- バージョニングと(過課金防止のための)ライフサイクル設定を追加
- Google Cloud Build · GitHub Marketplace をインストールしておき、 ソースリポジトリとGCP のプロジェクトを連携しておく
- Cloud Build をモジュールとして複数用意して、手元からデプロイ
- アプリケーションビルド用
- terraform apply 用
- (すでに手で作成済みだったので)App Engine を
terraform import
- アプリビルド用 CloudBuild を実行して ZIP を作成
- google_app_engine_standard_app_version をみながら バージョンを定義して作成済みのアーティファクトからデプロイするように
- Next.js アプリが見える
他に必要そうなもの(別スクラップで検討)
- terrafrom apply を Cloud Build から実行する
- タグないし package.json のバージョンに応じたアーティファクトのバージョニング
- GAE バージョン間のトラフィック移行
- GAE データベースの作成
- GAE Serverless NEG と IAP の定義
ここ見ながら
Cloud SDK をインストール
- asdf で Python を3.8.10 にした
- 次にここからインストールし https://cloud.google.com/sdk/docs/install?hl=JA
- opt ディレクトリに移動して、fish shell のパスを通した
- gcloud init した
- ログインが求められて初期設定が必要だったので実行した(設定ファイルどこ?
- 入門記事でGoogle 側の API を有効にしろとあるがデプロイしようとしているものが何のAPIに対応しているかがわからんのでいったんとばす
Terraformインストール
- これを見ながら
- バイナリを落としてパスを通す模様
terraform --help
Usage: terraform [global options] <subcommand> [args]
- 大丈夫そう
2021年6月9日 追記: Terraform アップデート
Upgrading to Terraform v1.0 - Terraform by HashiCorp
ここみる。
terraform --version
Terraform v0.15.3
on darwin_amd64
Your version of Terraform is out of date! The latest version
is 1.0.0. You can update by downloading from https://www.terraform.io/downloads.html
v0.15
は特に何も特別なことはしなくて良いらしい。(brew にしておけばよかった)
- zip をダウンロードして展開
-
~/opt/terraform/bin
に入れていたのでここに移動した- 古いバージョンは念の為
terraform015
とした
- 古いバージョンは念の為
terraform --version
Terraform v1.0.0
on darwin_amd64
大丈夫そう
サービスアカウントを作成〜設定JSON作成
gcloud config set project waddy
# サービスアカウント作成
gcloud iam service-accounts create terraform-serviceaccount --display-name "Account for Terraform"
# サービスアカウントに権限付与
# 使う範囲によりとなりますが、一旦「editor」ロールの権限を付与します。
gcloud projects add-iam-policy-binding waddy \
--member serviceAccount:terraform-serviceaccount@waddy.iam.gserviceaccount.com \
--role roles/editor
# アカウントのCredential発行
# Credentialのファイル名は「account.json」とする
gcloud iam service-accounts keys create credentials/account.json \
--iam-account terraform-serviceaccount@waddy.iam.gserviceaccount.com
# 専用の環境変数にCredentialファイルを設定する
export GOOGLE_CLOUD_KEYFILE_JSON=credentials/account.json
リソース定義作成〜デプロイ試し
- あとはここをみながらいろいろ定義していく
PRD、STGなどで分離したいときのアプローチ
ここで言及されてる。In particular, ~~
から。どうやらワークスペースを使うんではなく、共通モジュールを定義してそれを環境ごとに必要無分だけ流用せよとのこと。あんまりわかってないけど Iac を使う目的として環境差分を表現しつつも共有できる部分は共有したいというのがあるのでこの段階で残しておこう。
デプロイチャレンジ
作成したファイルはこれ。
## project ##
provider "google" {
project = "${var.project_name}"
region = "asia-northeast1"
zone = "asia-northeast1-a"
}
## App Engine ##
resource "google_app_engine_application" "app" {
project = "${var.project_name}"
location_id = "asia-northeast1"
}
terraform apply
google_app_engine_application.app: Creating...
╷
│ Error: Error creating App Engine application: googleapi: Error 403: The caller does not have permission, forbidden
│
│ with google_app_engine_application.app,
│ on main.tf line 9, in resource "google_app_engine_application" "app":
│ 9: resource "google_app_engine_application" "app" {
ここでAPIがないのでエラーになってるっぽい?
クレデンシャルを再検討
- GOOGLE_CLOUD_KEYFILE_JSON
- GOOGLE_APPLICATION_CREDENTIALS
どう違う?
これを参考にする。
こっちか。どうやら読み込まれる優先度が違うらしい。
credentials - (Optional) Either the path to or the contents of a service account key file in JSON format. You can manage key files using the Cloud Console. Your service account key file is used to complete a two-legged OAuth 2.0 flow to obtain access tokens to authenticate with the GCP API as needed; Terraform will use it to reauthenticate automatically when tokens expire. Alternatively, this can be specified using the GOOGLE_CREDENTIALS environment variable or any of the following ordered by precedence.
GOOGLE_CREDENTIALS
GOOGLE_CLOUD_KEYFILE_JSON
GCLOUD_KEYFILE_JSON
Using Terraform-specific service accounts to authenticate with GCP is the recommended practice when using Terraform. If no Terraform-specific credentials are specified, the provider will fall back to using Google Application Default Credentials. To use them, you can enter the path of your service account key file in the GOOGLE_APPLICATION_CREDENTIALS environment variable, or configure authentication through one of the following;
つまり GOOGLE_CREDENTIALS
、 GOOGLE_CLOUD_KEYFILE_JSON
、 GCLOUD_KEYFILE_JSON
をみにいくけど、そこになかったら GOOGLE_APPLICATION_CREDENTIALS
を読みに行くよって感じ? GOOGLE_CLOUD_KEYFILE_JSON
が設定されていればよさそう。
IAM を修正
- Terraform のサービスロールに
App Engine 管理者
を追加:変化なし
Getting Started をやってみる
ちょっと行き詰まったので Getting Started やってみた
結果、 デプロイできた (そしてはやい)。Google App Engine が特殊なのだとわかる。
Google App Engine デプロイチャレンジに戻る
サービスアカウントのログを見てみた。
IAM authority does not have the permission 'appengine.applications.create' required for action Applications_CreateApplication on resource ...
え… App Engine 管理者
だけだど駄目?
App Engine 作成者
が必要らしい。GCP の IAMロールこういうところあるよね。いたずらに包含させないところ。
terraform-serviceaccount@
に App Engine 作成者
を追加。再度 terraform apply
権限はOKぽいが、App Engine がすでに存在して409
となるらしい。これは App Engine 側の制限。というわけで、
- App Engine を消す(できるのか?)
- App Engine を terraform import する
を検討。1はできるとしても本番でやろうと思ったらダウンタイムが発生することになるのでまずは2
から検討する。
terraform import
ざっくり:
- あらかじめimportしたいリソースに対応する記述を
tf
ファイルに記載しておく terraform import
という流れの模様。今回は App Engine なので:
## project ##
provider "google" {
project = "${var.project_name}"
region = "asia-northeast1"
zone = "asia-northeast1-a"
}
## App Engine ##
resource "google_app_engine_application" "app" {
project = "${var.project_name}"
location_id = "asia-northeast2"
}
として
> terraform import google_app_engine_application.app waddy
Import successful!
The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
> terraform plan
google_app_engine_application.app: Refreshing state...
No changes. Infrastructure is up-to-date.
どうやらいけたっぽい。あとはサービスをデプロイしていく。
まずは state 管理用のバケットを作成
本格的にアプリをデプロイしていくにあたり、まず状態管理用のバケットを作って設定するところからやる。これは Terraform を複数人でデプロイする状況だとやったほうがいいみたい。Cloud Storage でバケットをつくってそれを状態管理用に設定するというもの。
Cloud Shell 使ってるけどパス通ってるし手元からやる。
# fish shell につき
PROJECT_ID=(gcloud config get-value project) gsutil mb gs://$PROJECT_ID-artifact
# バージョニングを有効にする
PROJECT_ID=(gcloud config get-value project) gsutil versioning set on gs://{$PROJECT_ID}-artifact
バージョニングのライフサイクル管理がめんどくさい…どないしよ…。許容する。
{
"lifecycle": {
"rule": [
{
"action": {
"type": "Delete"
},
"condition": {
"age": 30,
"isLive": true
}
},
{
"action": {
"type": "Delete"
},
"condition": {
"numNewerVersions": 2
}
},
{
"action": {
"type": "Delete"
},
"condition": {
"age": 35,
"isLive": false
}
}
]
}
}
PROJECT_ID=(gcloud config get-value project) gsutil lifecycle set bootstrap/bucket_lifecycle_config.json gs://{$PROJECT_ID}-artifact
PROJECT_ID=(gcloud config get-value project) gsutil lifecycle get gs://{$PROJECT_ID}-artifact
{"rule": [{"action": {"type": "Delete"}, "condition": {"age": 30, "isLive": true}}, {"action": {"type": "Delete"}, "condition": {"numNewerVersions": 2}}, {"action": {"type": "Delete"}, "condition": {"age": 35, "isLive": false}}]}
大丈夫そう。
terraform plan してみた
terraform plan
Backend reinitialization required. Please run "terraform init".
Reason: Initial configuration of the requested backend "gcs"
tfstateを管理するようにしたから init せよとのこと。大丈夫か?やってみる。
terraform init
Initializing the backend...
╷
│ Error: Variables not allowed
左様ですか。
init 時に config を渡すようにしました。
terraform init -backend-config="bucket=artifact"
Initializing the backend...
Do you want to copy existing state to the new backend?
Pre-existing state was found while migrating the previous "local" backend to the
newly configured "gcs" backend. No existing state was found in the newly
configured "gcs" backend. Do you want to copy this state to the new "gcs"
backend? Enter "yes" to copy and "no" to start with an empty state.
Enter a value: yes
Successfully configured the backend "gcs"! Terraform will automatically
use this backend unless the backend configuration changes.
ローカルにあったらコピーしてくれるんだ!すげー。これで tfstate のリモート化は完了。
アプリケーションコンテナを アップロードする
今後を考慮してもたぶんコンテナでデプロイできるようになっていたがほうがよさそう。
ここが参考になりそうか。
ここでわからなくなった:Terraform を使ったときの app.yaml の扱い
もともと app.yaml で GAE のサービスをデプロイするのが鉄板だと思うのだけど、with IaC になったときに誰がどこまでの役割を担うべきなのかわからなくなってしまった 🤔
GAE のドキュメントに今一度立ち戻る
.CloudBuild 上で Terraform を実行すること自体はアリのようだ
App Engine のAPIデプロイのドキュメント。
Google App Engine Admin API では、Cloud Storage バケット内にあるバージョンのすべてのリソースをアップロードしてステージングすることが求められます。このリソースには、関連するコードファイル、静的ファイル、使用されている Dockerfile などがあります。バージョンの定義と Cloud Storage 内のすべてのリソースのマニフェストを含む JSON 形式の構成ファイルも作成する必要があります。
なるほど。app deploy app.yaml
はもしかしたらこのへんを勝手にやってくれるのかもしれないですね。Terraform でバージョンをデプロイする場合、このあたりを手前でやる必要がありそう。
↓ここも見た。
ここまでのまとめ:
- 設定ファイルが増えてきて、IAPを設定したりもするので構成管理したい
- GCP だと Terraform が有力
- ただ、Terraform x GAE の情報が少ないので基本に立ち返って整理する
- Terraform は GCP の API を使って構成管理する
- なので、GAE のAPIを使ってのデプロイをまずは参考にする
- Admin API でアプリをデプロイする | App Engine Admin API | Google Cloud
- それによると大まかな手順は以下
- API によるデプロイの場合、Cloud Storage にコードベースがアップロードされている必要があるので、アップロードする
-
app.yaml
を定義する - App Engine の
versions
リソースに対してデプロイリクエストを送る
よって:
- Terrafrom は
google_app_engine_standard_app_version
を用意してくれている - これが実行できるように、
zip
またはfiles
のコードベースを CloudStorage にあげる - CloudStorage に上げる処理が、同じ Terraform で完結できるのか、それともデプロイとは別のフェーズで実施しなければならないかは、要検証
というわけで、まずは google_app_engine_standard_app_version
から攻めていく。
terraform を apply する Cloud Build
公式のチュートリアルでは GitHub App を利用しているもよう。Cloud Build で、同じリポジトリに対して複数のトリガーを設定することはできた。モノレポに対してアプリコードのビルドとインフラのデプロイといった具合に分けて実行することもできそう。
アプリコードをビルドする Cloud Build
GitHub と接続する Cloud Build を Terrafrom で書けるのか…?やってみる。
## App Engine ##
resource "google_app_engine_application" "app" {
project = var.project_name
location_id = "asia-northeast2"
}
## Frontend Service Builder ##
resource "google_cloudbuild_trigger" "build-trigger" {
name = "build-frontend"
filename = "build-frontend.yaml"
github {
owner = "cm-wada-yusuke"
name = "apprunner-nextjs-ssr"
push {
branch = "main"
}
}
}
これでトリガーを作成できた。あらかじめ GitHub App Google Cloud Build · GitHub Marketplace をインストールしておく必要はありそう。
おおそよつかめてきたのでここでモジュール化を組み込んでみよう。
モジュール化した Cloud Build
## Frontend Service Builder ##
resource "google_cloudbuild_trigger" "build-trigger" {
name = "build-frontend"
github {
owner = "cm-wada-yusuke"
name = "apprunner-nextjs-ssr"
push {
branch = "main"
}
}
build {
timeout = "600s"
step {
name = "node:14"
entrypoint = "yarn"
args = [
"install"]
dir = "."
}
step {
name = "node:14"
entrypoint = "yarn"
args = [
"build"]
dir = "."
}
step {
name = "gcr.io/cloud-builders/docker"
args = [
"build",
"-t",
"gcr.io/$PROJECT_ID/zip",
"."]
dir = "infra-terraform/modules/cicd/docker_zip"
}
images = [
"gcr.io/$PROJECT_ID/zip"]
step {
name = "gcr.io/$PROJECT_ID/zip"
args = [
"-r",
"artifact.zip",
".",
"-x",
"*/.git/*"]
dir = "."
}
step {
name = "bash"
args = [
"ls",
"-la",
]
dir = "."
}
artifacts {
objects {
location = var.artifacts_location
paths = [
"artifact.zip"]
}
}
}
}
- アーティファクトとなる
artifact.zip
を生成するように構成 - zip コマンドがないので Dockerfile を用意
FROM ubuntu
RUN apt-get -q update && \
apt-get -qqy install zip bzip2 gzip
ENTRYPOINT ["zip"]
これで artifact.zip
が Cloud Storage にアップされたことを確認した。
App Engine へのデプロイ
App Engine のステータスが停止(手作業)になっていたのでまずはそこを是正する。
## App Engine ##
resource "google_app_engine_application" "app" {
project = var.project_name
location_id = "asia-northeast2"
serving_status = "USER_DISABLED"
}
いったんGCPの状態に合わせて USER_DISABLED
としておき terraform apply
。
## App Engine ##
resource "google_app_engine_application" "app" {
project = var.project_name
location_id = "asia-northeast2"
serving_status = "SERVING"
}
その後 SERVING
として terraform apply
。
既存のバージョンを import する
タイムアウトになった残骸が残ってしまっていたので import を試みる。
resource "google_app_engine_standard_app_version" "next-ssr" {
version_id = "v1"
service = "next-ssr"
runtime = "nodejs14"
deployment {
zip {
source_url = var.artifacts_url
}
}
env_variables = {
port = "3000"
}
handlers {
url_regex = "/.*"
security_level = "SECURE_ALWAYS"
script {
script_path = "auto"
}
}
automatic_scaling {
max_concurrent_requests = 10
min_idle_instances = 0
max_idle_instances = 0
min_pending_latency = "1s"
max_pending_latency = "5s"
standard_scheduler_settings {
target_cpu_utilization = 0.5
target_throughput_utilization = 0.75
min_instances = 1
max_instances = 2
}
}
delete_service_on_destroy = true
}
terraform import google_app_engine_standard_app_version.next-ssr apps/waddy/services/next-ssr/versions/v1
Error: resource address "google_app_engine_standard_app_version.next-ssr" does not exist in the configuration.
えぇ…そこにあるじゃん…どうやら module にネストされた状態だとうまくimportしてくれないらしい。一度 main.tf
にもってきてから import
するとできた。
そして impot 後再度 terraform plan
すると…
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
- destroy
削除して作成する動きに。なるほど、リソースパスが google_app_engine_standard_app_version.next-ssr
と module.app_version.google_app_engine_standard_app_version.next-ssr
別物として扱われるぽい。とりあえずこれで apply
することに。
│ Error: Error when reading or editing AppVersion: googleapi: Error 400: Cannot delete the final version of a service (module). Please delete the whole service (module) in order to delete this version.
エラー。すっきりとはいきませんねえ。どうやら削除した結果バージョンがすべて消える状態になるようだと Google API 側でエラーになる模様。いったん v1
と v2
2つ作ってから v1
を削除する。
なんか v1
消したら記述が残っているはずの v2
も消えたんだけど…大丈夫かこれ…改めて v2
を作成。
やっとみえた!トラフィック移行とかデータベースとか他にもフルで考えるにはいろいろあるけどGAE のバージョンを terraform でデプロイするという当面の目標は達成しました。