🦔

VPC 作るだけでも しんどいよ 自動インフラ テスト設計

2024/02/24に公開

ここからはTerraformで構築されたインフラ環境の自動テスト設計/実装という、最大の課題を解いていくことになる。

なお、これまでTerraformのコードに関して説明する際には、説明材料として非常に優秀(シンプル機能/無料検証可能)なGCSバケットだけを用いてきたが、ここからは現実的にありえるアプリケーションの稼働環境をTerraformで構築して、それに対してunit、integration、e2eなどさまざまなテストが実行可能かを検証していかなければいけない。

このテスト対象環境は今のところ、GCP上にVPC、Cloud Run、Cloud SQL(MySQL) をプロビジョニングしたものを予定している。

GCPを普段使っていない人向けにCloud Runを説明すると、WebアプリケーションやAPIをコンテナイメージとしてパッケージングしてWebサービスとしてデプロイすることも出来れば、バッチアプリケーションやシェルを定期的に実行するバッチジョブとしてデプロイすることも出来るといった、汎用的なコンテナイメージ実行環境を提供するサービスである。

自分はしばらくAWS使っていないので知らなかったが、cloud run equivalent in awsで検索すると、AWS App Runnerという同等サービスがあるとのこと。

(Azureはまったく触ったことないので、残念だが何も分からない)

Cloud Runは、自分が使った経験として知識ゼロからの導入がかなり容易だった点と、外部にエンドポイントを公開するケースと内部のネットワークでのみ稼働するアプリケーションといった両方のケースを1つのサービスで表現出来るといった点から、今回の検証対象として採用した。

Cloud SQLは普通のRDBがインストールされたインスタンスをローンチすることが出来るサービスである。

なおMySQLは現代の大規模なデータや大量のトラフィックを扱うシステムでは、あまり利用されなくなってきているため、現実に即した検証環境の構築という点において、適しているとは言えないかもしれない。

一方でCloud SQLはテストの検証対象として面白い性質を持っていたりもする。

まずCloud SQLは同じインスタンス名をしばらくのあいだ再利用できないという問題(参考)を抱えていて、これはapplyとdestroyを大量に繰り返すTerraformのテストコードの実装方法を体系化する上で、必ず対処しなければいけない問題となる。

さらにCloud SQLのインスタンスは起動まで10分以上かかるといったように、インフラのテストが必ず取り扱わなければいけないテスト実行時間への対処について考える良い材料となる。

(インフラのテストコードはこのような理由から、アプリケーションのテストコードの実行に比べてはるかに時間がかかるが、この問題について革新的な解決方法を提示できている人を自分はまだ見たことがない)

あと何より、調べなくても一番自信を持って説明しやすいということもあってCloud SQL(MySQL)を採用した。

そして最後にVPCだが、これは大半のプロビジョニングリソースの基礎土台となるネットワークを提供するサービスである。

GCP上にVPC、サブネット、ルートをプロビジョニングする場合、Google公式がTerraform Registryに公開しているnetworkモジュールを定義するだけで、検証目的のネットワークであれば簡単に生成出来る。

またGCPのファイアウォールは全ての接続をデフォルトで拒否するようなので、必要になるまで特に設定の必要はない。

ではなぜ記事タイトルのように、VPCを作るだけで悩む必要があるのだろうか?

まずはVPCモジュールの設定例を挙げた上で、次に設計時に浮上した数々の問題について話していく。

VPCモジュール作成

まず、GCPにVPCを構築するモジュールを持つインフラプロジェクトを以下のように作成する。

フォルダ構成はGoogleのTerraformを使用するためのベストプラクティスドキュメントのものを参考にした。

$ mkdir -p /tmp/sample-project/terraform/modules/network
$ vi /tmp/sample-project/terraform/modules/network/main.tf
# 設定内容は下記
...
/tmp/sample-project/terraform/modules/network/main.tf
# 説明の便宜上、main.tfファイルに定義をまとめている
locals {
  service = "infra-testing-sample"
  project_id = join("-", [local.service, var.env])
}

