📗

入門Terraform

2022/07/24に公開

概要

個人の学習の備忘録としてTerraformの概要と動作原理、そして基本的な文法についてまとめていきます。

参考
https://www.terraform.io/

Terraformとは?

いわゆるinfrastructure as code(IAC)ツールでクラウドとオンプレミスの両環境のインフラのリソースを人が読みやすい設定ファイルで定義するものです。
そして一貫したworkflow(※後述)でインフラのリソースをプロビジョンしそしてライフサイクルを管理します。

参考
https://www.terraform.io/intro#what-is-terraform

Terraformの動作原理

TerraformではTerraformが提供するAPIを通じてインフラリソースを作成・管理します。
プロバイダーにより提供されるAPIを通じてTerraformでクラウドサービスのインフラやSaaSサービス等のインフラを管理します。

HashiCorp(Terraform提供元会社)やTerraformのコミュニティは1700以上ものプロバイダーを現時点で提供しています。Terraform Registryを通してこれらのProviderを検索することができます。例えば、AWS、GCP,Azure、Kubernetes、Helm、GitHubなどが含まれます。


https://www.terraform.io/intro#how-does-terraform-work

Terraform workflow

1: Write
作成・管理するリソースを定義するためにterraformの構成ファイルを作成します。

2: Plan
1で作成した構成ファイルを実際に適用する前に、その構成ファイルでどのようなリソースが作成・更新・削除されるのかを事前に確認するフェーズがPlanになります。

3: Apply
このフェーズで実際に構成ファイルで記述された内容をもとにリソースをプロビジョニングしていきます。Terraformでは正しい順番でリソースを操作し、依存関係を解決してくれます。

参考
https://www.terraform.io/intro/core-workflow

Terraform Language

前節でみたようにTerraformでは構成ファイルによりインフラのリソースを管理します。
その構成ファイルを記載する際に使用するのがTerraform Languageになります。

Terraform Languageのメインの目的はリソースを宣言的に扱うことです。
その他のTerraform Languageの機能はリソースの定義を柔軟にそして便利するためにのみ存在しています。

Terraform Languageのシンタックスはわずかな基本的な要素のみから成り立っています。

resource "aws_vpc" "main" {
	cidr_block = var.base_cidr_block
}

<BLOCK TYPE> "<BLOCK LABEL>" "<BLOCK LABEL>" {
	# Block body
	<IDENTIFIER> = <EXPRESSION> # Argument
}
  • Block: ブロックは他コンテンツ用のコンテナで大抵の場合リソースを表すオブジェクトです。ブロックはブロックタイプと0個以上ののラベルと任意の個数の引数とネストされたボディから成るボディで構成されます。
  • Argument: 引数はブロック内で名前に対して値を付与するものです。
  • Expression: 式は文字列または他の値を参照したり組み合わせた値を表します。

Terraform言語は宣言的で、手続き的にではなく意図した目標を記述するようにします。
ブロックの順番とファイルの構成は通常は影響はありません。操作の順番を決める際に、Terraformは暗黙的または明示的なリソースの関連性のみを考慮します。

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

variable "aws_region" {}

variable "base_cidr_block" {
  description = "A /16 CIDR range definition, such as 10.1.0.0/16, that the VPC will use"
  default = "10.1.0.0/16"
}

variable "availability_zones" {
  description = "A list of availability zones in which to create subnets"
  type = list(string)
}

provider "aws" {
  region = var.aws_region
}

resource "aws_vpc" "main" {
  # Referencing the base_cidr_block variable allows the network address
  # to be changed without modifying the configuration.
  cidr_block = var.base_cidr_block
}

resource "aws_subnet" "az" {
  # Create one subnet for each given availability zone.
  count = length(var.availability_zones)

  # For each subnet, use one of the specified availability zones.
  availability_zone = var.availability_zones[count.index]

  # By referencing the aws_vpc.main object, Terraform knows that the subnet
  # must be created only after the VPC is created.
  vpc_id = aws_vpc.main.id

  # Built-in functions and operators can be used for simple transformations of
  # values, such as computing a subnet address. Here we create a /20 prefix for
  # each subnet, using consecutive addresses for each availability zone,
  # such as 10.1.16.0/20 .
  cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 4, count.index+1)
}

ファイルとディレクトリ

ファイル拡張子

.tfまたは JSONベースの .tf.jsonの二種類

テキストエンコーディング

UTF-8

ディレクトリとモジュール

moduleはディレクトリ内で一緒にまとめられた .tfまたは .t.jsonファイルの集まりのこと。

Terraformのmoduleは1階層のディレクトリからのみなり、ネストされたディレクトリは別のmoduleとして扱われます。

