はじめに
Terraformでコードを記述する際にnull値と空のループ処理を活用できると、冗長なコードを書かずに済む&コードの書き方の幅が一気に広がるのでその紹介です。
本ブログに掲載している内容は、私個人の見解であり、所属する組織の立場や戦略、意見を代表するものではありません。あくまでエンジニアとしての経験や考えを発信していますので、ご了承ください。
例題:AWS CloudWatchアラームをループ処理で定義する
今回はAWS Cloudwatchアラームをお題にループ処理を記述してみたいと思います。
私が普段設定しているアラートだと以下の2種類のパターンで設定することが多いです。
パターン①:メトリクスを1つ指定してアラームを設定する
# EC2のCPU使用率が90%を超えた時に発火するアラーム
resource "aws_cloudwatch_metric_alarm" "alarm_EC2_CPUUtilization_Over_90" {
alarm_name = "EC2_CPUUtilization_Over_90"
alarm_description = "Alarm description"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 1
# アラームの基準とするメトリクス値の設定
namespace = "AWS/EC2" # メトリクスの名前空間
metric_name = "CPUUtilization" # メトリクス名
period = "300" # メトリクスを評価する期間(秒)
statistic = "Average" # メトリクスに適用する統計。今回は平均値
threshold = "90" # 閾値
dimensions = {
ImageId = "AMI ID"
InstanceId = "EC2のインスタンスID"
InstanceType = "EC2のインスタンスタイプ"
}
# アクションの設定
alarm_actions = [ ~ ] # 適時SNSトピックのARNなどを記載
ok_actions = [ ~ ] # 適時SNSトピックのARNなどを記載
}
パターン②:計算式を利用してアラームを設定する
# RDSのメモリ使用率が90%を超えた時に発火するアラーム
# ※拡張モニタリングを有効にしたうえで、すでにメトリクスフィルターでメトリクスを取得済みの前提です。
resource "aws_cloudwatch_metric_alarm" "alarm_RDS_MemoryUtilization_Over_90" {
alarm_name = "RDS_MemoryUtilization_Over_90"
alarm_description = "Alarm description"
comparison_operator = "GreaterThanOrEqualToThreshold"
evaluation_periods = 1
threshold = 90
# クエリ内容
metric_query {
id = "memory_used_percent"
expression = "(m1-m2)/m1*100" # 以下で定義しているm1とm2を参照した計算式を記述()
return_data = "true"
label = "RDS Memory Utilization"
}
## 単位:KB
metric_query {
id = "m1"
metric {
metric_name = "メモリ総量の値を取得しているカスタムメトリクス名"
namespace = "Custom"
period = "300"
stat = "Average"
}
}
## 単位:KB
metric_query {
id = "m2"
metric {
metric_name = "利用可能なメモリ容量を取得しているカスタムメトリクス名"
namespace = "Custom"
period = "300"
stat = "Average"
}
}
# アクションの設定
alarm_actions = [ ~ ] # 適時SNSトピックのARNなどを記載
ok_actions = [ ~ ] # 適時SNSトピックのARNなどを記載
}
①と②を比較すると同じaws_cloudwatch_metric_alarmリソースを使っていてもコードの中で定義するべき内容に大きく差があることがわかります。また②の中でもmetric_queryブロックが3つ定義されていますが、書き方のフォーマットが2種類あることが分かります。上記2種類のフォーマットのアラームをループ処理で定義しようとすると、ある時はdimensionsの定義が必要、別の時はdimensionsは不要だがmetric_queryが必要といった状況になります。こういった条件分岐が発生するループ処理を実装する際にnull値や空のループ処理が活用できます。
前提知識
実装方法ついて解説する前に、以下のTerraformの仕様について説明しておきます。
①null値をパラメータとして設定すると設定されていない扱いになる
例えばAWSのVPCを定義する際にリージョンのパラメータにnull値を渡すと、デプロイ時にエラー等は出ず、regionパラメータのデフォルトの挙動としてProvider定義で指定したリージョンが設定されます。
resource "aws_vpc" "this" {
cidr_block = "10.0.0.0/16"
region = null
}
②ループ処理の時に空のデータを渡すとループ処理が発生しない
for_eachやdynamicブロックでループ処理を回す際には何かしらのデータを与えますが、このデータの中身が空の時、ループ処理は行われずリソースのデプロイは行われません。
for_eachに渡すデータはmap,list,set,tuple,objectを指定することが可能です。
# for_eachに対し空のデータを与えるとループ処理が実行されずリソースが作成されない
## 例:サブネットが作成されない
resource "aws_subnet" "this" {
for_each = {} # 空のデータ
vpc_id = aws_vpc.this.id
cidr_block = each.value.cidr_ip
・
・
・
}
# dynamicブロックに対し空のデータを与えるとループ処理が行われず、リソース内の該当設定が設定されない
## 例:ELBはデプロイするが、アクセスログの設定はしない
resource "aws_lb" "this" {
for_each = { ~ } # 有効なMap型データを指定
name = ~
load_balancer_type = ~
・
・
・
dynamic "access_logs" {
for_each = {} # 空のデータ
content {
bucket = ~
・
・
・
}
}
}
例題の解答例
先ほどのTerraformの仕様を踏まえたうえで、例題に対する解答を考えてみたいと思います。
resource "aws_cloudwatch_metric_alarm" "this" {
for_each = local.metric_alert_map_data
alarm_name = "${var.prefix}${each.value.alert_name}"
alarm_description = each.value.description
comparison_operator = each.value.comparison_operator
evaluation_periods = each.value.evaluation_periods
namespace = each.value.name_space != "" ? each.value.name_space : null
metric_name = each.value.metric_name != "" ? each.value.metric_name : null
period = each.value.period != "" ? each.value.period : null
statistic = each.value.statistic != "" ? each.value.statistic : null
threshold = each.value.threshold != "" ? each.value.threshold :null
dimensions = each.value.dimension_map != {} ?yes each.value.dimension_map : null
dynamic "metric_query" {
for_each = contains(keys(var.parameters.metric_queries), each.value.alert_name) ? var.parameters.metric_queries[each.value.alert_name] : tomap({})
content {
id = metric_query.key
## 最終的な計算で使用するパラメータ
expression = contains(keys(metric_query.value), "expression") ? metric_query.value.expression : null
label = contains(keys(metric_query.value), "label") ? metric_query.value.label : null
return_data = contains(keys(metric_query.value), "return_data") ? metric_query.value.return_data : null
## 計算式に必要なメトリクス情報を定義する際に使用するパラメータ
#### metircブロックを定義する際にはmetric_nameを確実に定義するため、入力値のmapデータにそのキーがあればmetricブロックを定義するように実装
dynamic "metric" {
for_each = contains(keys(metric_query.value), "metric_name") ? ["define_metric_block"] : []
content {
metric_name = contains(keys(metric_query.value), "metric_name") ? metric_query.value.metric_name : null
namespace = contains(keys(metric_query.value), "namespace") ? metric_query.value.namespace : null
period = contains(keys(metric_query.value), "period") ? metric_query.value.period : null
stat = contains(keys(metric_query.value), "stat") ? metric_query.value.stat : null
dimensions = contains(keys(metric_query.value), "dimensions") ? metric_query.value.dimensions : null
}
}
}
}
alarm_actions = [ ~ ]
ok_actions = [ ~ ]
}
少々コードが長くなりましたが、やっていることとしては単純で設定するパラメータとしてはパターン①②の両方の項目を設定できるようにし、各項目の値については、入力(local.metric_alert_map_data)に含まれていれば値を設定する。入力になければnull値を入れておくといった実装をしています。またアラームによって設定が必要になるmetric_queryブロックはdynamicブロックを使って、入力(var.parameters.metric_queries)で定義があれば定義するようにしています。
※今回入力がlocal.metric_alert_map_dataとvar.parameters.metric_queriesに分かれていますが、こちらは当時私がこのコードを書いた際の入力フォーマットの問題で分割していただけです。
参考までにlocal.metric_alert_map_dataとvar.parameters.metric_queriesに入るデータをサンプルまでに載せておきます。
local.metric_alert_map_dataに入る値のサンプル
{
"aurora-1a_memoryUtil_over90" = {
"alert_name" = "aurora-1a_memoryUtil_over90"
"comparison_operator" = "GreaterThanOrEqualToThreshold"
"description" = "RDS(aurora-1a) memory utilization"
"dimension_map" = {}
"evaluation_periods" = "1"
"metric_name" = ""
"name_space" = ""
"period" = ""
"statistic" = ""
"threshold" = "90"
}
"ec2-step_cpuUtil_over90" = {
"alert_name" = "ec2-step_cpuUtil_over90"
"comparison_operator" = "GreaterThanOrEqualToThreshold"
"description" = "EC2(ec2-step001) cpu utilization"
"dimension_map" = {
"InstanceId" = "ec2_InstanceId"
}
"evaluation_periods" = "1"
"metric_name" = "CPUUtilization"
"name_space" = "AWS/EC2"
"period" = "300"
"statistic" = "Average"
"threshold" = "90"
}
"ec2-step_diskUtil_root_over80" = {
"alert_name" = "ec2-step_diskUtil_root_over80"
"comparison_operator" = "GreaterThanOrEqualToThreshold"
"description" = "EC2(ec2-step001) disk utilization of /"
"dimension_map" = {
"ImageId" = "ami_id"
"InstanceId" = "ec2_InstanceId"
"InstanceType" = "ec2_InstanceType"
"device" = "nvme0n1p4"
"fstype" = "xfs"
"path" = "/"
}
"evaluation_periods" = "1"
"metric_name" = "disk_used_percent"
"name_space" = "CWAgent"
"period" = "300"
"statistic" = "Average"
"threshold" = "80"
}
}
var.parameters.metric_queriesに入る値のサンプル
{
## 計算式に使用するメトリクス
"aurora-1a_memoryUtil_over90" = {
"m1" = {
metric_name = "memory_total"
namespace = "Custom/RDSOSMetrics"
period = "300"
stat = "Average"
dimensions = {
DBInstanceIdentifier = "${"DBInstanceIdentifier"}"
}
}
## 計算式に使用するメトリクス
"m2" = {
metric_name = "memory_free"
namespace = "Custom/RDSOSMetrics"
period = "300"
stat = "Average"
dimensions = {
DBInstanceIdentifier = "${"DBInstanceIdentifier"}"
}
}
## 計算式
"memory_used_percent" = {
expression = "100 - (m2 / m1 * 100)"
label = "RDS Memory Utilization"
return_data = true
}
}
}
最後に
今回はnull値とループ処理を利用したTerraformのコードの書き方をご紹介しました。要件さえ整理できればループ構造の部分は比較的簡単に書けると思います。このテクニックが使えると、汎用的なコードを書く際に生かせると思いますので、ぜひ学んでみてください。