Closed9

詳解 Terraform(第3版)まとめ

koizumi7010koizumi7010

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フォルダができて、そこに各ワークスペースのフォルダができる
        • ディレクトリ構成はシンプルに出来るが、terraform workspace コマンドを打たないとワークスペースがわからないので、間違えてstgにapplyするものをprdにapplyしてしまったりみたいなことが起きてしまう可能性がある
      • ファイルレイアウトによる分離
        • 環境(dev/stg/prd)ごとにディレクトリを分けて、それぞれにバックエンドを設定することで、statefileを分離する
          • S3バケット自体を分けることも可能なので、それぞれに異なるバケポリを設定できたりする
        • 認知負荷も低くなり、自分がどの環境にデプロイしようとしているのかが明確にわかる
        • さらに環境内のコンポーネントごと(sharedなiamやs3、vpc、service、db etc...)にディレクトリを分けることが本書では推奨している
          • 1日に何度も設定変更を行うようなコンポーネントと、滅多に変更しないVPCなどのコンポーネントを同じstatefileで管理するのは、誤って一緒にVPCのNW環境を壊したりするリスクがあるため
          • ただ、ディレクトリ構成は複雑になる
        • terraform_remote_stateデータソースを使ってoutput値も設定すれば、別ディレクトリからも参照可能
      • 本書では、ファイルレイアウトによる分離を推奨している
koizumi7010koizumi7010

4章 モジュール

  • モジュール(ローカルファイルパス)
    • 前章であったファイルレイアウトによるステートファイルの分離を行なった場合などに、それぞれの環境で同じような内容を何回も記述することになる
    • stg/prdのディレクトリとは別に、modulesディレクトリを作成して同じ記述を再利用できる
  • モジュールを使う際の注意点
    • ファイルパス
      • 例えばモジュール側でtemplatefile関数を使用している場合、モジュールを使う側で指定する相対パスが異なってしまう
      • そのようなときに path.module などのパス参照を使えば、モジュールを使う側でよしなにパスを取得してくれる
    • インラインブロック
      • terraform resourceの中には、aws_security_groupaws_route_tableなどのようにインラインブロックを記述できるものがある
      • このインラインブロックに書く記述は別リソースとして書き出すことが可能で、aws_security_groupであればaws_security_group_ruleとして別リソースに書き出すことができる
      • ただ、このインラインブロックと別リソースの記述が重複してしまうとエラーになってしまうので、どちらか一方のみを使う必要がある
      • 本書では、モジュールを使う時は常に別リソース(aws_security_group_ruleなど)を使うのを推奨してる
      • モジュール側でインラインブロックを使っていると、モジュールを使用する側で追加のルールを作成することができなくなるので、最低限のルールを別リソースとしてモジュール側で用意しておいて、使用する側でよしなにルールを別リソースで作成する運用が良い
  • モジュールのバージョン管理
    • ローカルファイルパスによるモジュールだと、stgとprdで強制的に使用するモジュールが一緒になる
    • stgである程度モジュールの検証をした後に、prdもそのモジュールを使いたい
    • その場合はモジュールを別リポジトリに分離して、Gitのタグ機能(git tag -a "v0.0.1")を使ってバージョン管理するのが良い
    • バージョン管理にはセマンティックバージョニングを利用すると使用する側もわかりやすくて良い
    • 弊社でもモジュールのバージョン管理をセマンティックバージョニングで行なっているが、https://github.com/markchalloner/git-semver でこのバージョン付けを自動化している
koizumi7010koizumi7010

5章 ループ、条件分岐、デプロイ、その他つまづきポイント

  • count

    • IAMユーザを複数個作りたい場合などにcountを使用できる
    • count == 0は作成しない
    • 制限事項
      • リソース内のインラインブロックには使用できない
      • リソースのリストを作成するのにcountを使っている場合、途中でリストの要素を削除すると、削除した要素以降のアイテムを全て削除して、イチから作り直してしまう
  • for_each

    • リソースに対してfor_eachを使う際にリストはサポートしていないので、tosetを使ってリストを集合に変換して渡す必要がある
    • each.key, each.valueで値を取れるが、キーと値から構成された集合の時のみeach.keyを使うことが多い
    • モジュールにはfor_eachを使うのが理想
  • 条件分岐

    • countパラメータを使ったif文
      • 条件式のtrue/falseの値を逆にすることでelse文のように挙動させられる
    • for_eachとforを使った条件分岐
      • リソースやモジュールを複数作成したい場合はfor_eachを使うべき
      • 条件分岐のロジックを実現するときは、countを使った方が単純
    • 条件付きで作成したいときはcount、それ以外のループや条件分岐にはfor_eachを使う
  • ゼロダウンタイムデプロイ

    • create_before_destroyライフサイクルを使って、置き換え先のリソースを先に作成し、それから元のリソースを削除できる
  • つまづきポイント

    • countとfor_eachの制限事項
      • random_integerリソースなどの出力値は参照できない
      • planフェーズ時にcountやfor_each内で計算することができないため(applyして初めて値が出力されるため)
    • ゼロダウンタイムデプロイの制限事項
      • 利用できるのであればインスタンスの更新のような専用のネイティブなデプロイ方法を選択すべき
        • ASGに関してはinstance_refreshブロックがある
    • 有効なプランも失敗することがある
      • Terraform管理外で同じリソース名で作成されているものがあれば、Planが成功してもApplyで失敗してしまう
        • importコマンドを使って管理下に移行すること
        • まとめてimportしたいなら、terraformerterracognitaのようなツールがある
    • リファクタリングは難しい
      • 安易にリソース名を変更したりすると、既存のリソースを削除して完全に新しいリソースを作ることになるため、ダウンタイムが発生してしまう
        • planコマンドを使って反映内容を常に確認するようにしたり、削除される前に作成するようにしたりする必要がある
        • terraform state mvコマンドを使って、ステートファイルを変更するのも手
