🌵

[New Relic] Terraform で AWS Integration を実装する

に公開

🌱 はじめに

NewRelicとAWSを連携させることで、AWSリソースのメトリクスをNewRelicで監視できるようになります。

本記事では、PUSHモード(CloudWatch Metric Streams) を使ったリアルタイム監視の実装に焦点を当てます。

😓 既存の方法の不便な点

NewRelicは公式にCloudFormationテンプレートやTerraformモジュールを提供していますが、実際の運用では以下のような不便な点がありました。

CloudFormation (NewRelic公式推奨)

NewRelicのUI上で提供されるCloudFormationテンプレートは、複数の子スタックを呼び出すネスト構造になっています。

親スタック (newrelic-integration.yml)
├── newrelic-integration-role.yml        # IAMロール作成
├── newrelic-link-account.yml            # GraphQL APIでアカウント連携
├── newrelic-configure-integrations.yml  # ポーリング設定
└── newrelic-metric-streams.yml          # メトリックストリーム設定

この構造により、以下の不便さがありました。

  • 保守性の低さ: テンプレート更新時に親・子スタックの両方を修正する必要があり、パラメータ変更時は複数スタック間の整合性調整が必要
  • 可視性の低さ: S3上の外部テンプレート参照やGraphQL APIによる自動設定により、AWS・NewRelic側で何が作られるか把握しづらい
  • マルチリージョン対応: CloudFront・WAF(us-east-1)など、リージョンをまたぐ場合は手動で複数回デプロイが必要

参考🔗: CloudFormationとCloudWatchメトリックストリームを介して統合

Terraform公式モジュール

Terraformで管理する場合でも、公式モジュールには以下の制約がありました。

  • PUSH/PULLモードの制御不足: 両方のモードが有効化され、コスト最適化が難しい
  • License Keyの柔軟性: 既存のキーを再利用できず、新規発行が必要

参考🔗: NewRelic - Cloud integrations guide

Terraformで管理するメリット

これらの不便さを解消し、複数環境への展開を効率化するため、必要なリソースだけを定義したシンプルなTerraformモジュールを作成しました。

  • フラットな構造 : ネストなしで、全リソースがわかりやすい
  • 1回のデプロイで完結 : 複数リージョンを terraform apply 1回で設定
  • 環境間の設定共有 : dev/prod などの複数の環境で同じモジュールを再利用
  • リアルタイム監視 : PUSHモードでメトリクスをリアルタイム配信
  • メトリクスの選択的収集 : 必要なメトリクスだけを指定して、データ転送量を最適化

🧩 全体構成

今回実装するシステムの全体像です。各リージョンでMetric StreamとFirehoseを構築し、NewRelicにデータを送信します。

AWS Account
├── ap-northeast-1 (Tokyo)
│   ├── CloudWatch Metric Stream → Kinesis Firehose → NewRelic
│   ├── S3 (Backup)
│   └── IAM Roles/Policies
│
├── us-east-1 (Virginia) 
│   ├── CloudWatch Metric Stream → Kinesis Firehose → NewRelic
│   ├── S3 (Backup)
│   └── IAM Roles/Policies
│
└── NewRelic Link Account (AWSアカウント全体とNewRelicの紐付け)

⚙️ 事前準備:ディレクトリ構成

モジュール化により、複数環境で同じコードを再利用できる構成にします。

terraform/
└── newrelic/
    ├── modules/
    │   └── integrations/          # 再利用可能なモジュール
    │       ├── main.tf
    │       ├── variables.tf
    │       └── terraform.tf
    └── environments/
        └── dev/
            └── integration/       # 環境固有の設定
                ├── main.tf
                ├── variables.tf
                ├── locals.tf
                └── terraform.tf

📝 実装

ここからは、実際のTerraformコードを順番に見ていきます。

1️⃣ モジュール: variables.tf

まず、モジュールの入力変数を定義します。

modules/integrations/variables.tf

variable "service_name" {
  description = "サービス名"
  type        = string
}

variable "env" {
  description = "環境名 (dev, prod)"
  type        = string
}

variable "aws_region" {
  description = "監視対象のAWSリージョン"
  type        = string
}

variable "name_suffix" {
  description = "リソース名に追加するサフィックス (例: use1, apne1)。空文字列の場合はサフィックスなし"
  type        = string
  default     = ""
}

variable "create_link_account" {
  description = "NewRelic Link Accountを作成するかどうか。同じAWSアカウント内の複数リージョンを監視する場合、1つだけtrueにする"
  type        = bool
  default     = true
}

