APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

Datadog Operatorを使ってAzure Database for PostgreSQLを監視する

こんにちは、クラウド事業部の山中です。
今回はDatadog OperatorをAKSにデプロイし、
Azure Database for PostgreSQL のフレキシブル サーバーのメトリクスを収集してみました。

手順は基本的にこちらに沿って進めます。

Azure側の準備

まずはAzure側のリソースを作成します。
検証用に以下のterraformコードを書きました。

terraformコード

# provider
terraform {
  required_version = "=1.13.3"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "=4.73.0"
    }
    azuread = {
      source  = "hashicorp/azuread"
      version = "=3.8.0"
    }
  }
}

provider "azurerm" {
  resource_provider_registrations = "none"
  features {}
  tenant_id       = var.tenant_id
  subscription_id = var.subscription_id
}

# variable

variable "tenant_id" {
  type = string
}

variable "subscription_id" {
  type = string
}

variable "resource_group_name" {
  type = string
}

variable "name" {
  type = string
}

variable "tags" {
  type = map(string)
}

variable "datadog_api_key" {
  type = string
}

variable "postgres_admin_password" {
  type = string
}

variable "postgres_datadog_password" {
  type = string
}

# data

data "azurerm_resource_group" "rg" {
  name = var.resource_group_name
}

# resource

resource "azurerm_virtual_network" "vnet" {
  name                = "dev-vnet"
  location            = data.azurerm_resource_group.rg.location
  resource_group_name = data.azurerm_resource_group.rg.name
  address_space       = ["10.2.0.0/16"]
}

resource "azurerm_subnet" "subnets" {
  for_each = {
    "aks-subnet" = {
      address_prefixes = ["10.2.1.0/24"]
    }
    "db-subnet" = {
      address_prefixes  = ["10.2.2.0/28"]
      service_endpoints = ["Microsoft.Storage"]
      delegation = {
        name = "db-subnet-delegation"
        service_delegation = {
          name    = "Microsoft.DBforPostgreSQL/flexibleServers"
          actions = ["Microsoft.Network/virtualNetworks/subnets/join/action"]
        }
      }
    }
    "keyvault-subnet" = {
      address_prefixes = ["10.2.2.16/28"]
    }
  }

  name                              = each.key
  resource_group_name               = data.azurerm_resource_group.rg.name
  virtual_network_name              = azurerm_virtual_network.vnet.name
  address_prefixes                  = each.value.address_prefixes
  service_endpoints                 = lookup(each.value, "service_endpoints", null) != null ? each.value.service_endpoints : null
  private_endpoint_network_policies = "NetworkSecurityGroupEnabled"

  dynamic "delegation" {
    for_each = lookup(each.value, "delegation", null) != null ? [each.value.delegation] : []
    content {
      name = delegation.value.name
      service_delegation {
        name    = delegation.value.service_delegation.name
        actions = delegation.value.service_delegation.actions
      }
    }
  }
}

resource "azurerm_private_dns_zone" "private_dns_zones" {
  for_each = {
    postgres_db = "privatelink.postgres.database.azure.com"
    keyvault    = "privatelink.vaultcore.azure.net"
  }
  name                = each.value
  resource_group_name = data.azurerm_resource_group.rg.name
  tags                = var.tags
}

resource "azurerm_private_dns_zone_virtual_network_link" "dns_link" {
  for_each = {
    "dev-vnet_postgres_db" = {
      private_dns_zone_name = "privatelink.postgres.database.azure.com"
      virtual_network_id    = azurerm_virtual_network.vnet.id
    }
    "dev-vnet_keyvault" = {
      private_dns_zone_name = "privatelink.vaultcore.azure.net"
      virtual_network_id    = azurerm_virtual_network.vnet.id
    }
  }
  name                  = each.key
  resource_group_name   = data.azurerm_resource_group.rg.name
  private_dns_zone_name = each.value.private_dns_zone_name
  virtual_network_id    = each.value.virtual_network_id
  tags                  = var.tags

  depends_on = [azurerm_virtual_network.vnet, azurerm_private_dns_zone.private_dns_zones]
}

