TerraformでGoogleAPPEngineを構築してみた

Terraformシリーズもいよいよ今回で最後かなぁと思っています。

前回に引き続いてPaaSの構築です。既にAlibabaに追い抜かれてはいますが、三大クラウドの一角のGCPです。GoogleAppEngineDataStoreを使用するアプリを作成し、そいつを展開するところをTerraformでやってみようというわけです。

まぁ↓こんな感じの構成です。



サンプルアプリはこちらに保存しています。Azureの時と同様にまずはCloudSDKで試してみてからTerraformにてやってみます。またしてもtfファイルはGitHubには上げていないという、、、。いい加減にGitに上げる様にした方がいいんだろうなぁと思うんですが後で見返す際にやりやすいのでこうしています。

カスタムドメインに関してはどうしても手動の部分があるので切り分けて共通項目としてくくりだしているのでTerraform化はしていません。カスタムドメインと無償SSLの箇所については他にいいサイトがあるのでそちらを参照いただいた方がいいかもしれないです。


◆設定作業編

1.CloudSDK版

# ソースコードのコピーとライブラリインストール
$ cd workdir
$ git clone https://github.com/Otazoman/GAEWebAppSample.git
$ cd GAEWebAppSample
$ mkdir lib
$ pip install --upgrade pip
$ pip install -t lib -r requirements.txt
#GCPプロジェクト作成
$ gcloud projects create ${project_name} --folder=${folder_id}
#GAE作成
$ gcloud app create --project=${project_name}

Please choose the region where you want your App Engine application
located:

 [1] asia-east2    (supports standard and flexible)
 [2] asia-northeast1 (supports standard and flexible)
 [3] asia-northeast2 (supports standard and flexible)
 [4] asia-northeast3 (supports standard and flexible)
 ~略~
 [20] cancel
Please enter your numeric choice:  2

# プロジェクトと請求先アカウントのリンク
$ gcloud beta billing projects link ${project_name} --billing-account ${billing_id}
# デプロイ
$ gcloud app deploy --project=${project_name}
Do you want to continue (Y/n)? Y
※初回はかなり時間がかかる
# プロジェクト削除
$ gcloud projects delete gaeappsample-20200816
Do you want to continue (Y/n)?  Y

2.Terraform版

2.1.事前準備作業

# ディレクトリ準備
$ mkdir gaeterraform
$ cd gaeterraform
$ mkdir credentials
$ mkdir gae
$ mkdir project
$ mkdir upload
#ソースコピー
$ git clone https://github.com/Otazoman/GAEWebAppSample.git src/GAEWebAppSample
# 親プロジェクト用シェル作成
$ vi src/make_project.sh
$ chmod 755 src/make_project.sh
------------------------------
#/bin/bash
#make project
gcloud projects create ${pr_project_name} \
--${folder_id} \
--set-as-default

#billing account
gcloud beta billing projects link ${pr_project_name} \
--billing-account ${billing_id}
gcloud config set project ${pr_project_name}

#make service account
gcloud iam service-accounts create terraform \
--display-name "Terraform admin account"

#get credential
gcloud iam service-accounts keys create ~/credential.json \
--iam-account ${service_account}@${pr_project_name}.iam.gserviceaccount.com

#api enabled
gcloud services enable cloudresourcemanager.googleapis.com
gcloud services enable cloudbilling.googleapis.com
gcloud services enable iam.googleapis.com
gcloud services enable serviceusage.googleapis.com
gcloud services enable appengine.googleapis.com
gcloud services enable storage-component.googleapis.com
gcloud services enable storage.googleapis.com
gcloud services enable storagetransfer.googleapis.com
gcloud services enable datastore.googleapis.com

#orgnize and billing
gcloud organizations add-iam-policy-binding ${org_id} \
--member serviceAccount:${service_account}@${pr_project_name}.iam.gserviceaccount.com \
--role roles/resourcemanager.projectCreator
gcloud organizations add-iam-policy-binding ${org_id} \
--member serviceAccount:${service_account}@${pr_project_name}.iam.gserviceaccount.com \
--role roles/billing.user
------------------------------
$ cp src/make_project.sh ./
※$で始まる個所は個別書替
# 親プロジェクト用Shell実行
./make_project.sh
# Terraformクレデンシャル取得用Shell準備
$ vi src/get_sakey.sh
$ chmod 755 src/get_sakey.sh
------------------------------
#/bin/bash
if [ -d credentials ]; then
   rm -Rf credentials
fi
gcloud iam service-accounts keys create ${credential} \
--iam-account ${service_account}@${project_name}.iam.gserviceaccount.com