variable "newrelic_account_id" {
  description = "NewRelic Account ID"
  type        = number
}

variable "newrelic_license_key" {
  description = "NewRelic Ingest License Key"
  type        = string
  sensitive   = true
}

variable "newrelic_account_principal_arn" {
  description = "NewRelicのAWS Principal ARN"
  type        = string
  default     = "754728514883"
}

variable "newrelic_endpoint_url" {
  description = "NewRelic Metric Stream Endpoint URL"
  type        = string
  default     = "https://aws-api.newrelic.com/cloudwatch-metrics/v1"
}

variable "output_format" {
  description = "Metric Streamの出力フォーマット"
  type        = string
  default     = "opentelemetry1.0"
}

variable "metric_namespaces" {
  description = "監視するCloudWatch名前空間とメトリクスのリスト"
  type = list(object({
    namespace    = string
    metric_names = optional(list(string), [])
  }))
  default = []
}

💡ポイント1: OpenTelemetry 1.0 (Protobuf) 形式を採用

output_format のデフォルトを opentelemetry1.0 に設定しています。

NewRelicは2025年10月に、AWS CloudWatch Metric StreamsからのOpenTelemetry 1.0 (Protobuf)形式に正式対応しました。従来の opentelemetry0.7 (JSON形式) と比較して、以下のメリットがあります。

  • データサイズの削減: Protobufバイナリ形式により、JSON形式より大幅に圧縮効率が向上
  • 転送コストの削減: データ転送量が減少することで、AWS Data Transfer料金を削減
  • 処理効率の向上: バイナリ形式のため、パース・処理が高速化

NewRelicは公式ブログで、opentelemetry1.0 形式の利用を推奨しています。

参考: AWS監視の効率とコストを最適化:OTel 1.0形式(Protobuf)サポート開始 - NewRelic Blog

💡ポイント2: メトリクスの柔軟な絞り込み

metric_namespacesoptional() を使って柔軟な指定を可能にしています。

# 全メトリクスを収集
{ namespace = "AWS/RDS" }

# 特定メトリクスのみ収集
{ 
  namespace = "AWS/RDS", 
  metric_names = ["CPUUtilization", "DatabaseConnections"]
}

💡ポイント3: リージョン別の命名制御

name_suffix を使うことで、リージョンごとに識別しやすいリソース名を付けられます。

  • 🇯🇵 ap-northeast-1: myapp-prod-metric-stream
  • 🇺🇸 us-east-1: myapp-prod-use1-metric-stream

2️⃣ モジュール: main.tf (Part 1 - IAM & S3)

modules/integrations/main.tf
data "aws_caller_identity" "current" {}

locals {
  prefix     = var.name_suffix != "" ? "${var.service_name}-${var.env}-${var.name_suffix}" : "${var.service_name}-${var.env}"
  account_id = data.aws_caller_identity.current.account_id
}

# 1. IAM Role - CloudWatch Metric Stream → Kinesis Firehose
resource "aws_iam_role" "metric_stream_to_firehose" {
  name = "${local.prefix}-metric-stream-firehose-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "streams.metrics.cloudwatch.amazonaws.com"
      }
    }]
  })
}

resource "aws_iam_role_policy" "metric_stream_to_firehose" {
  name = "${local.prefix}-metric-stream-to-firehose-policy"
  role = aws_iam_role.metric_stream_to_firehose.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = [
        "firehose:PutRecord",
        "firehose:PutRecordBatch"
      ]
      Effect   = "Allow"
      Resource = aws_kinesis_firehose_delivery_stream.newrelic_stream.arn
    }]
  })
}

# 2. IAM Role - Kinesis Firehose → NewRelic
resource "aws_iam_role" "firehose_to_newrelic" {
  name = "${local.prefix}-firehose-delivery-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "firehose.amazonaws.com"
      }
    }]
  })
}

# 3. S3 Bucket(Firehose失敗時のバックアップ)
resource "aws_s3_bucket" "firehose_backup" {
  bucket        = "${local.prefix}-firehose-backup-${local.account_id}"
  force_destroy = true
}

resource "aws_s3_bucket_ownership_controls" "firehose_backup" {
  bucket = aws_s3_bucket.firehose_backup.id

  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}

resource "aws_iam_role_policy" "firehose_s3_backup" {
  name = "${local.prefix}-firehose-s3-backup-policy"
  role = aws_iam_role.firehose_to_newrelic.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = [
        "s3:PutObject",
        "s3:GetObject",
        "s3:ListBucket"
      ]
      Effect = "Allow"
      Resource = [
        aws_s3_bucket.firehose_backup.arn,
        "${aws_s3_bucket.firehose_backup.arn}/*"
      ]
    }]
  })
}

