AWS accounts are a critical object for structuring, securing, and managing your AWS cloud resources. While they are foundational for best practices cloud usage, they can be annoying and tedious to set up appropriately and in a streamlined way.
In this tutorial, we'll cover how you can set up an AWS ControlTower Account Factory, and give developers an easy method to create new accounts - using Terraform (and all the benefits of infrastructure as code) under the hood!
We'll accomplish this by converting some Terraform modules into Resourcely Blueprints, generating a smart UI that developers can interact with instead of needing to work with Terraform modules directly.
Setting up an AWS Control Tower Account Factory for Terraform (AFT)
Control Tower Account Factory exists to streamline the creation, orchestration, and management of AWS accounts.
Module
There is a Terraform version of Account Factory, that lets Account Factory users enjoy the benefits of infrastructure as code: versioning, review, code as documentation, and more.
The top-level module in this repository sets up Account Factory inside of a landing zone.
Creating a Blueprint from the module
We'll turn this top-level module into a Blueprint and customize it. This will automatically create a UI that developers can use to generate Terraform code for making accounts, streamlining the process for spinning up Account Factory landing zones and new AWS accounts. To do so, we only need to import the module and make some adjustments.
Log in to Resourcely and navigate to the Import screen (Blueprints -> Create a Blueprint -> Import a Module).
Resourcely automatically detects the variables from the AFT module and assigns types, defaults, descriptions, and more.
You can choose which fields to omit, and which fields to include. Our version of the AFT landing zone Blueprint is available here.
Once you have chosen which fields to include and exclude, hit Continue and move to the Groups screen. This allows you to choose how the UI will be organized.
The following Blueprint is generated, based on our AFT module. The Blueprint automatically creates variables for each input, which will be exposed as inputs in our form (see the Developer Experience tab).
AFT Module Blueprint
---
variables:
account_customizations_repo_branch:
default: main
desc: Branch to source account customizations repo from
required: false
type: resource.aws_ssm_parameter.value
account_customizations_repo_name:
default: aft-account-customizations
desc: Repository name for the account customizations files. For non-CodeCommit
repos, name should be in the format of Org/Repo
required: false
type: resource.aws_ssm_parameter.value
account_provisioning_customizations_repo_branch:
default: main
desc: Branch to source account provisioning customization files
required: false
type: resource.aws_ssm_parameter.value
account_provisioning_customizations_repo_name:
default: aft-account-provisioning-customizations
desc: Repository name for the account provisioning customizations files. For
non-CodeCommit repos, name should be in the format of Org/Repo
required: false
type: resource.aws_ssm_parameter.value
account_request_repo_branch:
default: main
desc: Branch to source account request repo from
group: Repos
order: 0
required: false
type: resource.aws_ssm_parameter.value
account_request_repo_name:
default: aft-account-request
desc: Repository name for the account request files. For non-CodeCommit repos,
name should be in the format of Org/Repo
group: Repos
order: 1
required: false
type: resource.aws_ssm_parameter.value
aft_backend_bucket_access_logs_object_expiration_days:
default: 365
desc: Amount of days to keep the objects stored in the access logs bucket for
AFT backend buckets
required: false
type: resource.aws_s3_bucket_lifecycle_configuration.rule.noncurrent_version_expiration.noncurrent_days
aft_enable_vpc:
default: true
desc: Flag turning use of VPC on/off for AFT
group: VPCs
order: 0
required: false
type: bool
aft_feature_cloudtrail_data_events:
default: "false"
desc: Feature flag toggling CloudTrail data events on/off
required: false
type: resource.aws_ssm_parameter.value
aft_feature_delete_default_vpcs_enabled:
default: "false"
desc: Feature flag toggling deletion of default VPCs on/off
group: VPCs
order: 1
required: false
type: resource.aws_ssm_parameter.value
aft_feature_enterprise_support:
default: "false"
desc: Feature flag toggling Enterprise Support enrollment on/off
required: false
type: resource.aws_ssm_parameter.value
aft_framework_repo_git_ref:
desc: Git branch from which the AFT framework should be sourced from
group: Repos
order: 2
required: false
type: resource.aws_ssm_parameter.value
aft_framework_repo_url:
default: https://github.com/aws-ia/terraform-aws-control_tower_account_factory.git
desc: Git repo URL where the AFT framework should be sourced from
required: false
type: resource.aws_ssm_parameter.value
aft_management_account_id:
desc: AFT Management Account ID
type: resource.aws_ssm_parameter.value
aft_metrics_reporting:
default: "true"
desc: Flag toggling reporting of operational metrics
required: false
type: resource.aws_ssm_parameter.value
aft_vpc_cidr:
default: 192.168.0.0/22
desc: CIDR Block to allocate to the AFT VPC
required: false
type: resource.aws_vpc.cidr_block
aft_vpc_endpoints:
default: true
desc: Flag turning VPC endpoints on/off for AFT VPC
required: false
type: bool
aft_vpc_private_subnet_01_cidr:
default: 192.168.0.0/24
desc: CIDR Block to allocate to the Private Subnet 01
required: false
type: resource.aws_subnet.cidr_block
aft_vpc_private_subnet_02_cidr:
default: 192.168.1.0/24
desc: CIDR Block to allocate to the Private Subnet 02
required: false
type: resource.aws_subnet.cidr_block
aft_vpc_public_subnet_01_cidr:
default: 192.168.2.0/25
desc: CIDR Block to allocate to the Public Subnet 01
required: false
type: resource.aws_subnet.cidr_block
aft_vpc_public_subnet_02_cidr:
default: 192.168.2.128/25
desc: CIDR Block to allocate to the Public Subnet 02
required: false
type: resource.aws_subnet.cidr_block
audit_account_id:
desc: Audit Account Id
type: resource.aws_ssm_parameter.value
backup_recovery_point_retention:
desc: Number of days to keep backup recovery points in AFT DynamoDB tables.
Default = Never Expire
required: false
type: resource.aws_backup_plan.rule.lifecycle.delete_after
cloudwatch_log_group_retention:
default: 0
desc: Amount of days to keep CloudWatch Log Groups for Lambda functions. 0 =
Never Expire
required: false
type: resource.aws_cloudwatch_log_group.retention_in_days
concurrent_account_factory_actions:
default: 5
desc: Maximum number of accounts that can be provisioned in parallel.
required: false
type: number
ct_home_region:
desc: The region from which this module will be executed. This MUST be the same
region as Control Tower is deployed.
type: resource.aws_ssm_parameter.value
ct_management_account_id:
desc: Control Tower Management Account Id
type: resource.aws_ssm_parameter.value
github_enterprise_url:
default: "null"
desc: GitHub enterprise URL, if GitHub Enterprise is being used
required: false
type: resource.aws_ssm_parameter.value
gitlab_selfmanaged_url:
default: "null"
desc: GitLab SelfManaged URL, if GitLab SelfManaged is being used
required: false
type: resource.aws_ssm_parameter.value
global_codebuild_timeout:
default: 60
desc: Codebuild build timeout
required: false
type: number
global_customizations_repo_branch:
default: main
desc: Branch to source global customizations repo from
required: false
type: resource.aws_ssm_parameter.value
global_customizations_repo_name:
default: aft-global-customizations
desc: Repository name for the global customization files. For non-CodeCommit
repos, name should be in the format of Org/Repo
required: false
type: resource.aws_ssm_parameter.value
log_archive_account_id:
desc: Log Archive Account Id
type: resource.aws_ssm_parameter.value
log_archive_bucket_object_expiration_days:
default: 365
desc: Amount of days to keep the objects stored in the AFT logging bucket
required: false
type: resource.aws_s3_bucket_lifecycle_configuration.rule.noncurrent_version_expiration.noncurrent_days
maximum_concurrent_customizations:
default: "5"
desc: Maximum number of customizations/pipelines to run at once
required: false
type: resource.aws_ssm_parameter.value
terraform_api_endpoint:
default: https://app.terraform.io/api/v2/
desc: API Endpoint for Terraform. Must be in the format of https://xxx.xxx.
required: false
type: resource.aws_ssm_parameter.value
terraform_distribution:
default: oss
desc: Terraform distribution being used for AFT - valid values are oss, tfc, or
tfe
required: false
type: resource.aws_ssm_parameter.value
terraform_org_name:
default: "null"
desc: Organization name for Terraform Cloud or Enterprise
required: false
type: resource.aws_ssm_parameter.value
terraform_token:
default: "null"
desc: Terraform token for Cloud or Enterprise
required: false
type: resource.aws_ssm_parameter.value
terraform_version:
default: 1.6.0
desc: Terraform version being used for AFT
required: false
type: resource.aws_ssm_parameter.value
tf_backend_secondary_region:
default: ""
desc: AFT creates a backend for state tracking for its own state as well as OSS
cases. The backend's primary region is the same as the AFT region, but
this defines the secondary region to replicate to.
required: false
type: resource.aws_ssm_parameter.value
vcs_provider:
default: codecommit
desc: Customer VCS Provider - valid inputs are codecommit, bitbucket, github,
githubenterprise, gitlab, or gitLab self-managed
required: false
type: resource.aws_ssm_parameter.value
groups:
Repos:
order: 0
VPCs:
order: 1
---
module "{{ module_name }}_{{ __guid }}" {
source = "git::https://github.com/aws-ia/terraform-aws-control_tower_account_factory"
account_customizations_repo_branch = {{ account_customizations_repo_branch }}
account_customizations_repo_name = {{ account_customizations_repo_name }}
account_provisioning_customizations_repo_branch = {{ account_provisioning_customizations_repo_branch }}
account_provisioning_customizations_repo_name = {{ account_provisioning_customizations_repo_name }}
account_request_repo_branch = {{ account_request_repo_branch }}
account_request_repo_name = {{ account_request_repo_name }}
aft_backend_bucket_access_logs_object_expiration_days = {{ aft_backend_bucket_access_logs_object_expiration_days }}
aft_enable_vpc = {{ aft_enable_vpc }}
aft_feature_cloudtrail_data_events = {{ aft_feature_cloudtrail_data_events }}
aft_feature_delete_default_vpcs_enabled = {{ aft_feature_delete_default_vpcs_enabled }}
aft_feature_enterprise_support = {{ aft_feature_enterprise_support }}
aft_framework_repo_git_ref = {{ aft_framework_repo_git_ref }}
aft_framework_repo_url = {{ aft_framework_repo_url }}
aft_management_account_id = {{ aft_management_account_id }}
aft_metrics_reporting = {{ aft_metrics_reporting }}
aft_vpc_cidr = {{ aft_vpc_cidr }}
aft_vpc_endpoints = {{ aft_vpc_endpoints }}
aft_vpc_private_subnet_01_cidr = {{ aft_vpc_private_subnet_01_cidr }}
aft_vpc_private_subnet_02_cidr = {{ aft_vpc_private_subnet_02_cidr }}
aft_vpc_public_subnet_01_cidr = {{ aft_vpc_public_subnet_01_cidr }}
aft_vpc_public_subnet_02_cidr = {{ aft_vpc_public_subnet_02_cidr }}
audit_account_id = {{ audit_account_id }}
backup_recovery_point_retention = {{ backup_recovery_point_retention }}
cloudwatch_log_group_retention = {{ cloudwatch_log_group_retention }}
concurrent_account_factory_actions = {{ concurrent_account_factory_actions }}
ct_home_region = {{ ct_home_region }}
ct_management_account_id = {{ ct_management_account_id }}
github_enterprise_url = {{ github_enterprise_url }}
gitlab_selfmanaged_url = {{ gitlab_selfmanaged_url }}
global_codebuild_timeout = {{ global_codebuild_timeout }}
global_customizations_repo_branch = {{ global_customizations_repo_branch }}
global_customizations_repo_name = {{ global_customizations_repo_name }}
log_archive_account_id = {{ log_archive_account_id }}
log_archive_bucket_object_expiration_days = {{ log_archive_bucket_object_expiration_days }}
maximum_concurrent_customizations = {{ maximum_concurrent_customizations }}
terraform_api_endpoint = {{ terraform_api_endpoint }}
terraform_distribution = {{ terraform_distribution }}
terraform_org_name = {{ terraform_org_name }}
terraform_token = {{ terraform_token }}
terraform_version = {{ terraform_version }}
tf_backend_secondary_region = {{ tf_backend_secondary_region }}
vcs_provider = {{ vcs_provider }}
}
We'll customize this procedurally generated Blueprint to improve our description names and suggestions, giving anyone who is setting up AFT via our form more guidance.
Adding suggestions and grouping
---
variables:
aft_vpc_cidr:
default: 192.168.0.0/22
desc: CIDR Block to allocate to the AFT VPC
required: false
type: resource.aws_vpc.cidr_block
group: Network
vcs_provider:
suggest: "github"
desc: Customer VCS Provider - valid inputs are codecommit, bitbucket, github, or
githubenterprise
required: false
type: resource.aws_ssm_parameter.value
group: Repository
aft_enable_vpc:
default: true
desc: Flag turning use of VPC on/off for AFT
required: false
type: bool
group: Network
ct_home_region:
desc: The region from which this module will be executed. This MUST be the same
region as Control Tower is deployed.
type: resource.aws_ssm_parameter.value
suggest: "us-east-1"
group: Location
audit_account_id:
desc: Audit Account Id
type: resource.aws_ssm_parameter.value
suggest: "123456789012"
group: Accounts
aft_vpc_endpoints:
default: true
desc: Flag turning VPC endpoints on/off for AFT VPC
required: false
type: bool
group: Network
ct_management_account_id:
desc: Control Tower Management Account Id
type: resource.aws_ssm_parameter.value
suggest: "111122223333"
group: Accounts
aft_management_account_id:
desc: AFT Management Account ID
suggest: "777788889999"
type: resource.aws_ssm_parameter.value
group: Accounts
account_request_repo_branch:
default: main
desc: Branch to source account request repo from
required: false
type: resource.aws_ssm_parameter.value
group: Repository
tf_backend_secondary_region:
suggest: "us-west-2"
desc: AFT creates a backend for state tracking for its own state as well as OSS
cases. The backend's primary region is the same as the AFT region, but
this defines the secondary region to replicate to.
required: false
type: resource.aws_ssm_parameter.value
group: Location
aft_vpc_public_subnet_01_cidr:
default: 192.168.2.0/25
desc: CIDR Block to allocate to the Public Subnet 01
required: false
type: resource.aws_subnet.cidr_block
group: Network
aft_vpc_public_subnet_02_cidr:
default: 192.168.2.128/25
desc: CIDR Block to allocate to the Public Subnet 02
required: false
type: resource.aws_subnet.cidr_block
group: Network
aft_vpc_private_subnet_01_cidr:
default: 192.168.0.0/24
desc: CIDR Block to allocate to the Private Subnet 01
required: false
type: resource.aws_subnet.cidr_block
group: Network
aft_vpc_private_subnet_02_cidr:
default: 192.168.1.0/24
desc: CIDR Block to allocate to the Private Subnet 02
required: false
type: resource.aws_subnet.cidr_block
group: Network
cloudwatch_log_group_retention:
default: 0
desc: Amount of days to keep CloudWatch Log Groups for Lambda functions. 0 =
Never Expire
required: false
type: resource.aws_cloudwatch_log_group.retention_in_days
group: General
log_archive_account_id:
desc: Log Archive Account Id
suggest: "444455556666"
type: resource.aws_ssm_parameter.value
group: Accounts
backup_recovery_point_retention:
default: 365
desc: Number of days to keep backup recovery points in AFT DynamoDB tables.
Default = Never Expire
required: false
type: resource.aws_backup_plan.rule.lifecycle.delete_after
group: General
global_customizations_repo_name:
suggest: "myorg/aft-global-customizations"
desc: Repository name for the global customization files. For non-CodeCommit
repos, name should be in the format of Org/Repo
required: false
type: resource.aws_ssm_parameter.value
group: Repository
account_customizations_repo_name:
suggest: "myorg/aft-account-customizations"
desc: Repository name for the account customizations files. For non-CodeCommit
repos, name should be in the format of Org/Repo
required: false
type: resource.aws_ssm_parameter.value
group: Repository
log_archive_bucket_object_expiration_days:
default: 365
desc: Amount of days to keep the objects stored in the AFT logging bucket
required: false
type: resource.aws_s3_bucket_lifecycle_configuration.rule.noncurrent_version_expiration.noncurrent_days
group: General
account_provisioning_customizations_repo_name:
suggest: "myorg/aft-account-provisioning-customizations"
desc: Repository name for the account provisioning customizations files. For
non-CodeCommit repos, name should be in the format of Org/Repo
required: false
type: resource.aws_ssm_parameter.value
group: Repository
aft_backend_bucket_access_logs_object_expiration_days:
default: 365
desc: Amount of days to keep the objects stored in the access logs bucket for
AFT backend buckets
required: false
type: resource.aws_s3_bucket_lifecycle_configuration.rule.noncurrent_version_expiration.noncurrent_days
group: General
groups:
Accounts:
order: 1
General:
order: 2
Repository:
order: 3
Network:
order: 4
Location:
order: 5
---
module "{{ module_name }}_{{ __guid }}" {
source = "git::https://github.com/aws-ia/terraform-aws-control_tower_account_factory"
aft_vpc_cidr = {{ aft_vpc_cidr }}
vcs_provider = {{ vcs_provider }}
aft_enable_vpc = {{ aft_enable_vpc }}
ct_home_region = {{ ct_home_region }}
audit_account_id = {{ audit_account_id }}
aft_vpc_endpoints = {{ aft_vpc_endpoints }}
ct_management_account_id = {{ ct_management_account_id }}
aft_management_account_id = {{ aft_management_account_id }}
tf_backend_secondary_region = {{ tf_backend_secondary_region }}
aft_vpc_public_subnet_01_cidr = {{ aft_vpc_public_subnet_01_cidr }}
aft_vpc_public_subnet_02_cidr = {{ aft_vpc_public_subnet_02_cidr }}
aft_vpc_private_subnet_01_cidr = {{ aft_vpc_private_subnet_01_cidr }}
aft_vpc_private_subnet_02_cidr = {{ aft_vpc_private_subnet_02_cidr }}
cloudwatch_log_group_retention = {{ cloudwatch_log_group_retention }}
backup_recovery_point_retention = {{ backup_recovery_point_retention }}
global_customizations_repo_name = {{ global_customizations_repo_name }}
account_customizations_repo_name = {{ account_customizations_repo_name }}
log_archive_account_id = {{ log_archive_account_id }}
log_archive_bucket_object_expiration_days = {{ log_archive_bucket_object_expiration_days }}
account_provisioning_customizations_repo_name = {{ account_provisioning_customizations_repo_name }}
aft_backend_bucket_access_logs_object_expiration_days = {{ aft_backend_bucket_access_logs_object_expiration_days }}
}
Publishing and deploying AFT
Once you have customized your AFT module Blueprint to your liking, add metadata and publish the Blueprint. This will make it available for anyone to deploy.
Once you have published the Blueprint, deploy your AFT with it. Go to Resources, create a Pull Request, find your AFT Blueprint, fill it out to your liking, and then submit and deploy it.
Provisioning accounts using AFT
Now that we have AFT setup, we have all of the necessary IAM roles, VPCs, lambda functions, and other resources needed to set up an account.
Module
Within our AFT module, there is a submodule called aft-account-provisioning-framework. We'll create a Blueprint from this module, and use that as our primary vehicle for creating accounts using the AFT we just deployed.
Similar to previous steps, Resourcely will automatically infer variable tags such as description. Finish importing this module, add Groups, and then inspect your Blueprint code. It should look something like this:
Make sure to check out the Developer Experience tab in Foundry after you have generated your Blueprint. This is the form that your developers will see when deploying new accounts.
You can now add your own guidance by amending the variable descriptions, suggestions, and defaults. Note that Resourcely automatically creates variables that are inputs in the Blueprint form. If you don't want to give developers the ability to add their own custom references, and instead want to lock them in to the AFT resources, you can simply hardcode them in the Blueprint code.
For example, let's say that we don't want to expose an input for tag_account_lambda_function_name. This would make sure that consistent tags are always applied to your accounts, and developers won't be able to deviate. All you would need to do is replace the inline {{ tag_account_lambda_function_name }} variable reference with a hardcoded reference in Terraform.
Guardrails
Guardrailsare a great way to add rules to your input form that keep developers on track. One easy example would be adding a rule for the cloudwatch_log_group_retention variable. To do so, and create the following Guardrail:
GUARDRAIL "Retention in days > 364"
WHEN aws_cloudwatch_log_group
REQUIRE retention_in_days > 364
OVERRIDE WITH APPROVAL @default
After savings and navigating back to your Blueprint, this would appear as a Guardrail lock on your form:
Publishing and provisioning new accounts
Once you have finished customizing your Blueprint, add metadata and publish it to make it discoverable. Now, developers have an easy way to create a new AWS account: meeting the expectations that you set when creating the AFT, and utilizing isolation-related best practices.
Get started today
These Blueprints are available in Resourcely for you! If you want to streamline AWS account creation, making it easy for developers to follow best practices, all you need to do is sign up for your free account.
We love feedback! Let us know if you had any trouble with this.