resource "azurerm_private_endpoint" "private_endpoints" {
  for_each = {
    "keyvault-pe" = {
      private_connection_resource_id = azurerm_key_vault.keyvault.id
      subnet_id                      = azurerm_subnet.subnets["keyvault-subnet"].id
      private_dns_zone_ids           = [azurerm_private_dns_zone.private_dns_zones["keyvault"].id]
      subresource_names              = ["vault"]
    }
  }

  name                = each.key
  location            = data.azurerm_resource_group.rg.location
  resource_group_name = data.azurerm_resource_group.rg.name
  subnet_id           = each.value.subnet_id

  private_dns_zone_group {
    name                 = "${each.key}-pdzg"
    private_dns_zone_ids = each.value.private_dns_zone_ids
  }

  private_service_connection {
    name                           = "${each.key}-psc"
    private_connection_resource_id = each.value.private_connection_resource_id
    subresource_names              = each.value.subresource_names
    is_manual_connection           = false
  }

  tags = var.tags

  depends_on = [
    azurerm_virtual_network.vnet,
    azurerm_private_dns_zone.private_dns_zones,
    azurerm_key_vault.keyvault
  ]
}

resource "azurerm_user_assigned_identity" "users" {
  for_each = toset([
    "aks-user",
    "datadog"
  ])
  name                = each.value
  resource_group_name = data.azurerm_resource_group.rg.name
  location            = data.azurerm_resource_group.rg.location
  tags                = var.tags
}

resource "azurerm_key_vault" "keyvault" {
  name                = var.name
  resource_group_name = data.azurerm_resource_group.rg.name
  location            = data.azurerm_resource_group.rg.location
  tenant_id           = var.tenant_id
  tags                = var.tags
  sku_name            = "standard"
  #public_network_access_enabled = false
  rbac_authorization_enabled = true
}

resource "azurerm_key_vault_secret" "keyvault-secrets" {
  for_each = {
    datadog-api-key           = var.datadog_api_key
    postgres-datadog-password = var.postgres_datadog_password
  }
  name         = each.key
  value        = each.value
  key_vault_id = azurerm_key_vault.keyvault.id

  depends_on = [azurerm_key_vault.keyvault]
}

resource "azurerm_postgresql_flexible_server" "db" {
  name                          = var.name
  resource_group_name           = data.azurerm_resource_group.rg.name
  location                      = data.azurerm_resource_group.rg.location
  delegated_subnet_id           = azurerm_subnet.subnets["db-subnet"].id
  sku_name                      = "B_Standard_B1ms"
  administrator_login           = "psqladmin"
  administrator_password        = var.postgres_admin_password
  version                       = "16"
  private_dns_zone_id           = azurerm_private_dns_zone.private_dns_zones["postgres_db"].id
  public_network_access_enabled = false
  tags                          = var.tags

  lifecycle {
    ignore_changes = [zone]
  }
}

resource "azurerm_role_assignment" "roles" {
  for_each = {
    "datadog-keyvault" = {
      scope                = azurerm_key_vault.keyvault.id
      role_definition_name = "Key Vault Secrets User"
      principal_id         = azurerm_user_assigned_identity.users["datadog"].principal_id
    }
  }
  scope                = each.value.scope
  role_definition_name = each.value.role_definition_name
  principal_id         = each.value.principal_id

  depends_on = [azurerm_user_assigned_identity.users, azurerm_key_vault.keyvault]
}