variable "env" {
  type = string
}

variable "region" {
  type = string
  default = "asia-northeast1"
}

module "vpc" {
  source  = "terraform-google-modules/network/google"
  version = "9.0.0"
  project_id   = local.project_id
  network_name = "${var.env}-sample-vpc"
  subnets = [
    {
      subnet_name               = "${var.env}-sample-subnet"
      subnet_ip                 = "10.1.1.0/24"
      subnet_region             = var.region
      subnets_private_access    = "true"
    }
  ]
}

ネットワークの設定はいたって単純で、infra-testing-sampleサービスのテスト環境用のネットワークを10.1.0.0/16と定義した上で、その中に10.1.1.0/24のIPv4範囲を持つprivateなサブネットをasia-northeast1(東京)リージョンに作成している。

subnets_private_access = "true"は、サブネット内のVMインスタンスなどからGoogleの各種APIに対して、Global IP不要で接続するために必要な設定で、おそらく今後要るであろうことから設定している。

ファイアウォール設定はデフォルトだとingressは全ての接続を拒否、egressは全ての通信を許可するのようなのでいったんは問題ない。

ルートについても、0.0.0.0/0をデフォルトインターネットゲートウェイに送る経路が自動的に作成されるので、こちらも設定不要。

おそらくはテスト環境にCloud RunやCloud SQLなどを配置していくうちに、設定の追加や変更は必ず必要になるが、いったんはこの設定から始めるとする。

ちなみにTrivyでこのモジュールのスキャンをかけると大量の指摘がされるが、今はとりあえず気にしない。

ここからはテストコードを書くなり、Cloud Runなどをプロビジョニングすればいいはずなのだが、この定義を書いたところで自分の中では以下のような数々の懸念が出てきてしまい、完全に手が止まってしまった。

導入する企業のネットワーク設計への適合

上記にて、infra-testing-sampleサービスのテスト環境用のネットワークをひとまず10.1.0.0/16と定義したが、実際にサービスに適用をした場合、以下のようなネットワークの定義になることを想定している。

環境 CIDR
prod 10.1.0.0/16
staging 10.2.0.0/16
dev 10.3.0.0/16
test(CI専用) 10.4.0.0/16
sandbox1(個人検証環境1) 10.5.0.0/16
sandbox2(個人検証環境2) 10.6.0.0/16
... ...

prod、staging、devについては一般的な実稼働環境の名前であるため、特に説明の必要はないかと思う。

まずtest環境についてだが、これはCI時にインプラプロジェクトに対してTerraform Testterraform-execで実装されたテストコードを実行するための環境である。

この分け方について、devやstaging環境と同じ環境でテストコードを実行すればいいのではと思うかもしれない。

しかしTerraform Testのテスト実行時のデフォルトの挙動がplanではなくapplyであることからも分かる通り、インフラのテストコード界隈ではテスト実行の際にTerraformリソースのapplyとdestroyを繰り返すことを半ば常識としている節がある。

これは、planだけだとリソースのほとんどの値が(known after apply)となってテストできない上に、実際にリソースを生成出来るか確認する方が遥かに良いテストであるためと思われる。

そしてdestroyが実行されるということは、これまでstaging環境などでユーザ面からの手動テスト時に作成してきたデータなどがすべて初期化されてしまう事態が発生することから、基本的にテストコードの実行環境と手動テスト用環境は分けなければいけない。

次に、sandboxという環境名はあまり聞きなれないかもしれないが、これはインフラチームのメンバ1人1人、あるいはインフラに関係する各チームごとにテストを行うための環境をあらわす。

なお、sandbox環境という呼称は詳解 Terraform 第3版のP.321 9章 Terraformのコードをテストするより引用した。

...
したがって、開発者が他の環境に影響を及ぼす心配をせずに必要なインフラを立ち上げたり壊したりできる環境として、分離されたサンドボックス環境をすべてのチームが持つことを私は強く推奨しています。

