Exciting things are coming as part of re:Invent (picture from re:Invent 2021)

Organizations CloudFormation

It’s pre:Invent 2022, the time of year AWS releases a bunch of new products and features that aren’t big enough to make it on the keynote state of re:Invent. One of my long-awaited features was released last night: CloudFormation support for AWS Organizations!

Before this release, the management of Service Control Policies, Organizational Units, and AWS Accounts was either artisanal or via third-party tools like org-formation. I can finally manage my AWS Organization using the same IaC as I manage the accounts in that organization.

About this time last year, my team at the previous employer adopted org-formation. It worked reasonably well. I had a single YAML template to maintain my OUs, SCPs, and AWS Accounts. I built processes around org-formation so that we could generate a change-set, review it, and apply the change set, similar to terraform plan and terraform apply. It worked reasonably well, but it had some hiccups when processing it at the scale of 200+ AWS accounts.

For this post, I will document my (attempted) migration from org-formation to CloudFormation in my Fooli Media AWS organization.

Spoiler Alert: I ran into issues and do not recommend using CloudFormation at this time.

About CloudFormation for AWS Organizations

CloudFormation introduced three new resource types:

Unlike org-formation, the AWS::Organizations::Policy supports all the organizations’ policy types: Service Control, Backup, Tagging, and AI Services Opt-Out policies.

As would be expected for an AWS Account, there are several documented limitations on the Account resource type. You can’t change attributes of the account via CloudFormation that normally require a root login, specifically the Account Name and Email address. Account creation isn’t as deterministic as creating a resource, so the new account may not be 100% provisioned when the CloudFormation services says Create Complete. Creation of Multiple AWS accounts simultaneously will fail, as there is a strict limit on the number of action CreateAccount API calls that can be made at once.

In org-formation, the OU is the resource that defines which accounts are members of the OU and which SCPs apply to the OU.

  WorkloadsOU:
    Type: OC::ORG::OrganizationalUnit
    Properties:
      OrganizationalUnitName: Workloads
      ServiceControlPolicies:
        - !Ref SecurityControlsSCP
        - !Ref DisableRegionsPolicySCP
        - !Ref DenyUnapprovedServicesSCP
      Accounts:
        - !Ref FooliProdAccount
        - !Ref FooliDevAccount
        - !Ref FooliMemeFactoryAccount

With CloudFormation, the AWS::Organizations::Account resource defines OU membership with the ParentIds attribute. Similarly, the AWS::Organizations::Policy resource defines which OUs the policy applies to with the TargetIds attribute.

Working with these new Resource Types

AWS::Organizations::Policy

The first disappointing feature is that the Policy definition is:

Type: AWS::Organizations::Policy
Properties:
  Content: String
  Description: String <-- BOO
  Name: String
  Tags:
    - Tag
  TargetIds:
    - String
  Type: String

While the IAM Policy definition is:

Type: AWS::IAM::Policy
Properties:
  Groups:
    - String
  PolicyDocument: Json <-- YAY
  PolicyName: String
  Roles:
    - String
  Users:
    - String

The difference here of Content: String vs PolicyDocument: Json means that the Organizations policies can’t be defined as YAML in the template. They must be the formatted JSON Strings. I couldn’t simply copy the policies from my org-formation document into my CFT, rather I had to extract the org-formation rendered JSON via the AWS API and then insert it into my template.

This is the AWS CLI Command to do that. You’ll need to supply the correct policy-id for each existing SCP.

aws organizations describe-policy --policy-id p-REDACTED --query Policy.Content --output text

Once I did that, creating and managing SCPs worked:

  SecurityControlsSCP:
    Type: AWS::Organizations::Policy
    Properties:
      Name: SecurityControlsSCP-2
      Type: SERVICE_CONTROL_POLICY
      Description: Global Security Controls
      TargetIds:
        - "r-mlz5"
      Content: |
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Sid": "PreventCloudTrailModification",
              "Effect": "Deny",
              "Action": [
                "cloudtrail:DeleteTrail",
                "cloudtrail:PutEventSelectors",
                "cloudtrail:StopLogging",
                "cloudtrail:UpdateTrail",
                "cloudtrail:CreateTrail"
              ],
              "Resource": ["*"]
            }
            ...
        }

AWS::Organizations::OrganizationalUnit

OUs are pretty straightforward. I define the name and the parent ID like so:

  SandboxOU:
    Type: AWS::Organizations::OrganizationalUnit
    Properties:
      Name: Sandbox-2
      ParentId: !Ref pRootOUId

With OUs, here is a Major Red Flag

To update the ParentId parameter value, you must first remove all accounts attached to the organizational unit (OU). OUs can’t be moved within the organization with accounts still attached. (link)

This is a major limitation and one I recommend you architect your governance structure around.

AWS::Organizations::Account

Until now, I’ve been creating new Organizations resources in my payer account. I’m obviously not going to create all new accounts, so now I’m going to experiment with CloudFormation imports to see how viable this whole thing is.

First, I’m going to define the account in the CFT:

  SandboxAccount:
    Type: AWS::Organizations::Account
    DeletionPolicy: Retain
    Properties:
      AccountName: fooli-sandbox
      Email: <redacted>
      ParentIds:
        - !Ref SandboxOU
      Tags:
        - Key: ExecutiveOwner
          Value: Erlich Bachman
        - Key: TechnicalContact
          Value: Dinesh Chugtai
        - Key: DataClassification
          Value: None

Even though the CloudFormation documentation indicates that there is an implied DeletionPolicy: Retain for the Account resource, the CloudFormation import process requires it to be present in the template.

I decided to do this via the AWS Console. I first uploaded the new template with the Sandbox account definition in it. I then had to provide the AWS account ID for the Sandbox account.

&ldquo;Identify Resources&rdquo;

It then generated an import change set, and I had to confirm

Import Confirmation

Once imported, the AWS Account was not moved into the newly created OU specified in the ParentIds. It remained in my original Sandbox OU.

Attempting to Comment out the ParentIds and update the stack generated this error:

Resource handler returned message: "You specified an account that doesn't exist. (Service: Organizations, Status Code: 400, Request ID: 89c02268-5885-41b0-a8a2-32139e06fb9e)" (RequestToken: 337966af-89ef-7d7d-bc35-a4480137a302, HandlerErrorCode: NotFound)

And left my stack in the dreaded UPDATE_ROLLBACK_FAILED, which prevents me from doing further updates.

I then deleted the entire stack and attempted to import all the resources at once. That included my SecurityControlsSCP, four existing OUs, and the Fooli Sandbox AWS Account. That worked, and once imported, I was able to change the OU the Fooli Sandbox was a member of. However, when I imported another Fooli account, the same “You specified an account that doesn’t exist.” error occurred when trying to modify that AWS Account.

Real-World Use

The inability to import an account into an existing stack is a show stopper for me, and I don’t recommend enterprise use of CloudFormation to manage AWS Organizations. In my experience as an enterprise cloud admin, I regularly had new accounts get added to my organization via M&A activity or because we found someone engaging in shadow IT, and we needed to bring them into the fold.

I’ll update this post if and when the service’s behavior changes, but for now, I’m not migrating any more of my AWS Organizations away from org-formation.