Closed13

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 の定義

https://qiita.com/donko_/items/6289bb31fecfce2cda79

ここ見ながら

Cloud SDK をインストール

  • asdf で Python を3.8.10 にした

https://qiita.com/craymaru/items/0d22c1c542cf4a8326ee
  • 次にここからインストールし https://cloud.google.com/sdk/docs/install?hl=JA
  • opt ディレクトリに移動して、fish shell のパスを通した
  • gcloud init した
  • ログインが求められて初期設定が必要だったので実行した(設定ファイルどこ?
  • 入門記事でGoogle 側の API を有効にしろとあるがデプロイしようとしているものが何のAPIに対応しているかがわからんのでいったんとばす

Terraformインストール

https://learn.hashicorp.com/tutorials/terraform/install-cli
  • これを見ながら
  • バイナリを落としてパスを通す模様
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などで分離したいときのアプローチ

https://www.terraform.io/docs/language/state/workspaces.html

ここで言及されてる。In particular, ~~ から。どうやらワークスペースを使うんではなく、共通モジュールを定義してそれを環境ごとに必要無分だけ流用せよとのこと。あんまりわかってないけど Iac を使う目的として環境差分を表現しつつも共有できる部分は共有したいというのがあるのでこの段階で残しておこう。

デプロイチャレンジ

作成したファイルはこれ。

main.tf
## 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

どう違う?

https://qiita.com/kawakawaryuryu/items/58d8afbb21155c2e9572

これを参考にする。

https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/provider_reference#configuration-reference

こっちか。どうやら読み込まれる優先度が違うらしい。

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_CREDENTIALSGOOGLE_CLOUD_KEYFILE_JSONGCLOUD_KEYFILE_JSON をみにいくけど、そこになかったら GOOGLE_APPLICATION_CREDENTIALS を読みに行くよって感じ? GOOGLE_CLOUD_KEYFILE_JSON が設定されていればよさそう。

IAM を修正

  • Terraform のサービスロールに App Engine 管理者 を追加:変化なし

Getting Started をやってみる

ちょっと行き詰まったので Getting Started やってみた

https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/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 管理者 だけだど駄目?

gms3qn7q6v34hezk65oqlum1vemy

App Engine 作成者 が必要らしい。GCP の IAMロールこういうところあるよね。いたずらに包含させないところ。

terraform-serviceaccount@App Engine 作成者 を追加。再度 terraform apply

権限はOKぽいが、App Engine がすでに存在して409

となるらしい。これは App Engine 側の制限。というわけで、

  1. App Engine を消す(できるのか?)
  2. 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 でバケットをつくってそれを状態管理用に設定するというもの。

https://cloud.google.com/architecture/managing-infrastructure-as-code#configuring_terraform_to_store_state_in_a_cloud_storage_bucket

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

https://cloud.google.com/storage/docs/managing-lifecycles#gsutil_1

バージョニングのライフサイクル管理がめんどくさい…どないしよ…。許容する。

bucket_lifecycle_cinfig.json
{
    "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

左様ですか。

https://qiita.com/ymmy02/items/e7368abd8e3dafbc5c52

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 のリモート化は完了。

アプリケーションコンテナを アップロードする

今後を考慮してもたぶんコンテナでデプロイできるようになっていたがほうがよさそう。

https://github.com/bhidalto/terraform-appengine

ここが参考になりそうか。

ここでわからなくなった:Terraform を使ったときの app.yaml の扱い

もともと app.yaml で GAE のサービスをデプロイするのが鉄板だと思うのだけど、with IaC になったときに誰がどこまでの役割を担うべきなのかわからなくなってしまった 🤔

GAE のドキュメントに今一度立ち戻る

.CloudBuild 上で Terraform を実行すること自体はアリのようだ

https://cloud.google.com/architecture/system-testing-cloud-functions-using-cloud-build-and-terraform?hl=ja

App Engine のAPIデプロイのドキュメント。

https://cloud.google.com/appengine/docs/admin-api/deploying-overview

Google App Engine Admin API では、Cloud Storage バケット内にあるバージョンのすべてのリソースをアップロードしてステージングすることが求められます。このリソースには、関連するコードファイル、静的ファイル、使用されている Dockerfile などがあります。バージョンの定義と Cloud Storage 内のすべてのリソースのマニフェストを含む JSON 形式の構成ファイルも作成する必要があります。

なるほど。app deploy app.yaml はもしかしたらこのへんを勝手にやってくれるのかもしれないですね。Terraform でバージョンをデプロイする場合、このあたりを手前でやる必要がありそう。

↓ここも見た。

https://cloud.google.com/appengine/docs/standard/python/an-overview-of-app-engine

ここまでのまとめ:

  • 設定ファイルが増えてきて、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

https://cloud.google.com/architecture/managing-infrastructure-as-code?hl=ja

公式のチュートリアルでは GitHub App を利用しているもよう。Cloud Build で、同じリポジトリに対して複数のトリガーを設定することはできた。モノレポに対してアプリコードのビルドとインフラのデプロイといった具合に分けて実行することもできそう。

アプリコードをビルドする Cloud Build

GitHub と接続する Cloud Build を Terrafrom で書けるのか…?やってみる。

main.tf
## 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

cicd.tf
## 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 を用意
infra-terraform/modules/cicd/docker_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 のステータスが停止(手作業)になっていたのでまずはそこを是正する。

main.tf
## 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

main.tf
## App Engine ##
resource "google_app_engine_application" "app" {
  project = var.project_name
  location_id = "asia-northeast2"
  serving_status = "SERVING"
}

その後 SERVING として terraform apply

既存のバージョンを import する

タイムアウトになった残骸が残ってしまっていたので import を試みる。

infra-terraform/modules/app_version/app_version.tf
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 するとできた。

https://github.com/hashicorp/terraform/issues/25816

そして 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-ssrmodule.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 側でエラーになる模様。いったん v1v2 2つ作ってから v1 を削除する。

なんか v1 消したら記述が残っているはずの v2 も消えたんだけど…大丈夫かこれ…改めて v2 を作成。

やっとみえた!トラフィック移行とかデータベースとか他にもフルで考えるにはいろいろあるけどGAE のバージョンを terraform でデプロイするという当面の目標は達成しました。

このスクラップは2ヶ月前にクローズされました
ログインするとコメントできます