TECHSTEP

ITインフラ関連の記事を公開してます。

「詳解Terraform」の感想と読書メモ

今回は 詳解 Terraform 第3版 を一通り読んだので、その感想を簡単に残しておこうと思います。

TerraformはIaCツールの中でもかなり市民権を得ていると感じるツールです。にもかかわらず、ここ最近個人的にTerraformを使う機会もなく、情報を追いかけられていませんでした。なので復習の意味も込みで購入しました。

www.oreilly.co.jp

読書中のメモも併せて載せていますが、読んでて「これは助かるなあ」と思ったポイントを抜粋しておきます。

Terraformの前段階から説明してくれて助かる

Terraformを学習する前段階として、「そもそもIaCとは何か?何のために使うのか?」「どんな種類があるのか?」「何を使うべきか?」といった情報を知りたい方に役立つ本だと感じました。

本書の1章は なぜTerraformを使うか という題名がついており、IaCツールのカテゴリ、各ツールの特徴、どういった場面で何を利用すべきか、といった情報が紹介されています。なので、IaCに親しくない人も、久しぶりにTerraformを学ぶ人も、IaCツールの全体像やTerraformの立ち位置がどこか、といった点を知れたのは、とても役に立つと感じました。

Terraformを扱う上で気になるポイントが説明してあって助かる

Terraformを扱う上で注意するポイントとしては、例えばtfstateやモジュール、パスワードなどの秘匿情報の扱いなど、いくつかあると思います。なので、本格的にTerraformを扱う前に注意するポイントを知れるのはありがたく、導入後の試行錯誤の手間も少なくできるのでは、と思いました。Terraformが成熟してきたこともあり、「こういう時はこうする」というBest Practiceとも言えそうなパターンができていると思うのですが、そういった情報をここで確認できると感じました。

また、こういったよくある注意ポイントは、個別にはブログなどで紹介されていますが、1冊の書籍の中でまとめて載せてあるものを(ほかの書籍にもあるのかもしれませんが)私は把握しておらず、とりあえずこれ1冊でポイントを押さえられるのは、情報収集の点でもありがたいと感じました。

実践向けにやるべきことを整理しているのが助かる

これは後半の8~10章が該当するのですが、Terraformを自分のプロジェクトで利用するときに何を気にするべきかまとまっているのはありがたいと感じました。

例えば8章の 本番レベルのTerraformコード では、実際のプロジェクトで利用する場合にどのようにモジュールを作成すべきか、9章では作成したTerraformをどのようにテストするか、そして10章ではチームでTerraformを利用するため、CI/CDを利用したワークフローの整備など、本番向けにTerraformを扱う上で重要なポイントがまとめられています。なので、これから導入を検討している人、すでに導入しているがもっと改善したい人など、幅広い方に役立つ内容ではないかと思います。

なお、8章には「本番レベルのインフラを構築するのは時間がかかる」という観点から話が始まっており、なぜ時間がかかるのか?設計時にどういった観点が必要か?という情報も紹介されています。こちらはTerraformに限らず役立つ内容かと思うので、Terraformを使う予定のない方も、ここだけは眺めてみてもよいかもしれません。

読書時のメモ

以下に読んだ時のメモを残しています。長文なので展開注意です。

1章 なぜTerraformを使うか

IaCツールは以下の5種類のカテゴリが存在する。

  • アドホックスクリプトBash / Runby等の言語で主導実行するタスクを定義する。汎用的なプログラミング言語で自由にコードを書ける一方、自由に書けることで複数人でメンテナンスすることは困難となりやすい。小規模な1回限りのタスクには最適。
  • 設定管理ツール:Chef / Ansibleなどのツールをサーバにインストールし、管理する。アドホックにはない特徴として、コード規約の導入、冪等性の担保、配布のしやすさなどが挙げられる。
  • サーバテンプレーティングツール:Docker / Packerなどのツールは、サーバのスナップショットをあらかじめ定義し、定義通りのサーバを提供する。イミュータブルインフラ(サーバの定義コードを変更すると新しいサーバを作成する)へのシフトの鍵になる。
  • オーケストレーションツール:Kubernetesなどのツールは、サーバテンプレーティングツールによって作成されたサーバ・コンテナを管理する。
  • プロビジョニングツール:Terraformなどのツールは、これまでの「各サーバで動くコードを定義する」のとは異なり、「サーバ自身を作成する」役割を持つ。

