The Terraform Playground を作ろうとしている話

8 min読了の目安(約7600字TECH技術記事

これは terraform Advent Calendar 2020 22日目の記事です。

The Terraform Playground

普段 Terraform を書いていると、ちょっと関数の挙動を確かめたり新しいバージョンで使えるようになった機能を試したりしたくなる場面がよくありますよね。
そんなときでもわざわざ tfenv install で目的のバージョンを入れて tf ファイルを書いて terraform init && terraform apply する必要があるので少し不便だなと感じていました。
The Go Playground のようにブラウザで気軽に試せる環境が理想なのですが、探してもいい感じのものは見つけられず…。


Katakoda Terraform Playground -- v0.8.4 はさすがに触ったことなかった…

折角の機会なのでアドベントカレンダー駆動開発で作り始めましたが、見事に間に合いませんでした。ただ最低限形にはなったかなというところで滑り込みで投稿していますm(_ _)m

進捗

一応 UI からインフラまで一通り構築できました。
Apply ボタンがありますが現状 Plan までしか出来ません😇
他にも制約は多いですがぜひ触ってみてもらえると嬉しいです!

構成

フロントエンド

流行りに乗って Next.js を使ってみました。
もちろんオーバーエンジニアリングなんですが、実際触ってみると TypeScript との相性も良くて、Webpack とか Babel といったビルド周りの設定も隠蔽されて、正直素の React.js を使うより数倍わかりやすかったです。
SPA/SSR/SSG の違いも体感できたので最新フロントエンドのインフラ環境ってどうなるのかな〜、、みたいな発想にもつながって普通にためになりました。

yarn build
yarn run v1.22.10
$ next build
info  - Using external babel configuration from /Users/yukin01/ghq/github.com/yukin01/terraform-playground/frontend/.babelrc
info  - Creating an optimized production build  
info  - Compiled successfully
info  - Collecting page data  
info  - Generating static pages (2/2)
info  - Finalizing page optimization  

Page                                                           Size     First Load JS
┌ ○ /                                                          8.27 kB        68.6 kB
├   └ css/256f71d69462f60ab771.css                             876 B
├   /_app                                                      0 B            60.3 kB
└ ○ /404                                                       3.44 kB        63.7 kB
+ First Load JS shared by all                                  60.3 kB
  ├ chunks/97917a0b.8f6f5b.js                                  68 B
  ├ chunks/f6078781a05fe1bcb0902d23dbbb2662c8d200b3.dbe449.js  12.8 kB
  ├ chunks/framework.964e76.js                                 39.9 kB
  ├ chunks/main.f20a82.js                                      6.54 kB
  ├ chunks/pages/_app.9b5b09.js                                292 B
  ├ chunks/webpack.e06743.js                                   751 B
  ├ css/13cc94b0f0b8c4cb80b3.css                               28.8 kB
  └ css/1807b862fb0d481ded0a.css                               607 B

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)(Static)  automatically rendered as static HTML (uses no initial props)(SSG)     automatically generated as static HTML + JSON (uses getStaticProps)
   (ISR)     incremental static regeneration (uses revalidate in getStaticProps)

ビルド後のサイズが思っていたより小さくて、SSG で済む軽い Web アプリこそ Next.js の恩恵をかなり受けられるのでは?と思いました。CSS in JS とかの話はよくわからないのでその辺は雰囲気でやっています。


細かい判断はできないですが Lighthouse の結果も悪くなさそうな気がします

また、デプロイ先として Firebase Hosting を選びました。
前職で使っていて馴染みがあったという部分も大きいですが、特に Cloud Run との組み合わせが最高です。あと最近は Firebase プロジェクトの作成から課金アカウントの紐付けとか Google Analytics との統合までとてもスムーズにできるようになっていてちょっと感動しました。

バックエンド

ユーザーが送信した Terraform コードを実行するサンドボックス環境が必要なので、おそらくコンテナが最適かと思います。
Firebase Hosting を使っていると、下の設定だけで一部 API だけ Cloud Run にリクエストを流すことができるので、とても簡単にコンテナ環境を構築できました。

  "hosting": {
    ...
    "rewrites": [
      {
        "source": "/api/**",
        "run": {
          "serviceId": "backend-service",
          "region": "asia-northeast1"
        }
      }
    ]
  },

コンテナの中身としては、tfenv といくつかのバージョンの terraform を仕込んでおいて、リクエストごとに .terraform-version を tmp ディレクトリに吐いて実行するだけなので愚直に実装しました。
まだできていませんが HCL をパースしてバリデーションかけるつもりなので、言語は Go を採用しています。

インフラ

Terraform Advent Calendar なのにほとんど Terraform の話がでてこなくて申し訳ない気持ちではありますが、インフラもほぼ Firebase で完結しているので Terraform の出番があまりありません。
Cloud Run も managed の方は実質 PaaS なので CLI でのデプロイが基本になっているのですが、半分強引に Terraform で構築しました。

