Help us understand the problem. What is going on with this article?

Terraform設計・運用のノウハウ

概要

Terraformを使い始めて一年ほど経ったので、設計や運用上のノウハウを公開していこうと思います。
基本的にAWSを使うことが多いので、AWS寄りの話になります。

設計編

利用可能なプロバイダを調べる

TerraformはAWSやGCPといったクラウドサービスをコード化できることで有名ですが、オフィシャルだけでも様々なプロバイダに対応しています。
私はAWSのほか、GitHubやDatadogをコード化して運用してます。

ネットワーク設計

サービスを構築する上で本番環境のほかに開発やステージング環境を作りますが、環境の分け方は大きく分けて次のいずれかになることが多いと思います。

  1. 1つのVPCに全ての環境を入れる
  2. 環境ごとにVPCを分ける
  3. 環境ごとにAWSアカウントを分ける

2のパターンで設計する場合、Terraformで構成を組む際はVPCをdevelopmentstagingproductionに分離し、更に各VPCで共通となるサービスを配置するためのmanagementを作成します。managementには踏み台であったり、CI/CDを管理するインスタンスを配置しています。
ネットワーク上はmanagementから各環境にVPCペアリングで接続できますが、環境間(例えばdevelopmentからstagingstagingからproduction)の接続はルートテーブル上で制限しています。

開発基盤の概要-Page-3 (2).png

Workspaceを使う

Terraformで環境ごとのリソースを管理する方法は大きく分けて2つあります。

  1. 環境ごとにディレクトリを分けて管理
  2. Workspaceを使う

TerraformにはWorkspaceと呼ばれる環境ごとにリソースを管理する仕組みがあります。
先に上げたネットワーク構成にWorkspaceを適用することで、VPC単位のリソース構成が管理しやすくなります。

$ terraform workspace list
  default
* development
  production
  staging

managementVPCはWorkspace管理下から除外しています。

ディレクトリ設計

Terraformでリソースを管理する場合、悩むことになるのがディレクトリ構成かと思います。最もシンプルな構成は以下のような構成ではないでしょうか。

terraform
  aws
    ec2
      main.tf
      variables.tf
      outputs.tf
      providers.tf
      backend.tf
      version.tf
    security_group
      main.tf
      variables.tf
      outputs.tf
      providers.tf
      backend.tf
      version.tf
    ...

サービスが1つであれば上記構成でも運用できそうですが、サービスがスケールするにつれ、サブドメインで動く別のシステムや、リージョンをまたいだ構成も生まれる可能性があります。

最近私が設計していて割と汎用的に使えると思ってる構成は以下のようなものです。

terraform
  modules: モジュールの定義
    aws
      ec2
      iam
      s3
      ...
    datadog
    github
  providers
    aws
      global: AWS全般に関する設定
        general:
          global
            iam
            route53
            ...
          ap-northeast-1
            cognito
            cloudtrail
            ...
        management: Management VPC
          global
            iam
            waf
            ...
          ap-northeast-1
            vpc
            security_group
            ...
      {SERVICE NAME}: サービス名
        general
          global
            route53
            ...
          workspace
            ap-northeast-1
              global
                vpc
                ecs
                rds
                ...
    datadog
      ...
    github
      ...

利点としては、

  • Terraformの実行単位がAWSのサービス構成と同じ (誤ってリソースを操作した際の影響範囲が限定される)
  • AWS全般に関わる設定をbase、特定のサービスに依存する設定を{SERVICE_DOMAIN}下に配置することで、リソースの構成が視覚化される

などが挙げられます。

Terraform Best Practiceを読む

Terraform Best Practiceを読みましょう。中でもNaming conventionsは非常に有用です。いくつか気になるポイントをまとめてみました。

  • Resource and data source arguments
    • リソース名や変数、出力名の区切り文字には-の代わりに_を使う
    • リソース名にはリソースタイプを含めない (例えば、aws_route_tableのリソース名にroute_tableなどのタイプを含めない)
    • 単一リソースや妥当な名前がない場合、リソース名はthisとする
    • tagsdepends_onlifecycleはリソースの最後に記述する
  • Variables
    • default = []がある場合、type = "list"は省略する
    • default = {}がある場合、type = "map"は省略する
    • listmapの変数名は複数形を使用する
  • Outputs
    • 名前は{name}_{type}_{attribute}形式が望ましい

リソース名の変更

terraform state mvコマンドを使います。

# リソース名をfooからbarに変更
$ terraform state mv aws_kms_key.foo aws_kms_key.bar

Workspaceを利用してる場合はWorkspace単位で実行が必要となります。

リソースをモジュール化

基本的にはリソース名の変更と同じです。接頭辞としてmodule.{MODULE_NAME}を付けるだけです。

# リソースfooをbarモジュール内のaws_kms_key.bazに変更
$ terraform state mv aws_kms_key.foo module.bar.aws_kms_key.baz

実装編

Docker版Terraformを使う

Docker版のTerraformを利用しましょう。

バージョンを指定する

利用するバージョンを明確化することで、無用なトラブルを回避することができます。

Terraform