以上が各環境の設計案となるが、まず気になるのが通常のサービスの環境に比べてネットワークアドレスを倍以上の速度で消費する点である。

インフラ関係者のメンバ数次第では、1つのサービスにつき10以上の環境ができる可能性がある。

もちろん、数個のsandbox環境だけを作ってメンバ間で使いまわすことで、ある程度数は減らすことはできるものの、それでも通常よりもネットワークアドレスの消費が多くなることには変わりない。

自分は少数のシステムしか存在しないネットワークでしか作業に関わったことがなく、また大規模なシステムを運用する企業のエンタープライズレベルのネットワークがどのように設計されているかまるで分からないため、もしかしたら実はあまり問題ないことなのかもしれないが、基本的にネットワークアドレスは切り詰めて設計するのが普通なので、使用するアドレスの量は少ないに越したことはないはずである。

この件に関しては、導入先の企業のネットワーク全体を知る人物に確認および上記設計の実現可能性についてレビューを行なってもらう以外、自分だけでは解決方法がない状態となっている。

あとこの件を考えていてふと思ったのだが、会社を新設してまず最初のアーキテクチャ全体設計を行う際に、出来る限り常時稼働環境数を抑えるのは、その先に続く未来のシステムコストにおいてかなりの大差が出てくるような気がした。

一般的には1サービスに対して3~4環境を用意するものだが、全サービスを意地でも2環境に抑え続ける工夫が出来れば、単純計算で33~50%コストカットできる。

(実際には本番で利用するリソースのスペック差等を考えるとそこまで大きな差にはならないかもしれない)

逆に言うと、testおよびsandbox環境は、テストが完了次第リソースをdestroyするため常時稼働環境ではないものの、コストの面でも多少の懸念は指摘される可能性がある。

導入する企業のクラウドプロジェクト作成フローへの適応

上記ネットワークやコストの問題以外に、企業ごとに異なるクラウドのプロジェクト生成フローについて対処する必要がある。

ちなみにGoogleのベストプラクティスでは、テストコードを実行するGCPプロジェクトについて、以下のようにテスト実行のたびにGCPプロジェクトを再作成するべきとしている。

...
テストでは、多くのリソースが作成され、削除されます。リソースのクリーンアップ中に誤って削除されないように、開発環境または本番環境プロジェクトから環境を隔離します。最善のアプローチは、テストごとに新しいプロジェクトまたはフォルダを作成することです。構成ミスを回避するため、テストの実行ごとに個別のサービス アカウントを作成することをおすすめします。

一方で自分が試験導入を予定している企業では、GCPプロジェクトの作成は申請制だったりするため、むやみやたらに新しいプロジェクトを作れるわけではない。

新しいプロジェクトを作ってもらった直後にテスト実行 -> プロジェクト削除 -> 再申請などしようものなら、かなりのひんしゅくものである。

正直なところ、このような浪費ができるのはGoogleもしくは一部の特殊な運用を行なっている企業だけであって、ほとんどの企業では無理な話である。

しかしテスト実行のたびにプロジェクトの作成を繰り返す方法については1つ明確なメリットがあって、それはテストの設計/実装が非常にシンプルになることである。

テストを実行するごとにクリーンな環境を作れるのなら、テストコードのバグ等でどれだけシステムを破壊しても、新しいプロジェクトを作れば済む話になる。

一方でプロジェクトを消さずに、terraform destroyでプロジェクト内をcleanしたのち、再度テストを実行する方式を採用すると、消してはいけないリソースを明確にコントロールしてテストを実行する必要が出てくる。

可能か不可能かはまだ分からないが、できればTerraformのHCLファイルの記述はprod、staging、test環境などを問わず、DRYにリソースを定義したいところである。

しかしこれが仮に出来たとしても、一つ落とし穴がある。

例として、もし全IAMユーザおよび全サービスアカウントをHCLファイル上で管理するような実装をおこなった場合、テスト後のterraform destroy実行でこれら全アカウントが消失して次のテストを実行するどころか、誰もプロジェクトにログインすらできなくなるといった事件が発生する。

