監視用Elastic CloudデプロイメントをTerraformで構築

 こんにちは、エンジニアの石川です。

今回はステージング環境として利用しているElastic Cloudのデプロイメントの監視を行うステージング環境監視用デプロイメントをTerraformで構築しました。


構成図

今回のゴールは、既にステージング環境として運用しているステージングクラスターが所属しているステージングデプロイメントのログやメトリクスを、監視用のデプロイメントに含まれる監視用クラスターに送り、それを監視用デプロイメントのKibanaで確認できるようにすることです。

監視用デプロイメントの構築後は、Elastic Cloudのマネジメントコンソールでステージングデプロイメントのログとメトリクスを監視用デプロイメントに送るように選択するだけ(ステージングデプロイメント->Monitoring->Logs and metrics->Shipt to a deploymentで送信先のデプロイメントを選ぶ)であるため今回は説明を省略し、監視用のデプロイメントを構築する部分についてのみの説明となります。

ステージングデプロイメントのログやメトリクスを監視用クラスターに送る内部的な仕組みはElasticの公式ドキュメントを参照してください。(Monitoring overview | Elasticsearch Guide [8.11] | Elastic


ファイル構成

ファイル構成は以下のようになります。


workspace

│   main.tf

│   provider.tf

│   variables.tf

├───env

│       dev-infra.backend.tfvars

│       dev-infra.tfvars

└───modules

    ├───elasticcloud

    │       elasticcloud.tf

    │       output.tf

    │       provider.tf

    │       variables.tf

    │

    └───elasticstack

            elasticstack.tf

            provider.tf

            variables.tf


ルートモジュールでサブモジュールとしてElastic Cloudのデプロイメント構築するモジュールと、Kibana(Elastic Stackの1つ)に対してアラートのルールやアクションコネクターを作成するモジュールを呼び出しています。

ルートモジュールのプロバイダーでElastic CloudやElastic Stackの認証を行っています。
本来であればルートモジュールでのプロバイダーの設定(どのsourceのどのバージョンを利用するのか、など)はサブモジュールに受け継がれるのですが、今回利用しているプロバイダーであるelastic/ecとelastic/elasticstackはelasticが作成しているものであるためサブモジュールに受け継がれませんでした。
そのためサブモジュールにプロバイダの設定を記述する必要がありました。


今回はリモートバックエンドとしてAmazon S3を利用しています。dev-infra.backend.tfvarsはバックエンド設定用変数ファイルとして$ terraform init に利用しています。以下は利用例です。


$ terraform init -backend-config env/ dev-infra.backend.tfvars


dev-infra.tfvarsは$terraform planや$terraform apply時に利用される、変数の値を定義しているファイルです。以下は利用例です。


$ terraform plan  -var-file env/sandbox.tfvars


ソースコード

全部説明すると長すぎるため、一部抜粋します。

  • workspace/main.tf

サブモジュールであるecとelasticstackを呼び出しています。

module "ec" {

  source                 = "./modules/elasticcloud"

  deployment_name        = var.ec_deployment_name

  creator                = var.creator

  region                 = var.ec_region

  ec_version             = var.ec_version

  deployment_template_id = var.ec_deployment_template_id

}


module "elasticstack" {

  source     = "./modules/elasticstack"

  creator    = var.creator

  webhookurl = var.webhookurl

}


  • workspace/provider.tf

terraformのバージョンやプロバイダーの設定、またプロバイダーを利用する際の認証などの設定が書いてあります。
ここにあるrequired_providersはサブモジュールに受け継がれないため、各サブモジュールで利用するプロバイダーのrequired_providersを設定する必要があります。

Elastic Cloudで利用するAPIキーは変数の宣言時にsensitiveをtrueにしており、これによってログなどでAPIキーの値が表示されないようにしています。


terraform {

  required_version = ">=1.6.0"

  backend "s3" {

  }

  required_providers {

    ec = {

      source  = "elastic/ec"

      version = "0.9.0"

    }

    elasticstack = {

      source  = "elastic/elasticstack"

      version = "0.9.0"

    }

  }

}


provider "ec" {

  apikey = var.ec_apikey

}

provider "elasticstack" {


  kibana {

    username = module.ec.username

    password = module.ec.password

    endpoints = [

      module.ec.kibana_endpoint

    ]

  }

}


  • workspace/variables.tfの一部分

今回は初めてvalidationブロックを利用しました。

error_messageを書いたらconditionを作成してくれるCopilotには非常に助かりました。


variable "webhookurl" {

  description = "url for webhook to notify slack"

  type        = string

  validation {

    error_message = "webhookurl must start as https://"

    condition     = can(regex("^https://", var.webhookurl))

  }

}


...


  • workspace/modules/elasticcloud/provider.tf

サブモジュールに必要なプロバイダーの設定はこのような形です。ルートモジュールから必要な部分だけを抜き出しているだけです。


terraform {

  required_version = ">=1.6.0"

  required_providers {

    ec = {

      source  = "elastic/ec"

      version = "0.9.0"

    }

  }

}


  • workspace/modules/elasticcloud/elasticcloud.tfの一部分

Elastic Cloudのデプロイメントの構築をしています。


resource "ec_deployment" "deployment" {

  name                   = var.ec_deployment_name

  region                 = var.ec_region

  version                = var.ec_version

  deployment_template_id = var.ec_deployment_template_id


  elasticsearch = {

    hot = {

      autoscaling   = var.elasticsearch_hot.autoscaling

      zone_count    = var.elasticsearch_hot.zone_count

      size_resource = var.elasticsearch_hot.size_resource

      size          = var.elasticsearch_hot.size

    }

  }

  kibana = {

    size_resource = var.kibana.size_resource

    size          = var.kibana.size

    zone_count    = var.kibana.zone_count

  }



}


  • workspace/modules/elasticcloud/variables.tfの一部分

変数の型の用意とそのデフォルトの値を入れています。



variable "elasticsearch_hot" {

  description = "Configuration for the Elasticsearch 'hot' tier."

  type = object({

    autoscaling : map(any)

    zone_count : number

    size_resource : string

    size : string

  })

  default = {

    autoscaling   = {}

    zone_count    = 2

    size_resource = "memory"

    size          = "1g"

  }

}


variable "kibana" {

  description = "Configuration for Kibana."

  type = object({

    size_resource : string

    size : string

    zone_count : number

  })

  default = {

    size_resource = "memory"

    size          = "1g"

    zone_count    = 1

  }

}


  • workspace/modules/elasticstack/elasticstack.tf

アラートの設定を行っています。
今回はどのアラートであってもSlackに通知するため、どのルールでもSlackに通知するアクションコネクターを利用するようにしています。
ルールの設定部分でアクションコネクターのidの設定方法に違和感がありますが、このようにしないと正しく作成されません。


resource "elasticstack_kibana_action_connector" "slack-connector" {

  name              = "slack_${var.creator}"

  connector_type_id = ".slack"

  secrets = jsonencode({

    webhookUrl = var.webhookurl

  })

}


resource "elasticstack_kibana_alerting_rule" "alert_rule" {

  for_each = { for rule in var.alert_rules : rule.name => rule }


  consumer    = each.value.consumer

  name        = "${each.value.name}_${var.creator}"

  enabled     = each.value.enabled

  interval    = each.value.interval

  notify_when = each.value.notify_when

  throttle    = each.value.throttle

  params = jsonencode({

    duration  = each.value.params.duration

    threshold = each.value.params.threshold

    limit     = each.value.params.limit

  })

  rule_type_id = each.value.rule_type_id

  actions {

    id = element(split("/", elasticstack_kibana_action_connector.slack-connector.id), 1)

    params = jsonencode({

      message = each.value.action_message

    })

  }

}


  • workspace/modules/elasticstack/variables.tfの一部分

こちらはリスト型で変数の型を用意し、そのデフォルトの値を詰め込んでいます。


… 


variable "alert_rules" {

  description = "List of alert rules"

  type = list(object({

    consumer : string

    name : string

    enabled : bool

    interval : string

    notify_when : string

    throttle : string

    params : object({

      duration : string

      threshold : optional(number)

      limit : optional(string)

    })

    rule_type_id : string

    action_message : string

  }))

  default = [

    {

      consumer    = "alerts"

      name        = "jvm_alert"

      enabled     = true

      interval    = "1m"

      notify_when = "onThrottleInterval"

      throttle    = "1m"

      params = {

        duration  = "5m"

        threshold = 85

      }

      rule_type_id   = "monitoring_alert_jvm_memory_usage"

      action_message = "{{context.internalFullMessage}}"

    },

  ]

}



ハマった部分

  • Terraformのプロバイダーのドキュメントだけでは解決できないことがあった

Terraformのelastic/elasticstackのプロバイダーのドキュメントにはアクションコネクターやルールを作成する例がいくつかあるのですが、それでもどの変数にどのような値を設定すべきかという情報が足りない場合がありました。
その場合はElasticの公式ドキュメントのAPIに関するドキュメントが役立ちました。
今回の場合、ルール作成などに関してはAlerting APIsのドキュメント(Alerting APIs | Kibana Guide [8.11] | Elastic )、アクションコネクターに関してはAction and connector APIsのドキュメント(Action and connector APIs | Kibana Guide [8.11] | Elastic )を参照するとよいと思います。


  • Elasticの公式ドキュメントを読んでも解決できないことがあった

ですがドキュメントを読むだけで欲しい情報が取得できない場合ももちろんあります。
自分の場合、elasticstack_kibana_alerting_ruleというルールを作成するためのリソースで必要となるrule_type_idの一覧がなく非常に苦労しました。(結局自分でGet rule types APIを叩く必要がありました。)


  • IaCで作成したものを手動で削除してしまった

$ terrform planなどが成功しなくなっていました。
今回は作業者が自分だけでありまた完成版ではなかったため、Terraformのstateから特定のリソースを削除するという手法で解決しましたがもう二度と同じことはしたくないです。


  • モジュール化

Terraformの基本的な知識がまだ不足している部分があるため、特にルートモジュールのプロバイダーの設定がサブモジュールに受け継がれないというエラーの解決に時間がかかりました。

感想

今回はステージング環境監視用デプロイメントをTerraformで構築しました。
Elastic CloudをTerraformで構築するために、ElasticCloudやElasticStackの構成なども詳しく理解する必要があったため非常に勉強になりました。

また自分のミスやモジュール化を通してTerraformの基礎部分も学ぶことができました。

読んでいただきありがとうございました!

今回利用したドキュメントのリンク