Wrapping AWS Control Tower Account Factory

This post presents a small wrapper product for the AWS Control Tower Account Factory. The wrapper supports automation as it provides a stable interface (name and version) to the Control Tower Account Factory that doesn't change over time. The out of the box Control Tower Account Factory suffers from the fact that it is not versioned in a usable way as someone decided that 'Control Tower Account Factory' is a good version string ಠ~ಠ.

Introduction

AWS Control Tower provides a self service account creation functionality (often called account factory). The AWS Control Tower documentation refers to this functionality as the Control Tower Account Factory (CTAF).

CTAF is implemented by AWS Control Tower using AWS Service Catalog. The Control Tower Account Factory consists of one Service Catalog portfolio that contains one single product. Service Catalog manages portfolios, where a portfolio is a simple collection of products. Products are based on (CloudFormation) templates that can be provisioned. Provisioning a product causes CloudFormation to create a CloudFormation stack with the the parameters specified using the Service Catalog provision product form.

Service Catalog provides a mechanisms to manage different versions of the the template that's used by the product. Frequently updates to products are implemented by updating the template and providing a new version number. Versions can set to be active or inactive (not available for provisioning).

The Problem

The CTAF always provides two versions of the Control Tower Account Factory product. One active and one inactive.

The poor 2 Pizza Team that implemented CTAF decided that the version of both should always be set to "Control Tower Account Factory". This is unfortunate but somehow acceptable. The naive me was thinking that that shouldn't be too much of a problem as one can easily use the provisioning artefact id instead. Seems that is not the case.

I found out that the The Control Tower Account Factory provisioning artefact id is updated by the Control Tower service on several occasions:

  • In case the Control Tower version is updated
  • In case Control Tower Organization Units (OUs) are updated/created/removed.

While the first case is understandable, the second is very strange. Turns out that the service catalog form provides a drop down selection box that allows for a choice of the target OU of the account that should be created and once new OUs are present in the environment they're updating the template behind the curtains which yields a version bump.

Once the update happens, the new provisioning artefact id/version is created and set as the active version while the old version is set to inactive.

Provisioning new accounts using the Control Tower Account Factory requires a lookup (of the active version) and an adjustment of the Service Catalog API call to use the latest active version. While this is minor nuisance it can become a problem depending on the toolings.

If you're managing your accounts through some form of automation around the Service Catalog product you'll need to update the provisioning artefact ID of the new version on al Control Tower Updates and every time an OU is changed/added.

Solution

In order to solve the the problem as outlined I've created the following solution. The solution is event based and serverless and thus does not require management of the components itself.

ctaf-shim-architecture-hld.drawio.png

The solution is relies on a Service Catalog product that acts as a small shim. One can provision the shim which in turn will provision the Control Tower supplied Control Tower Account Factory product (steps 1,2,3). The shim parameters reflect the ones that are used by the original product. Once the shim is ordered it retrieves the active version of the Control Tower Account Factory from a Systems Center Parameter Store variable. This approach guarantees that provisioning the shim product is simple to use from automation tooling such as Terraform.

Workflow

ctaf-shim-architecture-lld.drawio.png Every time Control Tower updates the Service Catalog provisioned artefact (version) an AWS CloudTrail API event is logged. An AWS EventBridge rule is configured to listen on this API event being logged. Once the event is detected an AWS Lambda function is triggered by EventBridge. The Lambda function retrieves the active ID from the Control Tower Account Factory Product and updates the Systems Center Parameter Store variable with the value of the active version. This is described in the steps 1,2,3,4,5.

Components AWS Control Tower - AWS' managed Landing Zone service providing the Account Factory AWS Service Catalog - AWS service catalog provides the self service interface to provisioning of AWS accounts, either through CLI, Console or Terraform, etc. AWS System Center Parameter Store - AWS Systems Center Parameter store, used to store and retrieve the active provisioning ID of the Control Tower Account Factory product. AWS EventBridge - AWS EventBridge enables one to react to events within AWS, for example by triggering a Lambda function with the event's information available to the function.

Cost

The solution only cost is the execution of the Lambda function, which is triggered only once Control Tower updates the Control Tower Account Factory product. See the notes from above when this happens.

Implementation

The shim product itself is using the following CloudFormation template for the

Service Catalog Product

