Grafana Cloud

Configure CloudWatch metric streams with Terraform

This guide walks you through configuring CloudWatch metric streams using Terraform with detailed explanations of each component and includes the following tasks:

  1. Download the example configuration
  2. Configure the Grafana Cloud and AWS Terraform providers
  3. Create the Data Firehose delivery stream with backup S3 bucket and required IAM resources in AWS
  4. Create the CloudWatch metric stream and required IAM resources in AWS
  5. Configure an AWS resource metadata scrape job in Grafana Cloud

Note

Before starting, ensure you’ve generated an access policy token with metric:write permissions.

Before you begin

In addition to the general prerequisites, you need:

  • Terraform: Version 1.0 or later installed

  • AWS credentials: Configured for Terraform (with AWS CLI, environment variables, or IAM role)

    For more information on authentication options, see the AWS Terraform Provider documentation.

  • Grafana Cloud access policy token: With the following scopes:

    • integration-management:read
    • integration-management:write
    • stacks:read This token is in addition to the token with metric:write permission and allows Terraform to manage AWS integration resources in Grafana Cloud..

Create a Grafana Cloud access policy token

This token is separate from the metrics:write token. It allows Terraform to manage AWS integration resources in Grafana Cloud.

To create an access policy:

  1. Follow the Create an access policy for a stack instructions.

  2. In step 6, add the following scopes:

    • integration-management:read
    • integration-management:write
    • stacks:read
  3. After creating the policy, click Add token and generate a token.

  4. Store this token securely—you’ll use it to configure the Grafana Terraform provider.

Obtain the Cloud Provider API endpoint

The Grafana Terraform provider needs your stack’s Cloud Provider API endpoint.

To find your endpoint:

  1. Run the following command with your access policy token:

    shell
    curl -sH "Authorization: Bearer <ACCESS_TOKEN>" "https://grafana.com/api/instances" | \
    jq '[.items[]|{stackName: .slug, clusterName:.clusterSlug, cloudProviderAPIURL: "https://cloud-provider-api-\(.clusterSlug).grafana.net"}]'
  2. Find your stack in the output and note the cloudProviderAPIURL as in the following example:

    JSON
    [
      {
        "stackName": "my-stack",
        "clusterName": "prod-us-central-0",
        "cloudProviderAPIURL": "https://cloud-provider-api-prod-us-central-0.grafana.net"
      }
    ]

Download the example configuration

Download the example Terraform configuration as a starting point:

Refer to the Terraform documentation for more details on each of the following providers:

The following instructions explain the different parts of the example file.

Configure the providers

Configure the AWS and Grafana Terraform providers with your credentials and endpoints, as in the following example.

hcl
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    grafana = {
      source  = "grafana/grafana"
      version = ">= 3.24.1"
    }
  }
} 
Include both AWS and Grafana Terraform providers
provider "aws" {
  region  = var.aws_region  // FILLME: Your AWS region, e.g., "us-east-1"
  profile = var.aws_profile // FILLME: Your AWS CLI profile name
} 
Configure the AWS provider with your region and credentials
provider "grafana" { 
Configure Grafana provider to manage Cloud Provider resources
  cloud_provider_access_token = var.cloud_provider_token 
Token for accessing Grafana Cloud stack information
  cloud_access_policy_token   = var.cloud_provider_token 
Token for managing AWS integration resources
  cloud_provider_url          = var.cloud_provider_url
} 
Your stack’s Cloud Provider API endpoint
data "grafana_cloud_stack" "main" {
  slug = var.grafana_cloud_stack_slug // FILLME: Your stack slug, e.g., "mycompany"
} 
Fetch information about your Grafana Cloud stack
data "aws_region" "current" {}
 
Get current AWS region information
hcl
// Include both AWS and Grafana Terraform providers
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    grafana = {
      source  = "grafana/grafana"
      version = ">= 3.24.1"
    }
  }
}

// Configure the AWS provider with your region and credentials
provider "aws" {
  region  = var.aws_region  // FILLME: Your AWS region, e.g., "us-east-1"
  profile = var.aws_profile // FILLME: Your AWS CLI profile name
}

