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
- Fetch inventory - Query cloud provider for all resources
- Compare with state - Match against resources in Terraform states
- 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
- AWS Config must be enabled in your account
- Aggregator should be configured for multi-region
- 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
- Navigate to Gap Analysis in the Inventory menu
- Select your cloud provider (AWS)
- Click Scan or Refresh
- 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
- Select unmanaged resources
- Click Generate Import
- 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
- Add generated code to your Terraform configuration
- Run
terraform planto verify - Run
terraform applyto 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
- Regular scans - Run gap analysis periodically
- Triage results - Classify gaps as intentional or concerning
- Track over time - Monitor gap trends
- Import systematically - Bring unmanaged resources under control
- 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
- Dashboards - Create custom views
- Query Language - Query your inventory
- Environment Variables - Configuration