export -n GOOGLE_APPLICATION_CREDENTIALS
export GOOGLE_APPLICATION_CREDENTIALS="${credential}"
------------------------------
$ cp src/get_sakey.sh ./


2.2.Terraformファイル群


(1)ルートに置くもの

# main.tf
-----------------
provider "google" {
  project = var.project_name
  region  = var.region
}

module "project" {
  source          = "./project"
  project_name    = var.project_name
  service_account = var.service_account
  org_id          = var.org_id
  folder_id       = var.folder_id
  billing_account = var.billing_account
}

data "template_file" "makeshell" {
  template = file("${path.module}/get_sakey.sh")
  vars = {
    project_name    = var.project_name
    service_account = var.service_account
    credential      = var.credential
  }
}

resource "local_file" "get_sakey" {
  content  = data.template_file.makeshell.rendered
  filename = "${path.module}/get_sakey.sh"
  provisioner "local-exec" {
    command = "/bin/bash ${path.module}/get_sakey.sh"
  }
  depends_on = [module.project]
}

module "gae" {
  source       = "./gae"
  project_name = "${module.project.project_id}"
  project_id   = "${module.project.project_id}"
  region       = var.region
  credential   = var.credential
}
-----------------
# variable.tf
-----------------
variable "admin_user" {
  default = "your@example.com"
}
variable "project_name" {
  default = "project_name"
}
variable "project_id" {
  default = "project_id"
}
variable "org_id" {
  default = "org_id"
}
variable "folder_id" {
  default = "folder_id"
}
variable "billing_account" {
  default = "billing_id"
}

variable "service_account" {
  default = "terraform"
}
variable "credential" {
  default = "/home/user/gaeterraform/credentials/account.json"
}
variable "region" {
  default = "asia-northeast1"
}
-----------------

(2)projectディレクトリ配下

# main.tf
-----------------
resource "google_project" "project" {
  name       = var.project_name
  project_id = var.project_name
  #org_id          = var.org_id
  folder_id       = var.folder_id
  billing_account = var.billing_account
}

resource "google_service_account" "sa" {
  account_id   = var.service_account
  display_name = "Project Service Account"
  depends_on   = [google_project.project]
}

data "google_iam_policy" "sa-iam-policy" {
  binding {
    role = "roles/owner"
    members = [
      "serviceAccount:${google_service_account.sa.email}",
    ]
  }
}

resource "google_service_account_iam_policy" "sa-iam" {
  service_account_id = google_service_account.sa.id
  policy_data        = data.google_iam_policy.sa-iam-policy.policy_data
}

resource "google_project_service" "services" {
  project    = google_project.project.project_id
  for_each   = toset(var.services)
  service    = each.value
  depends_on = [google_project.project]
}
-----------------
# variable.tf
-----------------
variable "project_name" {}
variable "billing_account" {}
variable "org_id" {}
variable "folder_id" {}
variable "service_account" {}
variable "services" {
  default = [
    "appengine.googleapis.com",
    "storage-component.googleapis.com",
    "storage.googleapis.com",
    "storagetransfer.googleapis.com",
    "datastore.googleapis.com"
  ]
}
-----------------
# outputs.tf
-----------------
output "project_id" {
  value = google_project.project.project_id
}
-----------------

(3)gaeディレクトリ配下

# main.tf
-----------------
data "archive_file" "app-engine-source-zip" {
  type        = "zip"
  source_dir  = var.source_dir
  output_path = var.output_path
}

resource "google_storage_bucket" "deployment_config" {
  bucket_policy_only = true
  force_destroy      = true
  name               = "deployment-config-${var.project_id}"
  requester_pays     = false
  storage_class      = "STANDARD"
}

resource "google_storage_bucket_object" "app-engine-source-zip" {
  source     = var.source_file
  bucket     = google_storage_bucket.deployment_config.name
  name       = "app-engine-source-zip"
  depends_on = [data.archive_file.app-engine-source-zip]
}

resource "google_app_engine_application" "app" {
  timeouts {
    create = "15m"
    update = "15m"
  }
  project     = var.project_id
  location_id = var.region
}

resource "google_app_engine_standard_app_version" "app-version" {
  timeouts {
    create = "15m"
    delete = "15m"
  }
  version_id = data.archive_file.app-engine-source-zip.output_md5
  service    = var.service
  runtime    = var.runtime
  deployment {
    zip {
      source_url = "https://storage.googleapis.com/${google_storage_bucket.deployment_config.name}/${google_storage_bucket_object.app-engine-source-zip.name}"
    }
  }
  entrypoint {
    shell = var.shell
  }
  noop_on_destroy = true
  depends_on      = [google_storage_bucket_object.app-engine-source-zip]
}
-----------------

