記事一覧はこちら
Table of Contents
- 背景・モチベーション
- 参考文献
- Terraform
- まとめ
背景・モチベーション
8月から中途入社した会社ではInfrastructureの管理にTerraformを利用しています。業務において自分が利用したことがあるのは、AWSのCloudFormationとGCPのdeployment-managerのみでTerraformはプライベートな利用でとどまっていました。
雰囲気で利用している状況から抜け出すために、1つ1つの概念をちゃんと学習していくことにしました。幸い、HashiCorpのドキュメントはとても充実していたため公式のドキュメントをベースに学習を進めることが出来ました。
参考文献
Terraform
TerraformはTerraform Core
とTerraform Plugins
で構成されている。
図は Perform CRUD Operations with Providers | Terraform - HashiCorp Learn から引用させていただきました。
- Terraform Core 設定を読み込んで、resource dependency graphを構築する
- 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
- 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
である必要がある
- Terraformはカレントディレクトリの
- Command Line
- terraformコマンドの
-var
optionで指定することが可能 - example:
terraform apply -var='image_id_list=["ami-abc123","ami-def456"]' -var="instance_type=t2.micro"
- terraformコマンドの
- 環境変数
- Terraformは自プロセスに保持する
TF_VAR_
から始まる環境変数を読み込む、そのprefixの後には変数名が続く - example:
export TF_VAR_image_id=ami-abc123
- Terraformは自プロセスに保持する
- Terraform Cloudの場合はworkspace variablesも
Terraformは以下の順序で変数をロードし、後のソースが前のソースよりも優先される
環境変数の優先度が低いことに注意
- 環境変数
terraform.tfvars
terraform.tfvars.json
- Any
*.auto.tfvars
or*.auto.tfvars.json
(ファイル名の辞書順) - 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
, ordestroy
のときの表示は<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つを行う。
- Providerのバージョン指定/制限を
terraform
blockで行う - 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のペアを使用することができる
- Amazon SSM Parameter Store
- Publish with...
aws_ssm_parameter
resource type - Read with...
aws_ssm_parameter
data source
- Publish with...
- Amazon S3
- Publish with...
aws_s3_bucket_object
resource type - Read with...
aws_s3_bucket_object
data source
- Publish with...
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
andremote
.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.
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を、countやfor_eachで管理することでMaintenabilityを高める
- Functions を使って動的な設定を作成したり
templatefile()
関数を使ってUserdataを動的なものにしたりlookup()
を使ってmap[region]ami
のhashmapからamiを探し出したり
- 3項演算子を利用して動的な設定を構成したり(Expression
まとめ
Remote StateやLockファイル、dataのクエリ、Moduleなどプライベートな利用では中々踏み込まないところを公式のドキュメントを通して認識できた。
これで多少は既存のコードとかがちゃんと読めるようになっていると良いな!次はMongoDBについて勉強しよう。