TerraformでリストをループしてAzureMonitorの監視登録をする

countとか大してわかってないしリストからループするのどうやればいいのと何年か思ってたのが以下サイトにより最近大体解決した気がするので備忘録です。
https://stackoverflow.com/questions/62264013/terraform-failing-with-invalid-for-each-argument-the-given-for-each-argument

以下のバージョンでの実行です。
Terraform: 1.04
azurerm: 2.70

これで何が嬉しいかというと、画面から数十のアラート登録の必要がなくリストを与えてコマンド打つため、ミスがたぶん減る気がするのとBGデプロイやDRで切り替えなどの際にたぶん早くおわるあたり。

ここには監視アラートのリソースしか書かいてなくてPaaSのリソースはdataリソース取ってくる作りになっています。（Insightを書いただけで力尽きた）

resourceから始まるのがその実物を作成し管理できるやつ。dataから始まるテンプレートは、リソースにつけられた名前でそのデータを取ってきてくれる。それを他のリソースで書いておいたリソースidを取り出したりします。


### :notes: webtest,applicationInsight
- 変数スキーマ

```variables.tf
variable "appinsight" {
  default = {
    insight_name = ""
    resource_group_name = ""
  }
}
variable "webtests" {
  type = list(object({
    webtest_name = string
    id = string
    timeout = string
    guid = string
    url = string
    encoding = string
    statuscode = string
    duration = string
  }))
  default = [
    {
      webtest_name = ""
      id = ""
      timeout = ""
      guid = ""
      url = ""
      encoding = ""
      statuscode = ""      
      duration = ""
    }
  ]
}
variable "params" {
  description = "監視対象リソース以外のパラメータを定義"
  default = {
    log_analytics_ws_name = ""
    log_retention_days = ""
    env_tag = ""
    appservices = {
      threshold_cpu = ""
      threshold_memory = ""
      threshold_response_sec = ""
    }
  }
}
```

- 変数実体

```terraform.tfvars
appinsight = {
    insight_name = "hoge-app-insight-prod"
    resource_group_name = "rg-hoge-prod"
}

webtests = [
  {
    webtest_name = "www-hoge-jp"
    id = "ee5adbc8-0edb-46e1-xxxx-3ebbff62aa2c"
    timeout = "120"
    guid = "a99b7dc5-e9a9-fb5f-xxxx-1769d4333cc2"
    url = "https://www.hoge.jp/"
    encoding = "utf-8"
    statuscode = "200"
    duration = "4000"
  },
  {
    webtest_name = "admin-hoge-jp"
    id = "ee5adbc8-0edb-46e1-xxxx-3ebbff62aa2c"
    timeout = "120"
    guid = "a99b7dc5-e9a9-fb5f-xxxx-1769d4333cc2"
    url = "https://admin.hoge.jp/"
    encoding = "utf-8"
    statuscode = "200"
    duration = "2000"
  }
]
params = {
  log_analytics_ws_name = ""
  log_retention_days = ""
  env_tag = "hoge"
   insight = {
    failed_location_count = 2
    webtest_tag_key = "hidden-link:/subscriptions/xxxxead0-xxx-xxx-xxx-3b8aa531xxxx/resourceGroups/rg-hoge-prod/providers/microsoft.insights/components/hoge-app-insight-prod"
    webtest_tag_value = "Resource"
  }
}
```

- テンプレートに読み込まれる外部ファイル

可用性監視用

```webtest.xml
<WebTest Name="${name}" Id="${id}" Enabled="True" CssProjectStructure="" CssIteration="" Timeout="${timeout}" WorkItemIds=""
  xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010" Description="" CredentialUserName="" CredentialPassword="" PreAuthenticate="True" Proxy="default" StopOnError="False" RecordedResultFile="" ResultsLocale="">
  <Items>
    <Request Method="GET" Guid="${guid}" Version="1.1" Url="${url}" ThinkTime="0" Timeout="${timeout}" ParseDependentRequests="True" FollowRedirects="True" RecordResult="True" Cache="False" ResponseTimeGoal="0" Encoding="${encoding}" ExpectedHttpStatusCode="${statuscode}" ExpectedResponseUrl="" ReportingName="" IgnoreHttpStatusCode="False" />
  </Items>
</WebTest>
```
クエリログ監視用（ヒアドキュメント風にも書けるけど外部ファイルにしたほうが変数を埋め込むことが可能で便利）
https://docs.microsoft.com/ja-jp/azure/azure-monitor/essentials/app-insights-metrics
https://docs.microsoft.com/ja-jp/azure/azure-monitor/alerts/alerts-log-query

