Terraformの高度な手法(Terraform学習備忘録#4)

Terraformの高度な手法(Terraform学習備忘録#4)

Terraformを学習内容を備忘録として残します。versionはv1.6です。

補完

Terraformでは補完機能が備わっており、他のプログラミグ言語と同じような構文が使えます。

  • ${} で他の値を引用
    • variables: ${var.VARIABLE_NAME}
    • resources: ${aws_instance.name.id} (type.resource-name.attr)
    • data_sources:${data.template_file.name.rendered}(data.type.resource-name.attr)
  • 変数の値
    • string: var.name (${var.SOMETHING})
    • map:var.MAP[“key”](${var.AMIS[“us-east-1”]},$lookup(var.AMIS, var.AWS_REGION)})
    • list: var.LIST, var.LIST[i](${var.subnets[i]}, ${join(“,”, var.subnets)} )
  • モジュール
    • module.NAME.output (${module.aws_vpc.vpcid})
  • Count
    • count.FIELD (${count.index})
  • Path
    • path.TYPE (path.cwd(current directory), path.module(module path), path.root(root module path) )
  • Operators
    • !, –
    • *, /, %
    • >, >=, <, <=
    • ==, !=
    • &&
    • ||
  • if-else
    • CONDITION? TRUEVAL : FALSEVAL (${var.env == “prod” ? 2 : 1})

組み込み関数

Terraform言語にはいくつかの組み込み関数があり、式の中から呼び出して値を変換したり組み合わせたりすることができます。関数呼び出しの一般的な構文は、関数名の後にカンマで区切った引数を括弧で囲むというものです。(例:${file("mykey.pub")}

以下はよく使う組み込み関数です。

basename

ファイルシステムのパスを含む文字列を取り、最後の部分以外を削除します。

> basename("foo/bar/baz.txt")
baz.txt

coalesce

任意の数の引数を取り、NULLでも空文字列でもない最初の引数を返します。

> coalesce("a", "b")
a
> coalesce("", "b")
b
> coalesce(1,2)
1

element

要素はリストから1つの要素を取り出します。

> element(["a", "b", "c"], 1)
b

format

指定された文字列に従って他のいくつかの値をフォーマットして文字列を生成します。C言語のprintf関数や他のプログラミング言語の類似関数に似ています。

> format("Hello, %s!", "Ander")
Hello, Ander!
> format("There are %d lights", 4)
There are 4 lights

index

リスト内の指定された値の要素インデックスを見つけます。

> index(["a", "b", "c"], "b")
1

join

指定された文字列リストのすべての要素を指定された区切り文字で連結した文字列を生成します。

> join("-", ["foo", "bar", "baz"])
"foo-bar-baz"
> join(", ", ["foo", "bar", "baz"])
foo, bar, baz
> join(", ", ["foo"])
foo

tolist

引数をリスト値に変換します。set 値を tolist に渡すとリストに変換されます。set 要素は順序付けされないので、結果のリストは未定義の順序になりますが、Terraform の特定の実行内で一貫性が保たれます。

> tolist(["a", "b", "c"])
[
  "a",
  "b",
  "c",
]

lookup

マップのキーに対応する要素の値を取得します。指定されたキーが存在しない場合は、代わりに指定されたデフォルト値が返されます。

> lookup({a="ay", b="bee"}, "a", "what?")
ay
> lookup({a="ay", b="bee"}, "c", "what?")
what?

split

指定した文字列を指定した区切り文字で分割してリストを生成します。

> split(",", "foo,bar,baz")
[
  "foo",
  "bar",
  "baz",
]
> split(",", "foo")
[
  "foo",
]
> split(",", "")
[
  "",
]

substr

与えられた文字列からオフセットと(最大)長さによって部分文字列を取り出します。

> substr("hello world", 1, 4)
ello
// "-"を使うと末尾から数える、長さが-1の場合はオフセット以降のすべての値を返却する
> substr("hello world", -5, -1)
world

timestamp

RFC 3339形式のUTCタイムスタンプ文字列を返します。

この関数の結果は1秒ごとに変わるので、リソース属性でこの関数を直接使うとTerraformの実行ごとに差分が検出されてしまいます。リソース属性でこの関数を使うことは推奨しませんが、まれにignore_changesライフサイクルメタ引数と併用することで、リソースの初回作成時のみタイムスタンプを取得することができます。

戻り値が常に変化するため、この関数の結果はTerraformの計画段階では予測できず、タイムスタンプは計画が適用されて初めて取得されます。

> timestamp()
2018-05-13T07:44:12Z

upper

与えられた文字列の大文字小文字をすべて大文字に変換します。

> upper("hello")
HELLO
> upper("алло!")
АЛЛО!

uuid

ランダムバイトを使ってUUID形式の文字列を生成します。この関数は、128ビット値のよく理解された文字列表現を生成しますが、出力はRFCに準拠していません。

この関数は呼び出されるたびに新しい値を生成するため、リソース引数で直接使用するとスプリアス差分が発生します。リソース設定にuuid関数を使用することは推奨しませんが、ignore_changesライフサイクルメタ引数と併用することで、注意して使用することができます。

ほとんどの場合、代わりにrandomプロバイダを使うことを推奨します。randomプロバイダは一度だけランダムな値を生成することができ、それをTerraformのstateに保持して将来の操作で使用することができるからです。特にrandom_iduuid関数と同等のランダム性を持つ結果を生成できます。

> uuid()
b5ee72a3-54dd-c4b8-551c-4bdc0204cedb

values

マップを受け取り、そのマップの要素の値を含むリストを返します。値は対応するキーの辞書順で返されるので、keysから返されるキーと同じ順番で値が返されます。

> values({a=3, c=2, d=1})
[
  3,
  2,
  1,
]

ループ処理

for

for 式は、別の複合型の値を変換して複合型の値を作成します。入力値の各要素は、結果の1か0の値に対応することができ、任意の式を使用して各入力要素を出力要素に変換することができます。

例えば、var.list が文字列のリストであった場合、以下の式はすべて大文字の文字列のタプルを生成します。

[for s in var.list : upper(s)]

このfor 式は var.list の各要素を繰り返し、各要素に s を設定した upper(s) 式を評価します。そして、その式を同じ順序で実行したすべての結果で新しいタプル値を構築します。

input types

for 式の入力(in キーワードの後に与えられる)には、リスト、セット、タプル、マップ、オブジェクトを指定できます。上の例では、一時記号 s を1つだけ持つ for 式を示しましたが、for 式では、各項目のキーまたはインデックスも使用するために、オプションで一時記号のペアを宣言することができます。

[for k, v in var.map : length(k) + length(v)]
[for i, v in var.list : "${i} is ${v}"]

result types

for 式を囲む括弧の種類によって、どのような結果が得られるかが決まります。上記の例では [] を使用しており、これはタプルを生成します。代わりに {} を使用すると、結果はオブジェクトになり、=> 記号で区切られた 2 つの結果式を指定する必要があります。

{for s in var.list : s => upper(s)}
{
  foo = "FOO"
  bar = "BAR"
  baz = "BAZ"
}

for each

デフォルトでは、リソースブロックは1つのインフラストラクチャーオブジェクトを設定します(同様に、モジュールブロックは子モジュールの内容を1回だけ設定に含めます)。しかし、複数の似たようなオブジェクト(コンピュートインスタンスの固定プールなど)を、それぞれ別のブロックを書かずに管理したいこともあります。

リソースまたはモジュールブロックに for_each 引数があり、その値がマップまたは文字列の集合である場合、Terraformはそのマップまたは集合のメンバーごとにインスタンスを1つ作成します。

基本構文

for_each は Terraform 言語で定義されているメタ引数です。モジュールやすべてのリソースタイプで使うことができます。

for_each メタ引数はマップまたは文字列の集合を受け取り、そのマップまたは集合の各項目に対してインスタンスを作成します。各インスタンスには個別のインフラストラクチャオブジェクトが関連付けられ、コンフィギュレーションが適用されると、それぞれが個別に作成、更新、破棄されます。

resource "azurerm_resource_group" "rg" {
  for_each = {
    a_group = "eastus"
    another_group = "westus2"
  }
  name     = each.key
  location = each.value
}
resource "aws_iam_user" "the-accounts" {
  for_each = toset( ["Todd", "James", "Alice", "Dottie"] )
  name     = each.key
}
# my_buckets.tf
module "bucket" {
  for_each = toset(["assets", "media"])
  source   = "./publish_bucket"
  name     = "${each.key}_bucket"
}

標準的なモジュールのディレクトリ構成

標準モジュール構造とは、再利用可能なモジュールを個別のリポジトリで配布する際に推奨するファイルとディレクトリのレイアウトです。Terraformツールは標準モジュール構造を理解し、その構造を使ってドキュメントを生成したり、モジュールレジストリのためにモジュールをインデックス化したりするように作られています。

標準的なモジュール構造は以下に示すようなレイアウトを想定しています。リストは長く見えますが、ルートモジュール以外はすべてオプションです。ほとんどのモジュールは、標準的な構造に従うために余計な作業をする必要はありません。

  • Root module – 標準のモジュール構造で唯一必要な要素です。Terraformファイルはリポジトリのルートディレクトリに存在しなければなりません。これはモジュールの主要なエントリーポイントであるべきで、オピニオンであることが期待されています。Consulモジュールでは、ルートモジュールが完全なConsulクラスタをセットアップします。しかし、このモジュールは多くの仮定を行うので、上級ユーザーは特定のネストされたモジュールを使って、より注意深く自分の望むものをコントロールすることが期待されます。
  • README – ルート・モジュールとネストされたモジュールはREADMEファイルを持つべきです。このファイルの名前は README または README.md とします。後者はマークダウンとして扱われます。そこには、モジュールの説明と、そのモジュールが何に使われるべきかを書くべきです。このモジュールが他のリソースとどのように組み合わせて使えるかの例を含めたい場合は、このようにexamplesディレクトリに置いてください。モジュールが作成するかもしれないインフラストラクチャリソースとそれらの関係を描いた視覚的な図を含めることを検討してください。READMEは、モジュールの入力や出力を文書化する必要はありません。リポジトリ自体に含まれるファイルにリンクしたり画像を埋め込んだりする場合は、コミット固有の絶対 URL を使用して、将来リンクが間違ったバージョンのリソースを指すことがないようにしてください。
  • LICENSE – このモジュールが利用できるライセンス。モジュールを一般に公開する場合、明確なライセンスがない限り、多くの組織はモジュールを採用しません。オープンソースライセンスでなくても、常にライセンスファイルを持つことを推奨します。
  • main.tfvariables.tfoutputs.tfmain.tf は主要なエントリーポイントであるべきです。単純なモジュールの場合、ここですべてのリソースが作成されます。複雑なモジュールの場合、リソースの作成は複数のファイルに分けることができますが、入れ子になったモジュールの呼び出しはすべてmain.tf に記述します。variables.tfoutputs.tf には、それぞれ変数と出力の宣言を記述します。
  • 変数と出力は説明文を持つべき – すべての変数と出力には、その目的を説明する1文か2文の説明が必要です。これは文書化のために使用されます。
  • Nested modules – ネストされたモジュールは modules/ サブディレクトリの下に存在しなければなりません。README.md があるネストされたモジュールは、外部ユーザーが使用できるとみなされます。READMEが存在しない場合、内部利用のみとみなされます。Terraformが内部モジュールの利用を積極的に拒否することはありません。ネストされたモジュールは、複雑な動作を複数の小さなモジュールに分割し、上級ユーザが注意深く選択できるようにするために使うべきです。例えば、Consulモジュールには、必要なIAMポリシーを設定するモジュールとは別に、クラスタを作成するモジュールがネストされています。これにより、ユーザは自分のIAMポリシーの選択を持ち込むことができます。 ルートモジュールがネストしたモジュールへの呼び出しを含む場合、Terraformがそれらを別々にダウンロードし直すのではなく、同じリポジトリやパッケージの一部とみなすように、./modules/consul-cluster のような相対パスを使うべきです。 リポジトリやパッケージに複数のネストしたモジュールが含まれている場合、理想的には、呼び出し元が互いに直接呼び出してモジュールの深いネストしたツリーを作成するのではなく、コンポーザブルであるべきです。
  • Example – モジュールの使用例は、リポジトリのルートにある examples/ サブディレクトリの下にあるべきです。各サンプルには、そのサンプルの目的と使い方を説明する README を付けることができます。サブモジュールの例もルートの examples/ ディレクトリに置くべきです。サンプルはカスタマイズのために他のリポジトリにコピーされることが多いので、モジュールブロックのソースは相対パスではなく、外部呼び出し元が使用するアドレスに設定する必要があります。
$ tree minimal-module/
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
$ tree complete-module/
.
├── README.md
├── main.tf
├── variables.tf
├── outputs.tf
├── ...
├── modules/
│   ├── nestedA/
│   │   ├── README.md
│   │   ├── variables.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   ├── nestedB/
│   ├── .../
├── examples/
│   ├── exampleA/
│   │   ├── main.tf
│   ├── exampleB/
│   ├── .../

lock file

Terraformの設定は、自身のコードベースの外から来た2つの異なる種類の外部依存関係を参照することができます。

  • Providers はTerraformのプラグインであり、様々な外部システムとのやり取りをサポートしてTerraformを拡張します。
  • Modules は、Terraformのコンフィギュレーション・コンストラクト(Terraform言語で書かれたもの)のグループを再利用可能な抽象的なものに分割することを可能にします。

これらの依存関係のタイプはどちらも、Terraform自身や依存する設定から独立して公開・更新することができます。そのため、Terraformはこれらの依存関係のどのバージョンが現在の構成と互換性がある可能性があり、どのバージョンが現在使用するために選択されているかを判断しなければなりません。

コンフィギュレーション自体の中のバージョン制約が、どのバージョンの依存関係が潜在的に互換性があるかを決定しますが、各依存関係の特定のバージョンを選択した後、Terraformは依存関係ロックファイルで行った決定を記憶し、(デフォルトで)将来また同じ決定を行えるようにします。

依存関係ロックファイルはプロバイダの依存関係のみを追跡します。Terraformはリモートモジュールのバージョン選択を記憶しないので、Terraformは常に指定されたバージョン制約を満たす利用可能な最新バージョンのモジュールを選択します。Terraformが常に同じモジュールバージョンを選択するようにするには、正確なバージョン制約を使うことができます。

lock file の位置

依存関係ロックファイルは、コンフィギュレーション内の各モジュールに属するのではなく、コンフィギュレーション全体に属するファイルです。そのため、Terraformはこのファイルを作成し、Terraformを起動したときにカレント作業ディレクトリ(設定のルートモジュールの .tf ファイルを含むディレクトリ)にあることを期待します。

ロックファイルの名前は常に .terraform.lock.hcl で、この名前はTerraformが作業ディレクトリの .terraform サブディレクトリにキャッシュする様々なアイテムのロックファイルであることを示すためのものです。

Terraformは、terraform init コマンドを実行するたびに、依存関係ロックファイルを自動的に作成または更新します。このファイルをバージョン管理リポジトリに含めることで、設定自体の変更の可能性を議論するのと同じように、コードレビューを通じて外部依存関係の変更の可能性を議論することができます。

依存関係ロックファイルはメインのTerraform言語と同じ低レベルの構文を使いますが、依存関係ロックファイル自体はTerraform言語の設定ファイルではありません。この違いを示すために、.tf の代わりに .hcl という接尾辞が付けられています。

インストールするときの依存関係

terraform init がコンフィギュレーションに必要な全てのプロバイダをインストールする際に、Terraformはコンフィギュレーションのバージョン制約とロックファイルに記録されたバージョン選択の両方を考慮します。

特定のプロバイダに既存の選択項目が記録されていない場合、Terraformは指定されたバージョン制約にマッチする利用可能な最新バージョンを選択し、その選択項目を含むようにロックファイルを更新します。

特定のプロバイダがすでにロックファイルに記録された選択を持っている場合、Terraformは新しいバージョンが利用可能になったとしても、インストールのために常にそのバージョンを再選択します。terraform initを実行する際に -upgrade オプションを追加することでこの挙動を上書きすることができます。この場合、Terraformは既存の選択を無視し、バージョン制約に一致する利用可能な最新バージョンを再度選択します。

特定の terraform init 呼び出しがロックファイルに変更を加えた場合、Terraformはそのことを出力の一部として言及します。

Terraform has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.

このメッセージが表示されたら、バージョン管理システムを使ってTerraformが提案したファイルの変更を確認し、もしそれがあなたが意図的に行った変更であれば、チームの通常のコードレビュープロセスを通して変更を送ることができます。

Stateの操作

状態を管理するためstateファイルが利用されます。ここではstateに関するコマンドを見ていきます。

state list

terraform state list コマンドは、Terraformステート内のリソースを一覧表示するために使用します。このコマンドは、与えられたアドレス(もしあれば)にマッチするステートファイル内のすべてのリソースをリストアップする。アドレスが与えられていない場合は、すべてのリソースがリストされる。

コマンドライン・フラグはすべてオプションです。以下のフラグが使用できます。

  • -state=path – 状態ファイルへのパス。デフォルトは “terraform.tfstate”。リモート状態を使用する場合は無視されます。
  • -id=id – 表示するリソースのID。未設定の場合は無視されます。
$ terraform state list
aws_instance.foo
aws_instance.bar[0]
aws_instance.bar[1]
module.elb.aws_elb.main
$ terraform state list aws_instance.bar
aws_instance.bar[0]
aws_instance.bar[1]

state mv

Terraformのステートの主な機能は、設定内のリソースインスタンスアドレスとそれらが表すリモートオブジェクト間のバインディングを追跡することです。通常Terraformは、削除されたリモートオブジェクトのバインディングを削除するなど、プラン適用時のアクションに応じて自動的にstateを更新します。

terraform state mv の最も一般的な使い方は、設定中のリソースブロックの名前を変更した場合や、子モジュールにリソースブロックを移動した場合です。デフォルトでは、Terraformはリソース構成の移動や名前の変更を、古いオブジェクトを削除して新しいアドレスに新しいオブジェクトを作成する要求として理解します。terraform state mv では、Terraformの新しいアドレスに既存のオブジェクトを先取りしてアタッチすることで、その解釈を上書きすることができます。

(注意)

もし共同作業環境でTerraformを使っているのであれば、コードのリファクタリングのために terraform state mv を使うときは、設定を変更してから terraform state mv コマンドを実行するまでの間に誰も他の変更を加えないように、同僚と注意深くコミュニケーションを取らなければなりません。

このコマンドは以下のオプションも受け付けます。

  • -dry-run – 指定されたアドレスにマッチするリソースインスタンスを、実際に「忘れる」ことなくすべて報告します。
  • -lock=false – 処理中に状態ロックを保持しません。これは、同じワークスペースに対して他の人が同時にコマンドを実行する可能性がある場合に危険です。
  • -lock-timeout=DURATION-lock=false でロックを無効にしない限り、Terraformがエラーを返すまでの一定期間、ロックの取得を再試行するよう指示します。duration構文は、数値の後に時間単位の文字が続きます。

例:リソースの名前を変更する

$ terraform state mv packet_device.worker packet_device.helper
-resource "packet_device" "worker" {
+resource "packet_device" "helper" {
   # ...
 }

例:リソースをモジュールの中に移動する

$ terraform state mv packet_device.worker module.worker.packet_device.worker

state pull

terraform state pull コマンドは、リモートの状態を手動でダウンロードして出力するために使用します。このコマンドはローカルステートでも動作します。

このコマンドは現在の場所から状態をダウンロードし、ローカルにインストールされたTerraformと互換性のある最新のstateファイルのバージョンにローカルのコピーをアップグレードし、rawフォーマットを標準出力に出力します。

これは状態から値を読み出すのに便利です(このコマンドとjqのようなものを組み合わせる可能性があります)。また、手動で状態を修正する必要がある場合にも便利です。

このコマンドはリモートの状態のTerraformバージョンを検査するために使うことはできません。出力前に常に現在のTerraformバージョンに変換されるからです。

state push

terraform state push コマンドは、ローカルのstateファイルをリモートのstateに手動でアップロードするために使用します。このコマンドはローカルのstateでも動作します。

このコマンドは滅多に使うべきものではありません。このコマンドは、リモートstateに手動で介入する必要がある場合のユーティリティとしてのみ使用されます。

state replace-provider

terraform state replace-provider コマンドは、Terraformのstateにあるリソースのプロバイダを置き換えるために使います。terraform state replace-provider [options] FROM_PROVIDER_FQN TO_PROVIDER_FQN は、”from “プロバイダを使用しているすべてのリソースを更新し、プロバイダを指定された “to “プロバイダに設定します。これにより、現在状態のリソースを持つプロバイダのソースを変更することができます。

このコマンドは以下のオプションも受け付けます。

  • -auto-approve – 対話的な承認をスキップします。
  • -lock=false – 操作中に状態ロックを保持しません。これは、同じワークスペースに対して他の人が同時にコマンドを実行する可能性がある場合に危険です。
  • -lock-timeout=0s – 状態ロックを再試行する時間。
$ terraform state replace-provider hashicorp/aws registry.acme.corp/acme/aws

state rm

あまり一般的ではありませんが、既存のリモートオブジェクトのバインディングを破棄せずに削除したい場合、terraform state rm を使うことができます。

Terraformは指定されたリソースアドレスに一致するインスタンスの状態を検索し、それぞれのレコードを削除して、Terraformが対応するリモートオブジェクトを追跡しないようにします。つまり、オブジェクトはまだリモートシステムに存在し続けますが、その後のTerraform planには、”忘れられた “インスタンスそれぞれに新しいオブジェクトを作成するアクションが含まれます。

このコマンドは以下のオプションも受け付けます。

  • -dry-run – 指定されたアドレスにマッチするリソースインスタンスを、実際に「忘れる」ことなくすべて報告します。
  • -lock=false – 処理中に状態ロックを保持しません。これは、同じワークスペースに対して他の人が同時にコマンドを実行する可能性がある場合に危険です。
  • -lock-timeout=DURATION - -lock=false でロックを無効にしない限り、Terraformがエラーを返すまでの一定期間、ロックの取得を再試行するよう指示します。duration構文は、数値の後に時間単位の文字が続きます。
$ terraform state rm 'packet_device.worker'

state show

terraform state show コマンドは、Terraformの状態における単一のリソースの属性を表示するために使用します。

コマンドライン・フラグはすべてオプションです。以下のフラグが使用できます。

  • -state=path – stateファイルへのパス。デフォルトは “terraform.tfstate”。リモート状態を使用する場合は無視されます。

terraform state show の出力は、プログラムで利用するものではなく、人間が利用するためのものです。他のソフトウェアで使用するために状態データを抽出するには、terraform show -json を使用し、ドキュメントに記載されている構造で結果をデコードします。

$ terraform state show 'packet_device.worker'
# packet_device.worker:
resource "packet_device" "worker" {
    billing_cycle = "hourly"
    created       = "2015-12-17T00:06:56Z"
    facility      = "ewr1"
    hostname      = "prod-xyz01"
    id            = "6015bg2b-b8c4-4925-aad2-f0671d5d3b13"
    locked        = false
}

以上です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です