1クール続けるブログ

とりあえず1クール続けるソフトウェアエンジニアの備忘録

Terraformの基礎を学んだ

記事一覧はこちら

Table of Contents

背景・モチベーション

8月から中途入社した会社ではInfrastructureの管理にTerraformを利用しています。業務において自分が利用したことがあるのは、AWSのCloudFormationとGCPのdeployment-managerのみでTerraformはプライベートな利用でとどまっていました。

雰囲気で利用している状況から抜け出すために、1つ1つの概念をちゃんと学習していくことにしました。幸い、HashiCorpのドキュメントはとても充実していたため公式のドキュメントをベースに学習を進めることが出来ました。

参考文献

learn.hashicorp.com

www.terraform.io

Terraform

TerraformはTerraform CoreTerraform Plugins で構成されている。
図は Perform CRUD Operations with Providers | Terraform - HashiCorp Learn から引用させていただきました。

https://learn.hashicorp.com/img/terraform/providers/core-plugins-api.png

  1. Terraform Core 設定を読み込んで、resource dependency graphを構築する
  2. Terraform Plugins (providers and provisioners) は Terraform Core とそれぞれのターゲットとなるAPIの橋渡しを行います

Terraformは設定をHCLというHuman ReadableなHashiCorpの言語で記述する。ただ、Machine ReadableなJSON Syntaxも提供している。
詳細なスペックはthe HCL native syntax specificationに定義してある。

Resources

Define Infrastructure with Terraform Resources | Terraform - HashiCorp Learn

resource blockはTerraform構成において1つ以上のinfrastructure objectを表す。resource blockではresource typeとnameを宣言する。
typeとnameは、resource_type.resource_name という形式のresource identifier(ID)を形成する。下記の例だと、aws_instance.web となる。resource IDは workspace内で一意でなければいけない。

resourceはargumentとattribute、meta-argumentを持つ。

  • Arguments
    • 特定のリソースの設定を行うための引数
    • 多くのArgumentsはリソース固有のもの
    • 必須なものとOptionalなものがあり、必須なArgumentsを指定しない場合にTerraformはエラーを返す
  • Attributes
    • 存在するResourceで公開される値
    • Resourceのattributeへの参照は、resource_type.resource_name.attribute_name という形式になる
    • 設定を指定するArgumentsと異なり、AttributesはクラウドプロバイダやAPIによってassignされる
  • Meta-arguments
    • Resourceの振る舞いを変更するもの
    • 例えば、count というMeta-Argumentsを利用することで複数のResourceを作成することが出来る(他にも、depends_on, for_each , provider , lifecycle がある)
    • Terraform自体の機能なので、ResourceやProviderに固有のものではない
resource "aws_instance" "web" {
  // 以下3つはArguments
  ami                    = "ami-a0cfeed8"
  instance_type          = "t2.micro"
  user_data              = file("init-script.sh")  // file() function
}

Input Variables

Customize Terraform Configuration with Variables | Terraform - HashiCorp Learn Input Variables - Configuration Language - Terraform by HashiCorp

📝 variables.tf っていうファイル作ってそこに集めるのが良くあるパターンぽい

Input variables は、end userが設定をカスタマイズするために値を定義できるようにすることでより、Terraformの設定をより柔軟にする

Declare variables

variablesはImmutableで、Terraformが動くときに変更はない
variable <変数名> { ... } 句で宣言し、3つのOptionalな引数をとる。 全部設定しておくことを推奨。

  • Description: A short description to document the purpose of the variable.
  • Type: The type of data contained in the variable.
  • Default: The default value.

Default値を設定しない場合には、Terraformが設定をapplyする前にassignする必要がある。variableの値はリテラル値でなくてはいけないので、式とかは🙅

Typeがサポートしているkeywordは、 string, number, boolの3種類。
type constructorsを利用することで collectionのような複雑な型も宣言できる: list(<TYPE>), set(<TYPE>), map(<TYPE>), object({<ATTR_NAME> = <TYPE>, …>}), tuple([<TYPE>, …])
any keywordは任意の値がacceptableであることを示す