```query_responsecode.txt
//レスポンスコード 5分 200以外 異常閾値の状態を2連続で検知(severity1)
requests
| where timestamp > ago(10m) 
| where resultCode != 200 and resultCode != "302"
| where url == "${url}"
| summarize count() by bin(timestamp, 5m)
```
ステータスコードのあたりの絞り込みは以下でもよさそう
| where tolong(resultCode) >= 400

```query_responsetime.txt
//レスポンスタイム 5分 接続にduration秒以上 異常閾値の状態を2連続で検知(severity2)
requests
| where timestamp > ago(10m) 
| where url == "${url}"
| where duration > ${duration}
| summarize avgRequestDuration=avg(duration) by bin(timestamp, 5m)
```

- テンプレート

```monitor_application-insight.tf
## Application Insights and url alerts 

# insight data source
data "azurerm_application_insights" "app_insights" {
  name                = var.appinsight.insight_name
  resource_group_name = data.azurerm_resource_group.rg1.name
}

# webtest(url監視)
resource "azurerm_application_insights_web_test" "webtest" {
  for_each = { for x in var.webtests: x.webtest_name => x }
  name                = each.value.webtest_name
  resource_group_name = data.azurerm_resource_group.rg1.name
  location            = data.azurerm_resource_group.rg1.location
  application_insights_id = data.azurerm_application_insights.app_insights.id
  kind                    = "ping"
  frequency               = 300
  timeout                 = 60
  enabled                 = true
  geo_locations           = ["apac-sg-sin-azr", "apac-jp-kaw-edge","apac-hk-hkn-azr"]
  # https://docs.microsoft.com/en-us/azure/azure-monitor/app/monitor-web-app-availability#location-population-tags
  configuration = templatefile("./webtest.xml", {
    name = each.value.webtest_name
    id = each.value.id
    timeout = each.value.timeout
    guid = each.value.guid
    url = each.value.url
    encoding = each.value.encoding
    statuscode = each.value.statuscode
  })
    # https://docs.microsoft.com/en-us/rest/api/application-insights/web-tests/create-or-update
  # tagsは初回はコメントインして2回目に設定する
  tags = {
    "${var.params.insight.webtest_tag_key}" = "${var.params.insight.webtest_tag_value}"
  }
  lifecycle {
    ignore_changes = [
      enabled,
    ]
  }
}

# failed location
resource "azurerm_monitor_metric_alert" "failed_location" {
  for_each = azurerm_application_insights_web_test.webtest
  name                = "[ ${each.value.name} ] url test failed location"
  resource_group_name = data.azurerm_resource_group.rg1.name
  scopes              = [each.value.id,data.azurerm_application_insights.app_insights.id]
  description         = "${var.params.insight.failed_location_count}箇所以上からの接続失敗を検知しました。確認してください。"
  auto_mitigate       = true
  frequency           = "PT5M"
  severity            = 1
  application_insights_web_test_location_availability_criteria {
      web_test_id             = each.value.id
      component_id            = data.azurerm_application_insights.app_insights.id
      failed_location_count   = var.params.insight.failed_location_count
  }
  tags = {}
  action {
    action_group_id   = azurerm_monitor_action_group.hook1.id
    webhook_properties = {}
  }
  lifecycle {
    ignore_changes = [
      enabled,
    ]
  }
}

#例外エラー	
resource "azurerm_monitor_metric_alert" "exception_count" {
  name                = "[ ${data.azurerm_application_insights.app_insights.name} ] Insights exceptions Alerts"
  resource_group_name = data.azurerm_resource_group.rg1.name
  scopes              = [data.azurerm_application_insights.app_insights.id]
  description         = "${data.azurerm_application_insights.app_insights.name} Insights exceptions Alert"
  enabled             = true
  severity            = 2
  auto_mitigate       = true
  tags                = {}

  criteria {
    metric_namespace = "microsoft.insights/components"
    metric_name      = "exceptions/count"
    aggregation      = "Count"
    operator         = "GreaterThanOrEqual"
    threshold        = 5
  }

  action {
    action_group_id = azurerm_monitor_action_group.hook1.id
    webhook_properties = {}
  }
  lifecycle {
    ignore_changes = [
      enabled,
    ]
  }
}

#トレースログ	
#https://docs.microsoft.com/en-us/dotnet/api/microsoft.applicationinsights.datacontracts.severitylevel?view=azure-dotnet
resource "azurerm_monitor_scheduled_query_rules_alert" "trace_error" {
  #name                = format("[ %s ] log-error-traces", data.azurerm_log_analytics_workspace.log.name)
  name                = format("[ %s ] log-error-traces", data.azurerm_application_insights.app_insights.name)
  resource_group_name = data.azurerm_resource_group.rg1.name
  location            = data.azurerm_resource_group.rg1.location
#  data_source_id = data.azurerm_log_analytics_workspace.log.id
  data_source_id = data.azurerm_application_insights.app_insights.id
  description    = "tracesログにErrorレベルのログがでています。"
  severity       = 2
  enabled        = true
  # Count all requests with server error result code grouped into 5-minute bins
  query       = <<-QUERY
  traces 
  |where severityLevel >= 3
  QUERY
  frequency   = 5 ##min
  time_window = 5 
  trigger {
    operator  = "GreaterThan"
    threshold = 4
  }
  action {
    action_group = [azurerm_monitor_action_group.hook1.id]
  }
  lifecycle {
    ignore_changes = [
      enabled,
    ]
  }
}

# URL監視 レスポンスタイム
resource "azurerm_monitor_scheduled_query_rules_alert" "url_responsetime" {
  for_each = { for x in var.webtests: x.webtest_name => x }
  name                = format("[ %s ] url test responsetime", each.value.webtest_name)
  resource_group_name = data.azurerm_resource_group.rg1.name
  location            = data.azurerm_resource_group.rg1.location
#  data_source_id = data.azurerm_log_analytics_workspace.log.id
  data_source_id = data.azurerm_application_insights.app_insights.id
  description    = "${each.value.url}の応答に${each.value.duration}ミリ秒以上かかっています"
  severity       = 2
  enabled        = true
  query       = templatefile("./query_responsetime.txt", {
    url = each.value.url
    duration = each.value.duration
  })
  frequency   = 5 ##min
  time_window = 10
  trigger {
    operator  = "GreaterThan"
    threshold = 1
  }
  action {
    action_group = [azurerm_monitor_action_group.hook1.id]
  }
  lifecycle {
    ignore_changes = [
      enabled,
    ]
  }
}

# URL監視 レスポンスコード
resource "azurerm_monitor_scheduled_query_rules_alert" "url_responsecode" {
  for_each = { for x in var.webtests: x.webtest_name => x }
  name                = format("[ %s ] url test responsecode", each.value.webtest_name)
  resource_group_name = data.azurerm_resource_group.rg1.name
  location            = data.azurerm_resource_group.rg1.location
#  data_source_id = data.azurerm_log_analytics_workspace.log.id
  data_source_id = data.azurerm_application_insights.app_insights.id
  description    = "${each.value.url}の応答コードに200と302以外があります"
  severity       = 1
  enabled        = true
  query       = templatefile("./query_responsecode.txt", {
    url = each.value.url
  })
  frequency   = 5 ##min
  time_window = 10 
  trigger {
    operator  = "GreaterThan"
    threshold = 1
  }
  action {
    action_group = [azurerm_monitor_action_group.hook1.id]
  }
  lifecycle {
    ignore_changes = [
      enabled,
    ]
  }
}

#メモリーリーク	
#例外数
#セキュリティ	
# →以下手動対応
# smart detect
# ※まだこのリソースで定義できるdetector_typeがデフォで入るFailureAnomaliesDetectorのみ
# ※一つのインサイトに重複して2つ同じdetector_typeは設定できない
# https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/monitor_smart_detector_alert_rule
# 手動の場合はLink先のとおり, 「インサイトリソース＞スマート検出＞アラートに移行する（プレビュー）＞移行」とすると以下の5つのアラートが自動登録される
# RequestPerformanceDegradationDetector　応答待機時間の劣化
# DependencyPerformanceDegradationDetector　依存関係の期間の低下
# ExceptionVolumeChangedDetector　例外数の異常な上昇
# TraceSeverityDetector　潜在的なセキュリティの問題の検出
# MemoryLeakDetector　潜在的なメモリ リークの検出
# https://docs.microsoft.com/ja-jp/azure/azure-monitor/alerts/alerts-smart-detections-migration
#resource "azurerm_monitor_smart_detector_alert_rule" "example" {
#  name                = "test-smart-detector-alert-rule"
#  resource_group_name = data.azurerm_resource_group.rg1.name
#  severity            = "Sev0"
#  scope_resource_ids  = [data.azurerm_application_insights.app_insights.id]
#  frequency           = "PT1M"
#  detector_type       = "FailureAnomaliesDetector"
#  #detector_type       = "MemoryLeakDetector"
#
#  action_group {
#    ids = [data.azurerm_monitor_action_group.data1.id]
#  }
#}
```