resource "azurerm_kubernetes_cluster" "k8s" {
  name                              = var.name
  dns_prefix                        = var.name
  location                          = data.azurerm_resource_group.rg.location
  resource_group_name               = data.azurerm_resource_group.rg.name
  kubernetes_version                = "1.34"
  sku_tier                          = "Free"
  automatic_upgrade_channel         = "patch"
  node_os_upgrade_channel           = "NodeImage"
  role_based_access_control_enabled = true
  oidc_issuer_enabled               = true
  workload_identity_enabled         = true
  tags                              = var.tags
  #private_cluster_enabled           = true

  default_node_pool {
    name                        = "default"
    vm_size                     = "Standard_B4ms"
    node_count                  = "1"
    vnet_subnet_id              = azurerm_subnet.subnets["aks-subnet"].id
    auto_scaling_enabled        = "false"
    orchestrator_version        = "1.34"
    temporary_name_for_rotation = "tmpdefault"

    upgrade_settings {
      drain_timeout_in_minutes      = 0
      max_surge                     = "33%"
      node_soak_duration_in_minutes = 0
    }
  }

  identity {
    type = "UserAssigned"
    identity_ids = [
      azurerm_user_assigned_identity.users["aks-user"].id
    ]
  }

  # azure_active_directory_role_based_access_control {
  #   azure_rbac_enabled = true
  # }

  network_profile {
    network_plugin     = "azure"
    network_policy     = "cilium"
    network_data_plane = "cilium"
    #network_plugin_mode = "overlay"
    load_balancer_sku = "standard"
    service_cidr      = "172.16.0.0/24"
    dns_service_ip    = "172.16.0.254"
  }

  # key_vault_secrets_provider {
  #   secret_rotation_enabled = true
  # }

  depends_on = [azurerm_subnet.subnets, azurerm_user_assigned_identity.users]

}

resource "azurerm_kubernetes_cluster_node_pool" "nodepool-user" {

  kubernetes_cluster_id       = azurerm_kubernetes_cluster.k8s.id
  name                        = "user"
  vm_size                     = "Standard_B4ms"
  node_count                  = 1
  vnet_subnet_id              = azurerm_subnet.subnets["aks-subnet"].id
  temporary_name_for_rotation = "tmpuser"

  tags = var.tags

  upgrade_settings {
    drain_timeout_in_minutes      = 0
    max_surge                     = "33%"
    node_soak_duration_in_minutes = 0
  }

  depends_on = [azurerm_kubernetes_cluster.k8s]

}

resource "azurerm_federated_identity_credential" "aks-workload_identity-federations" {
  for_each = {
    "datadog-agent-federation" = {
      user_assigned_identity_id = azurerm_user_assigned_identity.users["datadog"].id
      subject                   = "system:serviceaccount:datadog:datadog-agent"
    }
    "datadog-cluster-agent-federation" = {
      user_assigned_identity_id = azurerm_user_assigned_identity.users["datadog"].id
      subject                   = "system:serviceaccount:datadog:datadog-cluster-agent"
    }
    "datadog-cluster-checks-runner-federation" = {
      user_assigned_identity_id = azurerm_user_assigned_identity.users["datadog"].id
      subject                   = "system:serviceaccount:datadog:datadog-cluster-checks-runner"
    }
  }
  name                      = each.key
  audience                  = ["api://AzureADTokenExchange"]
  issuer                    = azurerm_kubernetes_cluster.k8s.oidc_issuer_url
  user_assigned_identity_id = each.value.user_assigned_identity_id
  subject                   = each.value.subject

  depends_on = [azurerm_kubernetes_cluster.k8s, azurerm_user_assigned_identity.users]
}

作成済みリソースグループに対してvnetとsubnetを作成し、
AKS、AzureKeyVault、Azure Database for PostgreSQL のフレキシブル サーバーをデプロイし、リソースにアクセスするための各種設定をしています。

実行する場合は以下の点にご注意ください。

  • 変数になっている部分はterraform.tfvarsなどを準備してご自身の環境に合わせて設定してください
  • リソースグループに対して共同作成者キー コンテナー管理者ユーザー アクセス管理者のロールがあれば実行できると思います
  • VPNなどは手間なのでパブリックアクセスを許可しています
  • 料金がかかります

terraform applyが完了したら、以下のコマンドで作成したAKSクラスタにkubectlが実行できるようにしてください。

az aks get-credentials --resource-group <var.resource_group_name> --name <var.name> --overwrite-existing

Postgres 設定を構成する

こちらを参考に、データベース側の設定を変更します。

