> ## Documentation Index
> Fetch the complete documentation index at: https://docs.draftt.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy with Terraform

> Provision the IAM role and permissions Draftt needs in your AWS account with Terraform or OpenTofu, as an alternative to CloudFormation.

Terraform is an optional alternative to the CloudFormation-based [console setup](/aws). Each option below creates the same resources as the CloudFormation template, which remains the canonical definition. See [AWS IAM resources](/aws-iam-resources) for what each resource does.

## Prerequisites

* Terraform 1.1 or later, or any OpenTofu release
* AWS provider 5.67.0 or later. The StackSet option depends on the `aws_cloudformation_stack_instances` resource added in that version
* AWS credentials with permission to create IAM roles and policies in the target account
* The External ID from the Draftt setup dialog (**Integrations > AWS**)

All options use the AWS provider. Declare it once in your configuration:

```hcl theme={null}
terraform {
  required_version = ">= 1.1"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.67, < 7.0"
    }
  }
}
```

## Setup methods

<AccordionGroup>
  <Accordion title="Single Account">
    Creates the IAM role, trust policy, and permissions directly as Terraform resources. This gives you full control over the role, including tags, a permissions boundary, or a custom path.

    <Steps>
      <Step title="Download the read-only policy">
        Save the published policy next to your Terraform files so permission changes are reviewed in your own pull requests:

        ```bash theme={null}
        curl -sSfo DrafttReadOnlyPolicy.json \
          https://draftt-public.s3.amazonaws.com/DrafttReadOnlyPolicy.js
        ```
      </Step>

      <Step title="Declare the trust policy">
        Only the `draftt-fetcher` role in Draftt's AWS account can assume the role, and only with your External ID:

        ```hcl theme={null}
        data "aws_iam_policy_document" "draftt_assume_role" {
          statement {
            sid     = "AllowDrafttFetcherAssumeRole"
            effect  = "Allow"
            actions = ["sts:AssumeRole"]

            principals {
              type        = "AWS"
              identifiers = ["arn:aws:iam::339712924365:root"]
            }

            condition {
              test     = "StringEquals"
              variable = "aws:PrincipalArn"
              values   = ["arn:aws:iam::339712924365:role/draftt-fetcher"]
            }

            condition {
              test     = "StringEquals"
              variable = "sts:ExternalId"
              values   = [var.draftt_external_id]
            }
          }
        }
        ```
      </Step>

      <Step title="Create the role">
        Draftt requests 30-minute sessions, which fit within the default one-hour maximum session duration:

        ```hcl theme={null}
        resource "aws_iam_role" "draftt_access" {
          name                 = var.draftt_role_name
          description          = "Draftt cross-account read-only access role"
          assume_role_policy   = data.aws_iam_policy_document.draftt_assume_role.json
          max_session_duration = 3600

          tags = var.tags
        }
        ```
      </Step>

      <Step title="Attach the AWS managed policies">
        ```hcl theme={null}
        resource "aws_iam_role_policy_attachment" "draftt_managed" {
          for_each = {
            security_audit   = "arn:aws:iam::aws:policy/SecurityAudit"
            view_only_access = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"
          }

          role       = aws_iam_role.draftt_access.name
          policy_arn = each.value
        }
        ```
      </Step>

      <Step title="Attach DrafttReadOnlyPolicy">
        The policy is attached inline, matching the CloudFormation template. Decoding and re-encoding the file validates it at plan time and normalises whitespace:

        ```hcl theme={null}
        resource "aws_iam_role_policy" "draftt_read_only" {
          name   = "DrafttReadOnlyPolicy"
          role   = aws_iam_role.draftt_access.name
          policy = jsonencode(jsondecode(file("${path.module}/DrafttReadOnlyPolicy.json")))
        }
        ```
      </Step>

      <Step title="Declare inputs and outputs">
        ```hcl theme={null}
        variable "draftt_external_id" {
          description = "External ID shown in the Draftt setup dialog (Integrations > AWS)."
          type        = string
          sensitive   = true
          nullable    = false
        }

        variable "draftt_role_name" {
          description = "Name of the IAM role Draftt assumes. Keep it identical across all connected accounts."
          type        = string
          default     = "DrafttAccess-Role"
          nullable    = false
        }

        variable "tags" {
          description = "Tags applied to the Draftt IAM resources."
          type        = map(string)
          default     = {}
          nullable    = false
        }

        output "draftt_role_arn" {
          description = "Paste this ARN into the Draftt setup dialog."
          value       = aws_iam_role.draftt_access.arn
        }
        ```
      </Step>

      <Step title="Plan and apply">
        Provide the External ID through a variable file or environment variable rather than on the command line:

        ```bash theme={null}
        export TF_VAR_draftt_external_id="<provided-in-draftt>"
        terraform init
        terraform plan -out=draftt.tfplan
        terraform apply draftt.tfplan
        ```

        The plan shows one role, one inline policy, and two policy attachments.
      </Step>

      <Step title="Enter the Role ARN in Draftt">
        Copy the `draftt_role_arn` output into the **Role ARN** field in the Draftt setup dialog and click **Create**.
      </Step>
    </Steps>
  </Accordion>

  <Accordion title="CloudFormation Stack">
    Deploys Draftt's published CloudFormation template as a Terraform-managed stack. Terraform tracks the stack, and CloudFormation creates the role and policies exactly as the console setup does. Use this to keep the template canonical without maintaining a copy of the read-only policy.

    <Steps>
      <Step title="Create the stack">
        ```hcl theme={null}
        resource "aws_cloudformation_stack" "draftt" {
          name         = "DrafttStack"
          template_url = "https://draftt-public.s3.amazonaws.com/draftt-onboarding-cloudformation.json"
          capabilities = ["CAPABILITY_NAMED_IAM"]

          parameters = {
            DrafttRoleName = var.draftt_role_name
            ExternalId     = var.draftt_external_id
          }

          tags = var.tags
        }
        ```
      </Step>

      <Step title="Declare inputs and outputs">
        ```hcl theme={null}
        variable "draftt_external_id" {
          description = "External ID shown in the Draftt setup dialog (Integrations > AWS)."
          type        = string
          sensitive   = true
          nullable    = false
        }

        variable "draftt_role_name" {
          description = "Name of the IAM role the stack creates. Keep it identical across all connected accounts."
          type        = string
          default     = "DrafttAccess-Role"
          nullable    = false
        }

        variable "tags" {
          description = "Tags applied to the CloudFormation stack."
          type        = map(string)
          default     = {}
          nullable    = false
        }

        output "draftt_role_arn" {
          description = "Paste this ARN into the Draftt setup dialog."
          value       = aws_cloudformation_stack.draftt.outputs["DrafttRoleArn"]
        }
        ```
      </Step>

      <Step title="Plan and apply">
        ```bash theme={null}
        export TF_VAR_draftt_external_id="<provided-in-draftt>"
        terraform init
        terraform plan -out=draftt.tfplan
        terraform apply draftt.tfplan
        ```

        The plan shows a single CloudFormation stack. The role and policies appear in the CloudFormation console under the stack's resources.
      </Step>

      <Step title="Enter the Role ARN in Draftt">
        Copy the `draftt_role_arn` output into the **Role ARN** field in the Draftt setup dialog and click **Create**.
      </Step>
    </Steps>

    <Note>
      The template echoes the External ID in the `ExternalID` stack output, so it is visible to anyone with `cloudformation:DescribeStacks` in the account. The stack outputs are also stored in Terraform state.
    </Note>
  </Accordion>

  <Accordion title="CloudFormation StackSet (Multi Account)">
    Apply in the organization management account. Creates the organization-level role and a service-managed StackSet that deploys the per-account role to every member account. Requires CloudFormation StackSets trusted access in AWS Organizations.

    <Note>
      Delegated StackSets administrator accounts are not supported. Draftt reads the StackSet status without the delegated-administrator call mode, so a StackSet owned by a delegated administrator is not visible to Draftt.
    </Note>

    <Steps>
      <Step title="Declare the trust policy">
        The organization-level role uses the same trust policy and External ID as the per-account role:

        ```hcl theme={null}
        data "aws_iam_policy_document" "draftt_assume_role" {
          statement {
            sid     = "AllowDrafttFetcherAssumeRole"
            effect  = "Allow"
            actions = ["sts:AssumeRole"]

            principals {
              type        = "AWS"
              identifiers = ["arn:aws:iam::339712924365:root"]
            }

            condition {
              test     = "StringEquals"
              variable = "aws:PrincipalArn"
              values   = ["arn:aws:iam::339712924365:role/draftt-fetcher"]
            }

            condition {
              test     = "StringEquals"
              variable = "sts:ExternalId"
              values   = [var.draftt_external_id]
            }
          }
        }
        ```
      </Step>

      <Step title="Create the organization-level role">
        ```hcl theme={null}
        resource "aws_iam_role" "draftt_organization_access" {
          name                 = "DrafttReadOnlyAccess-OrganizationLevel"
          description          = "Draftt organization-level read-only access role"
          assume_role_policy   = data.aws_iam_policy_document.draftt_assume_role.json
          max_session_duration = 3600

          tags = var.tags
        }
        ```
      </Step>

      <Step title="Attach the AWS managed policies">
        Draftt uses these to list member accounts and read the StackSet rollout status:

        ```hcl theme={null}
        resource "aws_iam_role_policy_attachment" "draftt_organization_managed" {
          for_each = {
            organizations_read_only  = "arn:aws:iam::aws:policy/AWSOrganizationsReadOnlyAccess"
            cloudformation_read_only = "arn:aws:iam::aws:policy/AWSCloudFormationReadOnlyAccess"
          }

          role       = aws_iam_role.draftt_organization_access.name
          policy_arn = each.value
        }
        ```
      </Step>

      <Step title="Create the StackSet">
        ```hcl theme={null}
        resource "aws_cloudformation_stack_set" "draftt" {
          name             = "DrafttStackSet"
          description      = "Deploys the Draftt cross-account read-only role to organization accounts"
          template_url     = "https://draftt-public.s3.amazonaws.com/draftt-onboarding-cloudformation.json"
          permission_model = "SERVICE_MANAGED"
          capabilities     = ["CAPABILITY_NAMED_IAM"]

          parameters = {
            DrafttRoleName = var.draftt_role_name
            ExternalId     = var.draftt_external_id
          }

          auto_deployment {
            enabled                          = true
            retain_stacks_on_account_removal = false
          }

          tags = var.tags

          lifecycle {
            ignore_changes = [administration_role_arn]
          }
        }
        ```
      </Step>

      <Step title="Deploy the stack instances">
        IAM is global, so a single region is enough. Target the organization root to cover every account, or specific organizational units:

        ```hcl theme={null}
        resource "aws_cloudformation_stack_instances" "draftt" {
          stack_set_name = aws_cloudformation_stack_set.draftt.name
          regions        = ["us-east-1"]

          deployment_targets {
            organizational_unit_ids = var.target_organizational_unit_ids
          }

          operation_preferences {
            failure_tolerance_percentage = 100
            max_concurrent_percentage    = 25
          }
        }
        ```
      </Step>

      <Step title="Declare inputs and outputs">
        ```hcl theme={null}
        variable "draftt_external_id" {
          description = "External ID shown in the Draftt setup dialog (Integrations > AWS)."
          type        = string
          sensitive   = true
          nullable    = false
        }

        variable "draftt_role_name" {
          description = "Name of the IAM role the StackSet creates in each member account."
          type        = string
          default     = "DrafttAccess-Role"
          nullable    = false
        }

        variable "target_organizational_unit_ids" {
          description = "Organization root ID (r-xxxx) or organizational unit IDs (ou-xxxx-xxxxxxxx) to deploy into. Find them with `aws organizations list-roots`."
          type        = list(string)
          nullable    = false
        }

        variable "tags" {
          description = "Tags applied to the Draftt resources."
          type        = map(string)
          default     = {}
          nullable    = false
        }

        output "draftt_organization_role_arn" {
          description = "Paste this ARN into the Draftt setup dialog as the organization role."
          value       = aws_iam_role.draftt_organization_access.arn
        }

        output "draftt_stack_set_arn" {
          description = "Paste this ARN into the Draftt setup dialog so Draftt can track the rollout."
          value       = aws_cloudformation_stack_set.draftt.arn
        }
        ```
      </Step>

      <Step title="Plan and apply">
        ```bash theme={null}
        export TF_VAR_draftt_external_id="<provided-in-draftt>"
        export TF_VAR_target_organizational_unit_ids='["r-xxxx"]'
        terraform init
        terraform plan -out=draftt.tfplan
        terraform apply draftt.tfplan
        ```

        Confirm every target account shows `SUCCEEDED` under the StackSet's stack instances before continuing. Draftt waits up to 30 minutes for the rollout and only connects accounts that succeeded.
      </Step>

      <Step title="Enter the ARNs in Draftt">
        Copy the `draftt_organization_role_arn` output into the organization **Role ARN** field, and the `draftt_stack_set_arn` output into the **StackSet ARN** field in the Draftt setup dialog.
      </Step>
    </Steps>

    <Warning>
      Destroying `aws_cloudformation_stack_instances` removes the Draftt role from every member account at once. To stop onboarding new accounts without touching existing ones, set `auto_deployment.enabled` to `false` instead.
    </Warning>
  </Accordion>
</AccordionGroup>

## Notes

<Note>
  Because the External ID is marked `sensitive`, Terraform hides the whole rendered trust policy in plan output. To review the trust policy after apply, run `aws iam get-role --role-name DrafttAccess-Role`. The External ID is still stored in Terraform state, so protect your state file as you would any other secret.
</Note>

<Warning>
  Do not import a role or policy created by a CloudFormation stack into the Single Account option. CloudFormation keeps ownership of those resources, and deleting the stack later deletes the role even though Terraform tracks it.
</Warning>