Terraformはモジュール内の全ての構成ファイルを評価して、モジュール全体を一つのドキュメントとして処理します。ブロックを複数ファイルに分割することは利便性やメンテナンス性の観点からで、動作には影響しません。

Terraformのmoduleは module callsを使うことで明示的に他のmoduleを構成に読み込むことができます。child moduleは親モジュールにネストされたものや他のディスクにあるローカルディレクトリや、またはTerraform Registryなどの外部ソースから成ります。

https://registry.terraform.io/

ルートModule

Terraformは常にひとつのRootモジュールのコンテクスト内で実行されます。"Terraform configuration"はルートモジュールとchildモジュールのツリーからなります。

Terraform CLIではルートモジュールはTerraformが呼ばれた作業ディレクトリになります

ファイルの上書き

Terraformは通常ディレクトリ内の全ての .tf又は.tf.jsonファイルを読み込み、それぞれのファイルが個別の構成オブジェクトのセットを定義することを期待しています。
二つのファイルが同じオブジェクトを定義しようとする場合、Terraformはエラーを返します。

ごく稀なケースで、分割されたファイルにおける既存の構成オブジェクトの一部を上書きすることが便利なケースがあります。例えば、Terraform言語のネイティブ構文で人が編集した構成ファイルは、JSON構文でプログラムによって生成されたファイルを使用して部分的に上書きできます。

前述のようなレアケースでは、 Terraformは名前が_override.tf又は_override.tf.jsonで終わる任意の構成ファイルを特別に処理できます。

Terraformは初回に構成ファイルを読み込む際にこれらの上書きファイルの読み込みはスキップし、その後でそれぞれを順番に処理します。上書きファイルのトップレベルのブロック毎に、Terraformは既に定義された該当のブロックを見つけて、そして上書きブロックを既存のオブジェクトに対してマージします。

この上書きファイルは特定の場合でのみ利用するようにします。上書きファイルの多用は読みやすさを損ないます。上書きファイルを利用する場合は、元のファイルに対して、上書きファイルによってそれぞれのブロックに変更が適用されることを読み手に分かるようにコメントするようにしましょう。

example.tf
resource "aws_instance" "web" {
	instance type = "t2.micro"
	ami = "ami-408c7f28"
}

上記の元のtfファイルに対して、上書き用のtfファイルが次のようになります。

override.tf
resource "aws_instance" "web" {
	ami = "foo"
}

この上書きファイルの適用の結果、次のような結果になります。

resource "aws_instance" "web" {
	instance type = "t2.micro"
	ami = "foo"
}

マージの挙動

一般的な規則

  • 上書きファイルのトップレベルのブロックは同一のブロックヘッダー(ブロックタイプとそれに続くクオートされたラベルのこと)を持つ通常の構成ファイルのブロックをマージします

TODO

Dependency Lockファイル

Terraform構成ファイルは自身のコードベース外の2種類の外部依存を参照することがあります

  • Provider: 外部システムとの疎通をサポートするためのTerraformのプラグイン
  • Module: Terraform構成を複数のグループのまとまりに分割することで再利用性を高めるもの

これらの外部依存はTerraform自身と構成ファイルとは独立して公開または更新されます。
このため、Terraformは、現在の構成ファイルがどのバージョンと互換性があり、どのバージョンが利用に適しているのか外部依存のバージョンを決める必要があります。

構成ファイル内のVersion constraintsはどのバージョンの外部依存と互換性があるかを決定します。そしてTerraformはデフォルトで外部依存のlockファイルを作成することで、一貫して指定バージョンの外部依存を利用するようにします。

Lockファイルの配置場所

外部依存lockファイルはそれぞれの構成ファイルの分離されたmoduleに属するのではなく、構成全体に属します。このため、TerraformはTerraformを実行した現在の作業ディレクトリに生成されます。このディレクトリは構成ファイルのルートモジュールを含むディレクトリでもあります。

lockファイルは.terraform.lock.hclというファイル名で常に生成され、この命名は作業ディレクトリのサブディレクトリ.terraform内にTerraformがキャッシュした複数アイテムを対象にしたものであるということを示しています。

Terraformはterraform initコマンド実行時に自動でこのlockファイルを作成又は更新します。
外部依存の潜在的な変更に対してコードレビューできるようにこのlockファイルはgit管理に含めるようにします。

構文

構成構文

ここでTerraform Languageのnative syntaxについてみていきます。

