Create AWS Accounts with CloudFormation

Create AWS Accounts with CloudFormation

September 10, 2018 8 By Eric Shanks

In a previous post, we covered how to use an AWS Custom Resource in a CloudFormation template to deploy a very basic Lambda function. To expand upon this ability, lets use this knowledge to deploy something more useful than a basic Lambda function. How about we use it to create an AWS account? To my knowledge, the only way to create a new AWS account is to use the CLI or manually through the console. How about we use a custom resource to deploy a new account for us in our AWS Organization? Once this ability is available in a CloudFormation template, we could even publish it in the AWS Service Catalog and give our users an account vending machine capability.

Create the Lambda Function

Just as we did in the previous post, we’ll create a Lambda function, zip it up and place it into our S3 bucket. My function is Python 2.7 and can be found below.

#Import modules
import json, boto3, logging
from botocore.vendored import requests

#Define logging properties
log = logging.getLogger()
log.setLevel(logging.INFO)



#Main Lambda function to be excecuted
def lambda_handler(event, context):
    #Initialize the status of the function
    status="SUCCESS"
    responseData = {}
    client = boto3.client('organizations')


    #Read and log the input values
    acctName = event['ResourceProperties']['AccountName']
    ouName = event['ResourceProperties']['OUName']
    emailAddress = event['ResourceProperties']['Email']
    log.info("Account name is: " + acctName)
    log.info("Organizational Unit name is: " + ouName)
    log.info("Email Address is: " + emailAddress)

    #create a new Organizational Unit
    orgResponse = client.create_organizational_unit(
        ParentId="ou-3hvv-jqwq89r0", #My Parent OU. Change for your environment
        Name=ouName
    )

    log.info(orgResponse['OrganizationalUnit']['Id'])
    OUID=str(orgResponse['OrganizationalUnit']['Id'])

    #Create a new Account in the OU Just Created
    acctResponse = client.create_account(
        Email=emailAddress,
        AccountName=acctName 
    )

    #Check Account Status
    acctStatusID = acctResponse['CreateAccountStatus']['Id']
    log.info(acctStatusID)

    while True:
        createStatus = client.describe_create_account_status(
            CreateAccountRequestId=acctStatusID
        )
        log.info(createStatus['CreateAccountStatus']['State'])        
        if str(createStatus['CreateAccountStatus']['State']) != 'IN_PROGRESS':
            newAccountId = str(createStatus['CreateAccountStatus']['AccountId'])
            break

    #Move Account to new OU
    moveResponse = client.move_account(
        AccountId=newAccountId,
        SourceParentId='r-3hvv', #My root OU. Change for your environment
        DestinationParentId=OUID
    )

    #Set Return Data
    responseData = {"Message" : newAccountId} #If you need to return data use this json object

    #return the response back to the S3 URL to notify CloudFormation about the code being run
    response=respond(event,context,status,responseData,None)

    #Function returns the response from the S3 URL
    return {
        "Response" :response
    }

def respond(event, context, responseStatus, responseData, physicalResourceId):
    #Build response payload required by CloudFormation
    responseBody = {}
    responseBody['Status'] = responseStatus
    responseBody['Reason'] = 'Details in: ' + context.log_stream_name
    responseBody['PhysicalResourceId'] = context.log_stream_name
    responseBody['StackId'] = event['StackId']
    responseBody['RequestId'] = event['RequestId']
    responseBody['LogicalResourceId'] = event['LogicalResourceId']
    responseBody['Data'] = responseData

    #Convert json object to string and log it
    json_responseBody = json.dumps(responseBody)
    log.info("Response body: " + str(json_responseBody))

    #Set response URL
    responseUrl = event['ResponseURL']

    #Set headers for preparation for a PUT
    headers = {
    'content-type' : '',
    'content-length' : str(len(json_responseBody))
    }

    #Return the response to the signed S3 URL
    try:
        response = requests.put(responseUrl,
        data=json_responseBody,
        headers=headers)
        log.info("Status code: " + str(response.reason))
        status="SUCCESS"
        return status
    #Defind what happens if the PUT operation fails
    except Exception as e:
        log.error("send(..) failed executing requests.put(..): " + str(e))
        status="FAILED"
        return status

As before, lets break down a few of the relevant sections of the code so you can see whats happening. To begin, lets look at the main lambda_handler. First we’ll initialize some of our variables and set our boto3 client to organizations so that we can create our accounts. After this, we’re going to set some variables in our Lambda function that will be passed in from our CloudFormation template (shown later in this post). After we set our variables, we’ll log them so that we can see what CloudFormation actually passed to our function.

#Main Lambda function to be excecuted
def lambda_handler(event, context):
    #Initialize the status of the function
    status="SUCCESS"
    responseData = {}
    client = boto3.client('organizations')


    #Read and log the input values
    acctName = event['ResourceProperties']['AccountName']
    ouName = event['ResourceProperties']['OUName']
    emailAddress = event['ResourceProperties']['Email']
    log.info("Account name is: " + acctName)
    log.info("Organizational Unit name is: " + ouName)
    log.info("Email Address is: " + emailAddress)

Next, we’ll use boto3 to create an Organizational Unit on the fly. We’ll pass in the name of this OU from our CloudFormation template. To do this we’ll use the create_organizational_unit method and we’ll need to pass in the parent OU and the name of our new OU. When we’re done, we’ll log the ID of this OU and I’m setting a variable with the ID of this OU as well for later on in the function.

    #create a new Organizational Unit
    orgResponse = client.create_organizational_unit(
        ParentId="ou-3hvv-jqwq89r0", #My Parent OU. Change for your environment
        Name=ouName
    )

    log.info(orgResponse['OrganizationalUnit']['Id'])
    OUID=str(orgResponse['OrganizationalUnit']['Id'])

