This is the multi-page printable view of this section.
Click here to print.
Return to the regular view of this page.
Documentation
Welcome to the Org Kickstart documentation. This section covers everything you need to deploy
and manage a governed AWS Organization using Terraform.
In This Section
1 - Overview
What Org Kickstart is and why you’d use it.
What is Org Kickstart?
Org Kickstart is an opinionated Terraform module for bootstrapping and managing AWS Organizations.
It is PrimeHarbor’s alternative to AWS Control Tower — designed to give you the good parts of a
landing zone without the cost, complexity, and lock-in that Control Tower brings.
Deployed into a brand new AWS Management (Payer) account, a single terraform apply can stand up
a fully governed AWS Organization with security services enabled, organizational guardrails in place,
and an account factory ready to provision new accounts.

Why not just use Control Tower?
Control Tower is a massive beast designed to support highly regulated enterprises. For most
organizations, it brings significant drawbacks:
- Expensive: Heavy reliance on AWS Config means costs scale with the number of accounts and regions.
- Inflexible: Modifications require navigating AWS Service Catalog customizations.
- Slow to update: Features like Organization CloudTrail and GuardDuty Delegated Admin lagged behind
AWS best practices for years.
- Complex: You effectively need a PhD in AWS Service Catalog to change anything.
Most organizations deploy Control Tower because their AWS Solutions Architect recommended it, or because
they needed an account factory. Org Kickstart gives you the account factory, the security posture, and
the governance — without the baggage.
What Org Kickstart does
Org Kickstart deploys and manages:
- Security Account — Creates a dedicated security account and delegates GuardDuty, Macie, Inspector, Security Hub, SSO, and CloudFormation StackSets
to a dedicated Security Account. Configures all security services in every default region
across all accounts.
- CloudTrail — Creates a CloudTrail bucket in the Security Account and enables an Organizations
CloudTrail in the Management Account.
- Alternate Contacts — Sets Billing, Operations, and Security contacts for all AWS accounts.
- Organizational Units — Creates four default OUs (Workloads, Governance, Sandbox, Suspended)
plus any custom OUs you define.
- AI Opt-Out Policy — Creates and applies a default AI opt-out policy at the root OU.
- Account Management — Manages AWS accounts and OU placement from Terraform.
- Audit Role — Deploys an Audit Role via CloudFormation StackSet to all accounts, trusting
the Security Account.
- Billing Reports — Creates an S3 bucket for billing data and an Athena-compatible CUR report.
- Organization Services — Enables all important AWS Organization Integrated Services.
- Org Policies — Manages Service Control Policies, Declarative Policies, and Resource Control Policies
with Terraform templating support.
- SSO / Identity Center — Creates an
AdministratorAccess Permission Set, an admin group,
and assigns both to every account.
What is required vs. optional?
Cannot be disabled:
- Security Account
- Four default OUs (Governance, Workloads, Sandbox, Suspended)
- AI Opt-Out Policy
- Core Organization Integrated Services
Can be disabled via variables:
- CloudTrail management:
cloudtrail_bucket_name = null
- SSO management:
disable_sso_management = true
- Audit Role StackSet:
deploy_audit_role = false
- Security services:
security_services object with disable_* flags
- CUR reports:
cur_report_frequency = "NONE"
- Alternate contacts: omit contact blocks from tfvars
Where should I go next?
2 - 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
- Complete the Bootstrap steps in the AWS Console
- Set up your local repository — copy the
local-deploy sample, pin the
module version, and create your <env>.tfvars / <env>.tfbackend
- Initialize Terraform:
make env=your-org tf-init
- Create the Security Account first (required before full apply):
terraform apply -var-file="your-org.tfvars" -target module.security_account
- 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:
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.
2.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:
- Add MFA to root
- Enable IAM access to billing
- Go to Organizations and create an Organization
- Go to AWS IAM Identity Center (SSO) and enable it
- Add yourself as a user in Identity Center
- Create a Permission Set named
TempAdministratorAccess (4-hour session recommended)
- Assign the Permission Set to the Payer/Management Account for your user
- 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
- Check email and activate your IAM Identity Center account
- Add MFA to your Identity Center account
- Configure AWS credentials in your environment:
aws configure sso
# or
export AWS_PROFILE=your-sso-profile
- 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.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
2.3 - Importing an Existing Organization
How to adopt Org Kickstart into an existing AWS Organization.
Experimental & incomplete
Importing an existing organization is **experimental and far from complete**. It does not cover
every resource Org Kickstart manages, so a first `terraform plan` against an adopted org will often
show a mix of imports, in-place changes, and **new resources**. Review that plan extremely
closely — confirm exactly what is being imported versus created (and especially that nothing is
being **destroyed**) before you apply.
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
- Run
import_org.sh and review import-org.tf
- Add your CloudTrail or billing bucket to the import file if applicable
- Review SCPs you want to import:
aws organizations list-policies --filter SERVICE_CONTROL_POLICY \
--query 'Policies[].[Id,Name]' --output text
- 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
- Once satisfied, apply:
make env=your-org tf-apply
- 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
2.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"
3 - Examples
Example configurations and the local-deploy sample layout.
Local Deploy Sample
The examples/local-deploy
directory contains a sample layout for managing your organization with Terraform from your
workstation via its Makefile. It shows how to structure your organization-specific configuration
(<env>.tfvars, <env>.tfbackend) alongside the module.
Sample tfvars
See the Example tfvars page for a full annotated example
configuration file covering all major options.
Policy Library
Ready-to-use policy files are available in the
policies/ directory:
DenyRootSCP.json — Deny root user access
SecurityControlsSCP.json.tftpl — Base security guardrails
DisableRegionsPolicy.json.tftpl — Region restrictions
RCP_S3DataPerimeter.json.tftpl — S3 data perimeter
EC2IMDSv2Enforce_DCP.json — IMDSv2 enforcement
- And more…
Importing an Existing Organization
See Importing an Existing Organization for a step-by-step guide
on adopting Org Kickstart in an existing AWS Organization.
4 - Concepts
Key concepts and architecture behind Org Kickstart.
AWS Organizations Structure
Org Kickstart manages resources in two AWS accounts simultaneously using Terraform’s
multi-provider pattern:
- Management (Payer) Account — Where Terraform runs. Contains the Organization,
OUs, SCPs, RCPs, Declarative Policies, CloudTrail, and SSO/Identity Center.
- Security Account — A dedicated account that is Delegated Administrator for
GuardDuty, Macie, Inspector, Security Hub, CloudFormation StackSets, and optionally SSO.
Required Organizational Units
Four OUs are always created and cannot be disabled:
| OU |
Purpose |
Governance |
Security and payer accounts |
Workloads |
Production workload accounts |
Sandbox |
Development accounts with more freedom |
Suspended |
Accounts pending closure |
Policy Types
AWS Organizations supports three types of preventive controls, all managed by Org Kickstart:
| Type |
Abbreviation |
Scope |
| Service Control Policies |
SCP |
What IAM principals can do |
| Resource Control Policies |
RCP |
What can be done to resources |
| Declarative Policies |
DP |
Baseline configuration of AWS services (EC2 only) |
All three are defined using the same Terraform structure and support .tftpl templating.
Security Services
Org Kickstart enables and configures the following security services across all default
AWS regions and all accounts in the organization:
- GuardDuty — Threat detection. Payer and Security account both run detectors.
- Security Hub — Aggregated security findings. Security Account is delegated admin.
- Inspector — Vulnerability management. Security Account is delegated admin.
- Macie — S3 data security. Security Account is delegated admin.
Audit Role
A cross-account audit role is deployed to every account via CloudFormation StackSets.
The role trusts the Security Account, allowing centralized read access for security
tools and audits without needing IAM users in every account.
Account Factory Pattern
New AWS accounts are defined in the accounts map in tfvars. The account module
handles everything automatically:
- Creates the AWS Organizations account
- Places it in the correct OU
- Assigns SSO access (via Identity Center group and Permission Set)
- Sets billing, operations, and security alternate contacts
- Creates a budget with alert thresholds
- Applies SCPs/RCPs/Declarative Policies inherited from the parent OU
5 - Account Configurator
Deploy the pht-account-configurator so new accounts are automatically hardened on creation.
When AWS Organizations creates a new account, a number of AWS-recommended best practices are not
turned on by default. The pht-account-configurator
is a Step Function + Lambda that listens for the “account created” EventBridge event and configures
each new account for you. Org Kickstart can deploy and manage it as an optional add-on.
Driven by a config file (see below), the configurator can:
- Set a CIS-compliant IAM password policy.
- Enable account-wide S3 Block Public Access.
- Enable EBS snapshot Block Public Access in every enabled region.
- Enable EBS default encryption in every enabled region.
- Delete the default VPCs in every region (except regions you mark to preserve, and any VPC that
still has an ENI in it).
Sections omitted from the config file are simply skipped.
How Org Kickstart deploys it
Set the account_configurator block in your organization object. When present, Org Kickstart
creates a CloudFormation stack (org-kickstart-account-configurator) from the packaged template and
uploads your config file to the Terraform state bucket. Omit the block (or set it to null) to
disable the feature.
account_configurator = {
template = "https://s3.amazonaws.com/<state-bucket>/pht-account-configurator/AccountFactory-Template-Transformed-<version>.yaml"
account_factory_config_file = "<env>-account-config.yaml"
}
| Field |
Description |
template |
S3 URL of the packaged configurator CloudFormation template. The make account-configurator target sets this for you. |
account_factory_config_file |
Path (relative to your repo root) to the YAML config file. Terraform uploads it to the state bucket; the Lambda reads it from there. |
Setting it up
The configurator is maintained in its own repo and pulled into your Org Kickstart deployment repo as
a git submodule. The examples/local-deploy Makefile ships an account-configurator target that
does the rest.
-
Add the submodule (once):
git submodule add git@github.com:primeharbor/pht-account-configurator.git pht-account-configurator
-
Run the target. Using an environment named fooli:
make env=fooli account-configurator
This will:
- check out the submodule if needed;
- seed
fooli-account-config.yaml from the submodule’s sample if you don’t already have one;
package the configurator’s CloudFormation template into your Org Kickstart state bucket; and
- point
account_configurator.template and account_configurator.account_factory_config_file
in fooli.tfvars at the freshly packaged template and your config file.
-
Review and deploy. Edit fooli-account-config.yaml to taste, then apply as usual:
make env=fooli tf-plan
make env=fooli tf-apply
Re-run make env=fooli account-configurator whenever you want to ship a new version of the
Lambda/template; it repackages and updates the template URL, and the next apply rolls it out.
The configuration file
<env>-account-config.yaml controls what the Lambda does. A full example:
# Configure the IAM Password Policy per CIS Benchmarks
account_password_policy:
update_account_password_policy: true
password_policy:
MinimumPasswordLength: 24
RequireSymbols: True
RequireNumbers: True
RequireUppercaseCharacters: True
RequireLowercaseCharacters: True
AllowUsersToChangePassword: True
MaxPasswordAge: 90
PasswordReusePrevention: 24
HardExpiry: False
# Delete Default VPCs
default_vpc:
delete_default_vpc: true
# Regions in this list will keep their default VPCs
preserve_vpc_regions:
- "us-east-1"
- "eu-central-1"
# Enable Default Encryption of EBS in all enabled regions
enable_ebs_default_encryption: true
# Enable Block Public Access for EBS snapshots in all enabled regions
enable_ebs_block_public_access: true
# Enable Account Wide S3 Block Public Access
enable_account_s3_block_public_access:
BlockPublicAcls: True
IgnorePublicAcls: True
BlockPublicPolicy: True
RestrictPublicBuckets: true
Running against existing accounts
The Step Function normally runs on account creation, but you can also
trigger it manually against an existing
account. Be aware of two risks before doing so:
- Enabling account-wide S3 Block Public Access can break legitimately public S3 buckets.
- Enabling EBS default encryption may interact with existing custom KMS key configurations.
Pair S3 Block Public Access with an SCP that prevents accounts from disabling it, and route any
genuinely public content to a dedicated account.
6 - Tasks
How-to guides for common Org Kickstart operations.
This section contains task-oriented guides for common Org Kickstart operations.
Common Tasks
6.1 - Adding a New Account
How to add a new AWS account to your organization.
Adding a new AWS account is the most common operation in Org Kickstart. All account configuration
lives in the accounts map in your tfvars file.
Steps
-
Add an entry to the accounts map in your tfvars file:
accounts = {
# ... existing accounts ...
my_new_account = {
account_name = "my-org-new-account"
account_email = "aws+new-account@example.com"
parent_ou_name = "Workloads"
monthly_budget_amount = 500
}
}
-
Plan and apply:
make env=your-org tf-execute
Or step-by-step to review the plan before applying:
make env=your-org tf-plan
make env=your-org tf-show
make env=your-org tf-apply
make env=your-org granted # (if you're using granted)
Org Kickstart will create the AWS account, place it in the correct OU, assign SSO access,
set alternate contacts, and apply any policies that target the parent OU. See Granted Support for more information about how org-kickstart can seed your granted registry for you.
Account Options
| Option |
Description |
account_name |
Display name for the AWS account |
account_email |
Root email address (must be globally unique) |
parent_ou_name |
Place the account in this OU (by name) |
parent_ou_id |
Place the account in this OU (by ID) |
monthly_budget_amount |
Budget alert threshold in USD |
delegated_admin |
List of AWS services to delegate admin for |
close_on_deletion |
Whether to close the account when removed from Terraform |
primary_contact |
Override the global primary contact for this account |
Notes
- The
account_email must be unique across all AWS accounts globally
- New accounts are created by AWS Organizations and may take a few minutes to become available
- The Security Account is managed separately via the
security_account block
6.2 - Creating Policies
How to create and attach SCPs, RCPs, and Declarative Policies.
Org Kickstart manages three types of AWS Organizations policies via the same Terraform pattern:
Service Control Policies (SCPs), Resource Control Policies (RCPs), and
Declarative Policies.
Steps
-
Create a policy JSON file in the policies/ directory of your deployment repo.
For dynamic values, use a .tftpl extension and Terraform template syntax.
// policies/MyPolicy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "s3:DeleteBucket",
"Resource": "*"
}
]
}
-
Add the policy definition to the appropriate block in your tfvars file:
service_control_policies = {
deny_s3_delete = {
policy_name = "DenyS3BucketDeletion"
policy_description = "Prevent deletion of S3 buckets"
policy_json_file = "policies/MyPolicy.json"
policy_targets = ["Workloads", "Sandbox"]
}
}
-
Plan and apply:
make env=your-org tf-execute
SCPs and RCPs are high-impact changes. Always review the plan before applying:
make env=your-org tf-plan
make env=your-org tf-show
make env=your-org tf-apply
You can also run a security scan of the plan with Checkov before applying:
make env=your-org checkov
Policy Types
| Block |
AWS Type |
service_control_policies |
Service Control Policy (SCP) |
resource_control_policies |
Resource Control Policy (RCP) |
declarative_policies |
Declarative Policy (EC2) |
Declarative Policies also require policy_type = "DECLARATIVE_POLICY_EC2".
Targeting OUs
policy_targets accepts a list of OU names or OU IDs. Use "Root" to target the
organization root:
policy_targets = ["Root"] # all accounts
policy_targets = ["Workloads", "Sandbox"] # by name
policy_targets = ["ou-xxxx-xxxxxxxx"] # by ID
Templated Policies
For policies that need org-specific values, use a .tftpl file:
// policies/AuditRoleProtection.json.tftpl
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Deny",
"Action": ["iam:Delete*"],
"Resource": "arn:aws:iam::*:role/${audit_role_name}"
}]
}
service_control_policies = {
protect_audit_role = {
policy_name = "ProtectAuditRole"
policy_json_file = "policies/AuditRoleProtection.json.tftpl"
policy_vars = {
audit_role_name = "security-audit"
}
}
}
Sample Policies
See the policies/ directory
in the repository for a library of ready-to-use policies. Or check out PrimeHarbor’s respository of Organizational Policies.
7 - Granted Support
Generate an AWS CLI / Granted config from your org and share it with your team as a Granted Registry.
Granted is a tool for quickly assuming roles across many AWS accounts via
IAM Identity Center (SSO). Org Kickstart can generate a ready-to-use AWS config from your
Terraform outputs, and your org-kickstart repo can double as a Granted
Registry so your whole team gets the same set of account profiles.
The examples below use an environment named fooli. Substitute your own env name throughout.
Prerequisites
- Granted is installed (
brew install granted or see the Granted docs).
- IAM Identity Center is enabled and your org has been deployed at least once, so a
terraform apply has produced an output-<env>.json file. The generator reads the
account_map, sso_start_url, sso_region, and sso_role_name outputs.
Generating an AWS config with make granted
After an apply, run the granted Makefile target:
This runs scripts/generate_granted_config.sh against output-fooli.json and writes an AWS CLI
config to granted/aws-config. The file contains one sso-session block plus one profile per
account in your organization:
[sso-session fooli-sso]
sso_start_url = https://fooli.awsapps.com/start
sso_region = us-east-1
sso_registration_scopes = sso:account:access
[profile fooli-payer]
sso_session = fooli-sso
sso_account_id = 111111111111
sso_role_name = AdministratorAccess
[profile fooli-sandbox]
sso_session = fooli-sso
sso_account_id = 222222222222
sso_role_name = AdministratorAccess
Profiles are named after the keys in your accounts map (plus the payer and security accounts), so
re-running make granted after adding accounts keeps the config in sync. Regenerate it any time the
account list changes.
Once the profiles are in your AWS config you can assume any account with Granted:
assume fooli-sandbox
# or log the whole SSO session in first
aws sso login --sso-session fooli-sso
Sharing your repo as a Granted Registry
A Granted Registry lets everyone on the team pull the same account profiles straight from a git
repo — no copy/pasting config files. Since your org-kickstart repo already generates the config,
it’s the natural place to host the registry.
-
Confirm the granted.yml manifest. The examples/local-deploy sample already ships one at the
repo root pointing at the generated config, so there’s nothing to create — just keep it. It
looks like this:
awsConfig:
- ./granted/aws-config
-
Commit the generated config. Run make env=fooli granted and commit both granted.yml and
granted/aws-config to the repo. (Re-run and commit whenever accounts change.)
-
Team members add the registry once:
granted registry add -n fooli-admin -u git@github.com:fooli/fooli-org-kickstart.git
Granted clones the repo, reads granted.yml, and merges the referenced profiles into the user’s
~/.aws/config. From then on they can assume fooli-payer, assume fooli-sandbox, and so on.
-
Stay current. When the registry repo is updated, team members pull the latest profiles with:
Tip: keep the registry in a private repo. The generated config contains AWS account IDs and
your SSO start URL. Those aren’t secrets on their own, but there’s no reason to publish them.
8 - Reference
Variable reference, policy library, and module documentation.
This section contains low-level reference documentation for Org Kickstart.
In This Section
Source
The complete module documentation is generated from the Terraform source and available in the
ModuleDocs.md file in
the repository.
Policy Library
Sample policies are included in the policies/
directory of the repository:
| File |
Type |
Description |
DenyRootSCP.json |
SCP |
Deny use of root user in all accounts |
SecurityControlsSCP.json.tftpl |
SCP |
Base security controls (requires audit_role_name) |
DisableRegionsPolicy.json.tftpl |
SCP |
Restrict to approved AWS regions |
DenyUnapprovedInstanceTypes.json |
SCP |
Deny non-approved EC2 instance types |
DenyUnapprovedServices.json |
SCP |
Deny unapproved AWS services |
SuspendedAccountsPolicy.json.tftpl |
SCP |
Deny all activity in suspended accounts |
RCP_S3DataPerimeter.json.tftpl |
RCP |
Restrict S3 access to org principals |
EC2ImageBPA_DCP.json |
Declarative |
Block public sharing of AMIs |
EC2SnapshotBPA_DCP.json |
Declarative |
Block public sharing of EBS snapshots |
EC2IMDSv2Enforce_DCP.json |
Declarative |
Enforce IMDSv2 with hop limit of 2 |
Policies with the .tftpl extension support Terraform template variables via policy_vars.
8.1 - Module Documentation
Auto-generated Terraform module reference — inputs, outputs, resources, and sub-modules.
This page is auto-generated from the Terraform source in
[org-kickstart](https://github.com/primeharbor/org-kickstart).
Run `make generate-module-docs` in the `org-kickstart-site/` directory to refresh it.
Requirements
Providers
Modules
Resources
| Name |
Description |
Type |
Default |
Required |
| account_configurator |
Serverless Application to configure new accounts. See https://github.com/primeharbor/pht-account-configurator |
object({ account_factory_config_file = string template = string }) |
null |
no |
| accounts |
AWS accounts to provision in the organization. |
map( object({ account_name = string account_email = string delegated_admin = optional(list(string), []) operations_contact = optional(object({ name = string title = string email_address = string phone_number = string })) primary_contact = optional(object({ full_name = string company_name = optional(string) address_line_1 = string address_line_2 = optional(string) address_line_3 = optional(string) city = string district_or_county = optional(string) state_or_region = optional(string) postal_code = string country_code = string phone_number = string website_url = optional(string) })) # parent_ou_id can explicitly override the OU assignment and lookup by name. # parent_ou_id takes precedence over parent_ou_name parent_ou_name = optional(string, “Workloads”) parent_ou_id = optional(string, null) monthly_budget_amount = optional(number, 0) budget_alert_recipients = optional(list(string), []) service_control_policies = optional(list(string), []) resource_control_policies = optional(list(string), []) declarative_policies_ec2 = optional(list(string), []) }) ) |
n/a |
yes |
| admin_group_name |
Name of the Identity Store Group with all the admin users |
string |
"AllAdmins" |
no |
| admin_permission_set_name |
Name of the Default Admin Permission Set to Create |
string |
"AdministratorAccess" |
no |
| audit_role_name |
Name of the AuditRole to deploy |
string |
"security-audit" |
no |
| audit_role_stack_set_template_url |
URL that points to the Audit Role Policy Template |
string |
null |
no |
| aws_service_access_principals_to_enable |
List of AWS service access principals to enable if they’re not part of the default set. |
list(string) |
[] |
no |
| aws_service_access_principals_to_exclude |
List of AWS service access principals to exclude from the default set. |
list(string) |
[] |
no |
| backend_bucket |
Name of the S3 bucket used for the CloudFormation stacks and Terraform state backend |
string |
n/a |
yes |
| billing_alerts |
Triggers for billing alerts and who should receive them. |
object({ levels = map(number) subscriptions = list(string) }) |
null |
no |
| billing_data_bucket_name |
Name of the S3 Bucket for CUR reports. Set to null to disable CUR report generation. |
string |
null |
no |
| budget_defaults |
Default values for AWS Budgets. Some settings can be overridden in the account definition. |
object({ alert_recipients = optional(list(string), []) currency = optional(string, “USD”) warning_percentage = optional(number, 85) organizational_budget = optional(number, 0) }) |
{} |
no |
| cloudtrail_bucket_name |
Name of the S3 Bucket to create to store CloudTrail events. Set to null to disable CloudTrail management |
string |
null |
no |
| cloudtrail_loggroup_name |
Name of the CloudWatch Log Group in the payer account where CloudTrail will send its events. Set to null to disable CloudTrail to CloudWatch Logs. |
string |
null |
no |
| cur_report_frequency |
Frequency CUR reports should be delivered (DAILY, HOURLY, MONTHLY). Set to NONE to disable |
string |
"NONE" |
no |
| datatrail |
Details on the DataTrails |
object({ bucket_name = string trail_name = string enabled = optional(bool, true) excluded_buckets = list(string) }) |
null |
no |
| declarative_policies |
Map of Declarative Policies to create and attach. |
map(object({ policy_name = string policy_type = string policy_description = string policy_json_file = string policy_targets = optional(list(string), [“Root”]) policy_vars = optional(map(string), {}) do_not_attach = optional(bool, false) })) |
{} |
no |
| declarative_policy_bucket_name |
Name of S3 Bucket for Declarative Policy Reports. Set to null to disable Declarative Policy Reports. |
string |
null |
no |
| default_close_on_deletion |
If set, the AWS Account will be closed when it’s removed from org-kickstart. Set this with caution. |
bool |
false |
no |
| deploy_audit_role |
Boolean to determine if org-kickstart should manage a Security Audit Role. Set to false to disable the creation of an Audit Role stackset. |
bool |
true |
no |
| disable_sso_management |
Set to true to disable creating a default Admin Permission Set in Identity Center. |
bool |
false |
no |
| global_billing_contact |
Billing alternate contact to be applied to all accounts. |
object({ name = string title = string email_address = string phone_number = string }) |
null |
no |
| global_operations_contact |
Default operations alternate contact to be applied to all accounts. Can be overridden in account definition. |
object({ name = string title = string email_address = string phone_number = string }) |
null |
no |
| global_primary_contact |
Default primary account owner to be applied to all accounts. Can be overridden in account definition. |
object({ full_name = string company_name = optional(string) address_line_1 = string address_line_2 = optional(string) address_line_3 = optional(string) city = string district_or_county = optional(string) state_or_region = optional(string) postal_code = string country_code = string phone_number = string website_url = optional(string) }) |
null |
no |
| global_security_contact |
Security alternate contact to be applied to all accounts |
object({ name = string title = string email_address = string phone_number = string }) |
null |
no |
| macie_bucket_name |
Name of the S3 Bucket to create to store Macie Findings. Set to null to skip creation |
string |
null |
no |
| manage_state_bucket |
Manage the S3 bucket named by backend_bucket (the bucket that holds this Terraform state) with Terraform. The bucket must already exist (it has to, in order to run Terraform at all), so it is adopted via an import block in the calling module rather than created. When true, Terraform enforces versioning, public-access-block, and encryption on it. Set to false to leave the bucket entirely outside Terraform’s management. |
bool |
true |
no |
| organization_name |
Name of the Organization. This is used for resource prefixes and general reference |
string |
n/a |
yes |
| organization_policy_types_to_exclude |
List of organization policy types to exclude from the default set. |
list(string) |
[] |
no |
| organization_units |
Map of OUs to create. |
map( object({ name = string is_child_of_root = optional(bool) # This is ignored, it is retained for backwards compatibility parent_id = optional(string) }) ) |
{} |
no |
| payer_cloudformation_stacks |
Map of CloudFormation stacks to deploy into the payer account. Exactly one of template_file (local path relative to path.root) or template_url (S3/HTTPS URL) must be set per stack. If regions is omitted, the stack is deployed only in the base org-kickstart region. |
map( object({ stack_name = string template_file = optional(string) template_url = optional(string) regions = optional(list(string)) parameters = optional(map(string), {}) timeout_in_minutes = optional(number, 15) on_failure = optional(string, “DO_NOTHING”) }) ) |
{} |
no |
| payer_email |
Root Email address for the Organization Management account |
string |
n/a |
yes |
| payer_name |
Name of the Organization Management account |
string |
"AWS Payer" |
no |
| resource_control_policies |
Map of RCPs to create and attach. |
map( object({ policy_name = string policy_description = string policy_json_file = string policy_targets = optional(list(string), [“Root”]) policy_vars = optional(map(string), {}) do_not_attach = optional(bool, false) }) ) |
{} |
no |
| security_account |
Settings for the Security Account. |
object({ # These will move from the main module to here at some point. # account_name = string # account_email = string delegated_admin = optional(list(string), []) operations_contact = optional(object({ name = string title = string email_address = string phone_number = string })) primary_contact = optional(object({ full_name = string company_name = optional(string) address_line_1 = string address_line_2 = optional(string) address_line_3 = optional(string) city = string district_or_county = optional(string) state_or_region = optional(string) postal_code = string country_code = string phone_number = string website_url = optional(string) })) monthly_budget_amount = optional(number, 0) budget_alert_recipients = optional(list(string), []) }) |
n/a |
yes |
| security_account_name |
Name of the Security Account |
string |
"Security Account" |
no |
| security_account_root_email |
Root Email address for the security account |
string |
n/a |
yes |
| security_account_stacks |
Map of CloudFormation stacks to deploy into the security account. Exactly one of template_file (local path relative to path.root) or template_url (S3/HTTPS URL) must be set per stack. If regions is omitted, the stack is deployed only in the base org-kickstart region. |
map( object({ stack_name = string template_file = optional(string) template_url = optional(string) regions = optional(list(string)) parameters = optional(map(string), {}) timeout_in_minutes = optional(number, 15) on_failure = optional(string, “DO_NOTHING”) }) ) |
{} |
no |
| security_services |
Explicitly disable or not manage a security service |
object({ disable_guardduty = optional(bool, false) disable_macie = optional(bool, false) disable_inspector = optional(bool, false) disable_securityhub = optional(bool, false) disable_stacksets = optional(bool, false) }) |
n/a |
yes |
| service_control_policies |
Map of SCPs to create and attach. |
map( object({ policy_name = string policy_description = string policy_json_file = string policy_targets = optional(list(string), [“Root”]) policy_vars = optional(map(string), {}) do_not_attach = optional(bool, false) }) ) |
{} |
no |
| session_duration |
Admin Permission Set Session Duration |
string |
"PT8H" |
no |
| sso_instance_region |
Region where the AWS SSO instance is configured |
string |
"us-east-1" |
no |
| sso_start_url |
AWS SSO start URL (e.g., https://yourorg.awsapps.com/start) |
string |
"https://NOT-PROVIDED.awsapps.com/start" |
no |
| tag_set |
Default map of tags to be applied to all resources via all providers |
map(string) |
{} |
no |
| vpc_flowlogs_bucket_name |
Name of the S3 Bucket to create to store VPC Flow Logs. Set to null to skip creation |
string |
null |
no |
Outputs
8.2 - Parameter Reference
All Terraform variables for the Org Kickstart module.
All configuration is passed via the organization variable (an object type) and a few
top-level variables. Below are the key configuration parameters.
Top-Level Variables
| Variable |
Type |
Required |
Description |
organization |
object |
Yes |
Main configuration object (see below) |
backend_bucket |
string |
Yes |
S3 bucket name for Terraform state |
Organization Object
Core Identity
| Parameter |
Type |
Required |
Description |
organization_name |
string |
Yes |
Short name for the organization |
payer_name |
string |
Yes |
Display name for the management account |
payer_email |
string |
Yes |
Root email of the management account |
security_account_name |
string |
Yes |
Display name for the security account |
security_account_root_email |
string |
Yes |
Root email for the new security account |
State Bucket
| Parameter |
Type |
Default |
Description |
manage_state_bucket |
bool |
true |
Manage the backend_bucket (the S3 bucket holding Terraform state) with Terraform. The bucket must already exist; it is adopted via an import block in the calling module (see examples/local-deploy/main.tf), and Terraform then enforces versioning, public-access-block, and AES256 encryption on it. It is protected with prevent_destroy. Set to false to leave the bucket unmanaged. |
CloudTrail
| Parameter |
Type |
Default |
Description |
cloudtrail_bucket_name |
string |
required |
S3 bucket for CloudTrail logs. Set null to disable |
cloudtrail_loggroup_name |
string |
"CloudTrail/DefaultLogGroup" |
CloudWatch Log Group name |
SSO / Identity Center
| Parameter |
Type |
Default |
Description |
session_duration |
string |
"PT8H" |
ISO 8601 duration for SSO sessions |
admin_permission_set_name |
string |
"AdministratorAccess" |
Name of the admin Permission Set |
admin_group_name |
string |
"AllAdmins" |
Name of the admin Identity Center group |
disable_sso_management |
bool |
false |
Set true to stop Terraform from managing SSO |
sso_instance_region |
string |
"us-east-1" |
Region where the IAM Identity Center instance is configured |
sso_start_url |
string |
required when SSO is managed |
AWS access portal URL, e.g. https://yourorg.awsapps.com/start |
Audit Role
| Parameter |
Type |
Default |
Description |
deploy_audit_role |
bool |
true |
Deploy the cross-account audit role StackSet |
audit_role_name |
string |
"security-audit" |
Name of the audit role in each account |
audit_role_stack_set_template_url |
string |
required if deploy |
S3 URL to the CloudFormation template |
Billing
| Parameter |
Type |
Required |
Description |
billing_data_bucket_name |
string |
Yes |
S3 bucket for CUR reports |
cur_report_frequency |
string |
"DAILY" |
DAILY, HOURLY, MONTHLY, or "NONE" to disable |
declarative_policy_bucket_name |
string |
Yes |
S3 bucket for declarative policy documents |
Security Services
Configure which security services to enable/disable (each defaults to false):
security_services = {
disable_guardduty = false
disable_macie = false
disable_inspector = false
disable_securityhub = false
disable_stacksets = false
}
Log & Findings Buckets
| Parameter |
Type |
Default |
Description |
vpc_flowlogs_bucket_name |
string |
null |
S3 bucket to create for VPC Flow Logs. null to skip |
macie_bucket_name |
string |
null |
S3 bucket to create for Macie findings. null to skip |
Organization Services & Policy Types
org-kickstart enables an opinionated set of AWS service-access principals and organization policy types. Adjust the defaults with:
| Parameter |
Type |
Default |
Description |
aws_service_access_principals_to_enable |
list(string) |
[] |
Extra service principals to enable beyond the default set |
aws_service_access_principals_to_exclude |
list(string) |
[] |
Service principals to remove from the default set |
organization_policy_types_to_exclude |
list(string) |
[] |
Org policy types to exclude from the default set (e.g. when a partition doesn’t support one) |
Account Configurator
Optional integration with pht-account-configurator to harden new accounts on creation — see the Account Configurator guide. Set the account_configurator object, or omit it to disable:
| Field |
Type |
Description |
template |
string |
S3 URL of the packaged configurator CloudFormation template |
account_factory_config_file |
string |
Path (relative to repo root) to the YAML config file; Terraform uploads it to the state bucket |
DataTrail
Optional DataTrail (CloudTrail S3 data-event) configuration in the security account. Set the datatrail object, or omit it to disable:
| Field |
Type |
Default |
Description |
bucket_name |
string |
required |
S3 bucket holding the DataTrail logs |
trail_name |
string |
required |
Name of the CloudTrail data-event trail |
enabled |
bool |
true |
Whether the trail is enabled |
excluded_buckets |
list(string) |
required |
S3 buckets to exclude from data-event logging |
Organizational Units
Four OUs are always created: Governance, Workloads, Sandbox, Suspended.
Additional OUs can be defined:
organization_units = {
"MyOU" = {
name = "MyOU"
is_child_of_root = true
}
"NestedOU" = {
name = "NestedOU"
parent_id = "MyOU" # use parent OU name for direct children of custom OUs
}
}
Accounts
Each account in the accounts map:
accounts = {
my_account = {
account_name = "my-org-prod" # AWS account display name
account_email = "aws+prod@example.com" # root email (must be unique)
parent_ou_name = "Workloads" # OU name (or use parent_ou_id)
monthly_budget_amount = 1000 # optional, in USD
delegated_admin = ["service.amazonaws.com"] # optional
# Optional: override primary contact for this account
primary_contact = {
full_name = "Account Owner"
company_name = "My Org"
address_line_1 = "123 Main St"
city = "Atlanta"
state_or_region = "GA"
postal_code = "30332"
country_code = "US"
email_address = "owner@example.com"
phone_number = "+14041234567"
}
}
}
The org-wide default_close_on_deletion (bool, default false) controls whether an account is
closed — not just removed from the org — when it leaves org-kickstart. Use with caution.
Security Account
The security account is created and managed like a workload account, but its settings live in a
dedicated required security_account block. Its name and root email come from
security_account_name / security_account_root_email; everything else is optional:
security_account = {
delegated_admin = ["guardduty.amazonaws.com"] # services it is delegated admin for
monthly_budget_amount = 100 # optional, USD
budget_alert_recipients = ["security@example.com"] # optional
# operations_contact and primary_contact may also be set (same shape as an account entry)
}
Applied org-wide (overridable per account):
global_billing_contact = {
name = "Name"
title = "CFO"
email_address = "billing@example.com"
phone_number = "+1xxxxxxxxxx"
}
global_security_contact = {
name = "Name"
title = "CISO"
email_address = "security@example.com"
phone_number = "+1xxxxxxxxxx"
}
global_operations_contact = {
name = "Name"
title = "VP Engineering"
email_address = "ops@example.com"
phone_number = "+1xxxxxxxxxx"
}
global_primary_contact = {
full_name = "Name"
company_name = "My Org"
address_line_1 = "123 Main St"
city = "Atlanta"
state_or_region = "GA"
postal_code = "30332"
country_code = "US"
email_address = "aws@example.com"
phone_number = "+1xxxxxxxxxx"
}
Policies
All three policy types follow the same structure:
service_control_policies = {
my_policy = {
policy_name = "MyPolicy"
policy_description = "Description"
policy_json_file = "policies/MyPolicy.json"
policy_targets = ["Workloads", "ou-xxxx-xxxxxxxx"] # OU names or IDs
policy_vars = { # for .tftpl files
variable_name = "value"
}
}
}
The same structure applies to resource_control_policies and declarative_policies
(which also require policy_type = "DECLARATIVE_POLICY_EC2").
Billing Alerts
billing_alerts = {
levels = {
level1 = 100 # USD threshold
level2 = 500
oh_shit = 1000
}
subscriptions = ["email@example.com"]
}
budget_defaults = {
alert_recipients = ["email@example.com"]
currency = "USD"
warning_percentage = 80
organizational_budget = 1000
}
payer_cloudformation_stacks deploys arbitrary CloudFormation stacks into the
payer (management) account. It’s the right hook for things like billing
notifications, payer-scoped cost-explorer alarms, or other automation that has
to live in the payer because of where billing data and Organizations APIs are
exposed.
Each map entry creates one stack per region listed. The Terraform resource
addresses use a <key>-<region> composite, so adding a region later does not
rename existing stacks.
| Field |
Type |
Default |
Description |
stack_name |
string |
required |
CloudFormation stack name |
template_file |
string |
conditional |
Local path to template, relative to the caller’s path.root. Exactly one of template_file or template_url is required |
template_url |
string |
conditional |
HTTPS/S3 URL to the template. Exactly one of template_file or template_url is required |
regions |
list(string) |
base org-kickstart region |
Regions to deploy the stack into |
parameters |
map(string) |
{} |
CloudFormation parameter values |
timeout_in_minutes |
number |
15 |
Stack create/update timeout |
on_failure |
string |
"DO_NOTHING" |
One of DO_NOTHING, ROLLBACK, DELETE |
All stacks are created with the full set of CloudFormation capabilities
(CAPABILITY_IAM, CAPABILITY_NAMED_IAM, CAPABILITY_AUTO_EXPAND).
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-valued parameters can be written as heredocs. The module
# canonicalizes any value that parses as JSON, so multi-line
# formatting does not cause drift on subsequent plans.
pEventInput = <<-EOT
{
"threshold": "10",
"alert_percent": "20"
}
EOT
pSlackWebhookSecret = "SlackWebHook"
pRuleState = "ENABLED"
pAccountDescription = "My-Payer"
}
}
}
Security Account Stacks
security_account_stacks is the same idea as payer_cloudformation_stacks,
but the stacks are deployed into the Security Account via the
OrganizationAccountAccessRole. Use it for security-scoped automation that
needs to live in the delegated-admin account: Security Hub finding processors,
GuardDuty event handlers, KMS keys for security data, ChatBot integrations,
and similar.
Schema, defaults, composite-key behavior, capabilities, and JSON parameter
canonicalization are identical to 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"
}
}
}
9 - Contribution Guidelines
How to contribute to Org Kickstart
We welcome contributions to both the Terraform module and this documentation site.
All submissions require review via GitHub pull request.
Contributing to the Module
- Fork the org-kickstart repo on GitHub
- Create a feature branch
- Make your changes and test them against a real AWS Organization (see the CLAUDE.md for testing guidance)
- Send a pull request with a clear description of the change and why it’s needed
Testing
When modifying the module:
- Use
make env=your-org tf-plan extensively before applying
- Test in a non-production organization first
- Use targeted applies for risky changes:
terraform apply -var-file="your-org.tfvars" -target <resource>
- Verify SCPs don’t lock out root or prevent remediation
Contributing to the Docs
The documentation source lives in the org-kickstart-site/ directory alongside the module.
Running the Site Locally
Requirements:
- Hugo extended >= 0.155.0
- Node.js (for PostCSS/SCSS processing)
cd org-kickstart-site
make npm # install Node dependencies (first time only)
make test # starts dev server at http://localhost:1319/
Use make test-drafts to also render draft and future-dated content.
Editing Pages
- Fork the org-kickstart-site repo
- Edit files under
org-kickstart-site/content/en/
- Preview locally with
make test
- Submit a pull request
You can also click Edit this page in the top right of any documentation page to
jump directly to the source file on GitHub.
Creating an Issue
Found a bug or want to request a feature? Open an issue on
GitHub.
Useful Resources
10 - Releases
How Org Kickstart is versioned — the latest branch and tagged releases — and how to pin the module source.
Org Kickstart uses a simple versioning model: a rolling latest branch and fixed, tagged releases.
We target quarterly releases, but may cut a new version sooner when significant new AWS
organization-management features warrant it.
The latest branch
The default branch is latest. It always holds the most recent changes. Point your module
source at it (with no ?ref=) to track the newest code:
module "organization" {
source = "github.com/primeharbor/org-kickstart"
# ...
}
Use this if you want the newest features and are comfortable reviewing every terraform plan for
changes before you apply.
Tagged releases
Each release is a fixed, immutable git tag — for example 0.3.1. Pin to a tag for a stable,
reproducible deployment:
module "organization" {
source = "github.com/primeharbor/org-kickstart?ref=0.3.1"
# ...
}
The examples/local-deploy
sample pins to the current release by default. Bump the ?ref= value when you’re ready to move to a
newer release — and read its release notes for breaking changes first.
Release notes
Per-release changes — new features, breaking changes, and migration steps — are published as
release notes on the blog, and in each release’s docs/v<version>-notes.md file
in the repository.
Which should I use?
- Production: pin to a tagged release (
?ref=0.3.1) and upgrade deliberately after reviewing
the release notes.
- Evaluating or lab environments: track
latest for the newest capabilities.