// Configure Grafana provider to manage Cloud Provider resources
provider "grafana" {
  // Token for accessing Grafana Cloud stack information
  cloud_provider_access_token = var.cloud_provider_token
  // Token for managing AWS integration resources
  cloud_access_policy_token   = var.cloud_provider_token
  // Your stack's Cloud Provider API endpoint
  cloud_provider_url          = var.cloud_provider_url
}

// Fetch information about your Grafana Cloud stack
data "grafana_cloud_stack" "main" {
  slug = var.grafana_cloud_stack_slug // FILLME: Your stack slug, e.g., "mycompany"
}

// Get current AWS region information
data "aws_region" "current" {}

Define required variables

Create a variables.tf file or use the following Terraform variables:

hcl
variable "aws_region" {
  description = "AWS region to deploy resources"
  type        = string
}

variable "aws_profile" {
  description = "AWS CLI profile to use"
  type        = string
}

variable "grafana_cloud_stack_slug" {
  description = "Grafana Cloud stack slug"
  type        = string
}

variable "cloud_provider_token" {
  description = "Grafana Cloud access policy token"
  type        = string
  sensitive   = true
}

variable "cloud_provider_url" {
  description = "Cloud Provider API endpoint URL"
  type        = string
}

variable "metrics_write_token" {
  description = "Access policy token with metric:write permissions"
  type        = string
  sensitive   = true
}

variable "metric_stream_name" {
  description = "Name for the CloudWatch metric stream"
  type        = string
  default     = "grafana-cloud-metric-stream"
}

variable "fallback_bucket_name" {
  description = "S3 bucket name for failed metric deliveries"
  type        = string
}

variable "include_namespaces" {
  description = "List of AWS namespaces to include (empty list includes all)"
  type        = list(string)
  default     = []
}

Create IAM roles for authentication

Create three IAM roles that enable communication between AWS services and Grafana Cloud.

Create the Data Firehose IAM role

This role allows the Data Firehose delivery stream to write error logs and back up failed batches to S3.

hcl
resource "aws_s3_bucket" "fallback" {
  bucket = var.fallback_bucket_name
} 
S3 bucket for storing metric batches that failed to deliver
resource "aws_iam_role" "firehose" {
  name               = format("Firehose-%s", var.metric_stream_name)
  assume_role_policy = data.aws_iam_policy_document.firehose_assume_role.json
} 
IAM role that the Data Firehose stream assumes
data "aws_iam_policy_document" "firehose_assume_role" {
  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["firehose.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
} 
Trust policy allowing AWS Data Firehose service to assume this role
resource "aws_iam_role_policy" "firehose" {
  name = format("Firehose-%s", var.metric_stream_name)
  role = aws_iam_role.firehose.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [ 
Permissions policy for Data Firehose operations
      {
        Effect   = "Allow"
        Resource = ["*"]
        Action   = ["logs:PutLogEvents"]
      }, 
Allow Data Firehose to write error logs to CloudWatch Logs
      {
        Sid    = "s3Permissions"
        Effect = "Allow"
        Action = [
          "s3:AbortMultipartUpload",
          "s3:GetBucketLocation",
          "s3:GetObject",
          "s3:ListBucket",
          "s3:ListBucketMultipartUploads",
          "s3:PutObject",
        ]
        Resource = [
          aws_s3_bucket.fallback.arn,
          "${aws_s3_bucket.fallback.arn}/*",
        ]
      },
    ]
  })
}
 
Allow Data Firehose to back up failed batches to S3
hcl
// S3 bucket for storing metric batches that failed to deliver
resource "aws_s3_bucket" "fallback" {
  bucket = var.fallback_bucket_name
}

// IAM role that the Data Firehose stream assumes
resource "aws_iam_role" "firehose" {
  name               = format("Firehose-%s", var.metric_stream_name)
  assume_role_policy = data.aws_iam_policy_document.firehose_assume_role.json
}

