Gap Analysis

Gap analysis discovers resources in your cloud provider that exist but aren't managed by any Terraform state in Stategraph.

Overview

As infrastructure grows, resources can be created outside of Terraform:

  • Manual console changes
  • Scripts or CLI commands
  • Other IaC tools
  • Forgotten experiments

Gap analysis compares your cloud inventory against Terraform state to find these unmanaged resources.

How It Works

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│  Cloud Provider │    │   Stategraph    │    │   Gap Report    │
│    Inventory    │───▶│    Compare      │───▶│   Unmanaged     │
│                 │    │                 │    │   Resources     │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                                            │
         │                                            │
    AWS Config                                  • S3 bucket
    Resource Inventory                          • IAM role
                                               • EC2 instance
  1. Fetch inventory - Query cloud provider for all resources
  2. Compare with state - Match against resources in Terraform states
  3. Report gaps - List resources not found in any state

Supported Providers

AWS

Gap analysis uses AWS Config to discover resources:

  • Requires AWS Config aggregator
  • Supports cross-region discovery
  • Covers most AWS resource types

Setup Requirements

  1. AWS Config must be enabled in your account
  2. Aggregator should be configured for multi-region
  3. IAM permissions for Stategraph to query AWS Config

Supported Resource Types

Common types discovered:

Service Resource Types
EC2 Instances, Security Groups, VPCs, Subnets
S3 Buckets
RDS Instances, Clusters
IAM Roles, Policies, Users
Lambda Functions
DynamoDB Tables

Accessing Gap Analysis

Via UI

  1. Navigate to Gap Analysis in the Inventory menu
  2. Select your cloud provider (AWS)
  3. Click Scan or Refresh
  4. View unmanaged resources

Via CLI

First, get your tenant ID:

stategraph user tenants list

Output:

550e8400-e29b-41d4-a716-446655440000    my-org

Check configuration status:

stategraph tenant gaps config \
  --tenant 550e8400-e29b-41d4-a716-446655440000 \
  --provider aws

Response:

{
  "status": "ready",
  "ready_for_gap_analysis": true,
  "has_aggregator": true,
  "aggregator_region": "us-east-1",
  "indexed_regions": ["us-east-1", "us-west-2", "eu-west-1"],
  "index_count": 1500,
  "warnings": []
}

Run gap analysis:

stategraph tenant gaps analyze \
  --tenant 550e8400-e29b-41d4-a716-446655440000 \
  --provider aws

Response:

{
  "summary": {
    "total_aws_resources": 1500,
    "managed_by_stategraph": 1200,
    "unmanaged": 300
  },
  "unmanaged_resources": [
    {
      "arn": "arn:aws:s3:::orphaned-bucket",
      "service": "S3",
      "resource_type": "AWS::S3::Bucket",
      "region": "us-east-1",
      "owning_account_id": "123456789012"
    }
  ],
  "fetched_at": 1705312800,
  "from_cache": false
}

Understanding Results

Summary Metrics

Metric Description
total_aws_resources All resources found in AWS Config
managed_by_stategraph Resources matched to Terraform state
unmanaged Resources not in any state

Resource Details

Each unmanaged resource includes:

Field Description
arn Full AWS ARN
service AWS service name
resource_type AWS Config resource type
region AWS region
owning_account_id AWS account ID

Generating Import Blocks

Stategraph can generate Terraform import blocks for unmanaged resources:

Via UI

  1. Select unmanaged resources
  2. Click Generate Import
  3. Copy generated Terraform code

Via CLI

# Save unmanaged resources to a file
stategraph tenant gaps analyze \
  --tenant 550e8400-e29b-41d4-a716-446655440000 \
  --provider aws | jq '.unmanaged_resources' > unmanaged.json

# Generate import blocks
stategraph tenant gaps import \
  --tenant 550e8400-e29b-41d4-a716-446655440000 \
  unmanaged.json

Response:

{
  "import_blocks": "import {\n  to = aws_s3_bucket.orphaned_bucket\n  id = \"orphaned-bucket\"\n}\n",
  "provider_hcl": "provider \"aws\" {\n  region = \"us-east-1\"\n}\n",
  "generated_hcl": "resource \"aws_s3_bucket\" \"orphaned_bucket\" {\n  bucket = \"orphaned-bucket\"\n}\n",
  "supported_count": 1,
  "unsupported_count": 0,
  "unsupported_resources": []
}

Using Import Blocks

  1. Add generated code to your Terraform configuration
  2. Run terraform plan to verify
  3. Run terraform apply to bring under management
# generated import.tf
import {
  to = aws_s3_bucket.orphaned_bucket
  id = "orphaned-bucket"
}

resource "aws_s3_bucket" "orphaned_bucket" {
  bucket = "orphaned-bucket"
}
terraform plan
terraform apply

Common Gap Patterns

Legitimate Gaps

Some resources are intentionally unmanaged:

  • AWS-managed resources - Service-linked roles, default VPCs
  • Bootstrap resources - Resources needed to run Terraform itself
  • Temporary resources - Spot instances, test resources

Concerning Gaps

Gaps that often indicate problems:

  • Production resources - Should generally be managed
  • Security resources - IAM roles, security groups
  • Data resources - S3 buckets, databases
  • Network resources - VPCs, subnets

Filtering Results

By Service

Focus on specific services:

stategraph tenant gaps analyze \
  --tenant 550e8400-e29b-41d4-a716-446655440000 \
  --provider aws | jq '.unmanaged_resources | map(select(.service == "EC2"))'

By Region

stategraph tenant gaps analyze \
  --tenant 550e8400-e29b-41d4-a716-446655440000 \
  --provider aws | jq '.unmanaged_resources | map(select(.region == "us-east-1"))'

By Resource Type

stategraph tenant gaps analyze \
  --tenant 550e8400-e29b-41d4-a716-446655440000 \
  --provider aws | jq '.unmanaged_resources | map(select(.resource_type == "AWS::S3::Bucket"))'

Caching

Gap analysis results are cached to avoid rate limiting:

Parameter Value
source=cache Return cached results (default)
source=no-cache Force fresh scan
# Use cache (fast, default)
stategraph tenant gaps analyze \
  --tenant 550e8400-e29b-41d4-a716-446655440000 \
  --provider aws

# Fresh scan (slower)
stategraph tenant gaps analyze \
  --tenant 550e8400-e29b-41d4-a716-446655440000 \
  --provider aws \
  --source no-cache

The fetched_at timestamp and from_cache flag indicate data freshness.

AWS Config Setup

Enable AWS Config

resource "aws_config_configuration_recorder" "main" {
  name     = "default"
  role_arn = aws_iam_role.config.arn

  recording_group {
    all_supported = true
  }
}

resource "aws_config_delivery_channel" "main" {
  name           = "default"
  s3_bucket_name = aws_s3_bucket.config.bucket
}

resource "aws_config_configuration_recorder_status" "main" {
  name       = aws_config_configuration_recorder.main.name
  is_enabled = true
}

Create Aggregator

resource "aws_config_configuration_aggregator" "organization" {
  name = "organization"

  organization_aggregation_source {
    all_regions = true
    role_arn    = aws_iam_role.aggregator.arn
  }
}

Required IAM Permissions

Stategraph needs permission to query AWS Config:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "config:SelectAggregateResourceConfig",
        "config:DescribeConfigurationAggregators"
      ],
      "Resource": "*"
    }
  ]
}

Best Practices

  1. Regular scans - Run gap analysis periodically
  2. Triage results - Classify gaps as intentional or concerning
  3. Track over time - Monitor gap trends
  4. Import systematically - Bring unmanaged resources under control
  5. Document exceptions - Record why some resources remain unmanaged

Limitations

  • Only AWS is currently supported
  • Requires AWS Config (additional cost)
  • Some resource types may not be discoverable
  • Cross-account discovery requires organization aggregator

Next Steps