variable "public_subnet_count" {
  description = "Number of public subnets."
  type        = number
  default     = 2
}

variable "public_subnet_cidr_blocks" {
  description = "Available cidr blocks for public subnets."
  type        = list(string)
  default     = [
    "10.0.1.0/24",
    "10.0.2.0/24",
    "10.0.3.0/24",
  ]
}

variable "resource_tags" {
  description = "Tags to set for all resources"
  type        = map(string)
  default     = {
    project     = "project-alpha",
    environment = "dev"
  }
}

module "vpc" {
    source  = "terraform-aws-modules/vpc/aws"
    version = "2.44.0"
    public_subnets  = slice(var.public_subnet_cidr_blocks, 0, var.public_subnet_count)
    tags = var.resource_tags
    // …
}

Assign Values

varialeにdefault値を持たせなかったときはterraform apply 時にpromptで聞かれる。ただし、promptはエラーを誘発しやすい。

Assgin Valuesの方法(くわしくはここ

  • 変数を定義したファイル (.tfvars)
    • Terraformはカレントディレクトリのterraform.tfvarsに完全一致するファイルもしくは*.auto.tfvarsに部分一致するファイルを 自動的に全て読み込む
    • 上記に一致しないファイル名でも、-var-file flagで渡すことができるが、ファイルの拡張子は .tfvars or .tfvars.jsonである必要がある
  • Command Line
    • terraformコマンドの-var optionで指定することが可能
    • example: terraform apply -var='image_id_list=["ami-abc123","ami-def456"]' -var="instance_type=t2.micro"
  • 環境変数
    • Terraformは自プロセスに保持する TF_VAR_ から始まる環境変数を読み込む、そのprefixの後には変数名が続く
    • example: export TF_VAR_image_id=ami-abc123
  • Terraform Cloudの場合はworkspace variables

Terraformは以下の順序で変数をロードし、後のソースが前のソースよりも優先される
環境変数の優先度が低いことに注意

  1. 環境変数
  2. terraform.tfvars
  3. terraform.tfvars.json
  4. Any *.auto.tfvars or *.auto.tfvars.json (ファイル名の辞書順)
  5. Any -var and -var-file options on the command line (渡された順番)

Reference Values

variableを参照するときは var.<variable_name>
文字列補間でも参照することができる 例: name = "web-sg-${var.resource_tags["project"]}-${var.resource_tags["environment”]}”

また、Variablesをvalidateすることも可能で、variable ブロックの中で、validation フィールドを持つことができる。 regexall() 関数は正規表現をとり文字列をテスト一致した文字列のリストを返すので、これを使ってcondition に一致すればOKで一致しなければ error_message が出力される

variable "resource_tags" {
  description = "Tags to set for all resources"
  type        = map(string)
  default     = {
    project     = "my-project",
    environment = "dev"
  }

  validation {
    condition     = length(var.resource_tags["project"]) <= 16 && length(regexall("[^a-zA-Z0-9-]", var.resource_tags["project"])) == 0
    error_message = "The project tag must be no more than 16 characters, and only contain letters, numbers, and hyphens."
  }
}

Output Values

Output Data from Terraform | Terraform - HashiCorp Learn Output Values - Configuration Language - Terraform by HashiCorp

📝 outputs.tf っていうファイル作ってそこに集めるのが良くあるパターンぽい

output はTerraform Moduleの戻り値とも言えるようなもので、用途としては主に下記の3つとなる。

  • Child ModuleがParent Moduleにリソース属性のサブセットを公開するため
  • Root Moduleでは、terraform applyを実行した後に特定の値を表示するため
  • remote stateを利用している場合、root moduleのoutputsはterraform_remote_state data source経由(後述)で他の設定からアクセスできる。