koizumi7010koizumi7010

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

  • シークレット管理の基本

    • シークレットをプレーンテキストで保存しないこと ←絶対やっちゃいけない
    • シークレットの用途に適した管理ツールを使うことが不可欠
  • シークレットの種類

    • 個人シークレット
      • Webサイトにログインするためのusername/password など
    • 顧客シークレット
      • 顧客の個人情報など
    • インフラシークレット
      • DBのパスワード、API/APPキーなど
  • シークレットの保存方法

    • ファイルベースのシークレットストア
      • シークレットを暗号化したファイルに保存する
      • 暗号化キーはAWS KMSなどに保存する
    • 集中型のシークレットストア
      • MySQLなどのデータストアにシークレットを暗号化して保存する
      • その暗号化キーはAWS KMSなどに保存する
  • Terraformでの認証方法例

    • 兎にも角にもキーをハードコードするのは絶対にNG
    • 大きく分けて、人のユーザが実行する場合とマシンユーザ(CIサーバなど)が実行する場合とに分けられる
    • 人のユーザ
      • AWSの認証情報(IAM UserのAccessKey/Secret Key)を使って手元からapplyする
      • 環境変数に設定してapplyすることも可能だが、キーを覚えておくのは現実的ではないので、1passwordを使ってキーを管理するのがおすすめ
    • マシンユーザ
      • CIサーバが人のユーザに代わってapplyする
      • どのCIサービスを使うかによってやり方が変わってくる
      • CircleCIの場合
        • terraform applyを実行するワークフローで、contextを指定する
        • このcontextにAWSの認証情報を保存して管理する
      • Github Actionsの場合
        • OIDCを使用して認証を行えば、認証情報を手動で管理する必要がない
        • Terraformでaws_iam_openid_connect_providerリソースを使ってIAM OIDC IDプロバイダを作成する
        • この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関数を使ってパースしてローカル変数に渡してあげる
        • 簡単にシークレットの管理ができるし、ローテーションもしやすい
        • ただ、別の環境でシークレットの設定を追加し忘れたみたいなことが起きやすい
  • ステートファイル

    • どんな方法を使ったとしてもステートファイルにプレーンテキストとして保存されてしまう
    • 対策としては暗号化をサポートするバックエンドにTerraformステートを保存する
    • 尚且つアクセス制限をかけてアクセスできる人を厳しく制御する
  • プランファイル

    • terraform planの結果をファイルに出力できる
    • ステートファイルと同じく、プレーンテキストとして保存されてしまう
    • 対策としてはプランファイルを暗号化する(S3バケットに保存するなど)
    • プランファイルのバックエンドへのアクセス制御を行う
koizumi7010koizumi7010

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

  • terraformは2つのコンポーネントから構成される
    • コア(terraformバイナリ)
      • 各クラウドプラットフォームで使われるTerraformの共通の基本機能を提供する
    • プロバイダ
      • コアに対するプラグイン
      • 各クラウドプラットフォーム固有の機能を提供する
      • コアとプロバイダはRPCでやり取りする
      • プロバイダがそれぞれのプラットフォームにHTTPでやり取りしてリソースの作成などを行う
  • terraform initを実行したタイミングでTerraformが自動的にプロバイダのコードをダウンロードする
    • プロバイダのversionを指定したい場合はrequired_providersブロックを追加する
      • sourceではどこからプロバイダをダウンロードすべきかをURLで指定する
        • 何も指定しない場合(デフォルト)はパブリックなTerraformレジストリからプロバイダをダウンロードしてくる
        • awsの場合だとhashicoro/awsのように表記できる
      • versionも何も指定がない場合(デフォルト)は最新バージョンをダウンロードしてくる
  • hashicorpネームスペース内に存在しないプロバイダ(datadogなど)をインストールしたい場合はrequired_providers ブロックを書く必要がある
    • なので、常にrequired_providersを書くことが推奨されている
  • 複数リージョン or 複数アカウントにデプロイしたい場合
    • 同じプロバイダの場合
      • 複数リージョンにデプロイする場合
        • それぞれのリージョンを指定するproviderブロックを記述する
        • エイリアスを設定することでregionの指定ができるようになる(provider名がどちらもawsのため、エイリアスが設定されていないと指定ができない)
        • resourceブロックでは、デプロイしたいリージョンをエイリアスを使って指定できる
        • moduleでは、デプロイしたいリージョンをmapで指定する必要がある
          • これはモジュールでは複数リソースを一度にデプロイする場合があり、複数のプロバイダを使うことがあるため
  • 複数プロバイダを使用できるモジュールを作るには
    • モジュールのユーザ側でproviderを設定する
    • required_providersブロック内のconfiguration_aliases(設定エイリアス)を使用する
    • これにより通常と同じようにproviderパラメータを使ってリソースやデータソースに設定エイリアスを渡すことができる
    • モジュールのユーザ側でproviderブロック内のaliasパラメータを同じ値で設定する
    • ただ、一つのモジュールで複数のproviderを使うのは一般的にはアンチパターンとなる