# variable.tf
-----------------
variable "project_id" {}
variable "project_name" {}
variable "region" {}
variable "credential" {
  default = "/home/user/gaeterraform/credentials/account.json"
}
variable "source_file" {
  default = "/home/user/gaeterraform/upload/gaewebsample.zip"
}
variable "source_dir" {
  default = "/home/user/gaeterraform/src/GAEWebAppSample"
}
variable "output_path" {
  default = "/home/user/gaeterraform/upload/gaewebsample.zip"
}
variable "service" {
  default = "default"
}
variable "runtime" {
  default = "python37"
}

variable "shell" {
  default = "gunicorn -b :$PORT main:app"
}
-----------------

2.3.ディレクトリ構成

.
|-- credentials
|   `-- account.json
|-- gae
|   |-- main.tf
|   `-- variable.tf
|-- get_sakey.sh
|-- main.tf
|-- project
|   |-- main.tf
|   |-- make_project.sh
|   |-- outputs.tf
|   `-- variable.tf
|-- src
|   |-- GAEWebAppSample
|   |   |-- README.md
|   |   |-- app
|   |   |   |-- __init__.py
|   |   |   |-- __pycache__
|   |   |   |   |-- __init__.cpython-37.pyc
|   |   |   |   |-- app.cpython-37.pyc
|   |   |   |   |-- datastore_crud.cpython-37.pyc
|   |   |   |   `-- viewrender.cpython-37.pyc
|   |   |   |-- app.py
|   |   |   `-- templates
|   |   |       |-- error.html
|   |   |       |-- index.html
|   |   |       |-- select.html
|   |   |       `-- upload.html
|   |   |-- app.yaml
|   |   |-- appengine_config.py
|   |   |-- controllers
|   |   |   |-- __init__.py
|   |   |   |-- __pycache__
|   |   |   |   `-- viewrender.cpython-37.pyc
|   |   |   `-- viewrender.py
|   |   |-- main.py
|   |   |-- models
|   |   |   |-- __init__.py
|   |   |   |-- __pycache__
|   |   |   |   |-- datastore.cpython-37.pyc
|   |   |   |   `-- datastore_crud.cpython-37.pyc
|   |   |   |-- datastore.py
|   |   |   `-- datastore_crud.py
|   |   `-- requirements.txt
|   |-- get_sakey.sh
|-- upload
`-- variable.tf


2.4.Terraformコマンド

$ terraform init
$ terraform plan
$ terraform apply
$ terraform destroy

3.カスタムドメイン追加


3.1.DNSにサブドメイン追加

# プロジェクト切替
$ gcloud config set project ${project_name}
Updated property [core/project].
$ gcloud config configurations list | grep True | awk '{print $4}'
# コマンドラインからサブドメイン作成
$ gcloud dns managed-zones create gaeappsample \
    --description="gae web apps" \
    --dns-name=gae.example.com \
    --visibility=public
# NSレコードを取得する。
$ gcloud dns record-sets list --zone=gaeappsample --name="gae.example.com." --type="NS"
*上記で取得したレコードをメインドメイン管理のDNSへ追加する。
 →自分の場合はAWSのRoute53NSに追加しました。
 

3.2.ドメインマッピング

(1) GAEの設定からサブドメインとマッピングする

・GCPのコンソールでGAEの該当アプリを選択し[設定]→[カスタムドメイン]→[カスタムドメインを追加]をクリックする。








・「使用するドメインを選択する」から[新しいドメインの所有権を証明]を選択して利用するサブドメインを入力して[所有権を証明]をクリックする。









ウェブマスターセントラルの画面が開くのでTXTレコードを取得する。[確認方法]で[ドメイン名プロバイダ]を選択して[Google Domains]を選択して赤枠で囲んだ箇所をDNSのTXTレコードに貼り付ける









※自分の場合はGCPのCloudDNSにサブドメインを設定したので、CloudDNSにてレコードを追加した。







・TXTレコードを登録後、10分ほどしてから[確認]をクリックすると確認完了画面が表示される。






・所有権が確認できたら確認済み表示となる










・[続行]をクリックする。









・ドメインのマッピング項目でマッピングされている内容を参照し[マッピングを保存]をクリックする。









・マッピングが正常に完了して緑チェック表示になったら[続行]をクリックする。









・DNSに設定するAレコードAAAAレコードCNAMEが表示される。











(2)マッピング保存後に表示されたAレコードとAAAAレコードとCNAMEをDNSに追加する

