Closed9
詳解 Terraform(第3版)まとめ
オライリーのTerraform本を読んで得た知見と所感についての雑なまとめ
3章 Terraformステートを管理する
- statefileの管理について
- チーム開発においてはS3などの共有ストレージで管理
- 複数メンバーによる競合が起きないようにDynamoDBのテーブルを使ってロックの仕組みを入れる
- statefileを管理するリソース(S3、DynamoDB)はTerraformで管理するべきか
- これは直近の業務でも発生した
- 本書では最初はローカルバックエンドで上記のリソースを作成するtfファイルを書いてコードをデプロイして、その後にリモートバックエンドを使用するようにTerraformコードにbackend設定を追加する(backend設定を変更したため、terraform initを実行する必要がある)と推奨
- ただ、バックエンドのリソース設定を変更する機会はあまり無いし、マネコンからの作成でも良い気はしている
- 最近だとimport blockも出てきて後からimportも楽になったから、このやり方に拘る必要はないと感じた
- statefileの分離、およびディレクトリ戦略
- 大きく分けて分離方法は以下の2つ
- ワークスペースによる分離
- terraform workspaceでstatefileを分離する
- S3では
:env
フォルダができて、そこに各ワークスペースのフォルダができる
- S3では
- ディレクトリ構成はシンプルに出来るが、
terraform workspace
コマンドを打たないとワークスペースがわからないので、間違えてstgにapplyするものをprdにapplyしてしまったりみたいなことが起きてしまう可能性がある
- terraform workspaceでstatefileを分離する
- ファイルレイアウトによる分離
- 環境(dev/stg/prd)ごとにディレクトリを分けて、それぞれにバックエンドを設定することで、statefileを分離する
- S3バケット自体を分けることも可能なので、それぞれに異なるバケポリを設定できたりする
- 認知負荷も低くなり、自分がどの環境にデプロイしようとしているのかが明確にわかる
- さらに環境内のコンポーネントごと(sharedなiamやs3、vpc、service、db etc...)にディレクトリを分けることが本書では推奨している
- 1日に何度も設定変更を行うようなコンポーネントと、滅多に変更しないVPCなどのコンポーネントを同じstatefileで管理するのは、誤って一緒にVPCのNW環境を壊したりするリスクがあるため
- ただ、ディレクトリ構成は複雑になる
-
terraform_remote_state
データソースを使ってoutput値も設定すれば、別ディレクトリからも参照可能
- 環境(dev/stg/prd)ごとにディレクトリを分けて、それぞれにバックエンドを設定することで、statefileを分離する
- 本書では、ファイルレイアウトによる分離を推奨している
- ワークスペースによる分離
- 大きく分けて分離方法は以下の2つ
4章 モジュール
- モジュール(ローカルファイルパス)
- 前章であったファイルレイアウトによるステートファイルの分離を行なった場合などに、それぞれの環境で同じような内容を何回も記述することになる
- stg/prdのディレクトリとは別に、modulesディレクトリを作成して同じ記述を再利用できる
- モジュールを使う際の注意点
- ファイルパス
- 例えばモジュール側でtemplatefile関数を使用している場合、モジュールを使う側で指定する相対パスが異なってしまう
- そのようなときに
path.module
などのパス参照を使えば、モジュールを使う側でよしなにパスを取得してくれる
- インラインブロック
- terraform resourceの中には、
aws_security_group
やaws_route_table
などのようにインラインブロックを記述できるものがある - このインラインブロックに書く記述は別リソースとして書き出すことが可能で、
aws_security_group
であればaws_security_group_rule
として別リソースに書き出すことができる - ただ、このインラインブロックと別リソースの記述が重複してしまうとエラーになってしまうので、どちらか一方のみを使う必要がある
- 本書では、モジュールを使う時は常に別リソース(
aws_security_group_rule
など)を使うのを推奨してる - モジュール側でインラインブロックを使っていると、モジュールを使用する側で追加のルールを作成することができなくなるので、最低限のルールを別リソースとしてモジュール側で用意しておいて、使用する側でよしなにルールを別リソースで作成する運用が良い
- terraform resourceの中には、
- ファイルパス
- モジュールのバージョン管理
- ローカルファイルパスによるモジュールだと、stgとprdで強制的に使用するモジュールが一緒になる
- stgである程度モジュールの検証をした後に、prdもそのモジュールを使いたい
- その場合はモジュールを別リポジトリに分離して、Gitのタグ機能(
git tag -a "v0.0.1"
)を使ってバージョン管理するのが良い - バージョン管理にはセマンティックバージョニングを利用すると使用する側もわかりやすくて良い
- 弊社でもモジュールのバージョン管理をセマンティックバージョニングで行なっているが、https://github.com/markchalloner/git-semver でこのバージョン付けを自動化している
5章 ループ、条件分岐、デプロイ、その他つまづきポイント
-
count
- IAMユーザを複数個作りたい場合などにcountを使用できる
-
count == 0
は作成しない - 制限事項
- リソース内のインラインブロックには使用できない
- リソースのリストを作成するのにcountを使っている場合、途中でリストの要素を削除すると、削除した要素以降のアイテムを全て削除して、イチから作り直してしまう
-
for_each
- リソースに対してfor_eachを使う際にリストはサポートしていないので、
toset
を使ってリストを集合に変換して渡す必要がある -
each.key
,each.value
で値を取れるが、キーと値から構成された集合の時のみeach.key
を使うことが多い - モジュールには
for_each
を使うのが理想
- リソースに対してfor_eachを使う際にリストはサポートしていないので、
-
条件分岐
- countパラメータを使ったif文
- 条件式のtrue/falseの値を逆にすることでelse文のように挙動させられる
- for_eachとforを使った条件分岐
- リソースやモジュールを複数作成したい場合は
for_each
を使うべき - 条件分岐のロジックを実現するときは、
count
を使った方が単純
- リソースやモジュールを複数作成したい場合は
- 条件付きで作成したいときは
count
、それ以外のループや条件分岐にはfor_each
を使う
- countパラメータを使ったif文
-
ゼロダウンタイムデプロイ
-
create_before_destroy
ライフサイクルを使って、置き換え先のリソースを先に作成し、それから元のリソースを削除できる
-
-
つまづきポイント
- countとfor_eachの制限事項
-
random_integer
リソースなどの出力値は参照できない - planフェーズ時にcountやfor_each内で計算することができないため(applyして初めて値が出力されるため)
-
- ゼロダウンタイムデプロイの制限事項
- 利用できるのであればインスタンスの更新のような専用のネイティブなデプロイ方法を選択すべき
- ASGに関しては
instance_refresh
ブロックがある
- ASGに関しては
- 利用できるのであればインスタンスの更新のような専用のネイティブなデプロイ方法を選択すべき
- 有効なプランも失敗することがある
- Terraform管理外で同じリソース名で作成されているものがあれば、Planが成功してもApplyで失敗してしまう
- importコマンドを使って管理下に移行すること
- まとめてimportしたいなら、
terraformer
やterracognita
のようなツールがある
- Terraform管理外で同じリソース名で作成されているものがあれば、Planが成功してもApplyで失敗してしまう
- リファクタリングは難しい
- 安易にリソース名を変更したりすると、既存のリソースを削除して完全に新しいリソースを作ることになるため、ダウンタイムが発生してしまう
- planコマンドを使って反映内容を常に確認するようにしたり、削除される前に作成するようにしたりする必要がある
-
terraform state mv
コマンドを使って、ステートファイルを変更するのも手
- 安易にリソース名を変更したりすると、既存のリソースを削除して完全に新しいリソースを作ることになるため、ダウンタイムが発生してしまう
- countとfor_eachの制限事項
6章 シークレットを管理する
-
シークレット管理の基本
- シークレットをプレーンテキストで保存しないこと ←絶対やっちゃいけない
- シークレットの用途に適した管理ツールを使うことが不可欠
-
シークレットの種類
- 個人シークレット
- Webサイトにログインするためのusername/password など
- 顧客シークレット
- 顧客の個人情報など
- インフラシークレット
- DBのパスワード、API/APPキーなど
- 個人シークレット
-
シークレットの保存方法
- ファイルベースのシークレットストア
- シークレットを暗号化したファイルに保存する
- 暗号化キーはAWS KMSなどに保存する
- 集中型のシークレットストア
- MySQLなどのデータストアにシークレットを暗号化して保存する
- その暗号化キーはAWS KMSなどに保存する
- ファイルベースのシークレットストア
-
Terraformでの認証方法例
- 兎にも角にもキーをハードコードするのは絶対にNG
- 大きく分けて、人のユーザが実行する場合とマシンユーザ(CIサーバなど)が実行する場合とに分けられる
- 人のユーザ
- AWSの認証情報(IAM UserのAccessKey/Secret Key)を使って手元からapplyする
- 環境変数に設定してapplyすることも可能だが、キーを覚えておくのは現実的ではないので、1passwordを使ってキーを管理するのがおすすめ
- 1passwordにもCLIがある(知らなかった)
- https://developer.1password.com/docs/cli/
- マシンユーザ
- CIサーバが人のユーザに代わってapplyする
- どのCIサービスを使うかによってやり方が変わってくる
- CircleCIの場合
-
terraform apply
を実行するワークフローで、contextを指定する - このcontextにAWSの認証情報を保存して管理する
-
- Github Actionsの場合
- OIDCを使用して認証を行えば、認証情報を手動で管理する必要がない
- Terraformで
aws_iam_openid_connect_provider
リソースを使ってIAM OIDC IDプロバイダを作成する- その際に、
tls_certificate
データソースを使って取得したGithub Actionsのサムプリントを信頼するように設定する - UPDATE: 今年の7月からAWSにサムプリントを渡す必要がなくなった(API側がこの変更に追随していないため、サムプリントの文字列自体は渡してあげる必要がある)
- https://github.com/aws-actions/configure-aws-credentials/issues/357#issuecomment-1626357333
- その際に、
- このIDプロバイダを通してAssumeRoleすることができる
- ポリシーのConditionブロックで指定してGithubリポジトリとブランチだけがAssumeRoleできるようにするのを推奨している
- 指定のAWSアカウントに対して、全てのGithubリポジトリで認証できるようにしてしまわないようにするため
-
リソースとデータソース
- データベースの認証情報をどのように管理するか
- 方法は以下の3つ
- 環境変数
-
variable
ソースを使って、渡したいシークレットを入れる変数を宣言する - シークレットが含まれていることを表すために
sensitive = true
を設定する -
TF_VAR_
から始まる環境変数を設定することで各変数に値を渡せる - シークレットをセキュアに保存するために1passwordなどを活用する
- ただ、シークレットの管理がTerraform管理外となるため、誰かがセキュアでない方法で保存する可能性もある
-
- 暗号化されたファイル
- シークレットをファイルとして暗号化して保存する
- 暗号化キーをKMSなどで管理するようにして、AWS CLIを使ってそのキーを使用してシークレットをファイルとして暗号化する
- 暗号化されているのでGithubにチェックインできるようになる
- Terraformでそのファイルからシークレットを読み出して使用する
- ただ、扱いにくい点がデメリット(
aws kms encrypt
コマンドが長いなど)
- シークレットストア
- AWS Secrets Managerなどのシークレットストアで管理する方法
- シークレットの保存はマネコンから行う
- Terraformからは
aws_secretsmanager_secret_version
データソースを使って読み出せる - シークレットはJSONで保存されているので、
jsondecode
関数を使ってパースしてローカル変数に渡してあげる - 簡単にシークレットの管理ができるし、ローテーションもしやすい
- ただ、別の環境でシークレットの設定を追加し忘れたみたいなことが起きやすい
- AWS Secrets Managerなどのシークレットストアで管理する方法
-
ステートファイル
- どんな方法を使ったとしてもステートファイルにプレーンテキストとして保存されてしまう
- 対策としては暗号化をサポートするバックエンドにTerraformステートを保存する
- 尚且つアクセス制限をかけてアクセスできる人を厳しく制御する
-
プランファイル
-
terraform plan
の結果をファイルに出力できる - ステートファイルと同じく、プレーンテキストとして保存されてしまう
- 対策としてはプランファイルを暗号化する(S3バケットに保存するなど)
- プランファイルのバックエンドへのアクセス制御を行う
-
7章 複数のプロバイダを使う
- terraformは2つのコンポーネントから構成される
- コア(terraformバイナリ)
- 各クラウドプラットフォームで使われるTerraformの共通の基本機能を提供する
- プロバイダ
- コアに対するプラグイン
- 各クラウドプラットフォーム固有の機能を提供する
- コアとプロバイダはRPCでやり取りする
- プロバイダがそれぞれのプラットフォームにHTTPでやり取りしてリソースの作成などを行う
- コア(terraformバイナリ)
-
terraform init
を実行したタイミングでTerraformが自動的にプロバイダのコードをダウンロードする- プロバイダのversionを指定したい場合は
required_providers
ブロックを追加する-
source
ではどこからプロバイダをダウンロードすべきかをURLで指定する- 何も指定しない場合(デフォルト)はパブリックなTerraformレジストリからプロバイダをダウンロードしてくる
- awsの場合だと
hashicoro/aws
のように表記できる
-
version
も何も指定がない場合(デフォルト)は最新バージョンをダウンロードしてくる
-
- プロバイダのversionを指定したい場合は
- hashicorpネームスペース内に存在しないプロバイダ(datadogなど)をインストールしたい場合は
required_providers
ブロックを書く必要がある- なので、常に
required_providers
を書くことが推奨されている
- なので、常に
- 複数リージョン or 複数アカウントにデプロイしたい場合
- 同じプロバイダの場合
- 複数リージョンにデプロイする場合
- それぞれのリージョンを指定する
provider
ブロックを記述する - エイリアスを設定することでregionの指定ができるようになる(provider名がどちらもawsのため、エイリアスが設定されていないと指定ができない)
-
resource
ブロックでは、デプロイしたいリージョンをエイリアスを使って指定できる -
module
では、デプロイしたいリージョンをmap
で指定する必要がある- これはモジュールでは複数リソースを一度にデプロイする場合があり、複数のプロバイダを使うことがあるため
- それぞれのリージョンを指定する
- 複数リージョンにデプロイする場合
- 同じプロバイダの場合
- 複数プロバイダを使用できるモジュールを作るには
- モジュールのユーザ側でproviderを設定する
-
required_providers
ブロック内のconfiguration_aliases
(設定エイリアス)を使用する - これにより通常と同じように
provider
パラメータを使ってリソースやデータソースに設定エイリアスを渡すことができる - モジュールのユーザ側で
provider
ブロック内のaliasパラメータを同じ値で設定する - ただ、一つのモジュールで複数のproviderを使うのは一般的にはアンチパターンとなる
8章 本番レベルのTerraformコード
- 本番レベルのインフラを構築するのに時間がかかる理由
- インフラやDevOpsのプロジェクトは予想以上の時間がかかってしまう
- 理由としては以下のようなもの
- DevOpsは産業分野としてまだ未成熟
- Yak shaving(ヤクの毛刈り)に陥りやすい
- 実際にやりたいタスクの前にやる必要のあるさまざまなタスクが含まれる
- 本番用のインフラを構築するためにやることが多い(だが、大概の人は全部を網羅できない)
- 本番レベルのインフラのチェックリスト
- ほとんどの会社では本番に出すための必要条件の明確な定義はない
- 本書では本番レベルのインフラのチェックリストが記載されている
- 構築する際はチェックリストを参照し、どの項目を実装しないことにしたのかとその理由をADRのように意識的に明確に記録しておきべき
- 本番レベルのインフラモジュールのベストプラクティス
- 小さなモジュール
- モジュールが大きいと、さまざまなデメリットがある
- コマンドの実行が遅くなる
- 最小権限の原則を適用しづらくなる
- どこか壊れてしまうと全体に影響してしまう
- 認知負荷が高まる
- レビューしにくい
- テストしにくい
- 1つのことだけを行う小さくて独立した複数の関数に分割する
Clean Code
の原則をインフラコードにも適用する
- モジュールが大きいと、さまざまなデメリットがある
- 組み合わせ可能なモジュール
- 入力変数として必要な値を渡し、作成されたあらゆる値を出力変数として返すようにする
- テスト可能なモジュール
- モジュールのコードを書き始める前にサンプルコードを先に書くようにすることで、理想的なUXを実現することを考えるようになる
- バリデーション
- 変数には
validation
ブロックを追加することができる - これにより、一定の条件以外の値を指定した場合に
terraform apply
時にエラーを返すことができる- 例えば、インスタンスタイプを指定する変数があるとして、
validation
ブロックで無料枠のインスタンスタイプを指定するようにするなど
- 例えば、インスタンスタイプを指定する変数があるとして、
- ただ、
validation
ブロック内のcondition
では他の値を参照することができないため、複雑なことはできない
- 変数には
- precondition(事前条件)とpostcondition(事後条件)
- 先のvalidationの制約事項を解決するブロック
-
precondition
ブロック- resource側に記述するブロックでapply実行前に条件をチェックすることができる
-
postcondition
ブロック- apply実行後に条件をチェックすることができる
- 例えば、デプロイしたASGが2つ以上のAZに跨っているかなどをチェックすることができる
- [TIPS] 循環依存エラーを防ぐためにselfキーワードを使うことができる
- 小さなモジュール
- モジュールのバージョン管理
- 一般的なルールとしてTerraformコアやプロバイダなどの依存関係はバージョン固定するべき
- 後方互換性のない変更を間違ってダウンロードすることがないように、最低限メジャーバージョンは固定するようにする
9章 Terraformのコードをテストする
- 手動テスト
- テストに関する重要事項その1
- Terraformコードをテストするとき、ローカルホストは使えない
- 手動テストを行う唯一の方法は実際の環境にデプロイすること
- staging環境やproduction環境とは完全に分離しておく
- 究極の理想はサービス、ひいては開発者それぞれにサンドボックスアカウントを持たせることだが、注意しないと課金爆発してしまう恐れがある
- テストに関する重要事項その2
- サンドボックス環境を定期的に片付ける
- 何らかのテストを行った時は、
terraform destroy
を実行してデプロイしたものを後片付けする文化を作るべき - 弊社のサンドボックス環境でも導入している
aws-nuke
が便利
- テストに関する重要事項その1
- 自動テスト
- 基本的にはユニットテスト、統合テスト、E2Eテストを組み合わせて使う
- ユニットテスト
- Terraformにおける関数あるいはクラスに近いのは再利用可能なモジュール
- ただ、Terraformにおいては外部依存関係を全くなくす現実的な方法は存在しないため、純粋なユニットテストはできない
- テストに関する重要事項その3
- ユニットテストを書く基本的戦略
- 小さく、独立したモジュールを作る
- モジュールに対してデプロイしやすいサンプルコードを作る
-
terraform apply
して、サンプルコードを実際にデプロイする - テストするインフラの種類に対して適した方法で動作するか確認する
- 最後に
terraform destroy
を実行する
-
Terratest
を本書では紹介しているが、今は似たようなことをterraform test
機能で実装できる-
terraform test
でも実際にインフラをデプロイしてテストし、最後に環境を破棄するようなことができる - ただ、主な用途はmoduleの動作確認になる
- module以外でも使えるが、新規にリソースを作成するときにしか使用できない
-
- TODO:
terraform test
を使ってみる - E2Eテスト
- 実際には複雑なインフラを持つ会社のほとんどでは、ゼロから全てをデプロイするE2Eテストを実行していない
- その代わり、変更の増分のみを適用するようにE2Eテストの戦略を考える
- Policy as Code
- そのほかにも、OPAを用いてタグポリシーなどをTerrafomに適用することもできる
10章 チームでTerraformを使う
-
この章ではTerraformをチームで採用するには、どう運用するのかについて書かれている
- ほとんど既出のもの
- 上司の説得方法や、Gitでバージョン管理してPRベースで運用しましょう etc.
-
デプロイ方法
- ブルーグリーンデプロイ
- 利点
- ある時点において、ユーザに対して見えるアプリケーションのバージョンは1つでだけ
- デプロイ中にキャパシティが減った状態で動作することがない
- 利点
- カナリアデプロイ
- 利点
- 何らかの問題があった場合に、影響を最小限に抑えられる
- コードのデプロイと新しい機能のリリースを分離できる
- 利点
- ブルーグリーンデプロイ
-
デプロイ戦略
- Terraformは問題が発生しても自動的にはロールバックしない
- 問題は起こると想定して、それに対処する方法をしっかりと備えておくべき
- リトライ
-
terraform apply
を再実行すると解消する一時的なエラーがある - それには
Terragrunt
のエラーに対する自動リトライ機能が便利
-
- ステートのエラー
- apply実行後に、Terraformがステートの保存に失敗することが時々ある
- 例)applyの途中でインターネット接続が切断された場合
- この場合、Terraformは
errored.tfstate
というファイルとして、ディスク上にステートファイルを書き込む - インターネット接続が復活し次第、
terraform state push errored.tfsate
を行うとリモートバックエンドに変更内容をpushできる
- apply実行後に、Terraformがステートの保存に失敗することが時々ある
- ロックのリリースのエラー
- CIサーバが
terraform apply
の途中でクラッシュした場合に、ステートは永遠にロックされたままになる - この場合、ステートがロックされていることとそのロックIDがエラー表示される
-
terraform force-unlock <LOCK_ID>
で強制的にロックをリリースすることができる
- CIサーバが
-
デプロイサーバ
- CIサーバに付与する権限に管理者権限を与えるのは各開発者に管理者権限を与えていることと同義
- このリスクを最小限にする方法はいくつかある
- CIサーバのロックダウン
- CIサーバをパブリックにしない
- 承認フローを強制する(最低一人からapproveをもらう等)
- CIサーバに恒久的な認証情報を渡さない
- アクセスキーをCIサーバに渡すのではなく、IAMロールやOIDCのような一時的な認証メカニズムを使うべき
- Gruntwork Pipelinesを例にCIサーバに管理者権限の認証情報を渡さない
-
感想・まとめ
- 再利用可能なmodulesを開発者に提供することは一種のPlatform Engineeringではないか?
-
terraform test
をモジュールリポジトリで試してみても良いかも - ディレクトリ構成はステートファイル管理方法に直結する
- ワークスペースによる分離は望ましくないため、基本的にディレクトリで分離するようにする
このスクラップは2023/12/24にクローズされました