IaCは、コードを使うことで得られる多くの取り組みを与え、それがソフトウェアのデリバリプロセスを劇的に改善する。多くの取り組みとは、セルフサービス・スピードと安全性・ドキュメント化・バージョン管理・バリデーション・再利用性などがある。

IaCのどのツールを選択するかは、トレードオフと優先順位から決定される技術的判断である。ミュータブルかイミュータブルか、手続き型言語か宣言型言語か、有償か無償か、といった複数のトレードオフを考慮し、最適なツールを選択すべきである。また現実的には複数のツールを組み合わせて使うことになるだろう。代表的なパターンは、プロビジョニング+設定管理、プロビジョニング+サーバテンプレーティング、プロビジョニング+サーバテンプレーティング+オーケストレーション、の3つである。

このうちTerraformの特徴とは、以下のようなものである。

  • オープンソースであるがオプションとして有償サービスも利用できる
  • 複数のクラウドに対応している
  • イミュータブル
  • 宣言型言語
  • ドメイン特化の言語
  • マスタ、エージェントは不要
  • 巨大なコミュニティ
  • 成熟度は他と比べて中程度

3章 Terraformステートを管理する

Terraformを実行するたび、管理するインフラの情報をTerraform stateファイル (デフォルトは terraform.tfstate ) に記録する。tfstateにはTerraformのファイルに書かれたリソースと実際のリソースとのマッピング情報を記録する。

tfstateファイルはTerraformが内部で使う、プライベートAPIである。そのためtfstateファイルを手動で修正したりするべきではない。どうしても必要な場合は terraform import / terraform state コマンドを使う。

複数人でTerraformを扱う場合、tfstateに関連する以下のような問題が発生する

  • tfstateの共有ストレージ:Terraformを扱うチームメンバー全員がアクセスできる共有ストレージにtfstateファイルを配置しなければならない。
  • tfstateのロック:2人以上のメンバーが同時に更新するのに備え、ロックの仕組みが必要である。
  • tfstateの分離:例えば環境ごとにtfstateを分けることで、テスト環境と誤って本番環境を変更することのないようにしたい。

リモートバックエンドの利用

通常、複数メンバーでファイルを共有するにはバージョン管理システムで管理するが、Terraformの場合はいくつかの点からそれを推奨しない。そうではなくTerraformの持つリモートバックエンド機能を使い、Amazon S3 / Azure Blob Storage / Google Cloud Storageなどで管理することが推奨される。

例えばAWSの場合、Amazon S3でtfstateを管理し、ロックのためにDynamoDBを利用できる。

バックエンドの制限は主に2つある。

  • Terraformを管理するリソースをTerraformで作成する場合の作業順:最初のステップで必要なリソースを作成し、tfstateはローカルに保存する。次のステップでTerraformバックエンドの設定を変更し、ローカルからリモートにtfstateファイルをコピーする。削除する場合は逆の手順で行う。
  • backendブロックが変数や参照を使用できない:すべてのTerraformコードで似たような記述を書かなければならず、しかもkeyの値はそれぞれ一意でなければならない。これを避けるため、backend.hclのようなhclファイルに共通設定を記載し、-backend-config引数でファイル名を渡すことができる。またTerragruntを使うと、複数のTerraformのバックエンドの設定を1つのファイルで定義できる。

ステートファイルの分離

ステートファイルの分離は2通りある。

ワークスペース