Terraformのstateにoutputはロードされ、terraform output commandでクエリすることができる

  • 特定のものをクエリするには、terraform output lb_url と名前を引数に渡してあげるとよい
  • -raw フラグを使うと、stringに対してdouble quoteが付かなくなる

output blockにはsensitive フィールドを付与できる

  • plan , apply , or destroy のときの表示は <sensitive> となり見れない
  • terraform.tfstate ファイルには平文で入っているし、terraform output ではredactされないことに注意すること

terraform output -json のようにoptionを付けることで、json形式で出力できる。machine-readable format for automationなのでtoolから使うとき便利だよね

output "db_password" {
  value       = aws_db_instance.db.password
  description = "The password for logging in to the database."
  sensitive   = true
}

output "instance_ip_addr" {
  value       = aws_instance.server.private_ip
  description = "The private IP address of the main server instance."

  depends_on = [
    // このIPアドレスが実際に使用される前に、セキュリティグループのルールが作成されていなければ、サービスに到達できない
    aws_security_group_rule.local_access,
  ]
}

Locals

Simplify Terraform Configuration with Locals | Terraform - HashiCorp Learn

Terraformのlocals は設定の中で参照できる名前付きの値です。local valueを利用することで、繰り返しを避けTerraformの設定をsimpleに保つことが出来る。また、値をhard-codingするのではなく、意味のある名前付けを使うことで、より読みやすく出来る。

locals {
  name_suffix = "${var.resource_tags["project"]}-${var.resource_tags["environment"]}"
}

Dependency Lock File

Lock and Upgrade Provider Versions | Terraform - HashiCorp Learn Dependency Lock File (.terraform.lock.hcl) - Configuration Language - Terraform by HashiCorp

📝 versions.tf っていうファイル作ってそこに集めるのが良くあるパターンぽい

Terraformプロバイダは、TerraformとターゲットAPIの間で通信を行い、リソースを管理する。ターゲットAPIが変更されたり、機能が追加されたりすると、プロバイダーのメンテナはプロバイダーを更新してバージョンを上げる。
設定でプロバイダのバージョンを管理するには、以下の2つを行う。

  1. Providerのバージョン指定/制限をterraform blockで行う
  2. dependency lock fileを利用する

terraform init すると、Terraformはcurrent directoryに.terraform.lock.hcl ファイルを作成する。Terraformが、across your team and in ephemeral remote execution environmentsで同じプロバイダバージョンを使用するためにVCSに含める必要がある。

terraform init -upgrade で、すべてのプロバイダーを、設定であらかじめ設定されたバージョン制約内で一貫した最新バージョンにアップグレードします。

lockファイルで確認できるのは、実際に利用しているバージョンと、この選択を行うときの制約(constraint)。また、チェックサム検証を行うので、hashes というフィールドでハッシュを保持する。zh: はzip hashを意味する、これはレガシー。h1: は現在推奨されているhash scheme

ここで、providerのversion constraintsにも触れておく。詳細はここにある。

A module intended to be used as the root of a configuration, 互換性のない新バージョンへの誤ったアップグレードを避けるために、動作することを意図したプロバイダの最大バージョンも指定する必要がある。演算子 ~> は、特定のマイナー・リリース内のパッチ・リリースのみを許可するための便利な略記法です = 📝パッチバージョンしか上がらない?

terraform {
  required_providers {
    mycloud = {
      source  = "hashicorp/aws"
      version = "~> 1.0.4"
    }
  }
}

多くの設定で再利用しようとするモジュールの場合! たとえそのモジュールが特定の新しいバージョンと互換性がないことがわかっていても、~> (または他の最大バージョン制約)を使用しないでください。そうすることでエラーを防ぐことができる場合もありますが、多くの場合、モジュールのユーザーが定期的なアップグレードを行う際に多くのモジュールを同時に更新することを余儀なくされます。最小バージョンを指定し、既知の非互換性を文書化し、最大バージョンをルートモジュールに管理させます

Module

Modules Overview | Terraform - HashiCorp Learn