// Trust policy allowing AWS Data Firehose service to assume this role
data "aws_iam_policy_document" "firehose_assume_role" {
  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["firehose.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}

// Permissions policy for Data Firehose operations
resource "aws_iam_role_policy" "firehose" {
  name = format("Firehose-%s", var.metric_stream_name)
  role = aws_iam_role.firehose.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      // Allow Data Firehose to write error logs to CloudWatch Logs
      {
        Effect   = "Allow"
        Resource = ["*"]
        Action   = ["logs:PutLogEvents"]
      },
      // Allow Data Firehose to back up failed batches to S3
      {
        Sid    = "s3Permissions"
        Effect = "Allow"
        Action = [
          "s3:AbortMultipartUpload",
          "s3:GetBucketLocation",
          "s3:GetObject",
          "s3:ListBucket",
          "s3:ListBucketMultipartUploads",
          "s3:PutObject",
        ]
        Resource = [
          aws_s3_bucket.fallback.arn,
          "${aws_s3_bucket.fallback.arn}/*",
        ]
      },
    ]
  })
}

Create the Metric stream IAM role

This role allows the CloudWatch metric stream to push metrics to the Data Firehose delivery stream.

hcl
resource "aws_iam_role" "metric_stream_role" {
  name               = format("metric-stream-role-%s", var.metric_stream_name)
  assume_role_policy = data.aws_iam_policy_document.metric_stream_assume_role.json
} 
IAM role for CloudWatch metric stream
data "aws_iam_policy_document" "metric_stream_assume_role" {
  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["streams.metrics.cloudwatch.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
} 
Trust policy allowing CloudWatch metric stream service to assume this role
resource "aws_iam_role_policy" "metric_stream_role" {
  name = "AWSCloudWatchMetricStreamPolicy"
  role = aws_iam_role.metric_stream_role.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [ 
Policy allowing metric stream to write to Data Firehose
      {
        Action = [
          "firehose:PutRecord",
          "firehose:PutRecordBatch"
        ]
        Effect   = "Allow"
        Resource = [aws_kinesis_firehose_delivery_stream.stream.arn]
      },
    ]
  })
}
 
Grant permission to send metric data to Data Firehose
hcl
// IAM role for CloudWatch metric stream
resource "aws_iam_role" "metric_stream_role" {
  name               = format("metric-stream-role-%s", var.metric_stream_name)
  assume_role_policy = data.aws_iam_policy_document.metric_stream_assume_role.json
}

// Trust policy allowing CloudWatch metric stream service to assume this role
data "aws_iam_policy_document" "metric_stream_assume_role" {
  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["streams.metrics.cloudwatch.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}

// Policy allowing metric stream to write to Data Firehose
resource "aws_iam_role_policy" "metric_stream_role" {
  name = "AWSCloudWatchMetricStreamPolicy"
  role = aws_iam_role.metric_stream_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      // Grant permission to send metric data to Data Firehose
      {
        Action = [
          "firehose:PutRecord",
          "firehose:PutRecordBatch"
        ]
        Effect   = "Allow"
        Resource = [aws_kinesis_firehose_delivery_stream.stream.arn]
      },
    ]
  })
}

Create the Grafana resource metadata IAM role

This role allows Grafana Cloud to scrape AWS resource metadata and tags.