普通のメトリックアラートとログアラートとWebtestとデータリソース取ってくるあたりをループ処理で。

一応解説すると、以下のあたりで、var.webtestsに入ってるリストをforループでxに入れていて、コロンの後ろではインデックスとしてvar.webtestsの中身のどれかの変数名一個をインデックスみたいに指定してるらしい。
each.value.webtest_nameでその中身が取り出せて、each.key.webtest_nameでキーにあたる変数名が取り出せる。

`for_each = { for x in var.webtests: x.webtest_name => x }`


### :notes: メモ
- ループで取ってきたデータリソースを使用する際は、そのリソースをパラメータ無で指定すると、
中身で同じようにeach.valueが使える
- データリソースの変数指定で使えるのはデータリソースがoutputとして出すマニュアルに載ってるパラメータのみで初回にデータリソースを検索してくるために指定した変数が受け継がれるとかは特にない気がする。
- countとfor_eachは一つのリソースのうちで一つしか使うことができない。どっちも使うのも片方２つとかも無理です。
- for_each2つじゃないと無理そうになった場合、idをdataリソースからとってくるのをあきらめていくつかの変数を組み合わせる方式にすると1つですむこともあるかも。

```
  scopes              = [each.value.id]
　　　　　　　　　　　　　　　　↓
  scopes              = ["/subscriptions/${var.subscription_id}/resourceGroups/${each.value.resource_group_name}/providers/Microsoft.Web/sites/${each.value.appservice_name}"]
```
- もし変数を途中で編集したいというのがある場合localvaluesでどうこうすることができなくもないらしいけどそんなに複雑なことが可能かは知らない。  
https://www.mpon.me/entry/2018/03/30/200000  
- for_eachつかっているとcountで環境毎の切り替えとかはできないためworkspaceを分けるとかディレクトリを別にするとかの構造から考える必要がある   
  https://www.terraform.io/docs/language/settings/backends/remote.html#workspaces  