AWSTemplateFormatVersion: "2010-09-09"
Description: "AWS Service Catalog Product Shim for Control Tower Account Factory"
Parameters:
  AccountName:
    Description: "Account name, the new managed Account will be created with this name."
    Type: String
    AllowedPattern : ".+"
  AccountEmail:
    Description: "Account email, must be unique for each AWS Account."
    Type: String
    AllowedPattern : "[^\\s@]+@[^\\s@]+\\.[^\\s@]+"
  SSOUserFirstName:
    Description:  "SSO user first name."
    Type: String
    AllowedPattern : ".+"
  SSOUserLastName:
    Description:  "SSO user last name."
    Type: String
    AllowedPattern : ".+"
  SSOUserEmail:
    Description: "SSO user email. A new SSO user will be created for this email, if it does not exist. This SSO user will be associated with the new managed Account."
    Type: String
    AllowedPattern : "[^\\s@]+@[^\\s@]+\\.[^\\s@]+"
  ManagedOrganizationalUnit:
    Description: "Your account will be added to this registered organizational unit. Top-level and nested OUs registered with AWS Control Tower."
    Type: String
  ProductId:
    Description: Control Tower Account Factory ID, like prod-XXXXXXX
    Type: String
    Default: "<YOURPRODUCTID>"
Resources:
  ProductControlTowerAccountFactory:
    Type: AWS::ServiceCatalog::CloudFormationProvisionedProduct
    Properties:
      ProductId: !Ref ProductId
      ProvisionedProductName: !Sub "CTAF-Shim-${AWS::StackName}"
      ProvisioningArtifactId: "{{resolve:ssm:/org/ctaf-shim/activeid}}"
      ProvisioningParameters:
        - Key: AccountName
          Value: !Ref AccountName
        - Key: AccountEmail
          Value: !Ref AccountEmail
        - Key: SSOUserFirstName
          Value: !Ref SSOUserFirstName
        - Key: SSOUserLastName
          Value: !Ref SSOUserLastName
        - Key: SSOUserEmail
          Value: !Ref SSOUserEmail
        - Key: ManagedOrganizationalUnit
          Value: !Ref ManagedOrganizationalUnit

Lambda Service Catalog Version Updater

import re
import os
import logging
import json
import boto3
import traceback

logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.DEBUG)


ssm_client = boto3.client("ssm")
sc_client = boto3.client("servicecatalog")

SSM_PARAMETER_STORE_VARIABLE_NAME = "/org/ctaf-shim/activeid"
CONTROL_TOWER_ACCOUNT_FACTORY_PRODUCT_ID = "prod-<yourproductID>"


def lambda_handler(event, context):
    # """Main method for Lambda function, will handle the update of SSM parameter that reflects active provisioning artifact ID of the Control Tower Account Factory"""
    try:
        response = sc_client.list_provisioning_artifacts(
            ProductId=CONTROL_TOWER_ACCOUNT_FACTORY_PRODUCT_ID
        )
        logger.debug(
            "Provisioning artifact details for product '%s' are '%s'",
            CONTROL_TOWER_ACCOUNT_FACTORY_PRODUCT_ID,
            response,
        )

        if "ProvisioningArtifactDetails" not in response:
            logger.error(
                "The response did not provide the key ProvisioningArtifactDetails."
            )

        active_id = str("")
        for r in response["ProvisioningArtifactDetails"]:
            if r["Active"]:
                active_id = r["Id"]
            else:
                continue

        if active_id == "":
            logger.error("No active provisioning artifact ids found")
            raise Exception("No active provisioning artifact ids found")

        ssm_client.put_parameter(
            Name=SSM_PARAMETER_STORE_VARIABLE_NAME,
            Value=active_id,
            Type="String",
            Overwrite=True,
        )
        logger.info(
            "Updated ssm parameter '%s' to '%s'",
            SSM_PARAMETER_STORE_VARIABLE_NAME,
            active_id,
        )

    except Exception as err:
        message = {"Exception Details": str(err)}
        return message

Deployment

Note I've left out IAM bits and pieces and EventBridge rule for triggering the Lambda. You can easily set this up by going to the console in EventBridge and setup a trigger rule for UpdateProvisioningArtefact API call of service catalog. Adjust the Service Catalog product id with the one from your management account in the template. Once this is done proceed with. creating a new Service Catalog product in the existing Service Catalog portfolio of the account factory.

Test it out by provisioning the shim. You should see 2 product in the Service Catalog UI, one for the shim and one for the actual account. Terminating the shim will cause the account to be terminated as well.

I left hose out as I was too lazy to clean them up but can post the missing bits and pieces here if you ping me :)

Did you find this article valuable?

Support Grumpy Platform Engineer by becoming a sponsor. Any amount is appreciated!