terraform workspaceコマンドを利用すると、ワークスペースと呼ばれる空間を分離し、それぞれ異なるtfstateファイルを扱えるようになる。ただしこちらは以下の理由から、環境を分離するのには適切でない。

  • ワークスペースのtfstateは同じバックエンドに保存するため、全環境に対し同じアクセス権限を適用することになる。
  • terraform workspaceコマンドを実行しないとどのワークスペースにいるか確認できない
  • 上記2点から、ワークスペースの利用は操作ミスを引き起こす可能性が高まる。
ファイルレイアウト

tfstateを完全に分離するには、環境ごとにフォルダを分け、かつバックエンドも別々にする必要がある。また環境以外にも、例えばリソース間のライフサイクルの違いなどから、コンポーネントレベルで分けたい場合もあるだろう。Terraformの一般的なフォルダ構成が紹介されている。

このファイルレイアウトにより、コードと環境のレイアウトが明確になり、分離も程よいものとなる。一方でこの構成による欠点(1回のコマンドですべてのリソースをを更新できない、コピペが多くなる、依存関係の扱いが難しくなる)もあるが、Terragruntやterraform_remote_stateなどの利用で改善できる。

terraform_remote_stateは、そのリソースを管理するのと別のtfstateファイルを呼び出すことができる。そのため、tfstateに書かれたoutputsの設定を呼び出し、変数として利用できる。

IaCファイルに問題があったときの影響範囲はアプリと比べて大きくなる傾向にあるので、通常より多くの安全装置を設定することが推奨される。ロック、分離、ステートはその代表である。

4章 モジュールで再利用可能なインフラを作る

同じ構成を複数の環境で利用する場合、Terraformモジュールを利用することで、同じコードをコピペする必要がなくなる。

モジュールは入力変数を設定できるが、設定可能な入力として公開したくない場合はlocalsブロックを利用できる。localsブロックはそのモジュール内でしか参照できず、またモジュールの外から上書きすることもできない。

モジュールの注意点

  • ファイルパス:Terraformはデフォルトの場合、モジュール内部で相対パスを使用する場合、現在のディレクトリに対する相対パスと判断する。この仕様の都合が悪い場合は、path.というパス参照を利用する。例えばpath.moduleを使用すると、定義があるモジュールが存在するファイルシステムパスを返す。
  • インラインブロック:インラインブロックは、リソースのフォーマットの中で引数として設定するものである。例えばAWS Security Groupリソースはインラインブロックでingress / egressのセキュリティグループルールを設定できる。しかし、インラインブロックはモジュールの中にしか記述できず、モジュールを使用するユーザーはモジュールの外からインラインブロック内のリソースを追加することができない。そのため、モジュールの柔軟性を持たせるには、モジュールを使うときは常に別のリソースで定義する(インラインブロックを使わない)ことが推奨される。
  • モジュールのバージョン管理:異なる環境で同じモジュールを参照すると、モジュールに対する変更が次回のデプロイ時にすべて反映される。これを避けるには、各環境から参照するモジュールのバージョンを管理する。最も簡単なのはモジュールをGitで管理し、タグ等でバージョンを管理することである。Terraformはモジュールを参照するのにGit URLも利用できるので、URLの中でタグやrefを使用し、モジュールのバージョンを指定できる。

5章 Terraformを使うときのヒントとコツ

ループ

宣言型言語には一般的にfor文がない。Terraformにはいくつかの異なるループの仕組みがある

  • countパラメータ:リソースに対してループする。同じ種類の異なる名前のリソースを複数作成する場合、配列の検索文法やlength関数と組み合わせることで実現する。ただしcountはインラインブロック内では使用できない。またリストを扱った場合、リストの途中の値を変更すると、以降の要素をすべて削除して新しいリソースを作成しようと動く。
  • for_each式:リソースやリソースのインラインブロックに対してループする。for_eachでループの対象で指定するのはマップや集合であり、集合を扱うことでcountにあった課題を解決できる。
  • for式:リストやマップに対してループする。Pythonのリスト内包表記と似たような式で表現できる。
  • for文字列命令:文字列内のリストやマップに対してループする。