- なんでだかわからないがWebtestで指定するxmlの中身に書いてるIDは全部同じでも何の問題もなく登録可能だった。
- Webtest（可用性テスト）のタグが、戻ってきたやつは次にPlanうつと差分わかるのでそれを後から反映するため初回はタグのあたりをコメントに入れる必要がある。これもなんだかわからないが全部一緒だった。
- 2年前くらいはazurerm_monitor_metric_alertくらいしかなかった気がするので更新超ありがとうございます、という気持ち。

### :notes: 参考
terraform関連
https://stackoverflow.com/questions/62264013/terraform-failing-with-invalid-for-each-argument-the-given-for-each-argument
https://www.mpon.me/entry/2018/03/30/200000
https://qiita.com/VA_nakatsu/items/3df4136988e3d49e3cc2
https://dev.classmethod.jp/articles/how-to-use-terraform-workspace/
https://qiita.com/naomichi-y/items/4501331d114b4ef9d584
https://ngyuki.hatenablog.com/entry/2021/03/15/185207
https://tech-blog.cloud-config.jp/2020-01-10-terraform-appservice-backup-and-log/
https://www.terraform.io/docs/language/settings/backends/remote.html#workspaces
Azure関連
https://docs.microsoft.com/en-us/azure/azure-monitor/essentials/metrics-supported
https://resources.azure.com/