AWS Integration setup for drift detection and resource analysis
Terracotta's drift detection engine fetches your Terraform state file and then makes direct AWS SDK read calls for each managed resource — comparing state attributes against live cloud values. To do this, Terracotta needs AWS credentials with read-only access to every service you manage with Terraform.
Want to skip the manual steps? Use the automated setup script — it creates the IAM user or role and policy for you in one interactive run.
Two credential methods are supported. Choose the one that fits your security model:
| Method | Best For | Credential Type |
|---|---|---|
| Option A – Static Access Keys | Quick setup, single-account | IAM user access key + secret |
| Option B – IAM Role (STS AssumeRole) | Cross-account, time-limited | Role ARN assumed by Terracotta |
Both options use the same least-privilege IAM permissions policy — only the authentication method differs.
The Required IAM Permissions Policy
Before choosing a setup method, create this managed policy. It grants Terracotta read-only access to all AWS services supported by the drift engine (1,618 resource types across 96+ services).
In the AWS Console, go to IAM → Policies → Create policy, switch to the JSON editor, and paste this policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ComputeReadOnly",
"Effect": "Allow",
"Action": [
"ec2:Describe*",
"ec2:Get*",
"ec2:Search*",
"autoscaling:Describe*",
"lambda:Get*",
"lambda:List*",
"ecs:Describe*",
"ecs:List*",
"eks:Describe*",
"eks:List*",
"batch:Describe*",
"batch:List*",
"lightsail:Get*",
"apprunner:Describe*",
"apprunner:List*"
],
"Resource": "*"
},
{
"Sid": "StorageReadOnly",
"Effect": "Allow",
"Action": [
"s3:Get*",
"s3:List*",
"s3:HeadBucket",
"s3:HeadObject",
"s3control:Get*",
"s3control:List*",
"elasticfilesystem:Describe*",
"fsx:Describe*",
"fsx:List*",
"storagegateway:Describe*",
"storagegateway:List*",
"datasync:Describe*",
"datasync:List*",
"backup:Describe*",
"backup:Get*",
"backup:List*"
],
"Resource": "*"
},
{
"Sid": "DatabaseReadOnly",
"Effect": "Allow",
"Action": [
"rds:Describe*",
"rds:List*",
"dynamodb:Describe*",
"dynamodb:List*",
"elasticache:Describe*",
"elasticache:List*",
"docdb:Describe*",
"neptune:Describe*",
"keyspaces:Get*",
"keyspaces:List*",
"memorydb:Describe*",
"memorydb:List*",
"dax:Describe*",
"dax:List*",
"redshift:Describe*"
],
"Resource": "*"
},
{
"Sid": "NetworkingReadOnly",
"Effect": "Allow",
"Action": [
"elasticloadbalancing:Describe*",
"route53:Get*",
"route53:List*",
"route53domains:Get*",
"route53domains:List*",
"route53resolver:Get*",
"route53resolver:List*",
"cloudfront:Get*",
"cloudfront:List*",
"apigateway:GET",
"directconnect:Describe*",
"globalaccelerator:Describe*",
"globalaccelerator:List*",
"network-firewall:Describe*",
"network-firewall:List*",
"networkmanager:Get*",
"networkmanager:List*",
"networkmanager:Describe*",
"vpclattice:Get*",
"vpclattice:List*",
"appmesh:Describe*",
"appmesh:List*",
"servicediscovery:Get*",
"servicediscovery:List*"
],
"Resource": "*"
},
{
"Sid": "SecurityReadOnly",
"Effect": "Allow",
"Action": [
"iam:Get*",
"iam:List*",
"kms:Describe*",
"kms:Get*",
"kms:List*",
"acm:Describe*",
"acm:Get*",
"acm:List*",
"acm-pca:Describe*",
"acm-pca:Get*",
"acm-pca:List*",
"secretsmanager:Describe*",
"secretsmanager:GetResourcePolicy",
"secretsmanager:List*",
"guardduty:Get*",
"guardduty:List*",
"securityhub:Describe*",
"securityhub:Get*",
"securityhub:List*",
"macie2:Get*",
"macie2:List*",
"shield:Describe*",
"shield:Get*",
"shield:List*",
"waf:Get*",
"waf:List*",
"waf-regional:Get*",
"waf-regional:List*",
"wafv2:Get*",
"wafv2:List*",
"sso:Describe*",
"sso:Get*",
"sso:List*",
"identitystore:Describe*",
"identitystore:Get*",
"identitystore:List*",
"cognito-idp:Describe*",
"cognito-idp:Get*",
"cognito-idp:List*",
"cognito-identity:Describe*",
"cognito-identity:Get*",
"cognito-identity:List*",
"inspector2:Get*",
"inspector2:List*"
],
"Resource": "*"
},
{
"Sid": "MonitoringReadOnly",
"Effect": "Allow",
"Action": [
"cloudwatch:Describe*",
"cloudwatch:Get*",
"cloudwatch:List*",
"logs:Describe*",
"logs:Get*",
"logs:List*",
"logs:FilterLogEvents",
"cloudtrail:Describe*",
"cloudtrail:Get*",
"cloudtrail:List*",
"cloudtrail:LookupEvents",
"xray:Get*",
"xray:List*",
"xray:BatchGet*",
"config:Describe*",
"config:Get*",
"config:List*",
"grafana:Describe*",
"grafana:List*"
],
"Resource": "*"
},
{
"Sid": "MessagingAndEventsReadOnly",
"Effect": "Allow",
"Action": [
"sns:Get*",
"sns:List*",
"sqs:Get*",
"sqs:List*",
"mq:Describe*",
"mq:List*",
"kinesis:Describe*",
"kinesis:Get*",
"kinesis:List*",
"firehose:Describe*",
"firehose:List*",
"events:Describe*",
"events:Get*",
"events:List*",
"scheduler:Get*",
"scheduler:List*",
"kafka:Describe*",
"kafka:Get*",
"kafka:List*",
"iot:Describe*",
"iot:Get*",
"iot:List*"
],
"Resource": "*"
},
{
"Sid": "DataAnalyticsReadOnly",
"Effect": "Allow",
"Action": [
"athena:Get*",
"athena:List*",
"athena:BatchGet*",
"glue:Get*",
"glue:List*",
"glue:BatchGet*",
"lakeformation:Get*",
"lakeformation:List*",
"lakeformation:Describe*",
"es:Describe*",
"es:Get*",
"es:List*",
"aoss:Get*",
"aoss:List*",
"aoss:BatchGet*",
"emr:Describe*",
"emr:Get*",
"emr:List*",
"airflow:GetEnvironment",
"airflow:ListEnvironments"
],
"Resource": "*"
},
{
"Sid": "AIMLReadOnly",
"Effect": "Allow",
"Action": [
"bedrock:Get*",
"bedrock:List*",
"sagemaker:Describe*",
"sagemaker:Get*",
"sagemaker:List*"
],
"Resource": "*"
},
{
"Sid": "DeveloperToolsReadOnly",
"Effect": "Allow",
"Action": [
"codebuild:BatchGet*",
"codebuild:Describe*",
"codebuild:Get*",
"codebuild:List*",
"codedeploy:Get*",
"codedeploy:List*",
"codedeploy:BatchGet*",
"codepipeline:Get*",
"codepipeline:List*",
"codecommit:Get*",
"codecommit:List*",
"codecommit:BatchGet*",
"codeartifact:Describe*",
"codeartifact:Get*",
"codeartifact:List*",
"ecr:Describe*",
"ecr:Get*",
"ecr:List*",
"ecr:BatchGet*",
"ecr-public:Describe*",
"ecr-public:Get*",
"ecr-public:List*"
],
"Resource": "*"
},
{
"Sid": "ManagementReadOnly",
"Effect": "Allow",
"Action": [
"cloudformation:Describe*",
"cloudformation:Get*",
"cloudformation:List*",
"ssm:Describe*",
"ssm:Get*",
"ssm:List*",
"organizations:Describe*",
"organizations:List*",
"ram:Get*",
"ram:List*",
"controltower:Get*",
"controltower:List*",
"account:GetAccountInformation",
"account:GetAlternateContact",
"account:GetRegionOptStatus",
"account:ListRegions"
],
"Resource": "*"
},
{
"Sid": "ApplicationServicesReadOnly",
"Effect": "Allow",
"Action": [
"amplify:Get*",
"amplify:List*",
"appsync:Get*",
"appsync:List*",
"appconfig:Get*",
"appconfig:List*",
"connect:Describe*",
"connect:Get*",
"connect:List*",
"ses:Describe*",
"ses:Get*",
"ses:List*"
],
"Resource": "*"
}
]
}Name this policy TerracottaDriftReadOnly and save it. You will attach it in both setup options below.
Why so many services? Terracotta's drift engine supports 1,618 AWS resource types — 119 with deep field-level drift comparison and 1,499 with existence-only checks. The policy above covers all of them with read-only access. No write permissions are granted.
Option A — Static Access Keys
Use this path if you want the simplest setup, or if you are using a single AWS account.
Step 1: Create a dedicated IAM user
- Go to IAM → Users → Create user in the AWS Console.
- Name the user
terracotta-drift-reader(or similar). - Select "Attach policies directly" and attach the
TerracottaDriftReadOnlypolicy you created above. - Complete the user creation.
Step 2: Generate an access key
- Open the user you just created and go to the Security credentials tab.
- Under Access keys, click Create access key.
- Select "Application running outside AWS" as the use case.
- Copy the Access Key ID and Secret Access Key — you will only see the secret once.
Step 3: Enter credentials in Terracotta
-
In the Terracotta UI, open the repository you want to configure.
-
Go to the Credentials & Environment tab.
-
Add the following environment variables:
Variable Value AWS_ACCESS_KEY_IDYour Access Key ID AWS_SECRET_ACCESS_KEYYour Secret Access Key AWS_DEFAULT_REGIONYour primary AWS region (e.g., us-east-1) -
Save. Terracotta will use these credentials on the next drift or plan run.
Option B — IAM Role (STS AssumeRole)
Use this path for cross-account access, stronger security controls, or if your security policy prohibits long-lived static credentials. Terracotta will assume the role using STS and receive temporary credentials with a 1-hour TTL.
Step 1: Create the IAM role
- Go to IAM → Roles → Create role in the AWS Console.
- Select "AWS account" as the trusted entity type, then select "Another AWS account".
- Enter Terracotta's AWS account ID:
489507631213 - Click Next, attach the
TerracottaDriftReadOnlypolicy you created above, and proceed. - Name the role
TerracottaSDKExecutionRole(or similar) and create it.
Step 2: Configure the trust policy
After the role is created, open it and go to the Trust relationships tab. Click Edit trust policy and replace the contents with:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::489507631213:root"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "your-external-id-here"
}
}
}
]
}Replace your-external-id-here with a unique, hard-to-guess string (e.g., terracotta-YOUR_ORG-2026). This ExternalId is a shared secret that prevents unauthorized parties from assuming your role even if they know its ARN (the "confused deputy" problem).
Note on the Root ARN:
arn:aws:iam::489507631213:rootrefers to Terracotta's entire AWS account — not a root user. It allows any IAM principal in Terracotta's account withsts:AssumeRolepermission to assume your role, scoped to the ExternalId condition.
Step 3: Enter the role ARN in Terracotta
- After creating the role, copy its ARN — it looks like:
arn:aws:iam::YOUR_ACCOUNT_ID:role/TerracottaSDKExecutionRole - In the Terracotta UI, open the repository you want to configure.
- Go to the Credentials & Environment tab.
- Enter the Role ARN in the IAM Role field.
- Enter your ExternalId in the External ID field (if you added the condition above).
- Add the
AWS_DEFAULT_REGIONenvironment variable for your primary region. - Save. Terracotta will call
sts:AssumeRoleat the start of each drift or plan run and use temporary credentials for that session.
Verifying the Setup
Trigger a drift check from any open pull request by commenting tc:drift, or enable drift detection auto-run from the repository Overview tab. If credentials are valid, results appear as a GitHub Check Run or GitLab commit status within a few minutes.
If you see AccessDenied errors in Terracotta's output, the specific missing action will be listed — add it to the TerracottaDriftReadOnly policy and retry.
Additional Notes
- Least privilege by design: This policy grants only read operations (
Describe*,Get*,List*,Search*,Head*). No write, delete, or mutation actions are included. - No Terraform required: Terracotta fetches state directly from S3 and calls AWS APIs natively — it does not run
terraform planfor drift detection. - Two credential sets for state + resources: If your Terraform state file is stored in a different AWS account from your managed resources, configure one set of credentials for the S3 bucket and a separate role ARN for resource inspection.
- Monitoring: Enable AWS CloudTrail for the IAM user or role to audit all API calls Terracotta makes.
- Temporary credential TTL: For Option B, assumed role sessions default to 1 hour. This is sufficient for a drift run and automatically refreshed on the next check.
Updated about 23 hours ago
