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 Resource Explorer • S3 bucket
GCP Cloud Asset Inventory • IAM role
• Compute 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
| Provider | Inventory Source | Status |
|---|---|---|
| AWS | Resource Explorer | ✓ Supported |
| GCP | Cloud Asset Inventory | ✓ Supported |
| Azure | Resource Graph | Planned |
AWS
Gap analysis uses AWS Resource Explorer to discover resources.
Setup Requirements
- Resource Explorer must be enabled with at least one index
- Aggregator index recommended for cross-region discovery
- Default view must be configured
Required IAM Permissions
For gap analysis (read inventory):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"resource-explorer-2:Search",
"resource-explorer-2:ListIndexes",
"resource-explorer-2:GetDefaultView"
],
"Resource": "*"
}
]
}
For generating Terraform configurations (optional):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"arn:aws:iam::aws:policy/ReadOnlyAccess"
],
"Resource": "*"
}
]
}
Enable Resource Explorer
# Enable Resource Explorer in current region
aws resource-explorer-2 create-index --type LOCAL
# Create aggregator index (recommended for multi-region)
aws resource-explorer-2 update-index-type \
--arn "arn:aws:resource-explorer-2:us-east-1:123456789012:index/..." \
--type AGGREGATOR
# Create default view
aws resource-explorer-2 create-view --view-name default
aws resource-explorer-2 associate-default-view --view-arn "arn:aws:resource-explorer-2:..."
Check Configuration
# Set API base (or use --api-base flag)
export STATEGRAPH_API_BASE=https://stategraph.example.com
stategraph tenant gaps config \
--tenant=550e8400-e29b-41d4-a716-446655440000 \
--provider=aws
Response:
{
"status": "ready",
"ready_for_gap_analysis": true,
"ready_for_terraform_import": true,
"has_aggregator": true,
"aggregator_region": "us-east-1",
"indexed_regions": ["us-east-1", "us-west-2", "eu-west-1"],
"index_count": 3,
"warnings": []
}
GCP
Gap analysis uses GCP Cloud Asset Inventory to discover resources.
Setup Requirements
- Cloud Asset API must be enabled
- IAM permissions for Cloud Asset Inventory
- Scope access at project, folder, or organization level
Required IAM Permissions
For gap analysis (read inventory):
# Grant Cloud Asset Viewer role
gcloud projects add-iam-policy-binding PROJECT_ID \
--member='serviceAccount:SA_EMAIL' \
--role='roles/cloudasset.viewer'
For generating Terraform configurations (optional):
# Grant Viewer role (read-only access to all resources)
gcloud projects add-iam-policy-binding PROJECT_ID \
--member='serviceAccount:SA_EMAIL' \
--role='roles/viewer'
Enable Cloud Asset API
gcloud services enable cloudasset.googleapis.com --project=PROJECT_ID
Scope Configuration
GCP gap analysis can operate at different scopes:
| Scope | Environment Variable | Coverage |
|---|---|---|
| Project | GOOGLE_CLOUD_PROJECT |
Single project |
| Folder | GOOGLE_CLOUD_FOLDER |
All projects in folder |
| Organization | GOOGLE_CLOUD_ORGANIZATION |
Entire organization |
If no scope is set, Stategraph auto-detects from Application Default Credentials.
Check Configuration
# Set API base (or use --api-base flag)
export STATEGRAPH_API_BASE=https://stategraph.example.com
stategraph tenant gaps config \
--tenant=550e8400-e29b-41d4-a716-446655440000 \
--provider=gcp
Response:
{
"status": "ready",
"ready_for_gap_analysis": true,
"ready_for_terraform_import": false,
"scope_type": "project",
"scope_id": "projects/my-project",
"has_org_access": false,
"warnings": [
{
"code": "NO_VIEWER_PERMISSION",
"message": "Optional: To generate Terraform configurations for unmanaged resources, grant 'roles/viewer' (read-only) to your service account.",
"fix": "gcloud projects add-iam-policy-binding my-project \\\n --member='serviceAccount:123456-compute@developer.gserviceaccount.com' \\\n --role='roles/viewer'"
}
]
}
Supported GCP Resource Types
72 resource types are supported, including:
| Service | Resource Types |
|---|---|
| Compute Engine | Instances, Disks, Networks, Subnetworks, Firewalls, Load Balancers |
| Cloud Storage | Buckets |
| Cloud SQL | Instances |
| BigQuery | Datasets, Tables |
| IAM | Service Accounts, Roles |
| Cloud Functions | Functions |
| Pub/Sub | Topics, Subscriptions |
| Cloud Run | Services |
| GKE | Clusters, Node Pools |
| Cloud KMS | Key Rings, Crypto Keys |
Accessing Gap Analysis
Via UI
- Navigate to Inventory → Gap Analysis
- Select your cloud provider (AWS or GCP)
- Click Check Config to verify setup
- Click Analyze to run gap analysis
- View unmanaged resources
The UI shows:
- Coverage Summary - Total resources, managed vs unmanaged
- Service Breakdown - Resources grouped by service
- Resource Table - Details of each unmanaged resource
Via CLI
Run gap analysis:
# Set API base (or use --api-base flag)
export STATEGRAPH_API_BASE=https://stategraph.example.com
stategraph tenant gaps analyze \
--tenant=550e8400-e29b-41d4-a716-446655440000 \
--provider=gcp
Response:
{
"summary": {
"total_cloud_resources": 500,
"managed_by_stategraph": 450,
"unmanaged": 50,
"phantom_filtered": 12
},
"unmanaged_resources": [
{
"id": "//compute.googleapis.com/projects/my-project/zones/us-central1-a/instances/orphan-vm",
"asset_name": "//compute.googleapis.com/projects/my-project/zones/us-central1-a/instances/orphan-vm",
"asset_type": "compute.googleapis.com/Instance",
"service": "compute",
"region": "us-central1-a",
"project_id": "my-project"
}
],
"fetched_at": 1705312800,
"from_cache": false
}
Understanding Results
Summary Metrics
| Metric | Description |
|---|---|
total_cloud_resources |
All resources found in cloud inventory |
managed_by_stategraph |
Resources matched to Terraform state |
unmanaged |
Resources not in any state |
phantom_filtered |
Provider-managed resources excluded (e.g., default VPCs) |
Phantom Resources
Some resources are automatically filtered because they're provider-managed and can't be imported:
AWS:
- Default Athena workgroups and catalogs
- Service-linked IAM roles
- Default event buses
- Default S3 Storage Lens dashboards
GCP:
- Default compute service accounts
- Default VPC networks
- Default firewall rules
Generating Terraform Configurations
Select unmanaged resources and generate import-ready Terraform code.
Via UI
- Select resources using checkboxes
- Click Generate Config
- Review generated HCL
- Copy or download the
.tffile
Via CLI
# Set API base (or use --api-base flag)
export STATEGRAPH_API_BASE=https://stategraph.example.com
# Get unmanaged resources
stategraph tenant gaps analyze \
--tenant=550e8400-e29b-41d4-a716-446655440000 \
--provider=gcp | jq '.unmanaged_resources' > unmanaged.json
# Generate Terraform configuration
stategraph tenant gaps import \
--tenant=550e8400-e29b-41d4-a716-446655440000 \
--provider=gcp \
unmanaged.json
Response:
{
"generated_hcl": "resource \"google_compute_instance\" \"imported_orphan_vm\" {\n name = \"orphan-vm\"\n machine_type = \"e2-medium\"\n zone = \"us-central1-a\"\n ...\n}\n",
"import_blocks": "import {\n to = google_compute_instance.imported_orphan_vm\n id = \"projects/my-project/zones/us-central1-a/instances/orphan-vm\"\n}\n",
"provider_hcl": "terraform {\n required_providers {\n google = {\n source = \"hashicorp/google\"\n version = \"~> 5.0\"\n }\n }\n}\n",
"supported_count": 1,
"unsupported_count": 0,
"unsupported_resources": []
}
Using Generated Code
- Save generated HCL to a
.tffile - Run
tofu planorterraform planto verify - Run
tofu applyorterraform applyto import
# Save generated configuration
cat > import.tf << 'EOF'
import {
to = google_compute_instance.imported_orphan_vm
id = "projects/my-project/zones/us-central1-a/instances/orphan-vm"
}
resource "google_compute_instance" "imported_orphan_vm" {
name = "orphan-vm"
machine_type = "e2-medium"
zone = "us-central1-a"
# ... generated attributes
}
EOF
# Verify and import
tofu plan
tofu apply
Caching
Gap analysis results are cached to avoid rate limiting and reduce API calls:
| Setting | Default | Description |
|---|---|---|
| Cache TTL | 3 hours | How long results are cached |
| Cache location | /var/cache/stategraph/gap-analysis/ |
Cache directory |
# Set API base (or use --api-base flag)
export STATEGRAPH_API_BASE=https://stategraph.example.com
# Use cache (default, fast)
stategraph tenant gaps analyze \
--tenant=550e8400-e29b-41d4-a716-446655440000 \
--provider=gcp
# Force fresh scan (slower)
stategraph tenant gaps analyze \
--tenant=550e8400-e29b-41d4-a716-446655440000 \
--provider=gcp \
--source=no-cache
The fetched_at timestamp and from_cache flag indicate data freshness.
In the UI, click Refresh to bypass the cache.
Filtering Results
By Service
stategraph tenant gaps analyze \
--tenant=550e8400-e29b-41d4-a716-446655440000 \
--provider=gcp | jq '.unmanaged_resources | map(select(.service == "compute"))'
By Region
stategraph tenant gaps analyze \
--tenant=550e8400-e29b-41d4-a716-446655440000 \
--provider=gcp | jq '.unmanaged_resources | map(select(.region == "us-central1-a"))'
By Resource Type
stategraph tenant gaps analyze \
--tenant=550e8400-e29b-41d4-a716-446655440000 \
--provider=gcp | jq '.unmanaged_resources | map(select(.asset_type == "compute.googleapis.com/Instance"))'
Troubleshooting
AWS: No indexes found
"warnings": [{"code": "NO_INDEXES", "message": "No Resource Explorer indexes found"}]
Solution: Enable Resource Explorer in at least one region:
aws resource-explorer-2 create-index --type LOCAL
AWS: No aggregator
"warnings": [{"code": "NO_AGGREGATOR", "message": "No aggregator index configured"}]
Solution: Upgrade one index to aggregator type for cross-region discovery:
aws resource-explorer-2 update-index-type --arn "INDEX_ARN" --type AGGREGATOR
GCP: Permission denied
"warnings": [{"code": "PERMISSION_DENIED", "message": "Permission denied for scope projects/my-project"}]
Solution: Grant Cloud Asset Viewer role:
gcloud projects add-iam-policy-binding my-project \
--member='serviceAccount:YOUR_SA@my-project.iam.gserviceaccount.com' \
--role='roles/cloudasset.viewer'
GCP: Insufficient OAuth scopes
"warnings": [{"code": "INSUFFICIENT_SCOPES", "message": "VM OAuth scopes are insufficient"}]
Solution: Update VM scopes (requires restart):
gcloud compute instances stop INSTANCE_NAME --zone=ZONE
gcloud compute instances set-service-account INSTANCE_NAME \
--zone=ZONE --scopes=cloud-platform
gcloud compute instances start INSTANCE_NAME --zone=ZONE
Terraform import fails with permission denied
The gap analysis uses Cloud Asset API, but Terraform import needs actual resource read permissions.
Solution: Grant roles/viewer for Terraform operations:
gcloud projects add-iam-policy-binding my-project \
--member='serviceAccount:YOUR_SA@my-project.iam.gserviceaccount.com' \
--role='roles/viewer'
Best Practices
- Check config first - Run config check before analysis to verify permissions
- Regular scans - Run gap analysis periodically (weekly or after deployments)
- Triage results - Classify gaps as intentional or concerning
- Import systematically - Bring unmanaged production resources under control
- Use organization scope - For GCP, use organization-level access for complete visibility
- Document exceptions - Record why some resources remain unmanaged
Environment Variables
| Variable | Description |
|---|---|
GOOGLE_CLOUD_PROJECT |
GCP project ID for gap analysis scope |
GOOGLE_CLOUD_FOLDER |
GCP folder ID for broader scope |
GOOGLE_CLOUD_ORGANIZATION |
GCP organization ID for full visibility |
AWS_DEFAULT_REGION |
AWS region for Resource Explorer queries |
GAP_ANALYSIS_CACHE_TTL |
Cache TTL in seconds (default: 10800 = 3 hours) |
Credential Configuration
This section explains how to pass cloud provider credentials to Stategraph for gap analysis. By default, Stategraph uses the standard credential chain for each provider (instance metadata, environment variables, config files).
AWS Credentials
Default Behavior
Stategraph uses the standard AWS credential chain:
- Environment variables (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY) - Shared credentials file (
~/.aws/credentials) - IAM role for Amazon EC2 / ECS task role
- IAM Roles for Service Accounts (IRSA) in EKS
Docker Compose
Option 1: Environment Variables
Pass credentials directly (suitable for development):
services:
server:
image: ghcr.io/stategraph/stategraph-server:latest
environment:
# ... existing config ...
AWS_ACCESS_KEY_ID: "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_ACCESS_KEY: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
AWS_DEFAULT_REGION: "us-east-1"
# Optional: for temporary credentials
# AWS_SESSION_TOKEN: "your-session-token"
Option 2: Mount AWS Config Directory
Mount your local AWS credentials (recommended for development):
services:
server:
image: ghcr.io/stategraph/stategraph-server:latest
environment:
# ... existing config ...
AWS_DEFAULT_REGION: "us-east-1"
# Optional: specify a named profile
# AWS_PROFILE: "my-profile"
volumes:
- ~/.aws:/home/stategraph/.aws:ro
Option 3: Use .env File
Store credentials in a .env file (not committed to git):
# .env
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
AWS_DEFAULT_REGION=us-east-1
services:
server:
image: ghcr.io/stategraph/stategraph-server:latest
env_file:
- .env
Kubernetes
Option 1: Secret with Environment Variables
apiVersion: v1
kind: Secret
metadata:
name: aws-credentials
namespace: stategraph
stringData:
AWS_ACCESS_KEY_ID: "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_ACCESS_KEY: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: stategraph
namespace: stategraph
spec:
template:
spec:
containers:
- name: stategraph
envFrom:
- secretRef:
name: aws-credentials
env:
- name: AWS_DEFAULT_REGION
value: "us-east-1"
Option 2: IAM Roles for Service Accounts (IRSA) - Recommended for EKS
- Create an IAM role with the required permissions and trust policy for your EKS cluster
- Annotate the Kubernetes service account:
apiVersion: v1
kind: ServiceAccount
metadata:
name: stategraph
namespace: stategraph
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/stategraph-gap-analysis
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: stategraph
namespace: stategraph
spec:
template:
spec:
serviceAccountName: stategraph
containers:
- name: stategraph
env:
- name: AWS_DEFAULT_REGION
value: "us-east-1"
Option 3: EC2 Instance Profile
If running on EC2 or using node IAM roles, no additional configuration is needed. Ensure the node's IAM role has the required Resource Explorer permissions.
GCP Credentials
Default Behavior
Stategraph uses the standard GCP credential chain:
GOOGLE_APPLICATION_CREDENTIALSenvironment variable (path to service account JSON)- Application Default Credentials (ADC)
- Compute Engine / GKE metadata server
- Workload Identity (GKE)
Docker Compose
Mount your GCP service account key file and set GOOGLE_APPLICATION_CREDENTIALS to point to it:
services:
server:
image: ghcr.io/stategraph/stategraph-server:latest
environment:
# ... existing config ...
GOOGLE_APPLICATION_CREDENTIALS: "/secrets/gcp-sa-key.json"
GOOGLE_CLOUD_PROJECT: "my-project-id"
volumes:
- ./gcp-sa-key.json:/secrets/gcp-sa-key.json:ro
This approach:
- Uses the standard GCP authentication method via GOOGLE_APPLICATION_CREDENTIALS
- Mounts your service account key file into the container
- Works consistently across Docker Compose and Kubernetes environments
Note: Ensure your service account key file is excluded from version control (add to
.gitignore)
Kubernetes
Option 1: Secret with Service Account Key
- Create a secret from your service account JSON:
kubectl create secret generic gcp-credentials \
--namespace=stategraph \
--from-file=key.json=./gcp-sa-key.json
- Mount the secret and set the environment variable:
apiVersion: apps/v1
kind: Deployment
metadata:
name: stategraph
namespace: stategraph
spec:
template:
spec:
containers:
- name: stategraph
env:
- name: GOOGLE_APPLICATION_CREDENTIALS
value: "/secrets/gcp/key.json"
- name: GOOGLE_CLOUD_PROJECT
value: "my-project-id"
volumeMounts:
- name: gcp-credentials
mountPath: /secrets/gcp
readOnly: true
volumes:
- name: gcp-credentials
secret:
secretName: gcp-credentials
Option 2: Workload Identity (Recommended for GKE)
- Enable Workload Identity on your GKE cluster
- Create a GCP service account with the required permissions
- Bind the Kubernetes service account to the GCP service account:
gcloud iam service-accounts add-iam-policy-binding \
stategraph-sa@PROJECT_ID.iam.gserviceaccount.com \
--role="roles/iam.workloadIdentityUser" \
--member="serviceAccount:PROJECT_ID.svc.id.goog[stategraph/stategraph]"
- Annotate the Kubernetes service account:
apiVersion: v1
kind: ServiceAccount
metadata:
name: stategraph
namespace: stategraph
annotations:
iam.gke.io/gcp-service-account: stategraph-sa@PROJECT_ID.iam.gserviceaccount.com
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: stategraph
namespace: stategraph
spec:
template:
spec:
serviceAccountName: stategraph
containers:
- name: stategraph
env:
- name: GOOGLE_CLOUD_PROJECT
value: "my-project-id"
Option 3: GCE Metadata Server
If running on GCE or GKE without Workload Identity, the default service account is used automatically. Ensure it has the required Cloud Asset Viewer permissions.
Multi-Cloud Configuration
To enable gap analysis for both AWS and GCP simultaneously:
Docker Compose:
services:
server:
image: ghcr.io/stategraph/stategraph-server:latest
environment:
# AWS
AWS_DEFAULT_REGION: "us-east-1"
# GCP
GOOGLE_APPLICATION_CREDENTIALS: "/secrets/gcp-sa-key.json"
GOOGLE_CLOUD_PROJECT: "my-project-id"
volumes:
- ~/.aws:/home/stategraph/.aws:ro
- ./gcp-sa-key.json:/secrets/gcp-sa-key.json:ro
Kubernetes:
apiVersion: apps/v1
kind: Deployment
metadata:
name: stategraph
namespace: stategraph
spec:
template:
spec:
serviceAccountName: stategraph # With IRSA and/or Workload Identity
containers:
- name: stategraph
env:
# AWS
- name: AWS_DEFAULT_REGION
value: "us-east-1"
# GCP
- name: GOOGLE_APPLICATION_CREDENTIALS
value: "/secrets/gcp/key.json"
- name: GOOGLE_CLOUD_PROJECT
value: "my-project-id"
volumeMounts:
- name: gcp-credentials
mountPath: /secrets/gcp
readOnly: true
volumes:
- name: gcp-credentials
secret:
secretName: gcp-credentials
Next Steps
- Dashboards - Create custom views of your inventory
- Query Language - Query resources with MQL
- Environment Variables - Full configuration reference