hcl
variable "grafana_cloud_sts_aws_account_id" {
  description = "Grafana Cloud AWS account ID for assuming roles"
  type        = string
  default     = "008923505332" // Grafana Cloud's AWS account
} 
Variable for Grafana Cloud’s AWS account ID (provided by Grafana)
data "aws_iam_policy_document" "trust_grafana" {
  statement {
    effect = "Allow"
    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::${var.grafana_cloud_sts_aws_account_id}:root"]
    }
    actions = ["sts:AssumeRole"] 
Trust policy allowing Grafana Cloud to assume this role
    condition {
      test     = "StringEquals"
      variable = "sts:ExternalId"
      values   = [data.grafana_cloud_stack.main.prometheus_user_id]
    }
  }
} 
External ID condition prevents confused deputy attacks
resource "aws_iam_role" "grafana_cloud_aws_resource_metadata" {
  name               = "GrafanaAWSResourceMetadataScrapeJobAccess"
  description        = "Role used by Grafana CloudWatch integration"
  assume_role_policy = data.aws_iam_policy_document.trust_grafana.json
} 
IAM role for Grafana Cloud to scrape resource metadata
resource "aws_iam_role_policy" "grafana_cloud_aws_resource_metadata" {
  name = "GrafanaAWSResourceMetadataScrapeJobAccess"
  role = aws_iam_role.grafana_cloud_aws_resource_metadata.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow" 
Policy granting read-only access to AWS resource information
        Action = [
          "tag:GetResources",              // Get resources by tags
          "apigateway:GET",                // API Gateway information
          "aps:ListWorkspaces",            // Amazon Managed Prometheus
          "autoscaling:DescribeAutoScalingGroups",
          "dms:DescribeReplicationInstances",
          "dms:DescribeReplicationTasks",
          "ec2:DescribeTransitGatewayAttachments",
          "ec2:DescribeSpotFleetRequests",
          "shield:ListProtections",
          "storagegateway:ListGateways",
          "storagegateway:ListTagsForResource"
        ]
        Resource = "*" // These are read-only operations requiring wildcard
      }
    ]
  })
} 
Permissions to discover resources via tags and API calls
resource "time_sleep" "wait_iam_propagation" {
  depends_on = [
    aws_iam_role.grafana_cloud_aws_resource_metadata,
    aws_iam_role_policy.grafana_cloud_aws_resource_metadata
  ]
  create_duration = "10s"
}
 
Wait for IAM changes to propagate globally
hcl
// Variable for Grafana Cloud's AWS account ID (provided by Grafana)
variable "grafana_cloud_sts_aws_account_id" {
  description = "Grafana Cloud AWS account ID for assuming roles"
  type        = string
  default     = "008923505332" // Grafana Cloud's AWS account
}

// Trust policy allowing Grafana Cloud to assume this role
data "aws_iam_policy_document" "trust_grafana" {
  statement {
    effect = "Allow"
    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::${var.grafana_cloud_sts_aws_account_id}:root"]
    }
    actions = ["sts:AssumeRole"]
    // External ID condition prevents confused deputy attacks
    condition {
      test     = "StringEquals"
      variable = "sts:ExternalId"
      values   = [data.grafana_cloud_stack.main.prometheus_user_id]
    }
  }
}

// IAM role for Grafana Cloud to scrape resource metadata
resource "aws_iam_role" "grafana_cloud_aws_resource_metadata" {
  name               = "GrafanaAWSResourceMetadataScrapeJobAccess"
  description        = "Role used by Grafana CloudWatch integration"
  assume_role_policy = data.aws_iam_policy_document.trust_grafana.json
}

// Policy granting read-only access to AWS resource information
resource "aws_iam_role_policy" "grafana_cloud_aws_resource_metadata" {
  name = "GrafanaAWSResourceMetadataScrapeJobAccess"
  role = aws_iam_role.grafana_cloud_aws_resource_metadata.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        // Permissions to discover resources via tags and API calls
        Action = [
          "tag:GetResources",              // Get resources by tags
          "apigateway:GET",                // API Gateway information
          "aps:ListWorkspaces",            // Amazon Managed Prometheus
          "autoscaling:DescribeAutoScalingGroups",
          "dms:DescribeReplicationInstances",
          "dms:DescribeReplicationTasks",
          "ec2:DescribeTransitGatewayAttachments",
          "ec2:DescribeSpotFleetRequests",
          "shield:ListProtections",
          "storagegateway:ListGateways",
          "storagegateway:ListTagsForResource"
        ]
        Resource = "*" // These are read-only operations requiring wildcard
      }
    ]
  })
}

// Wait for IAM changes to propagate globally
resource "time_sleep" "wait_iam_propagation" {
  depends_on = [
    aws_iam_role.grafana_cloud_aws_resource_metadata,
    aws_iam_role_policy.grafana_cloud_aws_resource_metadata
  ]
  create_duration = "10s"
}

Create the Data Firehose delivery stream

The Data Firehose delivery stream batches and delivers metrics to your Grafana Cloud endpoint.

