This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Getting Started

Prerequisites, bootstrapping, and first deployment.

This section walks you through getting Org Kickstart deployed into a new AWS account.

Prerequisites

Before running Org Kickstart you need:

  • Terraform >= 1.0 (< 2.0)
  • AWS CLI configured with credentials for the Management (Payer) account
  • An S3 bucket for Terraform remote state
  • A few manual “artisanal” steps completed in the AWS console (see Bootstrap)

Steps

  1. Complete the Bootstrap steps in the AWS Console
  2. Set up your local repository — copy the local-deploy sample, pin the module version, and create your <env>.tfvars / <env>.tfbackend
  3. Initialize Terraform:
    make env=your-org tf-init
    
  4. Create the Security Account first (required before full apply):
    terraform apply -var-file="your-org.tfvars" -target module.security_account
    
  5. Deploy everything:
    make env=your-org tf-execute
    
    This runs tf-plan followed by tf-apply — saving the plan, applying it, and writing output-your-org.json to your state bucket.

For subsequent updates, use:

make env=your-org update

Using with an Existing Organization

If you already have an AWS Organization, see Importing an Existing Org for guidance on importing existing resources into Terraform state.

Example tfvars

See the Reference page for a full annotated example. The examples/local-deploy directory in the repository contains a sample layout with a Makefile, backend config template, and helper scripts for running Org Kickstart from your workstation. To pin to a specific release, see Releases.

1 - Bootstrap a New Account

Manual steps required in the AWS Console before running Terraform.

Before Org Kickstart can be deployed, a few steps must be completed via ClickOps in your new AWS Management (Payer) account. Terraform cannot perform these actions automatically.

Root Account Tasks

Log into the root user of your new AWS “payer” account and complete the following:

  1. Add MFA to root
  2. Enable IAM access to billing
  3. Go to Organizations and create an Organization
  4. Go to AWS IAM Identity Center (SSO) and enable it
  5. Add yourself as a user in Identity Center
  6. Create a Permission Set named TempAdministratorAccess (4-hour session recommended)
  7. Assign the Permission Set to the Payer/Management Account for your user
  8. Activate trusted access for CloudFormation StackSets — click “Activate trusted access with AWS Organizations to use service-managed permissions” (must be done via console)

Log out of root and never use it again.

Note: As of June 2026, aws login works for the root user with Terraform — but the root user cannot assume IAM roles, and Org Kickstart assumes roles (for example OrganizationAccountAccessRole into the security account). That is why you must run Terraform as an IAM Identity Center user or an IAM User rather than as root.

On Your Machine

  1. Check email and activate your IAM Identity Center account
  2. Add MFA to your Identity Center account
  3. Configure AWS credentials in your environment:
    aws configure sso
    # or
    export AWS_PROFILE=your-sso-profile
    
  4. Create the S3 bucket that will hold the Terraform state. It must exist before the first terraform init, since the S3 backend lives in it:
    aws s3 mb s3://org-kickstart-fooli
    
    This is the bucket you set as backend_bucket. By default (manage_state_bucket = true) Org Kickstart then adopts it into Terraform and enforces versioning, public-access-block, and encryption — see the Parameter Reference.

You are now ready to deploy Org Kickstart.

Next Steps

  • Create your tfvars file — see the Reference for all variables and a full example
  • Run terraform init and your first apply — see Getting Started

2 - Set Up Your Local Repository

Create your private deployment repo from the local-deploy sample and learn the Makefile workflow.

Once the Bootstrap steps are complete, set up a private repository to hold your organization’s configuration and deploy Org Kickstart from your workstation.

1. Copy the sample

Copy the examples/local-deploy directory into your own private git repository. It provides:

  • main.tf — calls the Org Kickstart module
  • import.tf — adopts the AWS Organization, the payer account, and the state bucket
  • Makefile — the env-based workflow (below)
  • sample.tfvars / sample.tfbackend — templates to copy
  • scripts/ and granted.yml — helper scripts and the Granted registry manifest

2. Pin the module version

main.tf pins the module to a tagged release:

source = "github.com/primeharbor/org-kickstart?ref=0.3.1"

See Releases for tracking the rolling latest branch versus pinning to a tag.

3. Create your environment files

Pick a short environment name ($env) for your org — for example fooli. Copy the samples so their names match it:

cp sample.tfvars    fooli.tfvars
cp sample.tfbackend fooli.tfbackend
  • Edit fooli.tfvars — see the Parameter Reference and the Example tfvars for all options.
  • Set the state bucket name in fooli.tfbackend — the bucket you created with aws s3 mb during Bootstrap. By default (manage_state_bucket = true) Org Kickstart adopts and manages it.

4. The Makefile workflow

Every target takes env=<your env>:

make env=fooli tf-init      # terraform init with your backend config
make env=fooli tf-plan      # write a saved plan
make env=fooli tf-apply     # apply the saved plan and write outputs
make env=fooli tf-execute   # tf-plan followed by tf-apply
make env=fooli update       # tf-init + tf-execute — your day-to-day command

The first apply creates the organization structure, OUs, and the security account.