TerraformでInfrastructureを管理していくと、どんどん複雑な設定になる。 設定ファイルの理解や操作が難しくなり、類似した設定のブロックが増えて重複が生まれる。 プロジェクトやチーム間で構成の一部を共有したいと思っても容易ではなく、切り貼りするだけではメンテナンス困難に。

What are modules for?

前述の問題をModuleは解決してくれる

  • Organize configuration
    • 設定の関連する部分をまとめておくことで、設定の理解や更新を容易にする
  • Encapsulate configuration
    • カプセル化することが出来るため、設定の一部の変更をしたときに意図しない変更を防ぐことが出来る
  • Re-use configuration
    • 自分自身、他のメンバー、moduleを公開している Terraform practitionersによって書かれた設定を再利用できる
  • Provide consistency and ensure best practices
    • 設定に一貫性を持たせることで、全ての構成にベストプラクティスが適用されることを保証できる

What is a Terraform module?

Terraform Moduleは、1つのディレクトリにあるTerraform設定ファイルのSetです。 1つのディレクトリに1つ以上の.tf ファイルを配置しただけのシンプルな設定でもModuleになる。このようなディレクトリから直接Terraformコマンドを実行した場合には、それはroot moduleと見なされる。

Calling Modules

設定ではmodule blockを使って他のディレクトリにあるmoduleを呼び出すことができる。Terraformはmodule blockに遭遇すると、そのmoduleの設定ファイルをloadして処理する。

他の設定から呼び出されたmoduleは、その設定のchild moduleと呼ばれることがある。

moduleはローカルのファイルシステム、リモートソースのどちらからも読み込むことができる。 TerraformはTerraform Registry, GitHubなどの様々なリモートソースをサポートしている。

Build and Use a Local Module

GitHub - hashicorp/learn-terraform-modules-create

静的ウェブサイトホスティングのためのAWS S3バケットを管理する例

典型的なchild moduleの構成

.
├── LICENSE
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
  • LICENSE はTerraformは利用せず公開するときに必要になるもの
  • README.md はTerraformでは利用しないが、Terraform Registryなどで使用される
  • main.tf はModuleの構成のmain setが含まれる
  • variable.tf で定義した変数は、Module利用者側からはmodule blockのArgumentsとして設定される
  • outputs.tf では、Moduleの出力を定義する。moduleで定義されたInfrastructureに関する情報を他の設定に渡すために利用される。

ローカルのModuleをInstallする場合には、terraform get コマンドを発行する

State

State - Terraform by HashiCorp

Purpose

Stateの目的は大きく4つ

  • Mapping to the Real World
  • Metadata
  • Performance
  • (Syncing)

Mapping to the Real World

State is a necessary requirement for Terraform to function. Terraformは設定をreal worldにmapするために、なんらかのデータベースを必要とする。 例えば、"aws_instance" "foo" という宣言がある場合、Terraformはmapを利用してインスタンス i-abcd1234 がそのリソースで表現されていることを知ることができる。

AWSのようなプロバイダの場合、Terraformは理論的にはAWSタグのようなものを使うことができる。Terraformの初期のプロトタイプでは、実際にステートファイルがなく、この方法を使っていた。 しかし、すぐに問題が発生。全てのproviderがタグをサポートしているわけではなかったのだ。

Terraform expects that each remote object is bound to only one resource instance, which is normally guaranteed by Terraform being responsible for creating the objects and recording their identities in the state.

Metadata

Alongside the mappings between resources and remote objects, Terraform must also track metadata such as resource dependencies.

Terraformは通常、依存関係の順序を決定するためにConfigurationを使用します。 しかし、TerraformのConfigurationからリソースを削除する場合、Terraformはそのリソースを削除する方法を知っている必要があります

Terraformは、設定にないリソースにマッピングが存在することを確認し、plan to destoryすることができます しかし、リソースのconfigurationが存在しなくなったため、configurationだけでは依存関係の順序を判断することができません

