🔌

アーキテクチャ図だけ描いてTerraformはGoogle Cloud Developer Cheat Sheetに書いてもらおう

2024/04/12に公開

初めての方は、初めまして。そうでない方も、初めまして。クラウドエース SRE 部で Professional Cooking Architect をしている zeta です。私はドンドコ島の充実度をオンライン1位(多分)にしましたが皆さんいかがお過ごしでしょうか。

はじめに

クラウドエースの SRE 部は Google Cloud のインフラの面倒を見ることが主な業務です。世の中の多くの企業の似たような役割を持つ部署でもそうだと思いますが、インフラの設計・構築・運用といったフェーズを行っていきます。こういったインフラエンジニア的なことをやっていると、みなさんも一度ぐらいは「設計だけやったら勝手にインフラ構築されねーかな〜」なんて思ったことはあるのではないでしょうか。技術の力でコンピュータに働かせてサボるというのは IT エンジニア開闢以来ずっと存在し続けた悲願であり、エンジニアリングのモチベーションの一つです。今回は、「俺の思った通りにコンピュータがシステムを勝手に作ってくれる」という夢物語に近づけるかもしれないサービス、 Google Cloud Developer Cheat Sheet を使ってみます。

Google Cloud Developer Cheat Sheet

Google Cloud Developer's Cheat Sheet[1] は、 GitHub レポジトリの README[2] によると、 Google Cloud ファミリーの全製品を4単語で簡単に説明したものが並んでいるチートシートとのことです。この記事の執筆時点ではマイナーバージョン(0.2.3)のサービスであり、ドキュメントはまだ見当たらず、名称の表記揺れも見られます。ページを開くとこのように Google Cloud のサービスを種別ごとにまとめたタイルのビューが表示されます。

Computeの種別をクリックするとタイルが裏返ってそれぞれのサービスを4単語で説明したフレーズが表示されます。

各サービスのタイルをクリックするとサービスのドキュメントに飛びます。

今回使う機能はこのサービスが一覧で載っているチートシートの機能ではなく、 上部メニュー右端にある Architecture と書かれたタブをクリックすると使える製図ツール[3]です。

製図ツール

このツールの名称が不明なので、非常に味気ないですが本記事では便宜上「製図ツール」と呼びます。製図ツールは Google Cloud のサービスで組むシステムのアーキテクチャ図を描くことができます。

画面左側のパレットから、 Google の推奨アーキテクチャや各サービスをドラッグアンドドロップでキャンバスに展開して使います。試しに Reference Artchitectures にある Auto ML Model を展開してみました。展開されたアーキテクチャ内の各コンポーネントや矢印などをダブルクリックすると編集できるので、これをベースに改造してアーキテクチャを作っていくということもできそうです。

次に Predefined Architectures から選んで展開してみます。ここには Google Cloud の基礎的なアーキテクチャが並んでいるようです。3-Tier AppというWeb3層構造っぽいやつを展開してみます。アーキテクチャの構成について軽い説明文が出てきます。

Comfirmをクリックするとパラメータを入れる画面が出てきます。


一旦後回しにして Cancel をクリックするとアーキテクチャが表示されました。編集もできます。Cloud Run → Cloud Run → Cloud SQLという構成の3層アーキテクチャのようですね。

もちろん1からアーキテクチャを作っていくこともできます。
パレットの Deployable Resouces などのカテゴリからパーツを探すこともできますし、パレット上の検索窓から検索することもできます。ただ、検索ワードを変更すると検索結果が一つずつアニメーションで入れ替わるので待ち時間が長いです。

 
 
 
ところで皆さん気づきましたか?

なんとアーキテクチャ図を描いただけなのに下の方に Terraform コードを生成してくれそうな夢のようなボタンが現れました。

早速押してみるとエラーが出ました。パラメータをある程度埋めないと Terraform コードは生成してくれないようです。実際には詳細設計レベルのことをやらないといけなくて、まぁそう甘くはないですね。

適当にパラメータを埋めてもう一度 Generate Terraform ボタンを押すと...

Terraformをデプロイするためのなんかすごいワンライナーが出てきます。ざっくり読み解くと、Cloud Storageに生成されたコードの圧縮ファイルがあって、それのダウンロードと展開をして、予め用意されている Cloud Functions の関数で deploy 用の Python スクリプトを動かすみたいな感じですかね。
Download terraformのボタンで生成されたコードの圧縮ファイルをダウンロードできます。展開した内容は下記のようになっています。

生成されたTerraformコード
dir_structure
google/
    ┣sql_database/
        ┣provider.tf
        ┣sql_database_instance.tf
    ┣cloud_run_service.tf
    ┣compute_global_address.tf
    ┣provider.tf
    ┣service_networking_connection.tf