koizumi7010koizumi7010

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コアやプロバイダなどの依存関係はバージョン固定するべき
    • 後方互換性のない変更を間違ってダウンロードすることがないように、最低限メジャーバージョンは固定するようにする
koizumi7010koizumi7010

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

  • 手動テスト
    • テストに関する重要事項その1
      • Terraformコードをテストするとき、ローカルホストは使えない
      • 手動テストを行う唯一の方法は実際の環境にデプロイすること
        • staging環境やproduction環境とは完全に分離しておく
        • 究極の理想はサービス、ひいては開発者それぞれにサンドボックスアカウントを持たせることだが、注意しないと課金爆発してしまう恐れがある
    • テストに関する重要事項その2
      • サンドボックス環境を定期的に片付ける
      • 何らかのテストを行った時は、terraform destroyを実行してデプロイしたものを後片付けする文化を作るべき
      • 弊社のサンドボックス環境でも導入しているaws-nukeが便利
  • 自動テスト
    • 基本的にはユニットテスト、統合テスト、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に適用することもできる
koizumi7010koizumi7010

10章 チームでTerraformを使う

  • この章ではTerraformをチームで採用するには、どう運用するのかについて書かれている

    • ほとんど既出のもの
    • 上司の説得方法や、Gitでバージョン管理してPRベースで運用しましょう etc.
  • デプロイ方法

    • ブルーグリーンデプロイ
      • 利点
        • ある時点において、ユーザに対して見えるアプリケーションのバージョンは1つでだけ
        • デプロイ中にキャパシティが減った状態で動作することがない
    • カナリアデプロイ
      • 利点
        • 何らかの問題があった場合に、影響を最小限に抑えられる
        • コードのデプロイと新しい機能のリリースを分離できる
  • デプロイ戦略

    • Terraformは問題が発生しても自動的にはロールバックしない
    • 問題は起こると想定して、それに対処する方法をしっかりと備えておくべき
    • リトライ
      • terraform applyを再実行すると解消する一時的なエラーがある
      • それにはTerragruntのエラーに対する自動リトライ機能が便利
    • ステートのエラー
      • apply実行後に、Terraformがステートの保存に失敗することが時々ある
        • 例)applyの途中でインターネット接続が切断された場合
      • この場合、Terraformはerrored.tfstateというファイルとして、ディスク上にステートファイルを書き込む
      • インターネット接続が復活し次第、terraform state push errored.tfsateを行うとリモートバックエンドに変更内容をpushできる
    • ロックのリリースのエラー
      • CIサーバがterraform applyの途中でクラッシュした場合に、ステートは永遠にロックされたままになる
      • この場合、ステートがロックされていることとそのロックIDがエラー表示される
      • terraform force-unlock <LOCK_ID>で強制的にロックをリリースすることができる
  • デプロイサーバ

    • CIサーバに付与する権限に管理者権限を与えるのは各開発者に管理者権限を与えていることと同義
    • このリスクを最小限にする方法はいくつかある
      • CIサーバのロックダウン
      • CIサーバをパブリックにしない
      • 承認フローを強制する(最低一人からapproveをもらう等)
      • CIサーバに恒久的な認証情報を渡さない
        • アクセスキーをCIサーバに渡すのではなく、IAMロールやOIDCのような一時的な認証メカニズムを使うべき
      • Gruntwork Pipelinesを例にCIサーバに管理者権限の認証情報を渡さない
  • 感想・まとめ

    • 再利用可能なmodulesを開発者に提供することは一種のPlatform Engineeringではないか?
    • terraform testをモジュールリポジトリで試してみても良いかも
    • ディレクトリ構成はステートファイル管理方法に直結する
      • ワークスペースによる分離は望ましくないため、基本的にディレクトリで分離するようにする
このスクラップは2023/12/24にクローズされました