Gluegent Blog

Gluegent Blog

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

  • 技術
監視用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/ dev-infra.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の基礎部分も学ぶことができました。読んでいただきありがとうございました!

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

(石川)