deploy_controller.py
destroy.py
sql_database/provder.tf
resource "google_cloud_run_service" "tfer--<project_id>-asia-northeast1-svc-a-0" {
  location = "asia-northeast1"
  name     = "svc-a"
  project  = "<project_id>"
}

resource "google_cloud_run_service" "tfer--<project_id>-asia-northeast2-svc-b-1" {
  location = "asia-northeast2"
  name     = "svc-b"
  project  = "<project_id>"
}
sql_database/sql_database_instance.tf
resource "google_sql_database_instance" "tfer--<project_id>-testdb-instance-asia-northeast1-0" {
  database_version = "MYSQL_8.0"
  name             = "testdb-instance"
  project          = "<project_id>"
  region           = "asia-northeast1"

  settings {
    ip_configuration {
      ipv4_enabled    = "false"
      private_network = "projects/<project_id>/global/networks/default"
      require_ssl     = "false"
    }

    tier = "db-custom-1-3840"
  }
}
cloud_run_service.tf
resource "google_cloud_run_service" "tfer--<project_id>-asia-northeast1-svc-a-0" {
  location = "asia-northeast1"
  name     = "svc-a"
  project  = "<project_id>"
}

resource "google_cloud_run_service" "tfer--<project_id>-asia-northeast2-svc-b-1" {
  location = "asia-northeast2"
  name     = "svc-b"
  project  = "<project_id>"
}
compute_global_address.tf
resource "google_compute_global_address" "tfer--<project_id>-testdbvpcpeering-0" {
  address_type  = "INTERNAL"
  name          = "testdbvpcpeering"
  network       = "projects/<project_id>/global/networks/default"
  prefix_length = "24"
  project       = "<project_id>"
  purpose       = "VPC_PEERING"
}
provider.tf
provider "google" {
  project = ""
}

terraform {
	required_providers {
		google = {
	    version = "~> 4.33.0"
		}
  }
}
service_networking_connection.tf
resource "google_service_networking_connection" "tfer--<project_id>-testdb-0" {
  network                 = "projects/<project_id>/global/networks/default"
  reserved_peering_ranges = ["${google_compute_global_address.tfer--<project_id>-testdbvpcpeering-0.name}"]
  service                 = "servicenetworking.googleapis.com"
}
deploy_controller.py
#!/usr/bin/python3
import os
import subprocess
import re
import shutil
import sys
import requests

# Note to developer: This script will be deprecated once blueprint controller is introduced, all deployments can then be done through the blueprint controller

def execute_command(command):
    p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
    out, err = p.communicate()
    if p.returncode != 0:
        # print("bitcoin failed %d %s %s" % (p.returncode, out, err))
        lines = err.splitlines()
        for line in lines:
            print(line.decode())
        raise
    lines = out.splitlines()
    for line in lines:
        print(line.decode())

def get_priority(directory_name:str):
    if directory_name == "sql_database":
        return 1000
    else:
        return 0

def print_help_text():
    print(" \n\n \
        Looks like you have used teams and environments. Note that every team\n\
        and environment needs to maintain their own terraform states.\n \
        You can deploy these resoruces by navigating to the team/environment directory\n\
        and executing the following commands in order:\n\
        - For initializing terraform: `terraform init`\n\
        - For planning: `terraform plan`\n\
        - For deploying: `terraform apply`\n\
    ")

def execute_terraform():
    print("Executing terraform init...")
    execute_command("terraform init -no-color")
    print("Executing terraform plan...")
    execute_command("terraform plan -no-color")
    print("Executing terraform apply...")
    execute_command("terraform apply -auto-approve")

def can_skip(dirs):
    if len(dirs) == 0:
        return False
    elif len(dirs) == 1:
        if ("sql_database" in dirs) or (".terraform" in dirs):
            return False
    elif len(dirs) == 2:
        if (".terraform" in dirs and "sql_database" in dirs):
            return False
    return True