正しい動作を保証するために、Terraformは最新の依存関係のセットのコピーをstate内に保持します。これでTerraformは、configurationから1つまたは複数のアイテムを削除しても、ステートから正しい破壊順序を決定することができます。

Performance

Terraform stores a cache of the attribute values for all resources in the state. 📝 例えば、EC2におけるPrivateIPとかもそう

When running a terraform plan, Terraformは希望の構成に到達するために必要な変更を効果的に判断するために、リソースの現在の状態を知る必要がある

小規模のインフラの場合、Terraformはプロバイダーに問い合わせて、すべてのリソースから最新の属性を同期することができます。これはTerraformのデフォルトの動作で、planとapplyのたびに、Terraformは状態のすべてのリソースを同期します。

大規模なインフラでは、すべてのリソースへの問い合わせは時間がかかりすぎます。APIのレート制限が掛かる可能性も高い。 Terraformの大規模なユーザーは、この問題を回避するために-refresh=falseフラグと-targetフラグを多用する。これらのシナリオでは、キャッシュされた状態がrecord of truthとして扱われます。

The terraform_remote_state Data Source

The terraform_remote_state Data Source - Terraform by HashiCorp

The terraform_remote_state data source retrieves the root module output values from some other Terraform configuration, using the latest state snapshot from the remote backend.

データを共有する上で、root moduleのoutputは便利!だけど欠点もある。ユーザはstate snapshot全体にアクセスする必要があるが、その中にはSensitiveな情報も含まれることがある。 可能であれば、remote state経由ではなく、別の場所に明示的に公開することを推奨!

構成間で明示的にデータを共有するためには、以下のような様々なプロバイダーのmanaged resource typeとdata sourcesのペアを使用することができる

terraform_remote_stateではなく、別の明示的なconfiguration storeを使用することの主な利点は、コンピュートインスタンス内の設定管理やスケジューラーシステムなど、Terraform以外のシステムでもデータを読み取ることができる可能性があることです

Backend: State Storage and Locking

Backends: State Storage and Locking - Terraform by HashiCorp Backend Overview - Configuration Language - Terraform by HashiCorp

Backends are responsible for storing state and providing an API for state locking. State locking is optional.

State Storage

Backends determine where state is stored. 例えば、 local(default) backendは、ディスク上のローカル JSON ファイルに状態を保存します

When using a non-local backend, Terraform will not persist the state anywhere on disk except in the case of a non-recoverable error where writing the state to the backend failed.

sensitive valueがstateに含まれている場合、remote backendを使用することで、そのstateがディスクに永続化されることなくTerraformを使用することができる。

バックエンドへの状態の永続化にエラーが発生した場合、Terraformはstateをlocalに書き込みます。これはデータ損失を防ぐためです。この場合、エラーが解決したらエンドユーザーが手動でリモートバックエンドに状態をプッシュする必要がある。

Manual State Pull/Push

You can still manually retrieve the state from the remote state using the terraform state pull command. This will load your remote state and output it to stdout.

You can also manually write state with terraform state push. これは非常に危険なので、可能であれば避けてください。これはリモートの状態を上書きします。

Backend Types

Backend Overview - Configuration Language - Terraform by HashiCorp

Terraform's backends are divided into two main types, according to how they handle state and operations:

  • Enhanced backends can both store state and perform operations. There are only two enhanced backends: local and remote.
    • local : The local backend stores state on the local filesystem, locks that state using system APIs, and performs operations locally.
    • remote : The remote backend stores Terraform state and may be used to run operations in Terraform Cloud.
  • Standard backends only store state, and rely on the local backend for performing operations.
    • s3 (with locking via DynamoDB): Stores the state as a given key in a given bucket on Amazon S3. This backend also supports state locking and consistency checking via Dynamo DB.
    • 他にも、gcs とかpg (postgres)とかある

Backends Configuration

Backend Configuration - Configuration Language - Terraform by HashiCorp

Backends are configured with a nested backend block within the top-level terraform block:

terraform {
  backend "remote" {
    organization = "example_corp"

    workspaces {
      name = "my-app-prod"
    }
  }
}