3️⃣ モジュール: main.tf (Part 2 - Firehose & Metric Stream)

modules/integrations/main.tf
# 4. Kinesis Firehose Delivery Stream
resource "aws_kinesis_firehose_delivery_stream" "newrelic_stream" {
  name        = "${local.prefix}-metric-stream"
  destination = "http_endpoint"

  http_endpoint_configuration {
    url                = var.newrelic_endpoint_url
    name               = "NewRelic ${var.env}"
    access_key         = var.newrelic_license_key
    buffering_size     = 1
    buffering_interval = 60
    s3_backup_mode     = "FailedDataOnly"
    role_arn           = aws_iam_role.firehose_to_newrelic.arn

    s3_configuration {
      role_arn           = aws_iam_role.firehose_to_newrelic.arn
      bucket_arn         = aws_s3_bucket.firehose_backup.arn
      buffering_size     = 10
      buffering_interval = 400
      compression_format = "GZIP"
    }

    request_configuration {
      content_encoding = "GZIP"
    }
  }
}

# 5. CloudWatch Metric Stream
resource "aws_cloudwatch_metric_stream" "newrelic" {
  name          = "${local.prefix}-metric-stream"
  role_arn      = aws_iam_role.metric_stream_to_firehose.arn
  firehose_arn  = aws_kinesis_firehose_delivery_stream.newrelic_stream.arn
  output_format = var.output_format

  dynamic "include_filter" {
    for_each = var.metric_namespaces
    content {
      namespace    = include_filter.value.namespace
      metric_names = include_filter.value.metric_names
    }
  }
}

💡ポイント4: Firehose の設定値

s3_backup_mode = "FailedDataOnly" にすることで、 正常配信されたデータはS3に保存せず、コストを削減します。

💡ポイント5: dynamic ブロックでメトリクスフィルター

dynamic "include_filter" を使うことで、変数で渡された名前空間分だけフィルターを動的に生成します。

4️⃣ モジュール: main.tf (Part 3 - NewRelic Integration)

modules/integrations/main.tf
# 6. IAM Role - NewRelicがAWSリソース情報を読み取る
resource "aws_iam_role" "newrelic_aws_role" {
  name        = "${local.prefix}-newrelic-integrations-role"
  description = "NewRelic Cloud integration role (PUSH mode only)"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        AWS = var.newrelic_account_principal_arn
      }
      Condition = {
        StringEquals = {
          "sts:ExternalId" = tostring(var.newrelic_account_id)
        }
      }
    }]
  })
}

resource "aws_iam_policy" "newrelic_aws_permissions" {
  name        = "${local.prefix}-newrelic-cloud-permissions"
  description = "Permissions for NewRelic to read AWS resource metadata"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = [
        "ec2:DescribeVpcs",
        "ec2:DescribeSubnets",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeNetworkInterfaces",
        "ec2:DescribeInternetGateways",
        "ec2:DescribeNatGateways",
        "ec2:DescribeVpcEndpoints",
        "ec2:DescribeNetworkAcls",
        "ec2:DescribeVpcAttribute",
        "ec2:DescribeRouteTables",
        "ec2:DescribeVpcPeeringConnections",
        "ec2:DescribeVpnConnections",
        "tag:GetResources",
        "cloudtrail:LookupEvents",
        "config:BatchGetResourceConfig",
        "config:ListDiscoveredResources"
      ]
      Effect   = "Allow"
      Resource = "*"
    }]
  })
}

resource "aws_iam_role_policy_attachment" "newrelic_aws_policy_attach" {
  role       = aws_iam_role.newrelic_aws_role.name
  policy_arn = aws_iam_policy.newrelic_aws_permissions.arn
}

# 7. NewRelic Cloud AWS Link Account(PUSHモードのみ)
# 同じAWSアカウント内の複数リージョンを監視する場合、1つのモジュールでのみ作成する
resource "newrelic_cloud_aws_link_account" "push" {
  count = var.create_link_account ? 1 : 0

  account_id             = var.newrelic_account_id
  arn                    = aws_iam_role.newrelic_aws_role.arn
  metric_collection_mode = "PUSH"
  name                   = "${local.prefix}-push"
}

count を使って、Link Accountの作成を制御します。複数リージョンを監視する場合でも、AWSアカウント全体に対して1つだけ作成します。

  • メインリージョン (ap-northeast-1): create_link_account = true
  • 追加リージョン (us-east-1): create_link_account = false