$ gcloud dns record-sets transaction start --zone="gaeappsample"
$ gcloud dns record-sets transaction add --name="www.gae.example.com." --ttl=300 --type=CNAME --zone="gaeappsample" "XXXX.com."
$ gcloud dns record-sets transaction add \
"IPv4addr_1" "IPv4addr_2" "IPv4addr_3" "IPv4addr_4" \
--name=gae.example.com. --ttl=300 --type=A --zone="gaeappsample"
$ gcloud dns record-sets transaction add \
"Ipv6addr_1" "Ipv6addr_2" "Ipv6addr_3" "Ipv6addr_4" \
--name=gae.example.com. --ttl=300 --type=AAAA --zone="gaeappsample"
$ gcloud dns record-sets transaction execute --zone="gaeappsample"


(3)登録が完了するとカスタムドメインの箇所にDNSの登録内容が表示される










◆参考サイト

・GAE入門

https://qiita.com/so-heee/items/2fbf523395889fad2887

https://www.topgate.co.jp/gcp06-how-to-use-cloud-datastore-gae

https://cloud.google.com/appengine/docs/standard/python3/using-cloud-datastore?hl=ja

https://codelabs.developers.google.com/codelabs/cloud-app-engine-python-ja/index.html?index=..%2F..next17-tok#2

https://note.com/10mohi6/n/n15cdabfe792d

https://dev.classmethod.jp/articles/gae-webapp/

https://cloud.google.com/appengine/docs/flexible/python/testing-and-deploying-your-app?hl=ja

https://qiita.com/leo1109/items/285b898ecf73621cbc17

https://cloud-textbook.com/2807/

https://www.serversus.work/topics/vyly8dwer5uql5ra5xdg/

https://note.com/yamaken0107/n/n1d19a988dfdb

http://westplain.sakuraweb.com/translate/GAE/Pricing-and-Quotas/Quotas.cgi

https://blog.hashihei.com/2020/03/08/google-app-engine%E3%81%A7paas%E3%81%AB%E5%85%A5%E9%96%80/

https://cloud.google.com/appengine/docs/standard/python3?hl=ja

https://mmtomitomimm.blogspot.com/2018/10/app-engine-cloud-datastore.html

https://codezine.jp/article/detail/5023

https://medium.com/@timakin/mercari-datastore%E5%AE%9F%E6%88%A6%E6%8A%95%E5%85%A5-a7211c56b77a


・DataStoreEmulator

https://blog-hello-world.web.app/posts/2020-01-12-cloud-datastore-local-docker/

https://qiita.com/ideodora/items/a292953a80940ea06d11

https://sanshiro.dev/article/5714489739575296

https://cloud.google.com/datastore/docs/tools/datastore-emulator?hl=ja

https://github.com/googleapis/google-cloud-python/issues/9097

https://blog.sky-net.pw/article/124

https://qiita.com/komtaki/items/550f02b1eda99a27ccbf

https://decoch.hatenablog.com/entry/2019/06/26/174306

https://engineer.crowdworks.jp/entry/2019/03/08/161559


・DataStore

https://qiita.com/tfuruya/items/fa529c6cf54adaa81aa7

https://qiita.com/miyuuuu/items/c9846ccdad9aee7c733c

https://qiita.com/Mahito/items/7578fe7b7a276a597784

https://techbooster.org/gae/15069/

https://www.366service.com/jp/qa/235f3b7802122f6c099e6c6944ea6ebb

https://nansystem.com/python-with-cloud-firestore/

https://komiyak.hatenablog.jp/entry/20170427/1493273145

https://cloud.google.com/datastore/docs/concepts/entities

https://www.the-swamp.info/blog/search-google-cloud-platform-cloud-datastore/

https://googleapis.dev/python/datastore/latest/queries.html

http://goslides.knightso.co.jp/2016/dssearch.slide#15


・GAE独自ドメイン設定

https://www.apps-gcp.com/gae-apply-customdomain-and-ssl-setting/#GAE-3

https://qiita.com/okaji00/items/d0457110169910e92f24

https://blog.kakakikikeke.com/2019/02/how-to-set-custom-domain-on-gae.html

https://cloud-textbook.com/2713/

https://cloud.google.com/appengine/docs/standard/python3/mapping-custom-domains?hl=ja


・DNSコマンド関連

https://cloud.google.com/dns/docs/zones?hl=ja

https://cloud.google.com/dns/records?hl=ja

https://apps-gcp.com/google-cloud-dns-api/

https://heartbeats.jp/hbblog/2014/06/google-cloud-dns.html

https://cloud.google.com/sdk/gcloud/reference/dns/record-sets/transaction/add?hl=ja