hcl
  target_endpoint = var.target_endpoint != "" ? var.target_endpoint : format(
    "%s/aws-metrics/api/v1/push",
    replace(
      replace(
        data.grafana_cloud_stack.main.prometheus_url,
        "prometheus",
        "aws-metric-streams"
      ),
      "-${data.grafana_cloud_stack.main.cluster_slug}",
      ""
    )
  )
}
resource "aws_kinesis_firehose_delivery_stream" "stream" {
  name        = format("%s-firehose", var.metric_stream_name)
  destination = "http_endpoint"
  http_endpoint_configuration { 
    url  = local.target_endpoint
    name = "Grafana AWS Metric Stream Destination" 
Grafana Cloud metrics ingestion endpoint
    access_key = format(
      "%s:%s",
      data.grafana_cloud_stack.main.prometheus_user_id,
      var.metrics_write_token
    ) 
Authentication: Prometheus user ID and metrics write token
    buffering_size = 1 
Buffer size in MB before delivery (1 MB minimum)
    buffering_interval = 60 
Buffer time in seconds before delivery (60 seconds for low latency)
    role_arn = aws_iam_role.firehose.arn 
IAM role for Firehose to access S3 and CloudWatch Logs
    s3_backup_mode = "FailedDataOnly"
    request_configuration { 
Only back up failed deliveries to S3
      content_encoding = "GZIP"
    } 
Compress data to reduce bandwidth and costs
    s3_configuration {
      role_arn           = aws_iam_role.firehose.arn
      bucket_arn         = aws_s3_bucket.fallback.arn
      buffering_size     = 5    // 5 MB buffer for S3
      buffering_interval = 300  // 5 minutes buffer for S3
      compression_format = "GZIP"
    } 
S3 bucket configuration for failed batches
    dynamic "cloudwatch_logging_options" {
      for_each = var.log_delivery_errors ? [1] : []
      content {
        enabled         = true
        log_group_name  = var.errors_log_group_name
        log_stream_name = var.errors_log_stream_name
      }
    }
  }
}
 
Optional: Enable CloudWatch Logs for delivery errors
hcl
locals {
  // Build the Grafana Cloud metrics ingestion endpoint from stack URL
  // Converts: https://prometheus-prod-03-prod-us-central-0.grafana.net
  // To: https://aws-metric-streams-prod-03.grafana.net/aws-metrics/api/v1/push
  target_endpoint = var.target_endpoint != "" ? var.target_endpoint : format(
    "%s/aws-metrics/api/v1/push",
    replace(
      replace(
        data.grafana_cloud_stack.main.prometheus_url,
        "prometheus",
        "aws-metric-streams"
      ),
      "-${data.grafana_cloud_stack.main.cluster_slug}",
      ""
    )
  )
}

resource "aws_kinesis_firehose_delivery_stream" "stream" {
  name        = format("%s-firehose", var.metric_stream_name)
  destination = "http_endpoint"

  http_endpoint_configuration {
    // Grafana Cloud metrics ingestion endpoint
    url  = local.target_endpoint
    name = "Grafana AWS Metric Stream Destination"
    
    // Authentication: Prometheus user ID and metrics write token
    access_key = format(
      "%s:%s",
      data.grafana_cloud_stack.main.prometheus_user_id,
      var.metrics_write_token
    )
    
    // Buffer size in MB before delivery (1 MB minimum)
    buffering_size = 1
    // Buffer time in seconds before delivery (60 seconds for low latency)
    buffering_interval = 60
    
    // IAM role for Firehose to access S3 and CloudWatch Logs
    role_arn = aws_iam_role.firehose.arn
    
    // Only back up failed deliveries to S3
    s3_backup_mode = "FailedDataOnly"

    request_configuration {
      // Compress data to reduce bandwidth and costs
      content_encoding = "GZIP"
    }

    // S3 bucket configuration for failed batches
    s3_configuration {
      role_arn           = aws_iam_role.firehose.arn
      bucket_arn         = aws_s3_bucket.fallback.arn
      buffering_size     = 5    // 5 MB buffer for S3
      buffering_interval = 300  // 5 minutes buffer for S3
      compression_format = "GZIP"
    }

    // Optional: Enable CloudWatch Logs for delivery errors
    dynamic "cloudwatch_logging_options" {
      for_each = var.log_delivery_errors ? [1] : []
      content {
        enabled         = true
        log_group_name  = var.errors_log_group_name
        log_stream_name = var.errors_log_stream_name
      }
    }
  }
}

