Overview
Before deploying infrastructure via CI/CD, you need to bootstrap your AWS accounts and GitHub organization for Terraform management. This is a one-time setup that creates the foundation for all automation.Decisions
This setup step requires deciding the following:Namespace
The namespace is a short prefix (3-5 characters) used to generate unique names for all AWS resources: S3 buckets, IAM roles, EKS clusters, etc. Choose something that identifies your organization.| Examples | Description |
|---|---|
acme | Company name abbreviation |
myco | Short identifier |
xyz | Project code |
ksk (“Kube Starter Kit”). You’ll use this namespace consistently across all configuration, it cannot be easily changed later.
Primary AWS Region
Choose a primary AWS region for your infrastructure. This region will host:- The Terraform state S3 bucket
- Your EKS clusters (staging and production)
- Most other AWS resources
us-east-2. Consider factors like latency to your users, service availability, and pricing when choosing.
How Cross-Account Access Works
The Infrastructure account is the central hub for all Terraform operations. There are two access paths:- CI/CD (GitHub Actions): Authenticates via OIDC, then assumes roles in target accounts
- Human admins (IAM Identity Center): Authenticates via SSO to the Infrastructure account, then assumes roles in target accounts
- GitHub Actions authenticates via OIDC to the GitHub OIDC role in the Infrastructure account
- That role assumes target account roles via cross-account IAM trust policies
- Terraform runs with credentials for the target account, state stored in Infrastructure account
- Admin authenticates to the Infrastructure account via IAM Identity Center (using Leapp)
- The SSO role in Infrastructure account can assume target account roles
- Admin runs Terraform locally with the same cross-account access as CI/CD
What Gets Created
Infrastructure Account (Central Hub)
| Resource | Purpose |
|---|---|
| S3 bucket | Stores Terraform state for all accounts with versioning enabled |
| GitHub OIDC provider | Enables keyless authentication from GitHub Actions |
| GitHub OIDC IAM role | Role that GitHub Actions assumes; can then assume roles in other accounts |
Each Target Account (Management, ECR, Staging, Production)
| Resource | Purpose |
|---|---|
| Terraform IAM role | Admin role with trust policy allowing both the GitHub OIDC role (for CI/CD) and SSO admin role (for human admins) from the Infrastructure account to assume it |
| Route53 hosted zone | DNS zone for the environment (e.g., staging.example.com); not created in Management or ECR accounts |
Prerequisites
Required Accounts
Based on the account structure, you need these AWS accounts:| Account | Purpose |
|---|---|
| Management | AWS Organizations, IAM Identity Center |
| Infrastructure | Terraform state, GitHub OIDC, CI/CD automation |
| ECR | Container registry (shared across environments) |
| Staging | Staging environment resources |
| Production | Production environment resources |
Bootstrapping Overview
Bootstrapping solves a chicken-and-egg problem: Terraform needs IAM roles and an S3 bucket to run, but we want Terraform to manage those resources. The solution is to manually create minimal resources, then let Terraform import and manage them. Each account needs a bootstrap IAM role. The Infrastructure account additionally needs an S3 bucket for Terraform state.Configure Leapp CLI
Before you can authenticate to AWS accounts, configure the Leapp CLI with your IAM Identity Center portal:<AWS_REGION> with your primary region.
Bootstrap the Infrastructure Account
The Infrastructure account is special, it hosts the S3 state bucket that all other accounts depend on.1
Log into the Infrastructure account
Start a Leapp session for the Infrastructure account:
2
Get your SSO role ARN
You’ll need this ARN for cross-account trust policies:Save this ARN, it looks like:
3
Create the S3 state bucket
4
Update Terraform configuration
Edit Replace the placeholders with your values from Decisions.
terraform/config.tm.hcl with your namespace and account details:5
Generate Terraform files
Propagate the configuration changes to generated files:
6
Apply the Infrastructure bootstrapping stack
This imports the S3 bucket and creates the GitHub OIDC provider and role:
Bootstrap Target Accounts
Each target account (Management, ECR, Staging, Production) needs an IAM role that can be assumed from the Infrastructure account.Manual Role Creation
For each account:1
Log into the target account
2
Create the IAM role
<INFRA_SSO_ROLE_ARN> with the SSO role ARN you recorded during Bootstrap the Infrastructure Account.Use the appropriate role name for each account:| Account | Role Name |
|---|---|
| Management | <NAMESPACE>-gbl-mgmt-bootstrap-admin |
| ECR | <NAMESPACE>-gbl-ecr-bootstrap-admin |
| Staging | <NAMESPACE>-gbl-staging-bootstrap-admin |
| Production | <NAMESPACE>-gbl-prod-bootstrap-admin |
AdministratorAccesspolicy attached- Trust policy allowing your SSO role from the Infrastructure account to assume it
Terraform Takeover
Once the manual roles exist, Terraform can import and manage them. Terramate generates animport block in each root module to bring the manually-created role into Terraform state.
How Import Works
The Terramate template interraform/imports/mixins/modules/bootstrapping.tm.hcl generates both the import block and module call for each bootstrapping stack:
namespace, environment, stage) to match the role name you created manually (e.g., "<NAMESPACE>-gbl-staging-bootstrap-admin").
Import blocks must be in root modules, not child modules. This is a Terraform requirement. The
account-bootstrapping module itself does not contain the import block; it’s generated by Terramate at the root module level.terraform apply, Terraform:
- Imports the existing IAM role into state (instead of trying to create it)
- Updates the role’s trust policy to allow both your SSO role and the GitHub OIDC role to assume it
Apply the Bootstrapping Stacks
The
import block was added in Terraform 1.5. It allows declarative imports without running terraform import commands manually. See the Terraform import block documentation for more details.Configure Domain Nameservers
The bootstrapping stacks create Route53 hosted zones for Staging and Production (e.g.,staging.example.com, prod.example.com). For DNS to work, you must configure your domain registrar to use the Route53 nameservers.
1
Get the Route53 nameservers
After applying the bootstrapping stacks, retrieve the nameservers for each hosted zone:You’ll see 4 nameservers like:
2
Configure your domain registrar
How you configure nameservers depends on your setup:If using a subdomain (e.g.,
staging.example.com):- Add NS records in your parent domain’s DNS pointing to the Route53 nameservers
- Example: Add NS records for
stagingsubdomain pointing to the 4 nameservers above
example-staging.com):- Update the domain’s nameservers at your registrar (Namecheap, GoDaddy, Route53, etc.)
- Replace the default nameservers with the 4 Route53 nameservers
3
Verify DNS propagation
DNS changes can take up to 48 hours to propagate, but usually complete within minutes. Verify with:You should see the Route53 nameservers in the response.
Troubleshooting
”Bucket already exists” error
S3 bucket names are globally unique. If the bucket name is taken:- Choose a different name with your organization prefix
- Update
backend_bucketinterraform/config.tm.hcl - Update any hardcoded references in bootstrapping stacks
State file in wrong location
All Terraform state is stored in the Infrastructure account’s S3 bucket, regardless of which account the resources are in. If you see state errors:- Verify
backend_bucketandbackend_regioninterraform/config.tm.hcl - Ensure the GitHub OIDC role has S3 permissions in the Infrastructure account
- Check that the bucket exists and has the expected state files