よって削除されてはいけないリソースを意図的にドリフトさせるなどの工夫が必要になり、これはすなわちHCLファイルにリソースを定義する度に"これはdestroyされても問題ないリソースか?"と永久に考え続けなければいけなくなるという話になる。

以上が組織に関連したTerraformのテストを設計/実装する際の懸念事項となる。

次は先ほど定義したVPCにまつわる設計/実装の課題について検討していく。

テスト設計/実装におけるVPC

AWS、GCP、Azureなどの各クラウドはそれぞれにVPCをサービスとして提供している。

VPCネットワーク、サブネット、ファイアウォールといったサービスのコンポーネントは似通っているが、その設計思想はそれぞれで微妙に異なる。

一般的によく言われるAWSとGCPの違いとして、AWSのサブネットは1つのAZ内に作成されるのに対して、GCPのサブネットは複数のAZにまたがることが出来る点などが挙げられる。

ちなみにVPCはほとんどのリソースから依存される存在であり、テストコードの設計において一番注意を払わなければいけないリソースといっても過言ではない。

木でたとえるなら幹のようなものであり、その幹の違いは枝葉すべてに影響を与えることになる。

いったい何を言いたいかというと、クラウドごとにテストコードの設計/実装パターンが異なる可能性が高いということである。

もちろん実際に進めてみたらそこまで大きな問題にはならないかもしれないが、現在のような作り始めの時点では本当にGCPに依存した設計/実装のまま進めていいものかと、どうしても立ち止まって考えてしまう。

(初期の設計の失敗は、開発最終盤やローンチ後に大きな代償を払うことになることからも)

他にもVPCに関連する検討事項として、デフォルトVPCを使うべきかという話がある。

なおGCPプロジェクトで最初から用意されているデフォルトVPCは、初期設定でファイアウォール設定としてSSH、RDPログインを許可するようになっていたり、ネットワークアドレスが自社のポリシーで決定できないなどの理由からなのか、一般的には削除したほうがいいものとして扱われている。

(昔は毎日のように実行していたsshコマンドだが、現在のようなクラウド/マネージドサービス全盛時代だとsshログインの許可は滅多に必要ないのかもしれない)

一方で、先ほど話題に挙がった、詳解Terraformの本ではテスト時に作成するリソースに対する外部依存としてデフォルトVPCを渡すテクニックを紹介(P.339)していたりする。

以前にも書いたが、この本の著者はTerraform用のテストコードを実装するフレームワークであるTerratestの開発関係者である。

(なお散々書いてきたがライセンスの問題で、Terratestの利用は現在よく考えたほうが良い状態)

そのようなTerraformのテストコード実装のエキスパートがデフォルトVPCを使うのには何か訳があるのかと、どうしても理由を色々と考えてしまう。

このようにVPC1つとっても判断が悩ましい点は次々と出てくる。

一応上記問題に対する現在の方針としては、

  • GCPのみを考慮してテストコード設計/実装を開始
  • デフォルトVPCは消して、カスタムVPCを使ってテストの依存を表現

で行こうと考えているが、実に不安である。

ここまででも十分嫌気が差してくるが、インフラのテストコードにはさらなる問題が待ち構えている。

アプリとは異なるインフラのテスト実装難易度

インフラのテストコードはサーバーサイドやフロントサイドのテストコードとは実装方法が明らかに異なる。

その最たるは、インフラのテストコードはmockできない点である。

たとえばアプリケーションのコードでは以下のようにVPCを実際に作らず代わりにmockを作って、依存関係を解決することも出来るが、インフラのテストコードではVPCが実際に存在しない状態でリソースを作ることはできない。

// ref. https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_network
var vpc = mock(GoogleComputeNetwork.class)
doReturn("projects/infra-testing-sample/global/networks/test-sample-vpc").when(vpc).id();

// ref. https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_networking_connection
var conn = GoogleServiceNetworkingConnection.builder()
  // 上記mock値を使用
  .network(vpc.id())
  .service("servicenetworking.googleapis.com")
  .reservedPeeringRanges(List.of())
  .build();