data "google_project" "this" {}

locals {
  repository_name = "${var.gcr_hostname}/${data.google_project.this.project_id}/backend"
  image_tag       = "0.1.1"
}

resource "google_cloud_run_service" "backend" {
  name     = "backend-service"
  location = var.gcp_region

  template {
    spec {
      containers {
        image = null_resource.build.triggers.image_tag
        resources {
          limits = {
            memory = "512Mi"
            cpu    = "1000m"
          }
        }
      }
    }
  }

  autogenerate_revision_name = true
}

data "google_iam_policy" "no_auth" {
  binding {
    role = "roles/run.invoker"
    members = [
      "allUsers",
    ]
  }
}

resource "google_cloud_run_service_iam_policy" "no_auth" {
  location = google_cloud_run_service.backend.location
  project  = google_cloud_run_service.backend.project
  service  = google_cloud_run_service.backend.name

  policy_data = data.google_iam_policy.no_auth.policy_data
}

resource "null_resource" "build" {
  triggers = {
    image_tag = "${local.repository_name}:${local.image_tag}"
  }

  provisioner "local-exec" {
    command     = "docker build --tag ${local.repository_name}:${local.image_tag} --target runner ."
    working_dir = "../backend"
  }

  provisioner "local-exec" {
    command     = "docker push ${local.repository_name}:${local.image_tag}"
    working_dir = "../backend"
  }
}

null_resource の Provisioner をつかってローカルでコンテナの build と push をやっています。
まず triggers に docker image のタグを渡して、それを null_resource の属性として google_cloud_run_service に渡すことで

image tag 更新 → Docker Build and Push → Cloud Run 更新

の流れを Terraform に依存関係として認識させているところがポイントです。
とは言ってもチーム開発ではなかなか使えない構成だと思うので100%趣味の領域です。。。

やりたいこと

Terraform Apply

ここまではやっておきたかった…。ので絶対やるぞという意味も込めて Apply ボタンもそのままにしています。
実際 Apply するだけならすぐにでも用意できるのですが、使える provider を制限したり local-exec provisioner を禁止したりしないといけないので、その辺りのバリデーションが実装でき次第といった感じです。

共有 URL を発行するしくみ

The Go Playground は自分が書いたコードに一意の URL を発行して簡単にシェアできる機能があってとても便利です。
正直この機能が一番欲しくて作り始めたところがあります…。むしろこの機能がないと使ってもらえないと思っているのでここまでは実装します。
The Go Playground では Cloud Datastore が使われている感じだったので、こっちでは無料枠も十分にある Cloud Firestore を使うつもりです。あとこれに関連して OGP も実装してみたいです。

CI/CD

いつもなら環境構築とか CI/CD を整備して満足してしまうくらい好きな作業なのですが後回しにしました。
実装に煮詰まったときの気晴らしとしてやるつもりなので結局すぐにできてしまいそうです。
GitHub Actions と組み合わせるとフロントエンドの PR ごとにプレビューチャネルにデプロイできるらしいと知ったのでこれも試してみたいです。

やってみたいこと

複数ファイル → module 対応

module を使ったときの挙動を確かめたくなることが多いので個人的には実装したいと思っていますが、技術的に厳しそうです。
Monaco Editor を使う形にはなりそうなんですが、そうなると Next.js を捨てて完全な SPA として作り変えた方がシンプルになりそうです。そして素の React.js を書くなら create-react-app があまり好きじゃないので webpack と向き合うことになりますし、また service worker あたりの理解もちゃんと理解する必要がありそうなので、今の自分のフロントエンド力ではかなり時間がかかりそうです。

やれなさそうなこと

Terraform の対応バージョンを増やす

各マイナーバージョンに対して1つか2つのパッチバージョンが限界かなと思っています。もちろんコンテナにバイナリを入れるだけなので難しいことは何もないのですが、1GB 超えの alpine コンテナを動かしたくはないなあという気持ちが勝ってしまいそうです。
ただ、マイナーバージョンごとに URL のパスを切って転送する Cloud Run のサービスを分ければ可能かもしれないな…と書いてて思いました。まあパッチバージョンの違いでは文法とかそこまで変わらないからそこまで嬉しくはなさそうですね。

パフォーマンス向上

Cloud Run には結構無料枠がありますが、常に1台以上起動しているとさすがに使い切ってしまうのでリクエストがないときはコンテナが落ちるようにしています。
そのためコールドスタート分のレイテンシがかかってしまうのと terraform init && terraform apply が普通に時間がかかるので THe Go Playground のようなサクサク感は出せないかなと思っています。(スペック上げるのにも結局お金がかかる…)

おわりに

いろいろなツールの素振りになるし、学ぶことも多いので個人開発って素晴らしいなという感想しかありませんでした。
もう CSS 触りたくないというのが唯一のネガティブな感情です。
何か良くない実装をしていたりしたら教えてもらえるととても喜びます…!