条件分岐

  • countパラメータ:条件付きリソースに使う。例えば ? <TRUE_VAL> : <FALSE_VAL> というフォーマットの条件式とcountを組み合わせ、ConditionがTrueならリソースを1つ作成する、と表現できる。
  • for_eachとfor式:条件付きのリソース、あるいはリソース内のインラインブロックに使う。countより複雑になるのでcountが推奨。
  • if文字列ディレクティブ:文字列内の分岐に使う。

ゼロダウンタイムデプロイ

ゼロダウンタイムを実現するには、先に変更後のリソースを作成し、その後に変更前のリソースを削除するよう設定する。TerraformのLifecycleパラメータで create_before_destroyをtrueにするとこの動きを実現できる。

つまづきポイント

  • countとfor_eachの制限事項:リソース作成の前にcountとfor_eachを計算できる必要がある。そのためハードコードされた値などは参照できるが、何かの計算の結果を参照することはできない。
  • ゼロダウンタイムの制限事項:すでに多くのクラウドサービスではネイティブにゼロタイムダウンタイムをサポートしており、可能ならそちらを利用すべきである。そうすることでTerraformのコード側で複雑性を持たせることなく、クラウドサービス側で担保できる。
  • 有効なプランも失敗することがある:tarreform planはtfstateファイルだけを見るため、Terraformの管理外の事象は検知できない。そのため、①Terraformを使い始めたらTerraformだけを使う、②既存インフラがあるならimportコマンドを使う、の2点が重要となる。
  • リファクタリングは難しい:変数や名前を変えるだけでリソースの削除・作成が発生することがあり、システム停止につながることがある。そのため①常にplanを実行する、②削除する前に作成する、③イミュータブルなパラメータもある、という点を気にするべきである。またリファクタリングにはステートの変更が必要な場合もある。その際はtfstateを手で修正するのでなく、terraform state mvやmovedブロックの追加で対応する。

※リソースのタグを組織内でルールづけている場合、provider aws内でdefault_tagsを設定すると、モジュール内のあらゆるAWSリソースに指定のタグを付与できる。

6章 シークレットを管理する

シークレット管理は「プレーンテキストで保存しない」ことを絶対に守る。プレーンテキストに保存してバージョン管理システムにコミットすると、以下のような問題がある。

  • VCSにアクセス権があればだれでもシークレット情報にアクセスできる
  • VCSにアクセス権のあるあらゆるコンピュータがシークレットをコピーして保存できる
  • 実行するあらゆるソフトウェアがシークレットにアクセスできる
  • シークレットへのアクセスを監査したり権限をはく奪する方法がない

そのためHashiCorp Vault / AWS Secrets Managerといったシークレット管理ツールと組み合わせることが必要となる。

プロバイダへの認証

人が認証情報を使う場合、例えばローカルの環境変数にシークレット情報を格納し、terraform実行時にその情報を渡すことができる。この時にシークレット管理ツールにシークレット情報を保存し、CLIからそれを呼び出すことでより安全に運用できる。

またマシン上で認証情報を使う場合、例えばCI/CDの実行基盤として使用する場合、利用するソフトウェア自体が提供するシークレット管理の仕組みを利用する、クラウドプロバイダ側が提供するIAMロールなどの仕組みを利用する、またはOIDCを使用することが考えられる。

リソースとデータソース:例えばDBへのアクセス情報

プロバイダへの認証と同様、環境変数を使うことが考えられる。ただしTerraform外で管理するため、管理の標準化が難しく、また設定漏れの可能性もある。

次にシークレット情報を暗号化し、暗号文をファイルに保存してバージョン管理システムで管理することも考えられる。暗号化キーの管理にはKMSやPGPキーを使うことで対応できる。ただし、一度プレーンテキストから暗号化したファイルを用意するという手間があり、誤ってプレーンテキストをバージョン管理システムにアップロードしないよう注意が必要となる。

最後にシークレットストアを利用する方法が考えられる。シークレットをシークレットストアに管理しTerraformから呼び出すことで、シークレット自体の管理を楽にする。ただしシークレットの更新漏れの可能性、金銭的なコストの発生、外部に管理することでの自動テストとの統合の難しさ、などが課題となる。