検証用のためオプションもすべて有効化します。
以下のようなコマンドで設定できると思います。

az postgres flexible-server parameter set --resource-group <var.resource_group_name> --server-name <var.name> --name azure.extensions --value PG_STAT_STATEMENTS
az postgres flexible-server parameter set --resource-group <var.resource_group_name> --server-name <var.name> --name track_activity_query_size --value 4096
az postgres flexible-server parameter set --resource-group <var.resource_group_name> --server-name <var.name> --name pg_stat_statements.track --value ALL
az postgres flexible-server parameter set --resource-group <var.resource_group_name> --server-name <var.name> --name pg_stat_statements.max --value 10000
az postgres flexible-server parameter set --resource-group <var.resource_group_name> --server-name <var.name> --name pg_stat_statements.track_utility --value off
az postgres flexible-server parameter set --resource-group <var.resource_group_name> --server-name <var.name> --name track_io_timing --value on
az postgres flexible-server restart --resource-group <var.resource_group_name> --name <var.name>

クエリはAKSに一時的にpgadminを立てて、そこから実行することにしました。
以下のようなmanifestを準備しました。
ホスト名などはご自身の環境に書き換えていただく必要があります。

pgadmin.yaml

apiVersion: v1
kind: Pod
metadata:
  labels:
    run: pgadmin
  name: pgadmin
  namespace: default
spec:
  containers:
  - image: dpage/pgadmin4:latest
    name: pgadmin
    env:
      - name: PGADMIN_DEFAULT_EMAIL
        value: "user@domain.com"
      - name: PGADMIN_DEFAULT_PASSWORD
        value: "SuperSecret"
      - name: PGADMIN_CONFIG_SERVER_MODE
        value: "False"
      - name: PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED
        value: "False"
      - name: TZ
        value: "Asia/Tokyo"
    ports:
      - name: http
        containerPort: 80
    resources: {}
    volumeMounts:
      - name: pgadmin-config
        mountPath: /pgadmin4/servers.json
        subPath: servers.json
        readOnly: true 
  volumes:
    - name: pgadmin-config
      configMap:
        name: pgadmin-config
  dnsPolicy: ClusterFirst
  restartPolicy: Always
status: {}
---
apiVersion: v1
kind: Service
metadata:
  name: pgadmin
spec:
  selector:
    run: pgadmin
  ports:
    - port: 80
      targetPort: 80
      name: http
  type: ClusterIP
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: pgadmin-config
data:
  servers.json: |
    {
      "Servers": {
        "1": {
          "Name": "<var.name>",
          "Group": "Servers",
          "Host": "<var.name>.postgres.database.azure.com",
          "Port": 5432,
          "MaintenanceDB": "postgres",
          "Username": "psqladmin",
          "SSLMode": "prefer"
        }
      }
    }

デプロイします。

kubectl apply -f .\pgadmin.yaml

ポートフォワードでアクセスします。

kubectl port-forward pgadmin 8080:80

無事起動できていたらブラウザでhttp://localhost:8080/にアクセスすればログインできると思います。
terraform実行時に設定したadministrator_passwordでDBに接続したら、
ドキュメント通りのクエリを実行してdatadogユーザー作成などを行ってください。

Agent のインストールと構成

こちらを参考にhelmでDatadog Operatorをデプロイします。

helm repo add datadog https://helm.datadoghq.com
helm install datadog-operator datadog/datadog-operator -n datadog --create-namespace

次にDatadogAgentのカスタムリソースをデプロイします。
以下のようなmanifestを準備しました。
一部ご自身の環境に書き換えてください。

datadog-agent.yaml

apiVersion: datadoghq.com/v2alpha1
kind: DatadogAgent
metadata:
  name: datadog
  namespace: datadog
