AWS CDK: Configure Alternative Operations & Security Contacts using StackSets

October 22nd, 2023 728 Words

To maintain a common foundation of resources across a complex AWS Organization, CloudFormation StackSet is the perfect companion. Usually, you want to deploy a common baseline of AWS resources and additionally specificy custom deployments; like setting custom contact information for billing, operations, or security communications.

AWS CloudFormation and CloudFormation StackSet support two different kind of deployment processes:

  • A self-managed StackSet can be deployed to individual AWS accounts and requires a custom IAM Role available in the destination account,
  • A service-managed StackSet can be deployed automatically to existing and future AWS accounts in an existing Organizational Unit.

In combination, you can deploy a foundational service-managed CloudFormation stack automatically to all of your existing and future AWS accounts. This stack may include the required IAM Role(s) for future individual self-managed CloudFormation stacks.

General Architecture

To enable support for self-managed AWS CloudFormation StackSet deployments, you need to create two IAM Roles: AWSCloudFormationStackSetExecutionRole and AWSCloudFormationStackSetExecutionRole . You can do this with the provided AWS CloudFormation templates or with a custom CDK configuration:

// StackSet Administration Role

const policy = new PolicyDocument({
    statements: [
        new PolicyStatement({
            effect: Effect.ALLOW,
            actions: ['sts:AssumeRole'],
            resources: [`arn:*:iam::*:role/AWSCloudFormationStackSetExecutionRole`],
        }),
    ],
}),

new Role(this, 'stackset-administration', {
    roleName: 'AWSCloudFormationStackSetAdministrationRole',
    assumedBy: new ServicePrincipal('cloudformation.amazonaws.com'),
    inlinePolicies: {
        'AssumeRole-AWSCloudFormationStackSetExecutionRole': policy
    },
})

Additionally, all member accounts need to have the following IAM Role for AWS CloudFormation StackSet deployments:

// StackSet Execution Role

new Role(scope, 'role-execution', {
    roleName: 'AWSCloudFormationStackSetExecutionRole',
    assumedBy: new ArnPrincipal(YOUR_MANAGEMENT_ACCOUNT),
    managedPolicies: [
        ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess')
    ],
})

Custom AWS CloudFormation Types

When using custom AWS CloudFormation resource types, you first need to activate every resource type and create an IAM Role with the required policy permission. AWS supports a custom AwsCommunity::Account::AlternateContact resources type and has a public registry for other custom resources.

new CfnTypeActivation(scope, 'activate-alternate-contact', {
    autoUpdate: false,
    typeName: 'AwsCommunity::Account::AlternateContact',
    publisherId: 'c830e97710da0c9954d80ba8df021e5439e7134b',
    type: 'RESOURCE',
    executionRoleArn
})

For activating a custom AWS CloudFormation resource type, you need to provide a corresponding IAM Role of course:

const executionRoleArn = new Role(scope, 'type-activation-role', {
    roleName: 'TypeAlternateContact',
    path: '/custom/',
    assumedBy: new ServicePrincipal('resources.cloudformation.amazonaws.com'),
    inlinePolicies: {
        'alternate-contact': new PolicyDocument({
            statements: [
                new PolicyStatement({
                effect: Effect.ALLOW,
                actions: [
                    'account:PutAlternateContact',
                    'account:DeleteAlternateContact',
                    'account:GetAlternateContact'],
                resources: ['*']
                })
            ]
        })
    }
})

Alternate Security Contact

After the custom CloudFormation resource type is activated, you can reference it:

new CfnResource(this, 'alternate-contact-security', {
    type: 'AwsCommunity::Account::AlternateContact',
    properties: {
        AccountId: Stack.of(this).account,
        AlternateContactType: 'SECURITY',
        Title: 'Mailbox',
        Name: 'Your AWS Security',
        EmailAddress: 'security@example.com',
        PhoneNumber: '001 …',
    },
})

Multi-Account Environment

The snippets above work fine in a single-account AWS environment. But, as soon as you manage multiple AWS accounts, you may want to automate the generic process of deploying all configured resources.

Using the cdk-stackset package, you can create a custom StackSetStack to create all resources of your foundational CloudFormation stack:

import { StackSetStack } from 'cdk-stacksets'

export class Foundation extends StackSetStack {
    constructor(scope: Stack, id: string) {
        super(scope, id)

        this.enableCustomTypeAlternateContact()
        this.enableSelfManagedStackSets()
    }

    enableCustomTypeAlternateContact() { /* … */ }
    enableSelfManagedStackSets() { /* … */ }
}

To automate the deployment of the Foundation stack, the services-managed AWS CloudFormation StackSet object supports applying a simple configuration for Organization Unit and AWS Region:

import { StackSet, StackSetTarget, StackSetTemplate } from 'cdk-stacksets'

export class Provisioner extends Stack {
  constructor(scope: App, id: string) {
    super(scope, id, props)

    new StackSet(this, 'foundation', {
        stackSetName: 'aws-foundation',
        description: 'Custom AWS Foundation',
        deploymentType: DeploymentType.serviceManaged({
            autoDeployEnabled: true,
            autoDeployRetainStacks: true,
        }),
        target: StackSetTarget.fromOrganizationalUnits({
            regions: [
                'eu-central-1'
            ],
            organizationalUnits: [
                'o-123456'
            ],
        }),
        operationPreferences: {
            regionConcurrencyType: RegionConcurrencyType.PARALLEL,
            maxConcurrentPercentage: 100,
            failureTolerancePercentage: 99,
        },
        template: StackSetTemplate.fromStackSetStack(
            new Foundation(this, 'foundation-stack')
        ),
        capabilities: [ Capability.IAM, Capability.NAMED_IAM ],
    })
  }
}

Custom StackSet Deployments

After deploying the foundational AWS resources, you can create additional CloudFormation StackSet stacks with the cdk-stacksets package:

import { StackSetStack } from 'cdk-stacksets'

export class AlternateContacts extends StackSetStack {
    constructor(scope: Stack, id: string) {
        super(scope, id)

        this.configureSecurityContact()
    }

    configureSecurityContact() { /* … */ }
  }
}

Custom self-managed CloudFormation StackSet stacks can be deployed to individual AWS accounts next:

new StackSet(this, 'alternate-contacts', {
    stackSetName: 'aws-alternate-contacts',
    deploymentType: DeploymentType.selfManaged({
        executionRoleName: ROLE_NAME_STACKSET_EXECUTION,
        adminRole,
    }),
    target: StackSetTarget.fromAccounts({
        accounts: [
            '000123456789'
        ],
        regions: [
            'eu-central-1'
        ],
    }),
    operationPreferences: {
        regionConcurrencyType: RegionConcurrencyType.PARALLEL,
        maxConcurrentPercentage: 100,
        failureTolerancePercentage: 99,
    },
    template: StackSetTemplate.fromStackSetStack(
        new AlternateContacts(this, 'alternate-contacts-stack')
    ),
    capabilities: [ Capability.IAM, Capability.NAMED_IAM ],
})

This is complex; this blog post is confusing. AWS CloudFormation is complex, the StackSet features are even more complex. For me, AWS CloudFormation StackSet provides a perfect cloud-native solution to automatically deploy AWS resources, and to configure individual CloudFormation stacks or member accounts.