Configure optional settings

After the main components are created, you can configure optional settings to customize how metrics are sent to Grafana Cloud.

Add static labels

To add static labels to all metrics from this stream, add a common_attributes block, as in the following example:

hcl
    http_endpoint_configuration { 
… other configuration from the delivery stream configured earlier in the section …
  request_configuration {
    content_encoding = "GZIP" 
… other configuration from the delivery stream configured earlier in the section …
    common_attributes {
      name  = "lbl_environment"
      value = "production"
    } 
Static label for environment (appears as “environment” in Grafana)
    common_attributes {
      name  = "lbl_team"
      value = "platform"
    }
  }
 }
}
 
Static label for team (appears as “team” in Grafana)
hcl
resource "aws_kinesis_firehose_delivery_stream" "stream" {
    // ... other configuration from the delivery stream configured earlier in the section ...
    http_endpoint_configuration {
      // ... other configuration from the delivery stream configured earlier in the section ...
  
  request_configuration {
    content_encoding = "GZIP"
    
    // Static label for environment (appears as "environment" in Grafana)
    common_attributes {
      name  = "lbl_environment"
      value = "production"
    }
    
    // Static label for team (appears as "team" in Grafana)
    common_attributes {
      name  = "lbl_team"
      value = "platform"
    }
  }
 }
}

Requirements:

Example:

  • Key: lbl_environment
  • Value: production

When querying in Grafana, do not include the lbl_ prefix, as in the following example query:

{job=~"cloud/aws/.+", environment="production"}

Set tag selection

Tag selection controls which AWS resource tags are attached to info metrics from the resource metadata scrape job. This control helps lower cardinality by excluding dynamic tags that change frequently.

Note

Tag selection only applies if you create a resource metadata scrape job. If you don’t create the scrape job, this setting has no effect.

To set tag selection for this stream, add a common_attributes block, as in the following example:

hcl
    http_endpoint_configuration { 
… other configuration from the delivery stream configured earlier in the section …
  request_configuration {
    content_encoding = "GZIP" 
… other configuration from the delivery stream configured earlier in the section …
    common_attributes {
      tag_selection  = "tag_Name,tag_Environment,tag_Owner"
    }
  }
 }
}
 
Example tag selection entry adding three tags: Name, Environment, and Owner
hcl
resource "aws_kinesis_firehose_delivery_stream" "stream" {
    // ... other configuration from the delivery stream configured earlier in the section ...
    http_endpoint_configuration {
      // ... other configuration from the delivery stream configured earlier in the section ...
  
  request_configuration {
    content_encoding = "GZIP"
    
    // Example tag selection entry adding three tags: Name, Environment, and Owner
    common_attributes {
      tag_selection  = "tag_Name,tag_Environment,tag_Owner"
    }
  }
 }
}

Tag format requirements:

  • Tags must begin with the prefix tag_
  • Example: tag_Name,tag_Environment,tag_Owner

Special values:

  • Empty string "": No tags are attached
  • Parameter not set: All tags are attached (default behavior)

Disable the optional _average statistic

By default, the integration generates an _average statistic series from the CloudWatch Sum and SampleCount statistics to maintain compatibility with CloudWatch metric scrape jobs and dashboards.

For example, from aws_ec2_disk_read_bytes_sum and aws_ec2_disk_read_bytes_sample_count, the integration calculates aws_ec2_disk_read_bytes_average.

To disable this optional calculation for this stream, add a common_attributes block, as in the following example:

hcl
    http_endpoint_configuration { 
… other configuration from the delivery stream configured earlier in the section …
  request_configuration {
    content_encoding = "GZIP" 
… other configuration from the delivery stream configured earlier in the section …
    common_attributes {
      include_average_statistic  = false
    }
  }
 }
}
 
