Terraformプロジェクトにおける開発・テストフローのフレームワーク化 v0.2.0
今回のバージョンアップによって、infra-testing-google-sampleプロジェクトのモジュールでテストコードの実装の仕組みが導入された。
その他にもdbモジュール内で定義していたVPCピアリング関連の設定について、networkモジュールで定義しても問題ないことが判明したため、引っ越しを行なった。
VPCピアリングのリソースがdbモジュール(tier2)からnetworkモジュール(tier1)に移動したことで、テストコード実行時に発生していたリソース削除時の問題を考えなくて良くなったのと、dbモジュールのapply時間が短縮されるといった改善に繋がっている。
そして今回のバージョンアップで一番重要なアップデートが、すべてのテスト操作をワンライナーのスクリプトで実行できるようになった点である。
これに伴い、個人環境(sandbox)での開発・テスト手法がほぼフレームワーク化されたので、今回追加したスクリプトの紹介を交えつつ、infra-testing-google-sampleプロジェクトでインフラ開発を実際に行う際の流れを説明していきたい。
開発・テストフロー
プロジェクトのチェックアウト
まず以降の内容を自分の環境で試す場合は、Githubから以下のコマンドでプロジェクトのチェックアウトを行ってほしい。
(さらに空のGCPプロジェクトの用意やサービスAPIの有効化等については各自で行う必要がある)
$ cd /path/to/work-dir
# 以下のどちらかのコマンドでローカルにclone
$ git clone https://github.com/erueru-tech/infra-testing-google-sample.git
$ gh repo clone erueru-tech/infra-testing-google-sample
# 新規チェックアウトであれば不要だが、既にclone済みの場合必要
$ git fetch
# この記事に紐づくバージョンに切り替え
$ git checkout -b 0.2.1 refs/tags/0.2.1
sandbox環境のtier1リソースを最新に更新
infra-testing-google-sampleベースのプロジェクトでインフラの開発を行うにあたって、まず一番最初にやらなければいけないのは自分の個人環境のtier1のstateをmainブランチの状態と同期することである。
1人でプロジェクト開発を行なっている場合、問題になるケースはほとんどないとは思うが、チームで開発をしている場合、他の開発者がテストコードの依存解決に使用しているtier1の定義を変更していた場合、古い状態のままの個人環境でテストを行うと、正しくない結果となってしまうためである。
そのためまずリモートのmainブランチから変更差分を取り込んでから、sbx/tier1ディレクトリ内でその変更をapplyする必要がある。
なお今回のバージョンからはterraform applyコマンドを直接実行することはせず、代わりにtier1ディレクトリ内のapply.shを使ってプロビジョニングを行う。
なおこのapply.shは以下のようにenvironments直下のscriptsディレクトリからシンボリックリンクを張ることで参照している。
$ tree terraform/environments/
terraform/environments/
├── sbx
│ ├── tier1
│ │ ├── apply.sh -> ../../scripts/apply.sh
│ │ ├── ...
│ ├── tier2
│ │ ├── apply.sh -> ../../scripts/apply.sh
│ │ ├── destroy.sh -> ../../scripts/destroy.sh
│ │ ├── ...
├── test
│ ├── tier1
│ │ ├── apply.sh -> ../../scripts/apply.sh
│ │ ├── ...
│ ├── tier2
│ │ ├── apply.sh -> ../../scripts/apply.sh
│ │ ├── destroy.sh -> ../../scripts/destroy.sh
│ │ ├── ...
| ...
├── scripts
│ ├── apply.sh
│ └── destroy.sh
...
すでにglobals.tfなどで大量にシンボリックリンクを使用していて、複雑さが増すため本当は使用したくないのだが、tier2ディレクトリやtestディレクトリ内でも全く同じ内容のスクリプトを作らなければいけなくなるため、DRYであることを優先してこのようになっている。
apply.shの他にもdestroyコマンドの実行をラップしているdestroy.shを用意しているが、これはdestroyを行う予定のないtier1ディレクトリでは不要なので配置せず、tier2ディレクトリでのみの使用を想定した状態となっている。
ちなみに本番環境(prod)とstaging環境(stg)のディレクトリではdestroy.shの実行は当然厳禁であるため不要だが、apply.shなら置いてもいいのではないかと最初は考えていた。
しかし、これらの環境に対してローカルから簡単にapplyを実行出来る状態にしたくはないため、現時点では置かない方針としている。
apply.shは以下のような処理となっている。
#!/bin/bash
set -eu
# validation checks
if [[ $TF_VAR_env != "test" && $TF_VAR_env != sbx-[0-9a-z]* ]]; then
echo "The value of \$TF_VAR_env must be 'test' or 'sbx-*', but it is '$TF_VAR_env'."
exit 1
fi
# run apply command
cd $(dirname $0)
terraform init -backend-config="bucket=$TF_VAR_service-$TF_VAR_env-terraform" -upgrade
if [[ -z ${CI:-} ]]; then
terraform apply
else
terraform apply -auto-approve
fi
まず最初にtest環境とsandbox環境以外からapply.shを実行しようとするとエラーになるバリデーションチェックが行われている。
次のcd $(dirname $0)
はtier1ディレクトリ内でapply.shを実行する場合は不要な処理だが、プロジェクトルートなどの他のディレクトリからでもこのシェルを実行出来るようにするために必要な処理となる。
あとは、state管理バケットを指定してterraform initコマンドを実行してから、applyを行なっているだけのシンプルなスクリプトとなっている。
ちなみにif [[ -z ${CI:-} ]]; then
の条件はローカル環境でapply.shを実行した場合は真となり、Github ActionsやCircleCI、Tranvis CI等といった主要なCI/CDツール上で実行するとCI=true
のような環境変数が自動的に付与されるために偽となる。
つまりローカルではapply時に'yes'を入力しないと実行されないのに対して、CI/CD時は自動的にapplyが行われるという意味になる。
そしてapplyコマンドの実行は以下のようになる。
$ TF_VAR_service=infra-testing-google-sample \
TF_VAR_env=sbx-e \
./apply.sh
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: # 実行計画に問題が無ければyesと入力
apply.sh実行時に指定する変数がserviceとenvだけとなっているが、これは他の必須入力の値はすべてsbx-tier1.auto.tfvarsで定義されているためである。
(この辺りの設定に関してはv0.1.0のドキュメント記事で説明されている)
ちなみに、destroy.shやこの後モジュールの開発で出てくるスクリプトについても、実行の際にserviceとenvだけを環境変数として渡すだけとなる。
それであれば.bash_profileなどに
export TF_VAR_service=infra-testing-google-sample
export TF_VAR_env=sbx-e
と定義しておけば、あとはスクリプト名だけで実行できるようになるのでかなり良いのではと考えたが、この方法は複数のプロジェクトの開発を行なっている場合に別のプロジェクトのリソース定義を誤ってapplyする事故が起きる可能性があるため、やめた方がいい。
(ディレクトリごとに環境変数を確実に切り替えてくれるツールが無いか調べた方よさそう)
モジュールの作成
事前準備の話が少し長くなってしまったが、ここからはインフラ開発とテストの具体的な手順について話していく。
今回はお題として、GCSバケットを作成するstorageモジュールを実装したのち、それをsandbox環境で利用するといった流れで説明を行っていく。
まずstorageモジュールの作成だが、scripts/create_terraform_module_template.shを使用することで、モジュールのスケルトンを作成することができ、すぐに実装を開始することができる。
なお、生成されたディレクトリ内には今まで説明したことがないシェルスクリプトが含まれているが、この後すぐに説明される。
$ cd /path/to/infra-testing-google-sample
$ ./scripts/create_terraform_module_template.sh storage
$ cd terraform/modules/storage/
$ tree -a
.
├── tests
│ ├── main.tftest.hcl
│ └── variables.tftest.hcl
├── _tfvars.sh
├── test.sh -> ../scripts/test.sh
├── apply_destroy.sh -> ../scripts/apply_destroy.sh
├── globals.tf -> ../../globals.tf
├── module-globals.tf -> ../../module-globals.tf
├── main.tf
├── variables.tf
└── outputs.tf
2 directories, 10 files
次にGCSバケットを作成するためのモジュールを以下のように定義する。
$ vi main.tf
# 設定内容は下記
$ vi variables.tf
# 設定内容は下記
$ vi outputs.tf
# 設定内容は下記
resource "google_storage_bucket" "sample" {
name = var.sample_bucket_name
location = "ASIA"
storage_class = "MULTI_REGIONAL"
versioning {
enabled = true
}
uniform_bucket_level_access = true
public_access_prevention = "enforced"
}
variable "sample_bucket_name" {
type = string
default = "erueru-tech-sample-bucket"
}
output "sample_bucket_name" {
value = google_storage_bucket.sample.name
}
GCSバケットを1つ作成するだけのシンプルな定義だが、一点だけ補足するとバケット名を変数化しているのは、テストの際にサービス(test、sandbox)側とモジュール側のテストで同時に同じ定義からバケットを作成する可能性があるためである。
サービス側ではデフォルト値であるerueru-tech-sample-bucketという名前でバケットを作成して、一方モジュール側でテスト実行時に作成されるバケットはプレフィックスにtest-を付けることで、リソース重複によるエラーが起きないようにしている。
(GCSバケットはグローバルリソースであるため、検証等で上記erueru-tech名を使うのは勘弁いただきたい)
モジュールの手動テスト
モジュールの実装が完了したら、次は動作確認を行う必要がある。
一番手っ取り早いのは、モジュールディレクトリ内でapplyを行ったのち、GCPコンソール上で意図した通りの設定でバケットが作成されていることを確認する方法である。
そしてapplyコマンドの実行は、これまた今回のバージョンで追加されたapply_destroy.shと _tfvars.sh を使用して行う。
apply_destroy.shはenvironments側と同様に、modules/scriptsディレクトリ内にあるスクリプトの実体に対してシンボリックリンクを張ることで、各モジュール内で使用できるようにしている。
一方で_tfvars.shは各モジュールごとに個別にファイルを定義していて、このファイルの詳細についてはこの後ですぐに明らかになる。
$ cd /path/to/infra-testing-google-sample
$ $ tree terraform/modules/
terraform/modules/
├── storage
│ ├── _tfvars.sh
│ ├── apply_destroy.sh -> ../scripts/apply_destroy.sh
│ ├── test.sh -> ../scripts/test.sh # 後で説明
│ ├── ...
├── network
│ ├── _tfvars.sh
│ ├── apply_destroy.sh -> ../scripts/apply_destroy.sh
│ ├── test.sh -> ../scripts/test.sh
│ ├── ...
├── db
│ ├── _tfvars.sh
│ ├── apply_destroy.sh -> ../scripts/apply_destroy.sh
│ ├── test.sh -> ../scripts/test.sh
│ ├── ...
└── scripts
├── apply_destroy.sh
└── test.sh
まずapply_destroy.shの実装は以下のようになる。
#!/bin/bash
set -eu
# validation checks
if [[ $TF_VAR_env != "test" && $TF_VAR_env != sbx-[0-9a-z]* ]]; then
echo "The value of \$TF_VAR_env must be 'test' or 'sbx-*', but it is '$TF_VAR_env'."
exit 1
fi
if [[ $1 != "apply" && $1 != "destroy" ]]; then
echo "The argument value must be 'apply' or 'destroy', but it is '$1'."
exit 1
fi
# run apply command
cd $(dirname $0)
terraform init -upgrade
source "./_tfvars.sh"
terraform $1
モジュールのapplyはテストでしか行われないため、test環境やsandbox環境以外から行おうとするとエラーとなるバリデーションチェックが行われているのはenvironments側と同じである。
次のバリデーションでは、このスクリプトはapplyとdestroyコマンドのみの実行を許可している旨を記述している。
ちなみにenvironemnts側とは異なりapplyとdestroyを同居させているのは、モジュール側で作られるリソースはすべてテスト用のものなので、間違って消してしまっても特に問題ないためである。
cd $(dirname $0)
は先ほども説明したとおり、プロジェクトルートなど他のディレクトリからでもこのシェルを実行出来るようにするための処理となる。
terraform init -upgrade
は-backend-configを指定していないため、モジュールディレクトリ内にstateファイルが作成されることになるが、テスト目的で一時的に作成と破棄を行うだけなのでこれで問題ない。
(なおプロジェクトルートの.gitignoreの設定によってstate関連ファイルはVCSにコミットされないようになっている)
そしてsource "./_tfvars.sh"
だが、これはapply時に必要となる変数を定義するためのシェルとなる。
テスト用のGCSバケットを作成する際のバケット名は常にtest-erueru-tech-sample-bucketで固定したいため、_tfvars.shには以下のような定義を行う。
export TF_VAR_sample_bucket_name="test-erueru-tech-sample-bucket"
_tfvars.shはシェルであるため、もちろん環境ごとに値を分岐させることも出来る。
例として、networkモジュールではVPC関連のCIDRの値を環境ごとに違う値が自動的に設定されるようにしている。
...
if [[ $TF_VAR_env == "test" ]]; then
export TF_VAR_subnet_ip="10.3.101.0/24"
export TF_VAR_peering_network_address="10.3.102.0"
elif [[ $TF_VAR_env == "sbx-e" ]]; then
export TF_VAR_subnet_ip="10.4.101.0/24"
export TF_VAR_peering_network_address="10.4.102.0"
fi
なおapply_destroy.shにベタ書きしないのは、この後で説明するtest.shでもterraform test実行時に内部的にapplyを行っていて、ここでもまた同じ環境変数の定義を使うためである。
以上でapply_destroy.shの内容について、分かってもらえたと思う。
モジュールのapplyは以下のコマンドで実行する。
$ TF_VAR_service=infra-testing-google-sample \
TF_VAR_env=sbx-e \
./apply_destroy.sh apply
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
applyが完了したら、GCPコンソール上でバケット名やロケーション、バージョニングの設定、公開設定などが正しく設定されているのが確認できたら、./apply_destroy.sh destroy
コマンドでリソースをクリーンアップすれば手動でのテストは完了となる。
モジュールの自動テスト
最低限手動テストさえ行っていれば、サービス側でモジュールを利用し始めて問題ないものの、同じような手動テストを繰り返し実行する手間の削減や、CI/CD時にリグレッションテストを実行することを考えると、テストコードは実装しておいた方が良い。
先日の記事にてTerraformのテストコードを実装する方法を説明したが、ここでも同様の方法でstorageモジュールのテストコードを以下のように実装する。
# var.sample_bucket_nameのデフォルト値は'erueru-tech-sample-bucket'である
run "assert_sample_bucket_name_1" {
command = plan
assert {
condition = var.sample_bucket_name == "erueru-tech-sample-bucket"
error_message = "The default var.sample_bucket_name value must be 'erueru-tech-sample-bucket'."
}
}
run "apply_storage" {
# module.storage #
# バケット名がtest-erueru-tech-sample-bucketであることを確認
assert {
condition = output.sample_bucket_name == "test-erueru-tech-sample-bucket"
error_message = "The output.sample_bucket_name value isn't expected. Please see the above values."
}
# ...
}
実用的にはバケットのバージョニングの設定や公開設定についてのテストを書いておいた方がいい気がするが、ここでは話の本筋ではないため省略している。
以上でテストコードの実装が完了したので、terraform testコマンドを実行することになるが、これはapplyの時と同様にtest.shというヘルパースクリプトを使ってテストの実行を行う。
test.shの実装は以下のようになる。
#!/bin/bash
set -eu
# validation checks
if [[ $TF_VAR_env != "test" && $TF_VAR_env != sbx-[0-9a-z]* ]]; then
echo "The value of \$TF_VAR_env must be 'test' or 'sbx-*', but it is '$TF_VAR_env'."
exit 1
fi
# run tests
cd $(dirname $0)
terraform init -upgrade
terraform validate
if [[ -z ${CI:-} ]]; then
terraform fmt
else
terraform fmt -check
fi
terraform test -filter=tests/variables.tftest.hcl
source "./_tfvars.sh"
terraform plan
terraform test -filter=tests/main.tftest.hcl
terraform initまではapply_destroy.shと基本的に同じで、以降はモジュールの設定に対するテストを連続的に行っている。
まず、手始めにvalidateやfmtで構文チェックやフォーマット(チェック)を行ったのち、variables.tftest.hclのテストコードを実行しているが、途中でエラーが発生してテストを再実行しなければならなくなる可能性を考えると、このように実行完了が速い順から定義する必要がある。
_tfvars.shは既にapply_destroy.shで触れているため特に詳しくは説明はしないが、source "./_tfvars.sh"
の定義位置については気をつけなければいけない。
仮にシェルの一番上に_tfvars.shの読み込み処理を定義しようとすると、variables.tftest.hclのテスト時に影響を与えてテスト結果が意図しないものとなってしまうので、その点は注意が必要となる。
terraform planについてはterraform testコマンドで内部的に実行されているため不要な気もしているが、いったんは様子見で記述している。
そして最後にterraform testで実際にリソースを作成するテストを行なって、テストは完了となる。
test.shの実行コマンドは以下のようになる。
$ TF_VAR_service=infra-testing-google-sample \
TF_VAR_env=sbx-e \
./test.sh
sandbox環境にモジュールを追加
これまでの作業でstorageモジュールは十分にテストが行われているため、sandbox環境でいつでも使える状態である。
なお、すべてのmoduleおよびrosourceはtier1とtier2のどちらに所属させるかを、毎回決めなければいけない。
今回追加したGCSバケットの用途次第では判断が変わるかもしれないものの、バケットを作るだけなら無料であることと、tier2は常に削除対象となるのに対して将来的に削除してはいけないリソースに変化するリスクなどを考えると、個人的にはtier1でいいと考えている。
しかし、記事の説明の都合もあり、今回はtier2側にstorageモジュールを定義することにしたい。
まず、storageモジュールで作成するGCSバケットがprod環境からsandbox環境までのすべての環境で必要になるリソースであると仮定した場合、tier2-main.tfにstorageモジュールを定義する必要がある。
なお、tier2-main.tfはシンボリックリンクで参照しているファイルであるため、一度リソースの定義を行うだけで全環境分の定義が完了する。
# これまでのプロジェクト開発で既に定義済みのモジュール
module "db" {
source = "../../../modules/db"
service = var.service
env = var.env
vpc_id = data.terraform_remote_state.tier1.outputs.vpc_id
availability_type = var.availability_type
}
# 今回開発したモジュールの定義を追加
module "storage" {
source = "../../../modules/storage"
service = var.service
env = var.env
}
あとはtier1の時と同様にapply.shでプロビジョニングを行なったのち、GCPコンソール上でサービスに必要なGCSバケットがきちんと作成されていることをチェックして、さらにアプリケーションなどから要件通りにバケットを利用できることを確認できれば、あとは今回開発したstorageモジュールのコードをVCSにコミットして、pushおよびプルリクエストの作成を行えば開発作業は完了となる。
最後にtier2で不要な課金が発生しないよう、以下のdestroy.shでクリーンアップを行う。
#!/bin/bash
set -eu
# validation checks
if [[ $TF_VAR_env != "test" && $TF_VAR_env != sbx-[0-9a-z]* ]]; then
echo "The value of \$TF_VAR_env must be 'test' or 'sbx-*', but it is '$TF_VAR_env'."
exit 1
fi
# run destroy command
cd $(dirname $0)
terraform init -backend-config="bucket=$TF_VAR_service-$TF_VAR_env-terraform" -upgrade
if [[ -z ${CI:-} ]]; then
terraform destroy
else
terraform destroy -auto-approve
fi
apply.shとの違いはapply
の部分をdestroy
に置換しただけとなっているため、特に説明の必要はないかと思われる。
そして最後に以下のコマンドで実行を行う。
$ TF_VAR_service=infra-testing-google-sample \
TF_VAR_env=sbx-e \
./destroy.sh
...
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: # 実行計画に問題が無ければyesと入力
以上で今回追加された全てのヘルパースクリプトの使い方と、このプロジェクトを利用して開発・テストを行う方法を概ね説明できたかと思う。
sandbox環境におけるテスト
これまでsandbox環境は手動テストを行う環境であると簡単に説明してきたが、工夫やアイディア次第では更にさまざまなテストが可能になる環境でもある。
sandbox環境はインフラ開発者一人一人に対して用意することが望ましい一方で、アプリケーション開発チームにも一つだけでいいのでsandbox環境を提供することで、アプリ改修に伴うインフラ変更依頼をドキュメントではなく、ある程度正しく動くインフラのコードとして提供してもらうような体制にすることが可能となる。
もちろん、開発チームのユーザアカウントからは特定サービスのsandbox環境に対してだけしかプロビジョニングを行えないよう、厳密にロールを管理する必要がある点などは注意しなければならない。
(またプロジェクト破損による再構築もある程度は覚悟しなければいけないかもしれない)
しかしアプリチームがインフラのことをあまりよく分からないために、共有された変更依頼がぼんやりとしていて、インフラチームがどのような修正をしていいか結局アプリのコードを確認しないと分からないといった、しばしば起きる問題に対しての有効な解決策の一つとなりうる。
あるいはsandboxをペアプロ環境として使うことで、お互いの改修意図を伝え合いながらアーキテクチャに関する認識の齟齬を無くして開発を進めるといった用途にも使える。
サービス開発において、工期終盤にいきなり各チームが開発したものを一気に結合するよりも、すべての職種の実装の意図を事前にsandbox環境上で一つに繋ぎ合わせることが出来ていれば、結合時に発生するバグは確実に減らすことが出来るので、やってみる価値は十分あると考えている。
他にも、sandbox環境は複雑でクリティカルなインフラリリースのリハーサルにも使えると想定している。
基本的にVCSのmainブランチは本番環境と同期されているはずであるため、コードをローカルにpullしてtier1とtier2に対してapply.shを実行すれば、本番と同じかあるいはかなり近いインフラ状態となるはずである。
そこからリリース手順書を元にリリースのリハーサルを行うことで、手順の間違いや抜けに気付けるといった活用方法を自分は考えている。
なお、有効なテストを行うためにはコストの問題はあるものの、本番環境と同等のデータをsandbox環境上にいつでもリロードできるような仕組みがあると尚良い。
もしそれが可能ならば、sandbox環境上でより本番環境に近い状態での負荷試験や、Playwright等によるブラウザを使用したユーザ操作のe2eテストが、より効果的に実施出来るようになるはずである。
sandbox環境の可能性
ところでsandbox環境の利用アイディアをここまで考えてみて、sandbox環境は常時稼働環境であるstaging環境の代替となりえるのではないかと思い始めた。
基本的にstaging環境は、tier2が常時稼働しているsandbox環境と言い換えることも出来る。
それならばテストに必要な時だけリソースを起動するsandboxに置き換えることで、更なるコストカットに繋がるのではと思ったが、大規模なデータを格納するGCPサービス(e.g. BigQuery, Bigtable, Vertex AI Search for Retail)を使用していて、かつテスト時にも大量のデータを必要とする場合、すぐに用意するのが難しいという問題に対処する必要があるのと、マイクロサービスの連携先がテスト連携を行いたい場合に都度やり取りを行う必要があるといった課題等がすぐに思いつくことから、やはり一筋縄ではいかないかもしれない。
他、test環境も名前と役割を変えただけのsandbox環境である。
もし仮にstaging環境をsandbox環境で代用出来るなら、サービスを構成するのに必要な環境は常時稼働のprodが一つと開発時に必要な役割分のsandboxを用意するだけといった、柔軟性の高いものに置き換えることが出来る。
そしてここまでの考えをまとめると、prod、test、sbxの関係は、git-flowやGitHub flowのmain、develop、featureブランチの関係とかなり似ていることに気づいた。
ということは、インフラプロジェクトをGitのワークフローにマッピングして、その知見を応用することで、実は新しいインフラプロジェクト管理方法が誕生するのではないかと、大きなロマンを感じ始めている。
とはいえ、あまりに過激派の発想であり、すでに完成されたエンタープライズアーキテクチャには確実に馴染まないことから、infra-testing-google-sampleでは今後も保守的にprod、stg、test、sbxの四環境での運用を前提として開発を行なっていく予定ではある。
(上記の件については今後メインの課題として体系化・解決する予定はないが、もし何か有益な知見が得られたら都度共有したい)
おわり
今回でかなり具体的にinfra-testing-google-sampleプロジェクトを用いた開発手法やその意図を説明することができた。
この先複雑なケースに遭遇した場合、おそらくはいくつかの点で綻びが生じるだろうが、先のことを考え過ぎても仕方ないのと現状最低限話の辻褄は合っていることから、このまま設計を進めていくことにする。
ところで、次のバージョンについてはCIの実装をするべきか、はたまたクリーンアップやドリフト検知の体系化を行うか、それともe2eテストを導入するべきかまったく決めていない状態なので、次回未定としておく。
続き
Discussion