...

これはテストの実行時間の短縮という点において大きな問題になる。

vpcネットワークのプロビジョニングについては30秒程度で終わるので、そこまで大きな問題にならないものの、これがCloud SQLなどのプロビジョニングに時間のかかるリソースが依存対象となると、テストを始めるまでにそれらがプロビジョニングされるまで長い間待たなければいけなくなる。

またmockを使用するメリットとして、リソースの作成失敗によってテストが失敗するといったflakyな実装を回避できる点が挙げられるが、この点についてもインフラのテストコードでは自前で対策を施さなければならなくなる。

ネットワークやGCPの不調などでリソースの作成に失敗した場合には、基本的にはリトライ(可能なら指数バックオフ)で解決することになるが、そもそもTerraformのリソースの設定やTerraform Testの機能だけで細かい制御ができるのかは怪しい。

terraform-execのようなGoでテストコードを実装する場合には問題ないが、その場合テストコード実装のための学習/保守コストが一気に上がることになる。

またテストが中途半端な地点で失敗した場合に、生成したリソースが残ってしまうといった事象も考慮しなければいけない。

これについてはAWSの場合aws-nuke、GCPの場合はproject_cleanupモジュールを使うことで定期的なクリーニングを実行できるようになり対処可能なようだが、これらの挙動をしっかり理解した上でテストを実装しないと予期せぬ挙動に陥りかねない。

他にも、

  • テスト実行時間を現実的な運用の範囲内で完了させなければいけない課題への対処
  • Cloud SQLなどのリソース再生成不可問題に対応するための工夫(識別子のランダマイズなど)
  • GCPサービスAPI有効化から数分はそのサービスを利用できない件のハンドリング

などと、明らかにアプリケーションのテストコードの実装に比べて検討事項が多い。

ちなみに練度の差の問題はあるものの、サーバーサイドのテスト設計はそこまで難しくなく、フロントサイドのunit、integration、e2eテストの体系化についても、週2.5営業日/2ヶ月程度の工数で本番投入できたのに対して、インフラのテストは同/3ヶ月経った現在でもまったく終わりが見えない状態となっている。

この点からも、現在インフラのテスト自動化を検討している方がいるなら、地獄の工期にならないよう、見積もりにかなり余裕を持たせたほうがいい気がしているというのが個人的な意見。

(既に本番投入が完了している方がいたら、さらにアドバイスいただければと)

余談だが、フロント、サーバサイド、現在開発中のインフラ、そして最後にMLと、全4種類のe2eテスト環境の構築が完成すれば、四団体統一王者のような真のe2eテストアーキテクチャが誕生することになる。

そんな夢とロマン溢れるアーキテクチャが輝く日は果たしてやって来るのだろうか。

おわり

以上、一言でまとめると何の成果も得られなかったと長々書いてきたが、気を取り直して次はテストコードを実装するための検証環境のセットアップをまずは行なっていきたい。

上記の問題について、何か良いアイディアやアドバイスなどがあれば、気軽にコメントなどに書いていただければと。

p.s. 直近設計の他にも、個人確定申告、びわ湖(3/10 8:20AM)、法人決算と、しんどいイベントが続くので進捗が少し遅くなるかも。

追記

インフラのコードはmockできない件について、一応Terraformの公式機能としてMockが提供されている。

おそらくmock_resource等でVPCを定義することで上記のmockサンプルコードと同じようなことは出来そうだが、結局VPCが実体として存在していないとその中にリソースを生成出来ないという問題があることには変わらない。

ただ、VPCのような実稼働環境で一度作成したら基本消すことがないようなリソースについては、テスト用に意図的にドリフトさせて、そのドリフトしたVPCのidをmockで定義して使うといった利用方法なら、テスト時のリソース作成時間短縮などに繋がって良いのかもしれない。

続き

https://zenn.dev/erueru_tech/articles/e24a1db7a39142

Discussion