Elastic Stackに対してTerraformのImportをやってみた

# はじめに
稼働中のElastic Stack環境 (ElasticsearchとKibana）に対して途中からTerraformを使って管理したい場合のTerraform importの仕方について試してみました。
最近はKibana AlertのルールもTerraformで管理できるようになったので、それを例としてやってみました。

なお、Elastic CloudデプロイメントのTerraform化に関しては以下の記事を参照ください。
[Elastic Cloudに対してTerraformのImportをやってみた](https://qiita.com/nobuhikosekiya/items/c474a21b7114f3371d85)

[更新履歴]
- 2025/03/10 ... v8.17での変更内容を反映

# 前提条件
今回のテストはElastic Cloudで動いているElastic Stackを使いますが、オンプレのものでも同じようにできると思います。Elastic Stackはv8.11を使っていますが、管理下にするリソースによっては多少古いバージョンでもできると思います。
(v8.17での新しい内容も追記しました)

# やってみること
* 稼働中のElastic StackをTerraform管理下にする
* デフォルトで作られているDefault Kibana SpaceをImportし、Terraform管理下にする
* 既に作られたこちらの2つのAlertルールをImportし、Terraform管理下にする
* 新しい3つ目のAlertルールをTerraformから作成する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/b20ed974-841a-ae62-40f3-d56a34c962df.png)

# 手順
## Terraform tfファイルの作成
以下のファイルを作成します。
```Terraform:elastic-stack.tf
terraform {
  required_version = "~> 1.6"

  required_providers {
    elasticstack = {
      source = "elastic/elasticstack",
      version = "~>0.11"
    }

  }
}

variable "elastic-password" {
  type = string
}

variable "elasticsearch-endpoint" {
  type = string
}

variable "kibana-endpoint" {
  type = string
}

import {
  to = elasticstack_kibana_space.space-001
  id = "${data.elasticstack_elasticsearch_info.cluster_info.cluster_uuid}/default"
}

import {
  to = elasticstack_kibana_alerting_rule.alert-rule-001
  id = "default/2da310f0-9fc1-11ee-b4a5-79169becf22d"
}

import {
  to = elasticstack_kibana_alerting_rule.alert-rule-002
  id = "default/2da310f0-9fc1-11ee-b4a5-79169becf22d"
}

provider "elasticstack" {
  elasticsearch {
    username  = "elastic"
    password  = var.elastic-password
    endpoints = [var.elasticsearch-endpoint]
  }
  kibana {
    username  = "elastic"
    password  = var.elastic-password
    endpoints = [var.kibana-endpoint]
  }
}

data "elasticstack_elasticsearch_info" "cluster_info" {
}
```
上のファイルでは、Importする対象のAlert RuleのIDをベタで指定しています。ここは各自の環境の値で上書きしてください。この値はAlert Ruleを画面で開いた時のここのURLの部分の文字列となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/2502babc-3c7b-f016-19a0-fe1b8457f5d0.png)


次に、以下の環境変数ファイルを作り、自分の環境の値を設定ください。
```Terraform:terraform.tfvars
elastic-password=""
elasticsearch-endpoint=""
kibana-endpoint=""
```
今回はelastic-passwordはElastic Cloudの作成の時に作られたelasticユーザーのパスワードを設定します。
endpointはElastic Cloudのデプロイメント管理画面から確認できるエンドポイントURLです。

## Terraform Importで取り込み

最初にterraform initコマンドで初期化します。
```shell:CLI
% terraform init

Initializing the backend...

Initializing provider plugins...
- Finding elastic/elasticstack versions matching "~> 0.11"...
- Installing elastic/elasticstack v0.11.0...
- Installed elastic/elasticstack v0.11.0 (signed by a HashiCorp partner, key ID 7FE579EDEC6DAA7B)

...
```

次に、terraform planを実行してみると、importのターゲットとなるリソースが記述されていないためにエラーとなります。このリソースを自動的に生成してくれる-generate-config-outオプションをコマンドを教えてくれます。

```shell:CLI
% terraform plan
data.elasticstack_elasticsearch_info.cluster_info: Reading...
data.elasticstack_elasticsearch_info.cluster_info: Read complete after 0s [id=3Q2f9ivpSE6M8TCj_Lnn_w]

Planning failed. Terraform encountered an error while generating this plan.

╷
│ Error: Import block target does not exist
│ 
│   on elsaticstack.tf line 25:
│   25: import {
│ 
│ The target for the given import block does not exist. If you wish to automatically generate config for this resource, use the
│ -generate-config-out option within terraform plan. Otherwise, make sure the target resource exists within your configuration.
│ For example:
│ 
│   terraform plan -generate-config-out=generated.tf
╵
...
```