def main():
    tf_gen_backend = os.getenv("TF_GENERATION_BACKEND")
    if tf_gen_backend == None:
        print("warning: TF_GENERATION_BACKEND environment variable not set, skipping registration with analytics")
    else:
        print("registering deployment with analytics")
        try:
            r = requests.get(f"{tf_gen_backend}/analytics",timeout=5)
            if r.status_code != 200:
                print("request failed, skipping registration with analytics")
        except Exception as e:
            print("error while trying to register with analytics",e)


    for root, dirs, files in os.walk("./google", topdown=True):
        dirs = sorted(dirs, key=get_priority)
        tf_found = False

        if not can_skip(dirs):
            cwd=os.getcwd()
            print(cwd+"/"+root+"/")
            os.chdir(cwd+"/"+root+"/")
            pattern = re.compile(".*\.tf")
            for file in files:
                if pattern.match(file):
                    tf_found = True
                    break
            if tf_found:
                try:
                    execute_terraform()
                    os.chdir(cwd)
                except Exception:
                    sys.exit(1)

            print(dirs)
            if "sql_database" in dirs: 
                tf_found = False
                os.chdir(cwd+"/"+root+"/"+"sql_database")
                for r, d, f in os.walk("./", topdown=True):
                    for fi in f:
                        if pattern.match(fi):
                            tf_found = True
                            break
                if tf_found:
                    try:
                        execute_terraform()
                        os.chdir(cwd)
                    except Exception:
                        sys.exit(1)
            break
        else:
            print_help_text()
            return
        

if __name__ == "__main__":
    main()
destroy.py
#!/usr/bin/python3
import os
import subprocess
import re
import shutil
import sys
import requests

# Note to developer: This script will be deprecated once blueprint controller is introduced, all deployments can then be done through the blueprint controller

def execute_command(command):
    p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
    out, err = p.communicate()
    if p.returncode != 0:
        # print("bitcoin failed %d %s %s" % (p.returncode, out, err))
        lines = err.splitlines()
        for line in lines:
            print(line.decode())
        raise
    lines = out.splitlines()
    for line in lines:
        print(line.decode())

def get_priority(directory_name:str):
    if directory_name == "sql_database":
        return 1000
    else:
        return 0

def print_help_text():
    print(" \n\n \
        Looks like you have used teams and environments. Note that every team\n\
        and environment needs to maintain their own terraform states.\n \
        You can deploy these resoruces by navigating to the team/environment directory\n\
        and executing the following commands in order:\n\
        - For initializing terraform: `terraform init`\n\
        - For planning: `terraform plan`\n\
        - For deploying: `terraform apply`\n\
    ")

def execute_terraform():
    print("Executing terraform init...")
    execute_command("terraform init -no-color")
    print("Executing terraform plan...")
    execute_command("terraform plan -no-color -destroy")
    print("Executing terraform destroy...")
    execute_command("terraform destroy -auto-approve")

def can_skip(dirs):
    if len(dirs) == 0:
        return False
    elif len(dirs) == 1:
        if ("sql_database" in dirs) or (".terraform" in dirs):
            return False
    elif len(dirs) == 2:
        if (".terraform" in dirs and "sql_database" in dirs):
            return False
    return True

def main():
    tf_gen_backend = os.getenv("TF_GENERATION_BACKEND")
    if tf_gen_backend == None:
        print("warning: TF_GENERATION_BACKEND environment variable not set, skipping registration with analytics")
    else:
        print("registering deployment with analytics")
        try:
            r = requests.get(f"{tf_gen_backend}/analytics",timeout=5)
            if r.status_code != 200:
                print("request failed, skipping registration with analytics")
        except Exception as e:
            print("error while trying to register with analytics",e)


    for root, dirs, files in os.walk("./google", topdown=True):
        dirs = sorted(dirs, key=get_priority)
        tf_found = False

        if not can_skip(dirs):
            cwd=os.getcwd()
            print(cwd+"/"+root+"/")
            os.chdir(cwd+"/"+root+"/")
            pattern = re.compile(".*\.tf")
            for file in files:
                if pattern.match(file):
                    tf_found = True
                    break
            if tf_found:
                try:
                    execute_terraform()
                    os.chdir(cwd)
                except Exception:
                    sys.exit(1)

            print(dirs)
            if "sql_database" in dirs: 
                tf_found = False
                os.chdir(cwd+"/"+root+"/"+"sql_database")
                for r, d, f in os.walk("./", topdown=True):
                    for fi in f:
                        if pattern.match(fi):
                            tf_found = True
                            break
                if tf_found:
                    try:
                        execute_terraform()
                        os.chdir(cwd)
                    except Exception:
                        sys.exit(1)
            break
        else:
            print_help_text()
            return
        

if __name__ == "__main__":
    main()

最低限という感じですが Terraform コードが生成されています。これがそのまま製品コードとして使用できるかというと、変数の分離やモジュール構成などもう少し考慮することがあると思います。また、 Python スクリプトでデプロイをする前提になっていますが、クラウドエースでは Cloud Build のビルド構成ファイルの yaml でこうした設定を記述することが多く、こうしたルールや文化にそぐわないということもあると思います。ただ、使い捨ての検証環境を作るぐらいの用途なら便利かもしれません。

使い心地