tfstate、planファイル

tfstateファイルにはシークレット情報もプレーンテキストとして保存される。現状では以下の対策が必要となる。

  • 暗号化をサポートするバックエンドにtfstateを保存する
  • Terraformバックエンドへのアクセス権を厳しくする

terraform planの実行結果をファイルに保存できるが、こちらにもシークレット情報がプレーンテキストで保存される。そのため、このファイルを暗号化する、アクセス権限を厳しくする、などの対応が必要である。

7章 複数のプロバイダを使う

1つのプロバイダを利用する場合

複数のリージョンを使う場合、各プロバイダにエイリアスを付けることで区別できる。

provider “aws” {
  region = “us-east-2”
  alias  = “region_1”
}

provider “aws” {
  region = “us-west-1”
  alias  = “region_2”
}

これを使って別のリージョンにデプロイする場合は以下のようになる

resource “aws_instance” “region_1” {
  provider = aws.region_1
  ami = …
  instance_type = …
}

resource “aws_instance” “region_2” {
  provider = aws.region_2
  …
}

ただし、エイリアスを使って複数リージョンに同じTerraformモジュールで管理すると、あるリージョンがダウンするとterraform plan/applyが失敗し、障害発生中のコード変更ができなくなる。またエイリアスを間違えて実行する可能性も高くなる。そのため、本来は各リージョンを別々のモジュールで管理すべきである。

複数リージョンを扱うエイリアスを使うべき場面としては、複数のプロバイダにまたがるインフラが完全に結合しており、一括でデプロイしたい場合がある。ただし一般的にエイリアスを使うことが多いのは、違うAWSアカウントに対して別々に認証するなど、異なる方法で認証を行う複数のプロバイダを使うときである。

1つのプロバイダで複数アカウントを扱うとき

クラウドプロバイダー上にシステムを作る場合、複数のアカウントに別々の環境を用意することが推奨される。例えばAWS Organizationsを使って複数アカウントを管理し、Terraformのproviderでどのアカウントを使うか指定し、構築・運用することができる。

provider “aws” {
  region = “us-east-2”
  alias = “parent”
}
provider “aws” {
  region = “us-east-2”
  alias = “child”
  assume_role {
    role_arn = “arn:aws:iam::<AWS Account ID>:role/<role name>}
}

ただし、通常複数アカウントを利用するのは、アカウントを分離して影響範囲を限定するためである。複数アカウントにまたがってデプロイを実行するモジュールを利用するのは、この方針に逆らっているため、何らかの理由で複数アカウントにまたがるリソースを結合させ、同時にデプロイしたいときだけ利用する。

複数プロバイダを扱う

複数のクラウドプロバイダを同じモジュール内で扱うのは稀であり、本来は別々のモジュールで管理すべきである。また、例えばAWSKubernetesという2つのモジュールを同時に管理する、という例も考えられる。ただしこれも一般的には別々のモジュールで管理することが推奨されるため、使いどころは考えなければならない。

8章 本番レベルのTerraformコード

本番レベルのインフラを構築するのは時間がかかる。それには3つの理由がある。

  • DevOpsという分野がまだまだ成熟しておらず急速に進化しているため。ツールが十分に成熟しきっていないためうまくいかない点もあれば、それを使う人間も深い経験を持っていないことが多い。
  • あるタスクを実行するために別の大量のタスクを行う必要がある (Yak shaving)。DevOpsは特にYak shavingに陥りやすい傾向にある。
  • そもそも本番用のインフラを準備するための準備リストが膨大である。このリストの項目を知らずに見積もりをすると、想定より大幅に時間がかかってしまう。

本番レベルのモジュールは、いくつかのポイントがある。

  • 小さくて組み合わせ可能なモジュールにする
  • テスト可能なものにする。各モジュールにはexampleフォルダを用意し、モジュールをデプロイするためのテスト用ルートモジュールを用意する。
  • バージョン管理する。各Terraformリソースのバージョンを固定することで、作成後も繰り返し再現可能なコードを提供できる。
  • testフォルダを用意して各サンプルコードに自動テストを書く。