terraform {
  required_version = "= 0.12.4"
}

Docker版Terraformを利用する場合、イメージのバージョンはlatestではなく、0.12.4のように番号で指定することをお勧めします。

プロバイダバージョン

provider "aws" {
  version = "= 2.19.0"
}

モジュールバージョン

module "foo" {
  source  = "terraform-aws-modules/security-group/aws"
  version = "3.0.1"
}

尚、sourceがGitHubの場合はversionが使えないので、代わりにrefでバージョンを指定する必要がある。

module "foo" {
  source = "github.com/azavea/terraform-aws-acm-certificate?ref=1.1.0"
}

tfstateファイルの管理方法

リソースの状態を管理するtfstateファイルはローカルやGitで管理するべきではありません。TerraformのBackend機能を使うことで、tfstateをDynamoDBやS3で管理することができます。
AWSを利用しているのであれば、S3がお手軽かと思います。

terraform {
  backend "s3" {
    bucket = "mybucket"
    key    = "path/to/my/key"
    region = "us-east-1"
  }
}

尚、S3を使う場合、対象バケットのバージョニングの有効化を強くお勧めします。tfstateを誤って削除したり、Terraformのバージョンアップでファイル構成が書き換わっても、簡単にステートファイルを復元することができます。

Terraform Module Registryを使う

Terraform Module Registryでは、汎用的に使えるモジュールが公開されてます。AWSで言えばEC2やSecurity Groupなど、いくつかの変数を渡すだけでリソースを作成してくれます。
Registryには大きく分けて、Terraformコミュニティが作ったものと、HashiCorpによって検証されたVerifiedモジュールがあります。
運用で使う場合は、検索時にVerifiedにチェックを付けることをお勧めします。

Screen_Shot_2019-07-15_at_23_21_12.png

フォーマットを整形する

fmtコマンドでディレクトリ下のファイルに対しまとめて整形してくれます。

# 0.11まで
$ terraform fmt

# 0.12以降
$ terraform fmt -recursive

環境ごとに異なる値を読み込む方法

変数名はWorkspace名を含めて定義します。

variable "rds_cluster" {
  default = {
    "default.engine_version"    = "5.7.12"
    "production.engine_version" = "5.7.22"
  }
}

リソース作成時にlookup関数を使うことで、現在有効なWorkspaceがproductionであれば5.7.22、それ以外であれば5.7.11の値が設定されます。

resource "aws_rds_cluster" "this" {
  engine_version = lookup(
    var.rds_cluster,
    "${terraform.workspace}.engine_version",
    var.rds_cluster["default.engine_version"],
  )
  ...

lifecycleでリソースを保護する

リソースにlifecycleブロックを指定することで、リソース作成・更新時の動作を変更することができます。lifecycleにはいくつかのタイプがありますが、prevent_destroyを指定することで、強制的なリソース再作成が発生したときにエラーを発生するようになります。

main.tf
lifecycle {
  prevent_destroy = true
}

destroyを実行した結果。

$ terraform destroy
...
Error: Instance cannot be destroyed

  on main.tf line 55:
  55: resource "aws_eip" "this" {

Resource aws_eip.this has lifecycle.prevent_destroy set, but the plan calls
for this resource to be destroyed. To avoid this error and continue with the
plan, either disable lifecycle.prevent_destroy or reduce the scope of the plan
using the -target flag

リソースを強制的に削除したい場合は、prevent_destroyfalseにするか、ブロックごと削除して、applyを実行します。

prevent_destroyはあくまでTerraform上で削除を保護するためのもので、AWSコンソールやCLIからは削除することが可能です。EC2やRDSなどいくつかのサービスには削除保護のオプションがあるので、別途有効化しておくことをお勧めします。

IAM PolicyのJSONはaws_iam_policy_documentで定義する

以下のリソースはIAMポリシーがヒアドキュメント形式で書かれており、可読性が低いです。

main.tf
resource "aws_iam_policy" "example" {
  # ... other configuration ...
  policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Action": "*",
    "Resource": "*"
  }
}
POLICY
}

ヒアドキュメントはこのように置き換えることができます。HCLで書けるのでだいぶスッキリします。

main.tf
resource "aws_iam_policy" "example" {
  # ... other configuration ...
  policy = data.aws_iam_policy_document.example.json
}

data "aws_iam_policy_document" "example" {
  version = "2012-10-17"
  statement {
   effect = "Allow"
    actions = ["*"]
    resources = ["*"
  }
}

デバッグログを出力する

環境変数TF_LOGにログレベルを渡すことで、ログを出力することができます。ファイルに出力したい場合はTF_LOG_PATHも指定すると良いでしょう。

実行速度を改善する

Terraformのコマンドに--parallelismを渡すことで、APIの同時呼び出し数を変更することができます。

$ terraform plan --parallelism=20

デフォルト値は10です。パフォーマンスを見て数値を変えて見ると良さそうです。
尚、環境変数で指定する場合はTF_CLI_ARGS_planTF_CLI_ARGS_applyTF_CLI_ARGS_destroyを指定します。

拡張ライブラリ

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away