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:
- Download the example configuration
- Configure the Grafana Cloud and AWS Terraform providers
- Create the Data Firehose delivery stream with backup S3 bucket and required IAM resources in AWS
- Create the CloudWatch metric stream and required IAM resources in AWS
- Configure an AWS resource metadata scrape job in Grafana Cloud
Note
Before starting, ensure you’ve generated an access policy token with
metric:writepermissions.
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:readintegration-management:writestacks:readThis token is in addition to the token withmetric:writepermission 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:
Follow the Create an access policy for a stack instructions.
In step 6, add the following scopes:
integration-management:readintegration-management:writestacks:read
After creating the policy, click Add token and generate a token.
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:
Run the following command with your access policy token:
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"}]'Find your stack in the output and note the
cloudProviderAPIURLas in the following example:[ { "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:
Download CloudWatch metric stream Terraform snippet
The example file includes:
- Provider configurations for AWS and Grafana
- All required IAM roles and policies
- Data Firehose delivery stream configuration
- CloudWatch metric stream configuration
- AWS Resource metadata scrape job configuration
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.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
grafana = {
source = "grafana/grafana"
version = ">= 3.24.1"
}
}
} 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
} provider "grafana" { cloud_provider_access_token = var.cloud_provider_token cloud_access_policy_token = var.cloud_provider_token cloud_provider_url = var.cloud_provider_url
} data "grafana_cloud_stack" "main" {
slug = var.grafana_cloud_stack_slug // FILLME: Your stack slug, e.g., "mycompany"
} data "aws_region" "current" {}
// 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:
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.
resource "aws_s3_bucket" "fallback" {
bucket = var.fallback_bucket_name
} 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
} data "aws_iam_policy_document" "firehose_assume_role" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = ["firehose.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
}
} 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 = [ {
Effect = "Allow"
Resource = ["*"]
Action = ["logs:PutLogEvents"]
}, {
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}/*",
]
},
]
})
}
// 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.
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
} data "aws_iam_policy_document" "metric_stream_assume_role" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = ["streams.metrics.cloudwatch.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
}
} 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 = [ {
Action = [
"firehose:PutRecord",
"firehose:PutRecordBatch"
]
Effect = "Allow"
Resource = [aws_kinesis_firehose_delivery_stream.stream.arn]
},
]
})
}
// 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.
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
} 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"] condition {
test = "StringEquals"
variable = "sts:ExternalId"
values = [data.grafana_cloud_stack.main.prometheus_user_id]
}
}
} 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
} 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" 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
}
]
})
} 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"
}
// 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.
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" access_key = format(
"%s:%s",
data.grafana_cloud_stack.main.prometheus_user_id,
var.metrics_write_token
) buffering_size = 1 buffering_interval = 60 role_arn = aws_iam_role.firehose.arn s3_backup_mode = "FailedDataOnly"
request_configuration { content_encoding = "GZIP"
} 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"
} 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
}
}
}
}
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:
http_endpoint_configuration { request_configuration {
content_encoding = "GZIP" common_attributes {
name = "lbl_environment"
value = "production"
} common_attributes {
name = "lbl_team"
value = "platform"
}
}
}
}
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:
- Static Label keys must be prefixed with
lbl_ - Static Label keys and values must follow the Prometheus data model specification
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:
http_endpoint_configuration { request_configuration {
content_encoding = "GZIP" common_attributes {
tag_selection = "tag_Name,tag_Environment,tag_Owner"
}
}
}
}
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:
http_endpoint_configuration { request_configuration {
content_encoding = "GZIP" common_attributes {
include_average_statistic = false
}
}
}
}
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.
role_arn = aws_iam_role.metric_stream_role.arn firehose_arn = aws_kinesis_firehose_delivery_stream.stream.arn output_format = "opentelemetry1.0" dynamic "include_filter" {
for_each = var.include_namespaces
content {
namespace = include_filter.value
}
}
}
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 instancesAWS/RDS- RDS databasesAWS/Lambda- Lambda functionsAWS/ELB- Elastic Load BalancersAWS/S3- S3 bucketsAWS/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.
resource "grafana_cloud_provider_aws_account" "main" {
depends_on = [time_sleep.wait_iam_propagation] stack_id = data.grafana_cloud_stack.main.id role_arn = aws_iam_role.grafana_cloud_aws_resource_metadata.arn regions = [data.aws_region.current.name]
} resource "grafana_cloud_provider_aws_resource_metadata_scrape_job" "main" {
stack_id = data.grafana_cloud_stack.main.id
name = "aws-resource-metadata-scraper" aws_account_resource_id = grafana_cloud_provider_aws_account.main.resource_id dynamic "service" {
for_each = var.include_namespaces
content {
name = service.value }
}
}
// 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.
Initialize Terraform:
terraform initReview the planned changes:
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"]'Apply the configuration:
terraform applyConfirm by typing
yeswhen prompted.
Tip
Instead of passing variables on the command line, create a
terraform.tfvarsfile or use environment variables prefixed withTF_VAR_.
Verify your configuration
After applying the Terraform configuration:
Check resource creation:
terraform outputVerify in AWS Console:
- CloudWatch metric stream status: Running
- Data Firehose delivery stream status: Active
- Check Data Firehose metrics for successful deliveries
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
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_tokenis 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_idis correctly used as the External ID - Run
terraform applyagain 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
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