https://cloud.google.com/sdk/gcloud/reference/dns/record-sets/transaction?hl=ja

https://stackoverflow.com/questions/30670661/google-cloud-dns-cname-creation-example

https://cloud.google.com/dns/docs/records

https://pekeq.hateblo.jp/entry/2019/11/18/120442


・Terraform関連

https://www.terraform.io/docs/providers/google/r/app_engine_flexible_app_version.html

https://www.terraform.io/docs/providers/google/guides/getting_started.html

https://www.devsamurai.com/ja/gcp-terraform-service-account-permission/

https://apps-gcp-tokyo-02.appspot.com/terraformit-gcp/

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

https://techblog.gmo-ap.jp/2017/11/16/terraform%E3%81%A7gcp%E7%92%B0%E5%A2%83%E3%82%92%E6%A7%8B%E7%AF%89%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B/

https://gist.github.com/MisaKondo/cb46b0ecd106e9c824a641b14954b8e1

https://www.terraform.io/docs/providers/google/r/app_engine_application.html

https://gb-j.com/column/terraform_gcp/

https://www.devsamurai.com/ja/gcp-terraform-101/

https://qiita.com/sky0621/items/c488e8adb2a51870ad22

https://tech.visasq.com/restart-gcp-infrastructure-as-code1/

https://www.1915keke.com/entry/2018/12/10/145653

http://ritchiekotzen.hatenablog.com/entry/2019/01/11/terraform_%E3%81%A7_Google_App_Engine_%E3%81%AE%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92import%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95

https://medium.com/slalom-technology/a-complete-gcp-environment-with-terraform-c087190366f0

https://blog.ri52dksla.dev/posts/gcp-terraform-cloud-run-and-gcs/

https://qiita.com/takat0-h0rikosh1/items/f821fe4e308226123bac

https://www.terraform.io/docs/providers/google/r/google_service_account.html

https://www.terraform.io/docs/providers/google/r/app_engine_domain_mapping.html

https://ken5scal.hatenablog.com/entry/2017/05/09/GCP%E4%B8%8A%E3%81%AE%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92Terraform%E3%81%A7%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B_%23GCP_%23Terraform

https://www.terraform.io/docs/providers/google/r/google_project.html

https://www.terraform.io/docs/providers/google/r/google_service_account_iam.html

https://www.terraform.io/docs/providers/google/r/google_project_service.html

https://pawel.ie/gcp/gcloud/terraform/2020/03/22/GCP-Error-setting-billing-account.html

https://medium.com/@gmusumeci/how-to-manage-google-groups-users-and-service-accounts-in-gcp-using-terraform-fadf472e574a



Terraformははまりました。AzureとかAWSと違ってGCPの場合は組織の考え方が結構独特で、その関連でプロジェクトをTerraformから作成するときに色々と権限周りではまりました。その辺を調べるのでかなり色々と探したんですが情報もなく、非常に苦労しました。
(自分が低スキルなだけですごい人はサクッとできるんだろうなぁスキルほしい。

GCPでTerraformやる場合はあらかじめプロジェクトを作成しておいてサービスアカウントを発行してそのクレデンシャルを使ってなんかする方が自然みたいです。
今回みたいにダミーで親プロジェクト作成してその親プロジェクトから子プロジェクトを作ってみたいな特殊なことはしない方が無難だと思います。まぁ、Azureの場合と同様ですが、あえてTerraform使うよりはDeploymentManager使う方がトラブル少ないと思います。Terraform使うシーンとしてはGCEVPCCloudSQLをたくさん使うというシーン向きかなという印象です。

さて、何とか2020年中でWebAppsGAEについて色々と検証してみるという目標は達成できました。PaaS+マネージドのNoSQLテーブルストレージの構成ってコスト面でも構成面でもAWSのEC2+DocumentDB構成に近いんですよねぇ。
そういう意味で行けばGAE+DataStoreで行くよりはCloudRunでコンテナ作ってデータベースはCloudSpaner、フロントにCloudLoadBalancingを噛ませるという構成の方が妥当なんでしょうけどねぇ。GAE+DataStoreはお手軽と言えばお手軽な構成ですね。

Terraformを3大クラウドで使ってみて、やっぱりAWSとは親和性高いし情報は多いけど、GCPとAzureになると途端に日本語の情報はほぼ皆無で、英語力がなければTerraformでGCPとかに立ち向かうのは無謀かもしれないということを学びました。

コメント

このブログの人気の投稿

証券外務員1種勉強(計算式暗記用メモ)

GASでGoogleDriveのサブフォルダとファイル一覧を出力する

マクロ経済学(IS-LM分析)