spec:
  global:
    clusterName: <var.name>
    site: datadoghq.com
    tags:
      - "env:<var.name>"
    credentials:
      apiKey: ENC[datadog-api-key]
  features:
    clusterChecks:
      enabled: true
      useClusterChecksRunners: true
  override:
    nodeAgent:
      env:
       - name: DD_SECRET_BACKEND_TYPE
         value: "azure.keyvault"
       - name: DD_SECRET_BACKEND_CONFIG
         value: '{"keyvaulturl":"https://<var.name>.vault.azure.net/"}'
      serviceAccountAnnotations:
        azure.workload.identity/client-id: "<Managed ID clientID>"
      labels:
        azure.workload.identity/use: "true"
    clusterChecksRunner:
      serviceAccountAnnotations:
        azure.workload.identity/client-id: "<Managed ID clientID>"
      labels:
        azure.workload.identity/use: "true"
      env:
       - name: DD_SECRET_BACKEND_TYPE
         value: "azure.keyvault"
       - name: DD_SECRET_BACKEND_CONFIG
         value: '{"keyvaulturl":"https://<var.name>.vault.azure.net/"}'
    clusterAgent:
      extraConfd:
        configDataMap:
          postgres.yaml: |-
            cluster_check: true
            init_config:
            instances:
              - dbm: true
                host: <var.name>.postgres.database.azure.com
                port: 5432
                username: datadog
                password: ENC[postgres-datadog-password]
                dbname: postgres
                collect_function_metrics: false
                ssl: required
                tags:
                  - "service:<var.name>"
                azure:
                  deployment_type: flexible_server
                  fully_qualified_domain_name: <var.name>.postgres.database.azure.com

podが無事に起動すればOKです。

kubectl get pod -n datadog
NAME                                             READY   STATUS    RESTARTS   AGE
datadog-agent-jnq64                              2/2     Running   0          50s
datadog-agent-r5g99                              2/2     Running   0          51s
datadog-cluster-agent-544b7d8c9b-sxgz5           1/1     Running   0          51s
datadog-cluster-checks-runner-59bf6cc75c-gwsq4   1/1     Running   0          51s
datadog-operator-64f5cdbb4c-jcdxp                1/1     Running   0          4m46s

secret管理について

datadog API Keysなどsecret情報はAzure KeyVaultからworkload identityを使って取得しています。

設定方法は こちらのネイティブ Agent サポートを使用します。

Operatorでの設定方法はAWS Secretsにしか書いてないですが、Azure KeyVaultでも使えました。
以下の設定部分です。

  override:
    nodeAgent:
      env:
       - name: DD_SECRET_BACKEND_TYPE
         value: "azure.keyvault"
       - name: DD_SECRET_BACKEND_CONFIG
         value: '{"keyvaulturl":"https://<var.name>.vault.azure.net/"}'

Azure側ではdatadogというマネージドIDを作成し、
Key Vault Secrets Userロールをアサインし、Datadog Operatorで自動的に作成される
datadog用のKubernetes Service Accountのフェデレーション設定をしています。
terraformコードをご確認いただければと思います。

そしてAKS側でpodのラベルにazure.workload.identity/use: "true"を追加、
Service Accountのアノテーションにazure.workload.identity/client-id: "<Managed ID clientID>"を追加してマネージドIDと紐づけます。

Datadog OperatorではDatadogAgentのカスタムリソースでoverrideで設定できました。
以下の部分ですね。

  override:
    nodeAgent:
      serviceAccountAnnotations:
        azure.workload.identity/client-id: "<Managed ID clientID>"
      labels:
        azure.workload.identity/use: "true"

マネージドIDのクライアントIDは以下のコマンドで取得できると思います。

az identity show -g <var.resource_group_name> -n datadog --query clientId -o tsv

Datadog AgentとDB Monitoringを実行するclusterChecksRunnerで設定が必要でした。

Workload Identityについては以下も参考になると思うのでご確認いただければと思います。

techblog.ap-com.co.jp

Datadog でメトリクス確認

こちらにあるようにpostgresql.から始まるメトリクスが収集できるようです。

無事収集できていれば、Metrics Summaryで確認できると思います。

こちらのメトリクスは Postgresのインテグレーション を有効化すると追加されるPostgres - Metricsのダッシュボードで確認できるので試してみてもらえればと思います。