このlow levelのTerraform Languageのsyntaxは*HCL`と呼ばれるsyntaxで定義されています。
このsyntaxはその他の構成言語でも使用されており、特にHashiCorpのTerraform以外のその他プロダクトでも採用されています。

https://www.terraform.io/language/syntax/configuration

引数とブロック

Terraform言語は引数とブロックという二つの主要な構文から成ります。

引数
引数はある特定の名前に対して値を割り当てます。

image_id = "abc123"

イコール記号(=)の前の識別子は"引数名"で、イコール記号の後の式が引数の値になります。
argumentが書かれるコンテキストに応じてどの値の形式が適切かが決まりますが、大抵の引数は任意のexpressions(文字列またはその他のプログラムによって生成された値)を受け入れます。

ブロック
ブロックはその他のコンテンツの入れ物になります。

resource "aws_instance" "example" {
	ami = "abc123"
	
	network_interface {
		# ...
	}
}

ブロックはタイプ(この例ではresource)を持ちます。
ブロックタイプに応じて0個以上のラベルがプロックタイプの後に続きます。
ブロックタイプresourceは二つのラベルを持ちます(この例ではaws_instanceexample)。あるブロックタイプでは任意の数のラベルを持てたり、network_interfaceタイプのように一つのラベルを持たないものもあります。

ブロックタイプキーワードと任意の数のラベルの後に、{}で区切られたブロックボディが続きます。ブロックボディ内で、さらに引数やブロックがネストされ、ブロックとそれに紐づいた引数の階層が作成されます。

Terraform Languageではトップレベルブロックと呼ばれる、その他のブロックレベルの外に位置されるブロックがあります。大抵のTerraformの機能(ex: resources, input variables, output variables, data sourcesなど)はトップレベルブロックとして実装されています。

識別子

識別子は、引数名、ブロックタイプ名、リソースのようなTerraformの特定のコンテンツ、インプット変数などが該当します。

識別子は 文字、数字、アンダースコア(_),ハイフン(-)を含むことができます。
リテラルの数字と区別するために、識別子の先頭文字に数字は使えません。

識別子の完全な規則用に、TerraformはUnicode identifier syntaxを実装しています。

http://unicode.org/reports/tr31/

コメント

Terraformには3種類のコメントの構文があります。

  • # : シングルラインコメント。
  • // : #の代替であるシングルラインコメント
  • /**/: 複数ラインのコメント

シングルラインコメントには通常#を使うようにします。

リソース(Resources)

リソースはTerraform Languageにおいて最も重要な要素で、それぞれのリソースブロックが1つ以上のインフラのオブジェクトを表しています。(ex: 仮想ネットワーク、computeインスタンス、DNSレコードなど)

https://www.terraform.io/language/resources/syntax

リソースブロック

リソースシンタックス

resource "aws_instance" "web" {
  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"
}

resourceブロックはリソースのタイプ(前述の例では"aws_instance")とローカル名("web")を宣言します。ローカル名はTerraformモジュール内でそのリソースがどこからきたものかを参照する際に使用されますが、モジュールの外には影響がありません。

リソースタイプと名前のセットが与えられたリソースの識別子となりモジュール内でユニークである必要があります。

ブロックボディはリソース自体の構成引数になります。大抵の引数はリソースタイプによって決まります。

リソースタイプ

それぞれのリソースは1つのリソースタイプに紐づいています。
リソースタイプは管理するインフラの種類と、そのリソースがサポートする引数と属性からなります。

プロバイダー(Provider)

それぞれのリソースタイプはプロバイダーによって実装されています。プロバイダーは大抵1つのインフラリソースを管理するためのリソースを提供しています。
プロバイダーはTerraformそれ自体とは独立していますが、Terraformのワーキングディレクトリ初期化する際、Terraformは大抵のプロバイダーを自動でインストールしてくれます。

リソースを管理するために、Terraformモジュールはそのリソースがどのプロバイダーが提供するものなのかを明示する必要があります。加えて、それぞれのプロバイダーはそれらプロバイダーリモートAPIにアクセスするための設定を必要とします。

リソース引数(Resource Arguments)

大抵の引数はリソースタイプに応じてリソースブロックのボディ内で特有の値になります。
リソースタイプのドキュメント上で定義されています。

その他いくつかのメタ引数がTerraform自身によって定義されており、全てのリソースタイプに適用されます。

メタ引数(Meta-Arguments)

メタ引数はリソース振る舞いを変更するために指定する引数です。

カスタムコンディションチェック

preconditionpostconditionブロックによりリソースがどのように操作されるかを保証することができます。下記の例ではAMIが正しく設定されていることをリソース作成前にチェックします。

resource "aws_instance" "example" {
  instance_type = "t2.micro"
  ami           = "ami-abc123"

  lifecycle {
    # The AMI ID must refer to an AMI that contains an operating system
    # for the `x86_64` architecture.
    precondition {
      condition     = data.aws_ami.example.architecture == "x86_64"
      error_message = "The selected AMI must be for the x86_64 architecture."
    }
  }
}

https://www.terraform.io/language/expressions/custom-conditions#preconditions-and-postconditions

オペレーションタイムアウト

いくつかのリソースタイプは timeoutsネストブロック引数を提供しています。
これによりリソース作成のタイムアウトを設定できます。
例えば、aws_db_instancecreateupdate、そしてdelete操作のタイムアウトを指定できます。

State

stateはterraformの構成ファイルと実際に作成されたリソースを紐づけ、metadatをトラッキングして、大きいインフラリソースのパフォーマンスの改善に利用されます。

このstateはデフォルトで terraform.tfstateと呼ばれるファイルに保存されます。
またはチームで共通のリソースを管理する際に便利なリモートでも管理することができます。

Terraformはこのlocal stateを使用してplanを作成したりインフラに対して変更を加えたりします。いずれの操作に先行して、Terraformはrefreshにより実際のリソースに対してstateの更新を行います。

https://www.terraform.io/language/state

検査と修正

stateファイルを扱う際は通常は直接ファイルを編集するのではなく、CLIのterraform stateコマンドを使って操作します。

terraform importコマンドやterraform state rmコマンドなどでstateのbindingを追加または削除した場合、実際に対象のリソースを手動で削除または再importすることでstateと実際のリソースを対応づける必要が義務付けられます。

terraform_remote_state Data Source

データソースterraform_remote_state指定されたstateのbackendから最新のstateのスナップショットを使い、他のTerraform構成からルートモジュールの出力値を取得します。

terraform_remote_stateは明治的に指定せずに使用できます。

https://www.terraform.io/language/state/remote-state-data

構成ファイル間でのData共有のその他の方法

ルートモジュールの出力とデータを共有することは便利ですが、欠点があります。 terraform_remote_stateは出力値のみを公開しますが、そのユーザーは状態スナップショット全体にアクセスできる必要があります。これには多くの場合、機密情報が含まれています。

構成ファイル間のデータを明示的に共有するために、マネージドリソースタイプとdata sourceの組み合わせを使用することができます。

システム 公開方法 読み込み方法
Amazon S3 aws_s3_bucket_object aws_s3_bucket_object data source
Google Cloud Storage google_storage_bucket_object google_storage_bucket_objectdata sourceとhttpdata source
... ... ...

https://www.terraform.io/language/state/remote-state-data#alternative-ways-to-share-data-between-configurations

Stateストレージとロック

backendはstateを保存するのとstate locking用のAPIを提供します。
state lockingはオプショナルです。

https://www.terraform.io/language/state/backends

Stateストレージ

backendはstateの保存場所を決定します。例えば、デフォルトであるlocal backendはlocalのJSONファイルにstateを保存します。

マニュアルStateプル/プッシュ

terraform state pullコマンドによりremote stateをローカルに取得できます。

terraform state pushコマンドではstateを手動で書き込むことができます。

Stateロック

いくつかのbackendではstate lockingを提供しています。

https://www.terraform.io/language/settings/backends/configuration

ワークスペース

それぞれのTerraform構成はbackendに紐づけられています。これによりどのように操作が実行されたのか、またTerraform Stateのような持続的なデータの保管場所を指定します。

backendに保存される持続的なデータはワークスペースに属します。
初期ではbackendは1つのワークスペースのみを持ち、"default"と呼ばれます。これによりTerraformのstateが1つの構成に紐づけられるようになります。

https://www.terraform.io/language/state/workspaces

いくつかのbackendは複数のワークスペースをサポートし、複数のstateが1つの構成に紐づけられます。

複数ワークスペースは下記のbackendで現在サポートされています。

backend S3

Amazon S3バケット内の指定されたkeyでStateを保存します。
加えて、DynamoDBによりstate lockingと持続的なチェッキング機能をサポートしています。
https://www.terraform.io/language/settings/backends/s3

ex)

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

Terraform設定

特別なterraform構成ブロックタイプはTerraform自身のいくつかの振る舞いを設定するために使用します。(ex: Terraformの最低限のversionの指定)

https://www.terraform.io/language/settings

Terraformブロックシンタックス

terraform {
  # ...
}

上記のブロック構造において、以下のセクションを指定できます。

Terraform Cloudの設定

ネストされたcloudブロックはTerraform Cloudを設定します。

Terraform Backendの設定

ネストされたbackendブロックはTerraformが使用するstate backendの設定をします。

https://www.terraform.io/language/settings/backends/configuration

Terraformバージョンの指定

`required_versionブロックはTerraform構成において使用するTerraformのバージョンの要求を指定できます。

Provider Requirementsの指定

required_providersブロックはプロバイダーの名前やソースアドレス、そしてversionなどを指定する際に使用されます。

https://www.terraform.io/language/providers/requirements

Backend設定

backendはTerraformのstateデータファイルの保管場所を定義します。

https://www.terraform.io/language/settings/backends/configuration

ex)

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

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

Discussion