Creating AWS Organizational Units and Accounts for Prod and Dev in CloudFormation

Here’s an AWS CloudFormation template and deployment script to set up Organizational Units and Accounts for Prod and Dev for a new project or department.

Deploying a CloudFormation stack from this template will require a user in the Organization’s management account with the AdministratorAccess permission set.

The created Organizational Unit (OU) and Account structure will look this:

Organization Root [pre-existing]

  ╚═ Management Account [pre-existing]

  ╚═ Project Parent OU

     ╚═ Project Prod OU

        ╚═ Project Prod Account

     ╚═ Project Dev OU

        ╚═ Project Dev Account

The OUs are not strictly necessary, but they help to group the whole project under a single OU, and to allow for other accounts to be added later within the Prod and Dev OUs if necessary.

The CloudFormation template looks like this:

---
AWSTemplateFormatVersion: '2010-09-09'
Description: >
  Create AWS Organizational Units and an Accounts for a project.

# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-organizations-organizationalunit.html
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-organizations-account.html

Parameters:

  OrganizationRootId:
    Type: String
    Description: ID of the root Organization of the management account.
    AllowedPattern: "^(r-[0-9a-z]{4,32})$"

  ProjectName:
    Type: String
    Description: Overall name for this project, used in OU and Account names.
    AllowedPattern: "^([a-zA-Z]+)$"

  EmailDomain:
    Type: String
    Description: Domain name for AWS Account email addresses.
    AllowedPattern: "^([a-zA-Z0-9]+).([a-zA-Z0-9\.]+)$"

Resources:

  ProjectParentOrganizationalUnit:
    Type: AWS::Organizations::OrganizationalUnit
    Properties:
      Name: !Sub "${ProjectName}ParentOU"
      ParentId: !Ref OrganizationRootId

  ProdOrganizationalUnit:
    Type: AWS::Organizations::OrganizationalUnit
    Properties:
      Name: !Sub "${ProjectName}ProdOU"
      ParentId: !Ref ProjectParentOrganizationalUnit

  ProdAccount:
    Type: AWS::Organizations::Account
    Properties:
      AccountName: !Sub "${ProjectName}Prod"
      Email: !Sub "${ProjectName}_aws_prod@${EmailDomain}"
      RoleName: !Sub "${ProjectName}ProdAdmin"
      ParentIds:
        - !Ref ProdOrganizationalUnit

  DevOrganizationalUnit:
    Type: AWS::Organizations::OrganizationalUnit
    Properties:
      Name: !Sub "${ProjectName}DevOU"
      ParentId: !Ref ProjectParentOrganizationalUnit

  DevAccount:
    Type: AWS::Organizations::Account
    Properties:
      AccountName: !Sub "${ProjectName}Dev"
      Email: !Sub "${ProjectName}_aws_dev@${EmailDomain}"
      RoleName: !Sub "${ProjectName}DevAdmin"
      ParentIds:
        - !Ref DevOrganizationalUnit

Outputs:

  ProjectParentOrganizationalUnitArn:
    Description: ARN of the Project Parent Organizational Unit
    Value: !GetAtt ProjectParentOrganizationalUnit.Arn

  ProdOrganizationalUnitArn:
    Description: ARN of the Prod Organizational Unit
    Value: !GetAtt ProdOrganizationalUnit.Arn

  ProdAccountArn:
    Description: ARN of the Prod Account
    Value: !GetAtt ProdAccount.Arn

  DevOrganizationalUnitArn:
    Description: ARN of the Dev Organizational Unit
    Value: !GetAtt DevOrganizationalUnit.Arn

  DevAccountArn:
    Description: ARN of the Dev Account
    Value: !GetAtt DevAccount.Arn

Here’s a sample deployment script to deploy the above template:

#!/usr/bin/env bash

set -e, -u, -x, -o pipefail

PARENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" || exit; pwd -P )
TEMPLATE_PATH="${PARENT_PATH}/cloudformation.yml"
PARAMS_PATH="${PARENT_PATH}/config/organizational.params"

ls "${TEMPLATE_PATH}"
if [ ! -f "${TEMPLATE_PATH}" ]; then
  echo "Cloudformation template file is missing"
fi

ls "${PARAMS_PATH}"
if [ ! -f "${PARAMS_PATH}" ]; then
  echo "Params file is missing"
fi

if [ -z ${AWS_PROFILE+x} ]; then
  echo "AWS_PROFILE is not set"
  exit 1
fi

# shellcheck disable=SC1090
source "${PARAMS_PATH}"

if [ -z ${OrganizationRootId+x} ]; then
  echo "OrganizationRootId is not set"
  exit 1
fi

# shellcheck disable=SC2154
echo "OrganizationRootId: ${OrganizationRootId}"
# shellcheck disable=SC2154
echo "ProjectName: ${ProjectName}"

# https://docs.aws.amazon.com/cli/latest/reference/cloudformation/deploy/

# shellcheck disable=SC2046

aws cloudformation deploy \
  --profile "${AWS_PROFILE}" \
  --template-file "${PARENT_PATH}/cloudformation.yml" \
  --stack-name "${ProjectName}-Organizational" \
  --parameter-overrides OrganizationRootId="${OrganizationRootId}" $(cat "${PARAMS_PATH}")

That requires a params file at ./config/organizational.params, which provides the ProjectName and EmailDomain params, for example:

ProjectName=FoobarProject
EmailDomain=kensiosoftware.co.uk

This assumes that a catch-all email is set up on the specified domain, so that the CloudFormation template can make up specific email addresses for each account.


Tech mentioned