`terraform plan -generate-config-out=generated.tf`を実行すると、Importするリソースファイルを生成してくれます。いくつかエラーが表示されているものの、ちゃんとgenerated.tfファイルが作成されているはずです。(v8.17時点ではこのエラーは発生しなくなってました）
```Terraform:CLI
% terraform plan -generate-config-out=generated.tf
elasticstack_kibana_alerting_rule.alert-rule-002: Preparing import... [id=default/c84d79a0-9fbd-11ee-b4a5-79169becf22d]
elasticstack_kibana_alerting_rule.alert-rule-001: Preparing import... [id=default/2df8a7d0-9fbd-11ee-b4a5-79169becf22d]
data.elasticstack_elasticsearch_info.cluster_info: Reading...
elasticstack_kibana_alerting_rule.alert-rule-001: Refreshing state... [id=default/2df8a7d0-9fbd-11ee-b4a5-79169becf22d]
elasticstack_kibana_alerting_rule.alert-rule-002: Refreshing state... [id=default/c84d79a0-9fbd-11ee-b4a5-79169becf22d]
data.elasticstack_elasticsearch_info.cluster_info: Read complete after 0s [id=3Q2f9ivpSE6M8TCj_Lnn_w]
elasticstack_kibana_space.space-001: Preparing import... [id=3Q2f9ivpSE6M8TCj_Lnn_w/default]
elasticstack_kibana_space.space-001: Refreshing state... [id=3Q2f9ivpSE6M8TCj_Lnn_w/default]

Terraform planned the following actions, but then encountered a problem:

  # elasticstack_kibana_space.space-001 will be imported
  # (config will be generated)
    resource "elasticstack_kibana_space" "space-001" {
        color             = "#00bfb3"
        description       = "This is your default space!"
        disabled_features = []
        id                = "3Q2f9ivpSE6M8TCj_Lnn_w/default"
        name              = "Default"
        space_id          = "default"
    }

Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
╷
│ Warning: Config generation is experimental
│ 
│ Generating configuration during import is currently experimental, and the generated configuration format may change in future
│ versions.
╵
╷
│ Error: expected notify_when to be one of ["onActionGroupChange" "onActiveAlert" "onThrottleInterval"], got 
│ 
│   with elasticstack_kibana_alerting_rule.alert-rule-001,
│   on generated.tf line 5:
│   (source code not available)
│ 
╵
╷
│ Error: expected notify_when to be one of ["onActionGroupChange" "onActiveAlert" "onThrottleInterval"], got 
│ 
│   with elasticstack_kibana_alerting_rule.alert-rule-002,
│   on generated.tf line 5:
│   (source code not available)
│ 
```

生成されたファイルはこちら。
```Terraform:generated.tf
# __generated__ by Terraform
# Please review these resources and move them into your main configuration files.

# __generated__ by Terraform
resource "elasticstack_kibana_alerting_rule" "alert-rule-001" {
  consumer     = "alerts"
  enabled      = true
  interval     = "1m"
  name         = "alert-rule-001"
  notify_when  = ""
  params       = "{\"count\":{\"comparator\":\"more than\",\"value\":75},\"criteria\":[{\"comparator\":\"does not match\",\"field\":\"agent\",\"value\":\"\\\"aa\\\"\"}],\"logView\":{\"logViewId\":\"default\",\"type\":\"log-view-reference\"},\"timeSize\":5,\"timeUnit\":\"m\"}"
  rule_id      = "2df8a7d0-9fbd-11ee-b4a5-79169becf22d"
  rule_type_id = "logs.alert.document.count"
  space_id     = "default"
  tags         = []
  throttle     = null
  actions {
    group  = "logs.threshold.fired"
    id     = "elastic-cloud-email"
    params = "{\"message\":\"{{context.reason}}\\n\\n{{rule.name}} is active.\\n\\n{{^context.isRatio}}{{#context.group}}{{context.group}} - {{/context.group}}{{context.matchingDocuments}} log entries have matched the following conditions: {{context.conditions}}{{/context.isRatio}}\\n{{#context.isRatio}}{{#context.group}}{{context.group}} - {{/context.group}} Ratio of the count of log entries matching {{context.numeratorConditions}} to the count of log entries matching {{context.denominatorConditions}} was {{context.ratio}}{{/context.isRatio}}\\n\\n[View alert details]({{context.alertDetailsUrl}})\\n\",\"subject\":\"Test Alert for alert-001\",\"to\":[\"xxx@example.com\"]}"
  }
}

# __generated__ by Terraform
resource "elasticstack_kibana_alerting_rule" "alert-rule-002" {
  consumer     = "alerts"
  enabled      = true
  interval     = "1m"
  name         = "alert-rule-002"
  notify_when  = ""
  params       = "{\"count\":{\"comparator\":\"more than\",\"value\":75},\"criteria\":[{\"comparator\":\"does not match\",\"field\":\"agent\",\"value\":\"\\\"xyz\\\"\"}],\"logView\":{\"logViewId\":\"default\",\"type\":\"log-view-reference\"},\"timeSize\":5,\"timeUnit\":\"m\"}"
  rule_id      = "c84d79a0-9fbd-11ee-b4a5-79169becf22d"
  rule_type_id = "logs.alert.document.count"
  space_id     = "default"
  tags         = []
  throttle     = null
  actions {
    group  = "logs.threshold.fired"
    id     = "elastic-cloud-email"
    params = "{\"message\":\"{{context.reason}}\\n\\n{{rule.name}} is active.\\n\\n{{^context.isRatio}}{{#context.group}}{{context.group}} - {{/context.group}}{{context.matchingDocuments}} log entries have matched the following conditions: {{context.conditions}}{{/context.isRatio}}\\n{{#context.isRatio}}{{#context.group}}{{context.group}} - {{/context.group}} Ratio of the count of log entries matching {{context.numeratorConditions}} to the count of log entries matching {{context.denominatorConditions}} was {{context.ratio}}{{/context.isRatio}}\\n\\n[View alert details]({{context.alertDetailsUrl}})\\n\",\"subject\":\"Test Alert for alert 002\",\"to\":[\"xxx@example.com\"]}"
  }
}

# __generated__ by Terraform from "3Q2f9ivpSE6M8TCj_Lnn_w/default"
resource "elasticstack_kibana_space" "space-001" {
  color             = "#00bfb3"
  description       = "This is your default space!"
  disabled_features = []
  initials          = null
  name              = "Default"
  space_id          = "default"
}
```
上のファイルの`notify_when  = ""`の部分だけ`notify_when  = "onActiveAlert"`など値を設定してあげてください。それをしないと、次のplan時に怒られます。(v8.17時点ではこの操作は不要となっていました）

```
% terraform plan
╷
│ Error: expected notify_when to be one of ["onActionGroupChange" "onActiveAlert" "onThrottleInterval"], got 
│ 
│   with elasticstack_kibana_alerting_rule.alert-rule-001,
│   on generated.tf line 10, in resource "elasticstack_kibana_alerting_rule" "alert-rule-001":
│   10:   notify_when  = ""
│ 
╵
╷
│ Error: expected notify_when to be one of ["onActionGroupChange" "onActiveAlert" "onThrottleInterval"], got 
│ 
│   with elasticstack_kibana_alerting_rule.alert-rule-002,
│   on generated.tf line 30, in resource "elasticstack_kibana_alerting_rule" "alert-rule-002":
│   30:   notify_when  = ""
│ 
╵
```

`terraform plan`を実行します。
```shell:CLI
% terraform plan
elasticstack_kibana_alerting_rule.alert-rule-002: Preparing import... [id=default/c84d79a0-9fbd-11ee-b4a5-79169becf22d]
elasticstack_kibana_alerting_rule.alert-rule-001: Preparing import... [id=default/2df8a7d0-9fbd-11ee-b4a5-79169becf22d]
elasticstack_kibana_alerting_rule.alert-rule-002: Refreshing state... [id=default/c84d79a0-9fbd-11ee-b4a5-79169becf22d]
elasticstack_kibana_alerting_rule.alert-rule-001: Refreshing state... [id=default/2df8a7d0-9fbd-11ee-b4a5-79169becf22d]
data.elasticstack_elasticsearch_info.cluster_info: Reading...
data.elasticstack_elasticsearch_info.cluster_info: Read complete after 0s [id=3Q2f9ivpSE6M8TCj_Lnn_w]
elasticstack_kibana_space.space-001: Preparing import... [id=3Q2f9ivpSE6M8TCj_Lnn_w/default]
elasticstack_kibana_space.space-001: Refreshing state... [id=3Q2f9ivpSE6M8TCj_Lnn_w/default]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
  ~ update in-place

Terraform will perform the following actions:

  # elasticstack_kibana_alerting_rule.alert-rule-001 will be updated in-place
  # (imported from "default/2df8a7d0-9fbd-11ee-b4a5-79169becf22d")
  ~ resource "elasticstack_kibana_alerting_rule" "alert-rule-001" {
        consumer              = "alerts"
        enabled               = true
        id                    = "default/2df8a7d0-9fbd-11ee-b4a5-79169becf22d"
        interval              = "1m"
...(省略）...
Plan: 3 to import, 0 to add, 2 to change, 0 to destroy.

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run
"terraform apply" now.
```
次、`terraform apply`を実行してImportを実施します。

```shell:CLI
% terraform apply
elasticstack_kibana_alerting_rule.alert-rule-001: Preparing import... [id=default/2df8a7d0-9fbd-11ee-b4a5-79169becf22d]
elasticstack_kibana_alerting_rule.alert-rule-002: Preparing import... [
...(省略）...

Plan: 3 to import, 0 to add, 2 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes
...(省略）...

Apply complete! Resources: 3 imported, 0 added, 2 changed, 0 destroyed.
```

## AlertルールをTerraformから変更
2点変更をしてみます。
まず、既存のルールのinterval間隔を1m->5mに変更します。
```Terraform:generated.tf
# __generated__ by Terraform
resource "elasticstack_kibana_alerting_rule" "alert-rule-002" {
  consumer     = "alerts"
  enabled      = true
  interval     = "5m"
```
次に、新しい3つめのAlert Ruleを作成します（ほぼ他のからコピペ。nameを変更し、rule_idは自動作成されるのでコメントアウト）
```Terraform:generated.tf
resource "elasticstack_kibana_alerting_rule" "alert-rule-003" {
  consumer     = "alerts"
  enabled      = true
  interval     = "1m"
  name         = "alert-rule-003"
  notify_when  = "onActiveAlert"
  params       = "{\"count\":{\"comparator\":\"more than\",\"value\":75},\"criteria\":[{\"comparator\":\"does not match\",\"field\":\"agent\",\"value\":\"\\\"xyz\\\"\"}],\"logView\":{\"logViewId\":\"default\",\"type\":\"log-view-reference\"},\"timeSize\":5,\"timeUnit\":\"m\"}"
  # rule_id      = "c84d79a0-9fbd-11ee-b4a5-79169becf22d"
  rule_type_id = "logs.alert.document.count"
  space_id     = "default"
  tags         = []
  throttle     = null
  actions {
    group  = "logs.threshold.fired"
    id     = "elastic-cloud-email"
    params = "{\"message\":\"{{context.reason}}\\n\\n{{rule.name}} is active.\\n\\n{{^context.isRatio}}{{#context.group}}{{context.group}} - {{/context.group}}{{context.matchingDocuments}} log entries have matched the following conditions: {{context.conditions}}{{/context.isRatio}}\\n{{#context.isRatio}}{{#context.group}}{{context.group}} - {{/context.group}} Ratio of the count of log entries matching {{context.numeratorConditions}} to the count of log entries matching {{context.denominatorConditions}} was {{context.ratio}}{{/context.isRatio}}\\n\\n[View alert details]({{context.alertDetailsUrl}})\\n\",\"subject\":\"Test Alert for alert 002\",\"to\":[\"xxx@example.com\"]}"
  }
}
```

`terraform apply`で変更を反映します。
```shell:CLI
% terraform apply
...(省略）...
Terraform will perform the following actions:

  # elasticstack_kibana_alerting_rule.alert-rule-002 will be updated in-place
  ~ resource "elasticstack_kibana_alerting_rule" "alert-rule-002" {
        id                    = "default/c84d79a0-9fbd-11ee-b4a5-79169becf22d"
      ~ interval              = "1m" -> "5m"
        name                  = "alert-rule-002"
        tags                  = []
        # (10 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

  # elasticstack_kibana_alerting_rule.alert-rule-003 will be created
  + resource "elasticstack_kibana_alerting_rule" "alert-rule-003" {
      + consumer              = "alerts"
      + enabled               = true
      + id                    = (known after apply)
      + interval              = "1m"
 ...(省略）...
 
Plan: 1 to add, 1 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

elasticstack_kibana_alerting_rule.alert-rule-002: Modifying... [id=default/c84d79a0-9fbd-11ee-b4a5-79169becf22d]
elasticstack_kibana_alerting_rule.alert-rule-003: Creating...
elasticstack_kibana_alerting_rule.alert-rule-002: Modifications complete after 2s [id=default/c84d79a0-9fbd-11ee-b4a5-79169becf22d]
elasticstack_kibana_alerting_rule.alert-rule-003: Creation complete after 3s [id=default/296e8270-9fd6-11ee-b4a5-79169becf22d]

Apply complete! Resources: 1 added, 1 changed, 0 destroyed.
```

## Terraformによる変更の結果
ちゃんとTerraformからの5minのintervalとなり、rule-003も作成されました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109197/c7a8efb7-e927-120b-14d6-d2a5cad862d4.png)