Now that we’ve created an OU, lets create the account. Again, we’ll use boto3 to call the create_account method. We’ll pass in an email address to be used for the new account and an account name. Again, when this is done, we’ll log the response which is the account status. After this, we’ll initiate a loop to check on the status of the account while it’s being created. Account creation isn’t an immediate thing, so its good to check on it until its either Successful or Failed. The loop checks the status, logs it and waits until its no longer IN_PROGRESS.

    #Create a new Account in the OU Just Created
    acctResponse = client.create_account(
        Email=emailAddress,
        AccountName=acctName 
    )

    #Check Account Status
    acctStatusID = acctResponse['CreateAccountStatus']['Id']
    log.info(acctStatusID)

    while True:
        createStatus = client.describe_create_account_status(
            CreateAccountRequestId=acctStatusID
        )
        log.info(createStatus['CreateAccountStatus']['State'])        
        if str(createStatus['CreateAccountStatus']['State']) != 'IN_PROGRESS':
            newAccountId = str(createStatus['CreateAccountStatus']['AccountId'])
            break

With any luck, our account has been created and we’ve got one more thing to do. Lets move the new account, into the new OU we created. Again, we’ll use boto3, but this time with the move_account method. I’m passing in the new AccountId we stored from our new create_account method, and the OUID we stored from our create_organizational_unit. I’m also specifying my root OU which will be different in your case. Fill it in, or do a search to find it in the Lambda function.

    #Move Account to new OU
    moveResponse = client.move_account(
        AccountId=newAccountId,
        SourceParentId='r-3hvv', #My root OU. Change for your environment
        DestinationParentId=OUID
    )

The account stuff is done, now we’re just setting some return data to be sent back to CloudFormation. The info I want sent back to CFn as an output is the accountID of our new account we created. After which we’ll send the data back to our signed S3 URL as we explained in the previous post about Custom Resorces.

    #Set Return Data
    responseData = {"Message" : newAccountId} #If you need to return data use this json object

    #return the response back to the S3 URL to notify CloudFormation about the code being run
    response=respond(event,context,status,responseData,None)

    #Function returns the response from the S3 URL
    return {
        "Response" :response
    }

 

Create the CloudFormation Template

If you’re following from the previous post, the only changes to the CloudFormation template are the variables being passed back and forth. For that reason we won’t go into much detail here, but the full template I used is found below.

---
AWSTemplateFormatVersion: '2010-09-09'
Description: Account Creation Stack
Parameters:
  ModuleName: #Name of the Lambda Module
    Description: The name of the Python file
    Type: String
    Default: "create-account"
  S3Bucket: #S3 bucket in which to retrieve the python script with the Lambda handler
    Description: The name of the bucket that contains your packaged source
    Type: String
    Default: "hollow-acct"
  S3Key: #Name of the zip file
    Description: The name of the ZIP package
    Type: String
    Default: "create-account.zip"
  AccountName: #Account Name
    Description: Account Name To Be Created
    Type: String
    Default: "HollowTest1"
  OUName: #Organizational Unit Name
    Description: Organizational Unit Name To Be Created
    Type: String
    Default: "HollowTest1"
  Email: #Email Address
    Description: Email Address used for the Account
    Type: String    
Resources:
  CreateAccount: #Custom Resource
    Type: Custom::CreateAccount
    Properties:
      ServiceToken:
        Fn::GetAtt:
        - TestFunction #Reference to Function to be run
        - Arn #ARN of the function to be run
      AccountName:
        Ref: AccountName
      OUName:
        Ref: OUName
      Email:
        Ref: Email
  TestFunction: #Lambda Function
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket:
          Ref: S3Bucket
        S3Key:
          Ref: S3Key
      Handler:
        Fn::Join:
        - ''
        - - Ref: ModuleName
          - ".lambda_handler"
      Role:
        Fn::GetAtt:
        - LambdaExecutionRole
        - Arn
      Runtime: python2.7
      Timeout: '30'
  LambdaExecutionRole: #IAM Role for Custom Resource
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      Policies:
      - PolicyName: root
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
            Resource: arn:aws:logs:*:*:*
      - PolicyName: Acct
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - organizations:*
            Resource: "*"
Outputs: #Return output from the Lambda Function Run
  Message:
    Description: Message returned from Lambda
    Value:
      Fn::GetAtt:
      - CreateAccount #Output from the Custom Resource
      - Message #Return property

 

Results

Now that the coding is done, we can deploy the CloudFormation template. I’ve chosen to do this through the command line but you could do it through the console as well. My command line execution is as follows:

aws cloudformation create-stack --stack-name theITHollowCreateAccount1 --template-body file://create-account-CFn.yml --capabilities CAPABILITY_IAM --parameters ParameterKey=AccountName,ParameterValue=theITHollowAcct1 ParameterKey=OUName,ParameterValue=theITHollowOU1 ParameterKey=Email,ParameterValue=aws-temp4@theithollow.com ParameterKey=ModuleName,ParameterValue=create-account ParameterKey=S3Bucket,ParameterValue=hollow-acct ParameterKey=S3Key,ParameterValue=create-account.zip

 

 

After a minute, you’ll see that the CFn stack has been deployed successfully, and that the output for the stack is the account number for the new AWS account.

If we open up the AWS Organizations console, we’ll see that the new account was created and the account number matches our output from the screenshot above.

As we look through the organizational units, we’ll see that a new OU was created and that our new account lives within that OU.

Summary

I hope that this post has shown you what kind of cool stuff you can do with a CloudFormation custom resource. Now think how neat this might be to put in AWS Service Catalog to deploy new accounts on demand. I will admit that this method does have some drawbacks, such as not being able to delete the stack and have the account deleted, but it is what it is.