5️⃣ モジュール: terraform.tf

モジュールで必要なプロバイダーのバージョン制約を定義します。

modules/integrations/terraform.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
    newrelic = {
      source  = "newrelic/newrelic"
      version = "~> 3.76.0"
    }
  }
}

6️⃣ 環境設定: terraform.tf

環境ごとのTerraformバージョン、プロバイダー設定、S3バックエンド設定を定義します。

environments/dev/integration/terraform.tf

terraform {
  required_version = "~> 1.10.0"

  required_providers {
    newrelic = {
      source  = "newrelic/newrelic"
      version = "~> 3.76.0"
    }
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }

  backend "s3" {
    bucket = "myapp-dev-tfstate"
    key    = "newrelic/environments/dev/integration/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

provider "newrelic" {
  account_id = local.newrelic_account_id
  region     = "US"
  api_key    = var.newrelic_user_key
}

provider "aws" {
  region = "ap-northeast-1"
}

# us-east-1用のプロバイダー (CloudFront, WAF監視用)
provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}

7️⃣ 環境設定: locals.tf & variables.tf

environments/dev/integration/locals.tf
locals {
  service_name        = "myapp"
  env                 = "dev"
  newrelic_account_id = 1234567  # あなたのNewRelic Account ID
}
environments/dev/integration/variables.tf
variable "newrelic_user_key" {
  description = "NewRelic User Key"
  type        = string
  sensitive   = true
}

variable "newrelic_license_key" {
  description = "NewRelic Ingest License Key"
  type        = string
  sensitive   = true
}

💡ポイント7: 2種類のキーが必要

NewRelicとの連携には、以下の2種類のAPIキーが必要です。

  • User Key 🔑: Terraform Providerの認証用。NerdGraphやREST API経由での操作・管理に使用
  • Ingest License Key 📥: AWSからNewRelicへのデータ送信用。監視対象からのデータ取り込み(インジェスト)に使用

参考: NewRelic APIキー - 公式ドキュメント

8️⃣ 環境設定: main.tf

environments/dev/integration/main.tf
# ap-northeast-1 リージョンの監視
module "newrelic_aws_integration" {
  source = "../../../modules/integrations"

  service_name         = local.service_name
  env                  = local.env
  aws_region           = "ap-northeast-1"
  name_suffix          = ""
  create_link_account  = true
  newrelic_account_id  = local.newrelic_account_id
  newrelic_license_key = var.newrelic_license_key
  output_format        = "opentelemetry1.0"

  metric_namespaces = [
    # 全メトリクスを収集
    { namespace = "AWS/ApplicationELB" },
    { namespace = "AWS/RDS" },
    { namespace = "AWS/ECS" },
    { namespace = "ECS/ContainerInsights" },
    { namespace = "AWS/ElastiCache" }
  ]

  providers = {
    aws      = aws
    newrelic = newrelic
  }
}

# us-east-1 リージョンの監視(CloudFront, WAF)
module "newrelic_aws_integration_use1" {
  source = "../../../modules/integrations"

  service_name         = local.service_name
  env                  = local.env
  aws_region           = "us-east-1"
  name_suffix          = "use1"
  create_link_account  = false
  newrelic_account_id  = local.newrelic_account_id
  newrelic_license_key = var.newrelic_license_key
  output_format        = "opentelemetry1.0"

  metric_namespaces = [
    { namespace = "AWS/CloudFront" },
    { namespace = "AWS/WAFV2" }
  ]

  providers = {
    aws      = aws.us_east_1
    newrelic = newrelic
  }
}

💡ポイント8: マルチリージョン監視の設定

  1. メインリージョン 🇯🇵 (ap-northeast-1)

    • create_link_account = true: AWSアカウントとNewRelicを紐付け
    • name_suffix = "": 既存リソース名との互換性維持
    • 主要なAWSサービスを監視(ECS, RDS, ALB等)
  2. 追加リージョン 🇺🇸 (us-east-1)

    • create_link_account = false: 既存のLink Accountを使用
    • name_suffix = "use1": リソース識別用のサフィックス
    • グローバルサービスを監視(CloudFront, WAF)
    • alias を使って複数のAWSプロバイダーを定義

🎉 まとめ

NewRelicとAWSの連携をTerraformで実装することで、CloudFormationのネスト構造や公式モジュールの制約から解放され、透明性の高い運用が可能になります。

この実装が、マルチリージョン環境でのNewRelic監視構築の参考になれば幸いです🚀

えみり〜でした|ωΦ)ฅ

🔗 参考資料

Discussion