Include average statistic set to false
hcl
resource "aws_kinesis_firehose_delivery_stream" "stream" {
    // ... other configuration from the delivery stream configured earlier in the section ...
    http_endpoint_configuration {
      // ... other configuration from the delivery stream configured earlier in the section ...
  
  request_configuration {
    content_encoding = "GZIP"
    
    // Include average statistic set to false
    common_attributes {
      include_average_statistic  = false
    }
  }
 }
}

Create the CloudWatch metric stream

The metric stream captures metrics from CloudWatch and forwards them to Data Firehose.

hcl
  role_arn = aws_iam_role.metric_stream_role.arn 
IAM role allowing metric stream to write to Data Firehose
  firehose_arn = aws_kinesis_firehose_delivery_stream.stream.arn 
Target Data Firehose delivery stream
  output_format = "opentelemetry1.0" 
Use OpenTelemetry format (required for Grafana Cloud)
  dynamic "include_filter" {
    for_each = var.include_namespaces
    content {
      namespace = include_filter.value
    }
  }
}
 
Optional: Include only specific AWS namespaces If omitted or empty, all namespaces are included
hcl
resource "aws_cloudwatch_metric_stream" "metric_stream" {
  name = var.metric_stream_name
  
  // IAM role allowing metric stream to write to Data Firehose
  role_arn = aws_iam_role.metric_stream_role.arn
  
  // Target Data Firehose delivery stream
  firehose_arn = aws_kinesis_firehose_delivery_stream.stream.arn
  
  // Use OpenTelemetry format (required for Grafana Cloud)
  output_format = "opentelemetry1.0"

  // Optional: Include only specific AWS namespaces
  // If omitted or empty, all namespaces are included
  dynamic "include_filter" {
    for_each = var.include_namespaces
    content {
      namespace = include_filter.value
    }
  }
}

Common AWS namespaces to include:

  • AWS/EC2 - EC2 instances
  • AWS/RDS - RDS databases
  • AWS/Lambda - Lambda functions
  • AWS/ELB - Elastic Load Balancers
  • AWS/S3 - S3 buckets
  • AWS/DynamoDB - DynamoDB tables

See the full list in AWS services that publish CloudWatch metrics.

Configure resource metadata scrape job

Create a resource metadata scrape job to enrich metrics with ARNs and resource tags.

hcl
resource "grafana_cloud_provider_aws_account" "main" {
  depends_on = [time_sleep.wait_iam_propagation] 
Register your AWS account with Grafana Cloud
  stack_id = data.grafana_cloud_stack.main.id 
Your Grafana Cloud stack ID
  role_arn = aws_iam_role.grafana_cloud_aws_resource_metadata.arn 
IAM role ARN that Grafana Cloud assumes
  regions = [data.aws_region.current.name]
} 
AWS regions to scrape (should match your metric stream regions)
resource "grafana_cloud_provider_aws_resource_metadata_scrape_job" "main" {
  stack_id = data.grafana_cloud_stack.main.id
  name     = "aws-resource-metadata-scraper" 
Create resource metadata scrape job
  aws_account_resource_id = grafana_cloud_provider_aws_account.main.resource_id 
Reference the AWS account created above
  dynamic "service" {
    for_each = var.include_namespaces
    content {
      name = service.value 
Scrape metadata for the same services as your metric stream
    }
  }
}
 
Optional: Customize scrape settings per service scrape_interval_seconds = 300 tag_filters = { “Environment” = “production” }
hcl
// Register your AWS account with Grafana Cloud
resource "grafana_cloud_provider_aws_account" "main" {
  depends_on = [time_sleep.wait_iam_propagation]
  
  // Your Grafana Cloud stack ID
  stack_id = data.grafana_cloud_stack.main.id
  
  // IAM role ARN that Grafana Cloud assumes
  role_arn = aws_iam_role.grafana_cloud_aws_resource_metadata.arn
  
  // AWS regions to scrape (should match your metric stream regions)
  regions = [data.aws_region.current.name]
}