9章 Terrraformのコードをテストする

手動テスト

手動テストを実行するには、Terraformで定義したリソースを実際に作成し、作成したリソースに対して何等かのテストを実行するしかない。そのため、開発者が自由に操作・作成できるサンドボックス環境の用意が推奨される。ただしサンドボックス環境を管理しないとテスト用のリソースが残って余計なコストが発生するので、定期的に掃除が必要となる。

自動テスト

ユニットテスト

Terraformにおけるユニットテストとは、用意したコードを使って実際の環境にデプロイすることを指す。具体的には、1つの独立したモジュールに対してデプロイ用のサンプルコードを作成し、実際の環境にデプロイしたあとで動作確認を行う。このような、手動テストで行うようなことをコードで実現することが自動テストになる。

Terraformの場合はTerratestというGo言語のパッケージを利用すると、terraform init / apply / destroy、作成後のテストなどを比較的簡単に表現できる。

統合テスト

Terraformにおける統合テストは、複数モジュールをデプロイして動作確認をすることが該当する。また統合テストでは、テスト中のステップをステージに分解し、必要に応じて一部ステージをスキップ可能な実装をすることでイテレーションを加速できる。さらにFlaky testに対応するためリトライを実行できるようにすることも推奨される。

E2Eテスト

TerraformにおけるE2Eテストは、本番環境に似せた環境に対してすべてをデプロイし、エンドユーザーの視点でそれをテストすることを指す。ただしすべてのリソースを毎回ゼロから作成しなおすのは速度と安定性の面から推奨されない。

一般的には事前に本番環境に近いテスト環境を構築し、その環境を起動状態にしておく。何かしらの変更がコードに入ったらその変更をテスト環境に適用し、テスト環境に対する動作確認を実行する。この方式だとデプロイプロセスも合わせてチェックできるという利点もある。

その他

静的解析、terraform planの出力に対するテスト、Serverspecなどを使用したサーバテストもある。すべてをカバーする万能な単一のテストは存在せず、すべてのテスト方法を組み合わせることが必要となる。

10章 チームでTerraformを使う

Terraformを扱うときはチームで利用することが多いだろう。まだTerraformを導入していない場合は上司への説得を含めた活動が必要だが、それを乗り越えた後でもまだやるべきことがいくつかある。

デプロイワークフローを整備する

インフラコードに対するデプロイワークフローは、アプリケーションに対するそれと同じような流れとなる。ただしその中身には大きな違いがある

バージョン管理する

Terraformでは、モジュール用のリポジトリ (moduleリポジトリ) とそれを利用した稼働中のリソース管理をするリポジトリ (liveリポジトリ) の2種類を利用する。

またTerraformで管理するうえでは、liveリポジトリのmainブランチが、本番環境にデプロイされたリソースに対し、1:1に対応していなければ、管理できているとは言えない状態である。これを守ることが重要である。

さらにTerraformと複数のブランチを組み合わせることの相性がよくないため、どんな共有環境でもデプロイは常に1つのブランチからのみ行うようにすべきである。

コードをローカルで実行する

Terraformを利用する場合localhostは利用できないので、専用のサンドボックス環境を用意すべきである。

コードに変更を加える

変更を加えるたびにterraform applyを実行し、サンドボックス環境に変更を適用する。インフラは更新完了まで時間がかかりがちなので、テストステージ等を利用してより高速にフィードバックを得るよう工夫すべきである。

変更に対するレビューを依頼する

変更が問題なく動くことを確認したらそれをmainブランチに提供するためレビューを依頼する。レビューをする上で、各種ドキュメントや自動テストの整備などを進めておく必要がある。

自動テストを実行する

変更をコミットするたびにCIサーバで自動テストを実行するよう整備すべきである。

変更をマージしてリリースする
デプロイする

デプロイ環境を用意するには、適切なデプロイツールの選定やデプロイ戦略、環境間の昇格などを考慮する必要がある。