Next steps

3 - Importing an Existing Organization

How to adopt Org Kickstart into an existing AWS Organization.

Org Kickstart can manage an existing AWS Organization. Several resources must be imported into Terraform state before running terraform apply.

The minimum required imports are the organization, the management account, and the Security Account (if it already exists). You may also want to import existing accounts, CloudTrail buckets, billing buckets, and SSO configuration.

Automated Import

The scripts/import_org.sh script generates an import-org.tf file and a TFVars snippet for your existing accounts.

# From the org-kickstart directory
./scripts/import_org.sh

Review the generated import-org.tf carefully before running Terraform.

Steps

  1. Run import_org.sh and review import-org.tf
  2. Add your CloudTrail or billing bucket to the import file if applicable
  3. Review SCPs you want to import:
    aws organizations list-policies --filter SERVICE_CONTROL_POLICY \
      --query 'Policies[].[Id,Name]' --output text
    
  4. Iterate with tf-plan until no unwanted changes appear:
    make env=your-org tf-plan
    make env=your-org tf-show    # review the plan output
    
  5. Once satisfied, apply:
    make env=your-org tf-apply
    
  6. Incrementally enable additional features

Manual Import Examples

Organizational Units

ROOT_OU=$(aws organizations list-roots --query Roots[0].Id --output text)
aws organizations list-organizational-units-for-parent \
  --parent-id $ROOT_OU \
  --query 'OrganizationalUnits[].[Id,Name]' --output text
import {
  to = module.organization.aws_organizations_organizational_unit.TF_VARS_KEY
  id = "ou-xxxx-xxxxxxxx"
}

IAM Identity Center (SSO)

To opt out of managing an existing Identity Center, set disable_sso_management = true in your tfvars. This is recommended for existing orgs with complex SSO configurations.

IDENTITY_STORE_ID=$(aws sso-admin list-instances \
  --query Instances[0].IdentityStoreId --output text)
SSO_INSTANCE_ARN=$(aws sso-admin list-instances \
  --query Instances[0].InstanceArn --output text)
import {
  to = module.organization.aws_identitystore_group.admin_group
  id = "$IDENTITY_STORE_ID/$GROUP_ID"
}
import {
  to = module.organization.aws_ssoadmin_permission_set.admin_permission_set
  id = "$PERMISSION_SET_ARN,$SSO_INSTANCE_ARN"
}
import {
  to = module.organization.aws_ssoadmin_managed_policy_attachment.admin_policy_attachments
  id = "arn:aws:iam::aws:policy/AdministratorAccess,$PERMISSION_SET_ARN,$SSO_INSTANCE_ARN"
}

CloudTrail

aws cloudtrail list-trails --query Trails[].TrailARN --output text
import {
  to = module.organization.aws_s3_bucket.cloudtrail_bucket[0]
  id = "YOUR_EXISTING_BUCKET_NAME"
}
import {
  to = module.organization.aws_cloudtrail.org_cloudtrail[0]
  id = "TRAIL_ARN"
}

Service Control Policies

aws organizations list-policies --filter SERVICE_CONTROL_POLICY \
  --query 'Policies[].[Id,Name]' --output text

# Get OUs targeted by a policy
aws organizations list-targets-for-policy --policy-id p-xxxxxx
import {
  to = module.organization.module.scp["POLICY_BLOCK_IDENTIFIER_FROM_TFVARS"].aws_organizations_policy.org_policy
  id = "p-xxxxxx"
}

Disabling Features for Initial Import

When importing an existing org, you may want to temporarily disable features to prevent unintended changes on the first apply:

  • CloudTrail: cloudtrail_bucket_name = null
  • SSO: disable_sso_management = true
  • Audit Role StackSet: deploy_audit_role = false
  • Security Services: use the security_services block with disable_* flags

4 - Example tfvars

A complete annotated example tfvars file.

The following is a complete example tfvars file showing all major configuration options. Copy this as a starting point and customize for your organization.