Google Cloudサービスを使ったアーキテクチャの作図には draw.io などを使う方が多いかと思いますが、そのあたりとの比較をしながら使い心地について所感を述べます。

製図ツールとして

製図ツールとしての書き味はdraw.ioと比べてかなり良いと思います。学生時代に回路図エディタで論理回路を描いたことがありますが、ああいったソフトに近い UI をしているなと感じます。

折れ線を描くときは、線を折るグリッドをクリックすればそこで線を別方向に向けることができます。矢印の終着点をクリックしたときもこの挙動になるので、終点で止めるときはダブルクリックする癖をつけると良さそうです。
また、 draw.io だと線を繋いだ後にパーツを動かすと明後日の方向に複雑な曲がり方をしてしまって修正が大変だったりしますが、このツールはそういったことが起こらず、直感的な動き方をしてストレスがないなと感じます。線が基本的にグリッドの上に乗るようになっているので、 draw.io を使っているとたまにある、頑なに線がまっすぐになってくれない現象に苛まれることも今回使っているうちにはなかったです。

draw.io や Google Slide と比較してストレスなく作図でき、製図ツールとして圧倒的に UI の完成度が高いと思います。

図の見た目

draw.ioで使える Google Cloud サービスのカードに比べて、カードに書ける情報量が多い分サービスアイコンが小さいです。これによって各カードの差分がパッと見で分かりづらく、出来上がる図の視認性という面では draw.io の方が優れているように思います。

有用性

図上で矢印を繋いだら 生成される Terraform コードでも Google Cloud サービス間の接続もいい感じにやってくれるとうれしいですが、そういったことはなく、自分でパラメータを埋めてネットワーク等の連携をする必要があります。例えば、ロードバランサーのバックエンドに線で繋いだ Cloud Run を自動的に入れてくれたりはしません。また、最終的に Terraform コードが生成できるようになるまでに、 Cloud Console でポチポチ構築するのと同じぐらいパラメータを入れないといけないし、それらが仮でも決まっている必要があります。とはいえ、 Cloud Console で構築する各サービスのページを転々としながら値を入れていくのに比べて、アーキテクチャ図を見ながら統合的なインターフェースでパラメータ入力ができるのということは、頭の整理という面では便利そうです。
Terraformコード生成については前述した通り、そのまま製品コードとして使うには課題があると思いますが、使い捨ての検証環境を作るぐらいの用途なら有用だと感じました。

注意点

この Google Cloud Developer Cheat Sheet の製図ツールは Web アプリケーションとして提供されていますが、ここに大きな罠があります。draw.ioや Google Workspace のアプリケーションなどは、編集した内容が自動でクラウドに保存されて、ページのリロードが走ってもデータが維持されています。しかしながら、この製図ツールはリロードによって編集したデータが飛ぶようです。ある程度アーキテクチャ図を編集した段階で発行したシェア用のリンクを数日後に開いたら、誰かが描いた別の図になっていました。
確実に作業したデータを保存するためには、デスクトップアプリで作業するようにプロジェクトファイルの保存をする必要があります。

左上のメニューの右から2番目のエクスポートボタンを押すとこのような画面が出ます。

Save to Google Driveは Google Drive の Admin 権限が必要らしく検証できませんでしたが、 Save to disk でプロジェクトファイルをローカルに保存できます。プロジェクトファイルは excalidraw なる聞き慣れない拡張子です。オープンソースで開発されていて VSCode の拡張もあるようです[4]。この excalidraw ファイルを左上メニューの右から3番目のロードボタンを使って続きから作業を再開します。

まとめ

今回は Google Cloud Developer Cheat Sheet の製図ツールとそれに付随する Terraform コード生成機能を使ってみました。製図ツールとして完成度が高い一方で、生成されたコードの完成度やスタイルなどは、製品として求められる要求に応えられるものになるよう修正が必要だと感じました。また、まだマイナーバージョンしか存在しないサービスということもあり、挙動に不安定なところが見受けられます。Terraformコードを生成するボタンも、実は表示される時とされない時があり、運良く出ている時にスクショを撮っています。リロードが走ると作業中のデータが消えるのも正直不便です。製図ツールとしては良いサービスだと思うので、今後のアップデートでこのあたりの課題を解決して有用なサービスになれば良いなと思います。

脚注
  1. https://googlecloudcheatsheet.withgoogle.com/ ↩︎

  2. https://github.com/priyankavergadia/google-cloud-4-words#the-google-cloud-developers-cheat-sheet ↩︎

  3. https://googlecloudcheatsheet.withgoogle.com/architecture ↩︎

  4. https://github.com/excalidraw/excalidraw ↩︎

Discussion