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:

MethodBest ForCredential Type
Option A – Static Access KeysQuick setup, single-accountIAM user access key + secret
Option B – IAM Role (STS AssumeRole)Cross-account, time-limitedRole 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

  1. Go to IAM → Users → Create user in the AWS Console.
  2. Name the user terracotta-drift-reader (or similar).
  3. Select "Attach policies directly" and attach the TerracottaDriftReadOnly policy you created above.
  4. Complete the user creation.

Step 2: Generate an access key

  1. Open the user you just created and go to the Security credentials tab.
  2. Under Access keys, click Create access key.
  3. Select "Application running outside AWS" as the use case.
  4. Copy the Access Key ID and Secret Access Key — you will only see the secret once.

Step 3: Enter credentials in Terracotta

  1. In the Terracotta UI, open the repository you want to configure.

  2. Go to the Credentials & Environment tab.

  3. Add the following environment variables:

    VariableValue
    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)
  4. 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

  1. Go to IAM → Roles → Create role in the AWS Console.
  2. Select "AWS account" as the trusted entity type, then select "Another AWS account".
  3. Enter Terracotta's AWS account ID: 489507631213
  4. Click Next, attach the TerracottaDriftReadOnly policy you created above, and proceed.
  5. 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:root refers to Terracotta's entire AWS account — not a root user. It allows any IAM principal in Terracotta's account with sts:AssumeRole permission to assume your role, scoped to the ExternalId condition.

Step 3: Enter the role ARN in Terracotta

  1. After creating the role, copy its ARN — it looks like: arn:aws:iam::YOUR_ACCOUNT_ID:role/TerracottaSDKExecutionRole
  2. In the Terracotta UI, open the repository you want to configure.
  3. Go to the Credentials & Environment tab.
  4. Enter the Role ARN in the IAM Role field.
  5. Enter your ExternalId in the External ID field (if you added the condition above).
  6. Add the AWS_DEFAULT_REGION environment variable for your primary region.
  7. Save. Terracotta will call sts:AssumeRole at 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 plan for 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.