はじめに
お久しぶりです。
ちょっと前に「やっぱりクラウドバックエンドの仕事したいなあ...」と書いていたら、偶然にもIaC(Infrastructure As Code)のタスクを担当することができました。(またすぐフロントに戻るんでしょうけど)
で、今回は(Serverless FramewrokでもAWS CDKでもなく)Terraformをやってるのですが、これはこれで面白いですね。
本当にそれぞれメリットがあり、全部使いこなせると便利だなあと思いました。
で、今回は「Terraformユーザーのバイブル」とも言われている「実践Terraform」を読んでていまいち分からなかった箇所を含め、基本的な文法の解説について書きたいと思います。
TL;DR
下記について解説
- resource
- variable
- local
- output
- data
- module
- dynamic
書かないこと
- terraformのインストール&初期設定
- 基本的な構成(*.tfファイルに記載する、providerの設定...など)
- 各種ファイルの説明(terraform.tfvars, terraform.tfstateなど)
前提
- 対象providerはAWSとします。(他は知らないから)
- variable, local, outputなどは実際は別ファイルに記載するのが推奨みたいですが、今回は同じ箇所に記載します。
resource
対象providerのリソースを作成する。
resource <固有リソース名> <識別子>
形式で記載。
<固有リソース名>は下記URL参照
https://registry.terraform.io/providers/hashicorp/aws/latest/docs
またリソースの各種プロパティは、<固有リソース名>.<識別子>.<プロパティ名>
で取得可能。
# 例:S3バケットを作成する。 resource "aws_s3_bucket" "hoge" { bucket = "my-tf-bucket-hoge" } # 上記S3バケットのポリシーを設定 resource "aws_s3_bucket_policy" "hoge" { # 上記S3バケットのid(=バケット名)を取得する bucket = aws_s3_bucket.hoge.id #policyの設定は省略 policy = ... }
variable
各リソースの各種プロパティに対して、変数を割り当てる。
変数には任意の値を設定できるので、各種プロパティに任意の値を設定できる。
設定した変数の値はvar.<変数名>
形式で取得可能。
主な用途は下記(?)
- アプリ名・ドメイン設定など、プロジェクトで共通で使用する値の定義
- terraform.tfvars(※)などに値を設定する
- モジュール(後述)に設定する値の定義
- 関数の「引数」みたいな使い方をする
※外部ファイル(*.tfvars)の中でも特殊なもので、なにも設定しなくてもこのファイルの内容から変数の値を取得してくれる。(他の外部ファイルはコマンド実行時に指定が必要)
# 例1:プロジェクトで共通で使用する値の定義 # どこか(terraform.tfvarsと同じ階層のファイル内)にvariableを定義する variable "app_name" { type = string } # terraform.tfvarsなどで設定する。 app_name = hogehoge # そうすると、リソース定義などの際にそれを利用できる。 # 文字列内で利用する場合は${var.name}とする resource "aws_s3_bucket" "hoge" { bucket = "${var.app_name}-bucket-hoge" tags = { APP_NAME = var.app_name } }
# 例2:モジュールの引数として使用する値の定義 # モジュール用のvariableを定義する variable "bucket_prefix" { type = string } # モジュール側で、variableの値を参照する。 resource "aws_s3_bucket" "this" { bucket = "${var.bucket_prefix}-bucket-fuga" } # そうすると、モジュールの呼び出し側でそれを利用できる。 # 結果として、「makky12-bucket-fuga」という名前のバケットができる module "aws_s3_bucket_piyo" { # 上記「resource "aws_s3_bucket" "this"」を参照しているものとする source = "./module/s3" bucket_prefix= "makky12" }
local
ローカル変数。variableと似ているが、下記の点が異なる。
- 定義した(≒同じフォルダ内の)resourceやmoduleにのみ作用する。
- サブフォルダなどにまたがって使用することはできない。
- 任意の値を後から設定することはできない。(宣言時の値で固定)
- プログラム言語の定数みたいなもの。
- ただしvariableと組み合わせることで、任意の値を初期値に設定することはできる。
- もちろん、その値はもう変更できない
具体的な使い方は、下記サイトが参考になります。
[terraform] VariableとLocal Valueの使い所 - Qiita
# モジュールで使用する場合。 variable bucket_suffix { type = string } locals { # localsは定義した値で固定。 # ただし値にvariableを使用すれば、任意の値を設定可能。 # (もちろん、変更は不可) local_prefix = "makky12" local_suffix = var.bucket_suffix } resource "aws_s3_bucket" "this" { # localの値を参照する。 bucket = "${local.bucket_prefix}-bucket-piyo-${local.bucket_suffix}" } # そうすると、リソースを呼び出す側でそれを利用できる。 # 結果として、「makky12-bucket-fuga」という名前のバケットができる module "aws_s3_bucket_piyo" { # 上記「resource "aws_s3_bucket" "this"」を参照しているものとする source = "./module/s3" bucket_prefix= "makky12" }
output
以下の用途に用いられる値。
terraform apply
時に、実際の値を出力する。- ただし
sensitive = true
にすると、出力されない。
- ただし
- モジュールにおける、モジュールの固有のプロパティを設定できる
- プロパティは、モジュール呼び出し側から参照できる。
取得する際は<該当モジュール名>.<output名>
形式で取得可能。
# 例1:terraform applyのoutputとして利用する # S3バケットを定義する resource "aws_s3_bucket" "hoge" { bucket = "hoge-fuga-bucket" } # こうすると、terraform apply時に上記S3バケットのARNが出力される output "s3_arn" { value = aws_s3_bucket.hoge.arn } # sensitive=trueの場合、値は出力されない output "s3_domain_name" { value = aws_s3_bucket.hoge.domain_name sensitive = true }
# 例2:モジュールの引数として使用する値の定義 # 共通モジュール側に、先程のようにS3バケットとoutputを定義する。 resource "aws_s3_bucket" "hoge" { bucket = "hoge-fuga-bucket" } output "s3_id" { value = aws_s3_bucket.hoge.id } # 上記共通モジュールを呼び出す module "aws_s3_bucket_piyo" { source = "./module/s3" } # 下記のようにすることで、outputの値(=S3バケットのID, =バケット名)を取得できる resource "aws_s3_bucket_policy" "hoge" { bucket = module.aws_s3_bucket_piyo.s3_id policy = ... }
data
terraform外部の値を取得することができる。 例えば以下のような使い方ができる。
- AWSアカウントから、アカウント情報(アカウントID、リージョン情報など)や既存リソースのARNを取得する
- ファイルからデータを読み込む(externalと併用)
定義はdata <固有data名> <識別子>
形式で取得可能。(取得は下記ソース参照)
externalについては、下記サイトを参照。
- https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/data_source#processing-json-in-shell-scripts
- terraform の external data source を使って外部コマンドの実行結果を variable として使用する · GitHub
# 例1:AWSから各種情報を取得する # ACMから、発行済ドメインの証明書ARNを取得する data "aws_acm_certificate" "hoge" { domain = "hogehoge.fugafuga.com" statuses = ["ISSUED"] } # terraformに紐づいているAWSアカウント情報を取得する。 data "aws_caller_identity" "fuga" {} # CloudFrontで、上記dataの情報を参照する resource "aws_cloudfront_distribution" "hoge" { (一部省略) tags = { ACCOUNT_ID = data.aws_caller_identity.fuga.account_id } viewer_certificate { acm_certificate_arn = data.aws_acm_certificate.hoge.arn } } # sensitive=trueの場合、値は出力されない output "s3_domain_name" { value = aws_s3_bucket.hoge.domain_name sensitive = true }
# 例2:ファイルの内容を読み込む # catコマンドでjsonファイルを読み込む。 data "external" "hoge" { program = ["cat", "test.json"] } # 上記jsonファイルのキーの値を参照する。 resource "aws_s3_bucket" "hoge" { bucket = value = "${data.external.hoge.result["piyo"]}" }
module
リソース定義などの処理を共通モジュール化できる。
共通モジュール化することで、毎回同じ定義を書かずとも、最低限のvariableを共通モジュールに渡すだけで該当のリソースを何個も作成できる。
1プロジェクト内でたくさん作成するリソースの定義をモジュール化しておくと便利。(Lambdaとか)
またモジュールは自作する他に、Terraform Registryで公開されているものを使用することができる。
これを利用した場合、特に何もしなくても各モジュールが用意しているoutputを参照できる。
# 例1:自作モジュールを利用する場合 # (共通)モジュール側は、通常のリソース定義と同様の書き方でOK。 variable "filename" { type = string } variable "function_name" { type = string } variable "handler" { type = string default = "index.handler" } variable "env" { type = string default = "dev" } resource "aws_lambda_function" "this" { filename = var.filename function_name = var.function_name handler = var.handler role = ... #省略 runtime = "nodejs14.x" environment { variables = { ENV= var.env } } } # 共通モジュールを呼び出す側は「module <任意のモジュール識別子名>」で # 共通モジュールを呼び出す。 # sourceで共通モジュールのパス、あとはvariablesを指定する。 # 結果として、function_hoge、function_fuga、function_piyoの # 3つのLambda関数ができる module "hoge" { source = "./modules/lambda" filename = "./hoge_function.zip" function_name = function_hoge } module "fuga" { source = "./modules/lambda" filename = "./fuga_function.zip" function_name = function_fuga env = "test" } module "piyo" { source = "./modules/lambda" filename = "./piyo_function.zip" function_name = function_piyo env = "stg" handler = "index.main" }
# 例2:Terraform Registryの公開モジュールを利用する場合 # sourceやversionは、各モジュールページ右上の「Provision Instructions」に # 記載してあるので、それをコピペする。 # variable, outputも各モジュールページやリンク先のGitHubに # 記載してあるので、それを参照する。 # 今回は「AWS lambda」の共通モジュールを使用する。 # https://registry.terraform.io/modules/terraform-aws-modules/lambda/aws/latest module "lambda" { source = "terraform-aws-modules/lambda/aws" version = "2.34.1" function_name = "function_hogehoge" description = "hogehoge lambda function" handler = "index.handler" runtime = "nodejs14.x" source_path = "../src/hogehoge" } # 各公開モジュールで定義されているoutputは、そのまま # 使用することができる。 resource "aws_s3_bucket_notification" "bucket_notification" { bucket = "bucket_hogehohe" lambda_function { # ここで「lambd_function_arn(=Lambda関数のARN)」を参照 lambda_function_arn = module.lambda.lambd_function_arn events = ["s3:ObjectCreated:*"] } }
dynamic
リソース内で複数個設定可能なmap形式の同一プロパティについて、配列で指定することで配列の値を繰り返し適用できる。(string, numberなどプリミティブなプロパティにはfor_eachなどでループできる)
プログラム言語のforループ文みたいな使い方ができる。
# 例2:ファイルの内容を読み込む # catコマンドでjsonファイルを読み込む。 resource "aws_waf_web_acl" "hoge" { name = "hogehoge" metric_name = "hogehoge" default_action { type = "BLOCK" } # 実際にrulesに設定する内容がvar.rulesに配列で渡されるとする dynamic "rules" { for_each = var.rules content { priority = rule.value["priority"] rule_id = rule.value["rule_id"] # ネスト構造の場合はこうする。 # https://www.terraform.io/language/expressions/dynamic-blocks dynamic "action" { for_each = rule.value.action content { type = action.value.type } } } } }
まとめ
掛け足でしたが、ざっとterraformの基本構文についてまとめてました。(自分の備忘録も兼ねて)
今までTerraformは触ってませんでしたが、使ってみるとこれはこれで便利な部分も多く、面白いなあと感じました。
IaCツールは色々特徴や強み・弱みがあるので、適材適所使い分けれらるようになりたいなあ、と思います。
それでは、今回はこの辺で。