State Locking

State: Locking - Terraform by HashiCorp

If supported by your backend, Terraform will lock your state for all operations that could write state. This prevents others from acquiring the lock and potentially corrupting your state.

Data Sources

Query Data Sources | Terraform - HashiCorp Learn

Terraformはdata sourcesを利用して、disk image IDなどcloud provider APIからの情報や、他のTerraformワークスペースのoutputsを通して残りの環境情報を取得する

下記の例のaws_availability_zones data sourceは、AWS providerの一部。resources blockと同じように、data source blocksも引数を取る。この場合、state Argumentsは現在利用可能なものに限定する。

data sourcesのattributeを参照する場合には、 data.<NAME>.<ATTRIBUTE> という形式をとる。

data "aws_availability_zones" "available" {
  state = "available"
}

resource "aws_subnet" "primary" {
  availability_zone = data.aws_availability_zones.available.names[0]

  // ...
}

他のTerraformワークスペースのoutputを参照するためには、terraform_remote_state を使うことが出来る。

data "terraform_remote_state" "vpc" {
  backend = "remote"

  config = {
    organization = "hashicorp"
    name = "vpc-prod"
  }
}

provider "aws" {
  region = data.terraform_remote_state.vpc.outputs.aws_region
}

Resource Targeting

Target resources | Terraform - HashiCorp Learn

Command: plan - Terraform by HashiCorp

通常のTerraformワークフローでは、plan全体を一度に適用します。ネットワーク障害、upstream cloud platformの問題、Terraformまたはそのproviderのバグが原因で、Terraformの状態がリソースと同期しなくなった場合など、planの一部のみを適用したい場合があります。

You can use the -target option to focus Terraform's attention on only a subset of resources.

depends_on Meta-Arguments

Create Resource Dependencies | Terraform - HashiCorp Learn

ほとんどの場合、Terraformは与えられた設定を元に依存関係を推論し、正しい順序で作成・削除されるようにする。しかし、場合によってTerraformが依存関係を推論できないことがあるので、depends_on 引数で明示的な依存関係を作る必要がある。

aws_eip resourceは、EC2インスタンスにElastic IPを割り当てて関連付けするもの。Elastic IPが作成される前にEC2インスタンスが存在する必要がある。 以下は暗黙的な依存関係の例である。

resource "aws_instance" "example_a" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t2.micro"
}

resource "aws_eip" "ip" {
    vpc = true
    instance = aws_instance.example_a.id // ここの参照から依存グラフをTerraformで作成する
}

明示的に依存関係を宣言しなくてはいけないケースもある。 EC2インスタンス上で、S3バケットの使用を期待するアプリケーションが実行されているとする。この依存関係はアプリケーションがもたらすものなので、Terraformからは見えない。

depends_on Argumentsは任意のresourcesもしくはmodule blockで受け入れられる。これを使って明示的な依存関係を構築できる。

resource "aws_s3_bucket" "example" {
  acl    = "private"
}

resource "aws_instance" "example_c" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t2.micro"

  depends_on = [aws_s3_bucket.example]
}

module "example_sqs_queue" {
  source  = "terraform-aws-modules/sqs/aws"
  version = "2.1.0"

  depends_on = [aws_s3_bucket.example, aws_instance.example_c]
}

Other

  • 似たようなresourceを、countfor_eachで管理することでMaintenabilityを高める
  • Functions を使って動的な設定を作成したり
    • templatefile() 関数を使ってUserdataを動的なものにしたり
    • lookup() を使ってmap[region]ami のhashmapからamiを探し出したり
  • 3項演算子を利用して動的な設定を構成したり(Expression

まとめ

Remote StateやLockファイル、dataのクエリ、Moduleなどプライベートな利用では中々踏み込まないところを公式のドキュメントを通して認識できた。
これで多少は既存のコードとかがちゃんと読めるようになっていると良いな!次はMongoDBについて勉強しよう。