organization = {
  organization_name                 = "my-org"
  payer_name                        = "My Org Management Account"
  payer_email                       = "aws+payer@example.com"
  security_account_name             = "my-org-security"
  security_account_root_email       = "aws+security@example.com"
  cloudtrail_bucket_name            = "my-org-cloudtrail"
  cloudtrail_loggroup_name          = "CloudTrail/DefaultLogGroup"
  billing_data_bucket_name          = "my-org-cur"
  cur_report_frequency              = "DAILY"   # DAILY, HOURLY, or MONTHLY
  session_duration                  = "PT8H"
  admin_permission_set_name         = "AdministratorAccess"
  admin_group_name                  = "AllAdmins"
  disable_sso_management            = false
  deploy_audit_role                 = true
  audit_role_name                   = "security-audit"
  audit_role_stack_set_template_url = "https://s3.amazonaws.com/pht-cloudformation/aws-account-automation/AuditRole-Template.yaml"
  declarative_policy_bucket_name    = "my-org-account-status"
  vpc_flowlogs_bucket_name          = "my-org-flowlogs"
  macie_bucket_name                 = "my-org-macie-findings"

  # Custom OUs (in addition to the four required OUs)
  organization_units = {
    "Platform" = {
      name             = "Platform"
      is_child_of_root = true
    }
  }

  # AWS Accounts
  accounts = {
    dev = {
      account_name          = "my-org-dev"
      account_email         = "aws+dev@example.com"
      monthly_budget_amount = 500
    }

    prod = {
      account_name   = "my-org-prod"
      account_email  = "aws+prod@example.com"
      parent_ou_name = "Workloads"
    }

    sandbox = {
      account_name   = "my-org-sandbox"
      account_email  = "aws+sandbox@example.com"
      parent_ou_name = "Sandbox"
    }

    sso = {
      account_name    = "my-org-sso"
      account_email   = "aws+sso@example.com"
      parent_ou_name  = "Governance"
      delegated_admin = ["sso.amazonaws.com"]
    }
  }

  # Alternate contacts applied to all accounts (can be overridden per account)
  global_billing_contact = {
    name          = "Finance Team"
    title         = "CFO"
    email_address = "billing@example.com"
    phone_number  = "+14041234567"
  }

  global_security_contact = {
    name          = "Security Team"
    title         = "CISO"
    email_address = "security@example.com"
    phone_number  = "+14041234567"
  }

  global_primary_contact = {
    full_name       = "IT Operations"
    company_name    = "My Org, Inc."
    address_line_1  = "123 Main Street"
    city            = "Atlanta"
    state_or_region = "GA"
    postal_code     = "30332"
    country_code    = "US"
    email_address   = "aws@example.com"
    phone_number    = "+14041234567"
  }

  # Service Control Policies
  service_control_policies = {
    deny_root = {
      policy_name        = "DenyRoot"
      policy_description = "Denies use of root user"
      policy_json_file   = "policies/DenyRootSCP.json"
    }

    security_controls = {
      policy_name        = "DefaultSecurityControls"
      policy_description = "Base security controls for all accounts"
      policy_json_file   = "policies/SecurityControlsSCP.json.tftpl"
      policy_vars = {
        audit_role_name = "security-audit"
      }
    }
  }

  # Resource Control Policies
  resource_control_policies = {
    s3_data_perimeter = {
      policy_name        = "S3DataPerimeter"
      policy_description = "Restricts S3 to principals inside the org"
      policy_json_file   = "policies/RCP_S3DataPerimeter.json.tftpl"
      policy_vars = {
        org_id = "o-xxxxxxxxxxxx"
      }
    }
  }

  # Declarative Policies
  declarative_policies = {
    deny_public_ami = {
      policy_name        = "Block_Public_AMIs"
      policy_description = "Deny public sharing of all AMIs"
      policy_type        = "DECLARATIVE_POLICY_EC2"
      policy_json_file   = "policies/EC2ImageBPA_DCP.json"
      policy_targets     = ["Workloads", "Governance", "Sandbox"]
    }
  }

  # Security Services
  security_services = {
    disable_guardduty   = false
    disable_securityhub = false
    disable_macie       = false
  }

  # CloudFormation stacks to deploy into the payer account.
  # Use this for billing notifications, cost alarms, or other payer-scoped
  # automation. Exactly one of template_file or template_url is required.
  payer_cloudformation_stacks = {
    billing_alerts = {
      stack_name    = "slack_billing_alerts"
      template_file = "cloudformation/slack-Template.yaml"
      regions       = ["us-east-1"]
      parameters = {
        pExecutionRate = "cron(0 09 * * ? *)"
        # JSON parameter values can use a heredoc; the module canonicalizes
        # JSON-valued parameters so whitespace differences don't cause drift.
        pEventInput = <<-EOT
          {
            "threshold": "10",
            "alert_percent": "20"
          }
        EOT
        pSlackWebhookSecret = "SlackWebHook"
        pRuleState          = "ENABLED"
        pAccountDescription = "My-Payer"
      }
    }
  }

  # CloudFormation stacks to deploy into the security account via the
  # OrganizationAccountAccessRole. Same schema as payer_cloudformation_stacks.
  security_account_stacks = {
    findings_processor = {
      stack_name    = "security-findings-processor"
      template_file = "cloudformation/findings-processor-Template.yaml"
      regions       = ["us-east-1"]
      parameters = {
        pSlackWebhookSecret = "SlackWebHook"
        pSeverityThreshold  = "MEDIUM"
      }
    }
  }

  # Billing Alerts
  billing_alerts = {
    levels = {
      level1  = 100
      level2  = 500
      oh_shit = 1000
    }
    subscriptions = ["billing-alerts@example.com"]
  }

  budget_defaults = {
    alert_recipients      = ["finance@example.com"]
    currency              = "USD"
    warning_percentage    = 80
    organizational_budget = 1000
  }
}

backend_bucket = "my-org-terraform-state-123456789012"

Backend Config File

Create a my-org.tfbackend file alongside your tfvars:

bucket = "my-org-terraform-state-123456789012"
key    = "org-kickstart.tfstate"
region = "us-east-1"

Then initialize with:

terraform init -backend-config="my-org.tfbackend"