2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Terraform】null値と空のループ処理を使いこなす

Posted at

はじめに

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のコードの書き方をご紹介しました。要件さえ整理できればループ構造の部分は比較的簡単に書けると思います。このテクニックが使えると、汎用的なコードを書く際に生かせると思いますので、ぜひ学んでみてください。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?