// Create resource metadata scrape job
resource "grafana_cloud_provider_aws_resource_metadata_scrape_job" "main" {
  stack_id = data.grafana_cloud_stack.main.id
  name     = "aws-resource-metadata-scraper"
  
  // Reference the AWS account created above
  aws_account_resource_id = grafana_cloud_provider_aws_account.main.resource_id

  // Scrape metadata for the same services as your metric stream
  dynamic "service" {
    for_each = var.include_namespaces
    content {
      name = service.value
      
      // Optional: Customize scrape settings per service
      // scrape_interval_seconds = 300
      // tag_filters = {
      //   "Environment" = "production"
      // }
    }
  }
}

Apply the configuration

Apply the Terraform configuration to create all resources.

  1. Initialize Terraform:

    terraform init

  2. Review the planned changes:

    hcl
    terraform plan \
      -var="aws_region=us-east-1" \
      -var="aws_profile=default" \
      -var="grafana_cloud_stack_slug=your-stack" \
      -var="cloud_provider_token=glsa_..." \
      -var="cloud_provider_url=https://cloud-provider-api-prod-us-central-0.grafana.net" \
      -var="metrics_write_token=glc_..." \
      -var="fallback_bucket_name=grafana-cloudwatch-fallback-unique-name" \
      -var='include_namespaces=["AWS/EC2","AWS/RDS"]'
    1. Apply the configuration:

      terraform apply

    2. Confirm by typing yes when prompted.

Tip

Instead of passing variables on the command line, create a terraform.tfvars file or use environment variables prefixed with TF_VAR_.

Verify your configuration

After applying the Terraform configuration:

  1. Check resource creation: terraform output

  2. Verify in AWS Console:

    • CloudWatch metric stream status: Running
    • Data Firehose delivery stream status: Active
    • Check Data Firehose metrics for successful deliveries
  3. Verify metrics in Grafana Cloud:

    • Open Observability > Cloud Provider > AWS in your Grafana Cloud stack
    • Go to Services
    • You should see your AWS services listed with a Status of Sending data
  4. Check for errors:

    • S3 fallback bucket should be empty
    • CloudWatch Logs (if enabled) should show no errors

Troubleshooting

Terraform apply fails with IAM errors

Problem: Terraform cannot create IAM roles or policies.

Solution:

  • Verify your AWS credentials have sufficient permissions
  • Ensure your AWS user/role has iam:CreateRole, iam:PutRolePolicy, and related permissions
  • Check for IAM role name conflicts if re-creating resources

Grafana provider authentication fails

Problem: Terraform cannot authenticate with Grafana Cloud API.

Solution:

  • Verify the access policy token has the required scopes
  • Check that the Cloud Provider API URL matches your stack’s region
  • Ensure the token hasn’t expired

Resources created but no metrics in Grafana

Problem: Terraform applies successfully but no metrics appear.

Solution:

  • Check Data Firehose delivery stream metrics in Amazon CloudWatch
  • Verify the metrics_write_token is correct
  • Check S3 fallback bucket for failed deliveries
  • Review Data Firehose error logs (if CloudWatch Logs are enabled)

External ID mismatch error

Problem: Grafana cannot assume the resource metadata IAM role.

Solution:

  • Ensure data.grafana_cloud_stack.main.prometheus_user_id is correctly used as the External ID
  • Run terraform apply again to update the trust policy
  • Verify no manual changes were made to the IAM role in AWS Console

Multi-region deployment

To deploy metric streams in multiple regions, use Terraform workspaces or modules:

Use modules

hcl
module "metric_stream_us_east_1" {
  source = "./modules/metric-stream"
  
  aws_region         = "us-east-1"
  metric_stream_name = "grafana-cloud-us-east-1"
  // ... other variables
}

module "metric_stream_eu_west_1" {
  source = "./modules/metric-stream"
  
  aws_region         = "us-west-2"
  metric_stream_name = "grafana-cloud-eu-west-1"
  // ... other variables
}### Using provider aliases

provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}

provider "aws" {
  alias  = "eu_west_1"
  region = "eu-west-1"
}

// Duplicate resources with different provider aliases