cottonformation by Example

1. Basic Template

To get started, let’s learn how to define a very standard cloudformation template including a simple Parameter, two Resource depending on each other, and an Output. Eventually we deploy it to AWS Console. Everything is PURE PYTHON.

I recommend to NOT COPY AND PASTE but typing the code in Pycharm to see how type hint / auto complete / doc hint helps you to accelerate code writing

You are responsible to prepare your AWS Credential to call cloudformation API and a S3 bucket to upload template. Follow this boto3 Session reference document to create your own boto session object for authentication. My recommendation is to create a named profile in ${HOME}/.aws/credentials and ${HOME}/.aws/config. And then the code to create boto session looks like this https://github.com/MacHu-GWU/cottonformation-project/blob/main/cottonformation/tests/boto_ses.py.

# -*- coding: utf-8 -*-

"""
To get started, let's learn how to define a very standard cloudformation template
including a simple Parameter, two Resource depending on each other, and
an Output. Eventually we deploy it to AWS Console. Everything is PURE PYTHON.

**I recommend to NOT COPY AND PASTE but typing the code in Pycharm to see
how type hint / auto complete / doc hint helps you to accelerate code writing**

You are responsible to prepare your AWS Credential to call cloudformation API
and a S3 bucket to upload template. Follow this
`boto3 Session reference <https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html>`_
document to create your own boto session object for authentication.
My recommendation is to create a named profile in
``${HOME}/.aws/credentials`` and ``${HOME}/.aws/config``. And then the code
to create boto session looks like this
https://github.com/MacHu-GWU/cottonformation-project/blob/main/cottonformation/tests/boto_ses.py.
"""

# First, import cottonformation, I prefer to use ctf for a short name
import cottonformation as cf

# import the aws service module you need
from cottonformation.res import iam, awslambda

# create a ``Template`` object to represent your cloudformation template
tpl = cf.Template(
    Description="Sample CloudFormation template build on cottonformation library",
)

# create a ``Parameter`` object
param_env_name = cf.Parameter(
    "EnvName",
    Type=cf.Parameter.TypeEnum.String,
)
# the declared ``Parameter`` object is not associated to ``Template`` yet
# we need to explicitly add it to template
tpl.add(param_env_name)

# create a ``Resource`` object for aws iam role
iam_role_for_lambda = iam.Role(
    "IamRoleForLambdaExecution",
    # you don't need to remember the exact name or syntax for
    # trusted entity / assume role policy, cottonformation has a helper for this
    rp_AssumeRolePolicyDocument=cf.helpers.iam.AssumeRolePolicyBuilder(
        cf.helpers.iam.ServicePrincipal.awslambda()
    ).build(),
    p_RoleName=cf.Sub("${EnvName}-iam-role-for-lambda", dict(EnvName=param_env_name.ref())),
    p_Description="Minimal iam role for lambda execution",

    # you don't need to remember the exact ARN for aws managed policy.
    # cottonformation has a helper for this
    p_ManagedPolicyArns=[
        cf.helpers.iam.AwsManagedPolicy.AWSLambdaBasicExecutionRole,
    ]
)
# add resource object to template
tpl.add(iam_role_for_lambda)


# create a ``Resource`` object for aws lambda function
lbd_source_code = """
def handler(event, context):
    return "hello cottonformation"
""".strip()

lbd_func = awslambda.Function(
    "LbdFuncHelloWorld",
    # rp_ stands for Required Property, it will gives you parameter-hint
    # for all valid required properties.
    rp_Code=awslambda.PropFunctionCode(
        p_ZipFile=lbd_source_code,
    ),

    # normally we need to explicitly call GetAtt(resource, attribute)
    # and you need to remember the exact attribute name
    # but cottonformation allow you to instantly reference the attribute
    # powered by auto-complete. the prefix rv_ stands for Return Value
    rp_Role=iam_role_for_lambda.rv_Arn,

    # p_ stands for Property, it will gives you parameter-hint
    # for all valid properties
    p_MemorySize=256,
    p_Timeout=3,

    # some constant value helper here too
    p_Runtime=cf.helpers.awslambda.LambdaRuntime.python37,
    p_Handler="index.handler",
    ra_DependsOn=iam_role_for_lambda,
)
# add resource object to template
tpl.add(lbd_func)

out_lambda_role_arn = cf.Output(
    "LbdRoleArn",
    Description="aws lambda basic execution iam role for reuse",
    Value=iam_role_for_lambda.rv_Arn
)
# add Output object to template
tpl.add(out_lambda_role_arn)


if __name__ == "__main__":
    # my private aws account session and bucket for testing
    from cottonformation.tests.boto_ses import bsm, bucket

    # define the Parameter.EnvName value
    env_name = "ctf-1-quick-start-1-basic"

    # create an environment for deployment, it is generally a boto3 session
    # and a s3 bucket to upload cloudformation template
    env = cf.Env(bsm=bsm)
    env.deploy(
        template=tpl,
        stack_name=env_name,
        stack_parameters=dict(
            EnvName=env_name,
        ),
        bucket_name=bucket,
        include_iam=True,
    )

2. Batch Tagging

Tagging is very important and can be used for many things:

  1. aggregate your bill by project

  2. implement automation based on tag

  3. isolate resources for different environment

  4. optimize cost and automatically shut down invalid resources

In this example, let’s learn how to use the batch_tagging() method to manage tag at scale. You don’t need to memorize whether an AWS Resource supports Tagging. cottonformation will handle that automatically for you.

# -*- coding: utf-8 -*-

"""
Tagging is very important and can be used for many things:

1. aggregate your bill by project
2. implement automation based on tag
3. isolate resources for different environment
4. optimize cost and automatically shut down invalid resources

In this example, let's learn how to use the
:meth:`cottonformation.core.template.Template.batch_tagging` method
to manage tag at scale. You don't need to memorize whether an AWS Resource
supports Tagging. cottonformation will handle that automatically for you.
"""

import cottonformation as cf
from cottonformation.res import iam

# declare a Template
tpl = cf.Template(
    Description="Tagging best practice in cottonformation demo",
)

# declare one Parameter and two Resource
param_env_name = cf.Parameter(
    "EnvName",
    Type=cf.Parameter.TypeEnum.String,
)
tpl.add(param_env_name)

# this iam role doesn't have existing tag
iam_role_for_ec2 = iam.Role(
    "IamRoleEC2",
    rp_AssumeRolePolicyDocument=cf.helpers.iam.AssumeRolePolicyBuilder(
        cf.helpers.iam.ServicePrincipal.awslambda()
    ).build(),
    p_RoleName=cf.Sub("${EnvName}-ec2-role", dict(EnvName=param_env_name.ref())),
)
tpl.add(iam_role_for_ec2)

# this iam role has existing tag
iam_role_for_lambda = iam.Role(
    "IamRoleLambda",
    rp_AssumeRolePolicyDocument=cf.helpers.iam.AssumeRolePolicyBuilder(
        cf.helpers.iam.ServicePrincipal.awslambda()
    ).build(),
    p_RoleName=cf.Sub("${EnvName}-lbd-role", dict(EnvName=param_env_name.ref())),
    p_Tags=cf.Tag.make_many(
        Creator="bob@email.com",
    ),
)
tpl.add(iam_role_for_lambda)

# a common best practice is to define some common tag and assign to all
# AWS resource that support Tags. For example, you can use ``ProjectName``
# to indicate what project it belongs to and you can use it to calculate
# AWS Billing. Another example could be using the ``Stage`` tag to implement
# some automation to process resource in different stage differently.
# For instance, ec2 in dev will be automatically shut down to save cost,
# but ec2 in prod will never be stopped.
tpl.batch_tagging(
    dict(
        EnvName=param_env_name.ref(),
        Creator="alice@example.com",
    ),
    mode_overwrite=True,# you can use overwrite flag to choice whether you want to overwrite existing tag
)


if __name__ == "__main__":
    # my private aws account session and bucket for testing
    from cottonformation.tests.boto_ses import bsm, bucket

    # define the Parameter.EnvName value
    env_name = "ctf-1-quick-start-2-tagging"

    env = cf.Env(bsm=bsm)
    env.deploy(
        template=tpl,
        stack_name=env_name,
        stack_parameters=dict(
            EnvName=env_name,
        ),
        bucket_name=bucket,
        include_iam=True,
    )

3. Nested Stack / Template

The best way to organize multi tier / multi app infrastructure is using CloudFormation nested stack https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-nested-stacks.html. However, with cottonformation, your nested template / resource can be easily declared and import. The original purpose of CloudFormation nested stack is to split big template into multiple small one. With cottonformation, this is unnecessary.

But if you still want to do that in nested stack, cottonformation can easily do that too.

Assume you have a complex architect design like this. Each line represent a CloudFormation Stack / Template. The infrastructure tier defines the common resource for all other tier, for example, IAM Role, Security Group. And the shared app tier can define the resources used for all other apps, for example s3 bucket. Eventually the concrete app1, 2, ... can define the resources needed to run the app:

infrastructure
|-- app tier
    |-- app1
    |-- app2
|-- data tier
    |-- data1
    |-- data2
...
# -*- coding: utf-8 -*-

"""
The best way to organize multi tier / multi app infrastructure is using
CloudFormation nested stack https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-nested-stacks.html.
However, with cottonformation, your nested template / resource can be easily declared
and import. The original purpose of CloudFormation nested stack is to split
big template into multiple small one. **With cottonformation, this is unnecessary**.

But if you still want to do that in nested stack, cottonformation can easily do
that too.

Assume you have a complex architect design like this. Each line represent a
CloudFormation Stack / Template. The ``infrastructure tier`` defines
the common resource for all other tier, for example, IAM Role, Security Group.
And the ``shared app tier`` can define the resources used for all other apps,
for example s3 bucket. Eventually the concrete ``app1, 2, ...`` can define
the resources needed to run the app::

    infrastructure
    |-- app tier
        |-- app1
        |-- app2
    |-- data tier
        |-- data1
        |-- data2
    ...
"""

import cottonformation as cf
from cottonformation.res import iam, awslambda, cloudformation

# global parameter
param_project_name = cf.Parameter(
    "ProjectName", Type=cf.Parameter.TypeEnum.String
)

param_stage = cf.Parameter(
    "Stage", Type=cf.Parameter.TypeEnum.String
)


# We declared lots of template here. Each one can be deployed independently
# but cottonformation will put them together using nested stack at the end.
#--- Template(infra) ---
tpl_infra_tier = cf.Template(Description="the master/infra tier")

tpl_infra_tier.add(param_project_name)
tpl_infra_tier.add(param_stage)

iam_role_for_lambda = iam.Role(
    "IamRoleForLambdaExecution",
    rp_AssumeRolePolicyDocument=cf.helpers.iam.AssumeRolePolicyBuilder(
        cf.helpers.iam.ServicePrincipal.awslambda()
    ).build(),
    p_RoleName=cf.Sub(
        "${ProjectName}-${Stage}-lambda-role",
        dict(ProjectName=param_project_name.ref(), Stage=param_stage.ref())
    ),
    p_Description="Minimal iam role for lambda execution",
    p_ManagedPolicyArns=[
        cf.helpers.iam.AwsManagedPolicy.AWSLambdaBasicExecutionRole,
    ]
)
tpl_infra_tier.add(iam_role_for_lambda)


#--- Template(app tier) ---
param_lambda_role_arn = cf.Parameter(
    "LambdaRoleArn", Type=cf.Parameter.TypeEnum.String,
)

tpl_app_tier = cf.Template(Description="the app tier")
tpl_app_tier.add(param_project_name)
tpl_app_tier.add(param_stage)
tpl_app_tier.add(param_lambda_role_arn)


#--- Template(app1) ---
tpl_app1 = cf.Template(Description="the app1")

tpl_app1.add(param_project_name)
tpl_app1.add(param_stage)
tpl_app1.add(param_lambda_role_arn)

lbd_source_code = """
def handler(event, context):
    return "this is app1"
""".strip()

lbd_func_app1 = awslambda.Function(
    "LbdFuncApp1",
    rp_Code=awslambda.PropFunctionCode(
        p_ZipFile=lbd_source_code,
    ),
    rp_Role=param_lambda_role_arn.ref(),
    p_FunctionName=cf.Sub(
        "${ProjectName}-${Stage}-app1",
        dict(ProjectName=param_project_name.ref(), Stage=param_stage.ref())
    ),
    p_MemorySize=128,
    p_Timeout=3,
    p_Runtime=cf.helpers.awslambda.LambdaRuntime.python37,
    p_Handler="index.handler",
)
tpl_app1.add(lbd_func_app1)


#--- Template(app2) ---
tpl_app2 = cf.Template(Description="the app2")

tpl_app2.add(param_project_name)
tpl_app2.add(param_stage)
tpl_app2.add(param_lambda_role_arn)

lbd_source_code = """
def handler(event, context):
    return "this is app2"
""".strip()

lbd_func_app2 = awslambda.Function(
    "LbdFuncApp2",
    rp_Code=awslambda.PropFunctionCode(
        p_ZipFile=lbd_source_code,
    ),
    rp_Role=param_lambda_role_arn.ref(),
    p_FunctionName=cf.Sub(
        "${ProjectName}-${Stage}-app2",
        dict(ProjectName=param_project_name.ref(), Stage=param_stage.ref())
    ),
    p_MemorySize=128,
    p_Timeout=3,
    p_Runtime=cf.helpers.awslambda.LambdaRuntime.python37,
    p_Handler="index.handler",
)
tpl_app2.add(lbd_func_app2)


#--- associate nested stack and template ---
# If you don't know how to do nested stack in classic way
# you can read: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-nested-stacks.html
#
# cottonformation gives you a simple way to define nested stack / template relationship
# you just need to create a ``cloudformation.Stack`` object in parent stack
# and use the ``parent_template.add_nested_stack(nested_stack_resource, child_template``
# method to associate them together. Then cottonformation will automatically
# handle the deployment for you


# define a stack resource to represent the nested stack
# you have to pass in the parameter into child stack from here
app_tier_stack = cloudformation.Stack(
    "AppTier",
    rp_TemplateURL="",  # will know later, just use "" as dummy data now
    p_Parameters={
        param_project_name.id: param_project_name.ref(),
        param_stage.id: param_stage.ref(),
        param_lambda_role_arn.id: iam_role_for_lambda.rv_Arn,
    },
)
tpl_infra_tier.add(app_tier_stack)
# associate stack and template 'infra tier' <---> 'app tier'
tpl_infra_tier.add_nested_stack(app_tier_stack, tpl_app_tier)


# repeat this for 'app tier' <---> 'app1 tier'
app1_stack = cloudformation.Stack(
    "App1",
    rp_TemplateURL="", # will know later
    p_Parameters={
        param_project_name.id: param_project_name.ref(),
        param_stage.id: param_stage.ref(),
        param_lambda_role_arn.id: param_lambda_role_arn.ref()
    },
)
tpl_app_tier.add(app1_stack)
tpl_app_tier.add_nested_stack(app1_stack, tpl_app1)

# repeat this for 'app tier' <---> 'app2 tier'
app2_stack = cloudformation.Stack(
    "App2",
    rp_TemplateURL="", # will know later
    p_Parameters={
        param_project_name.id: param_project_name.ref(),
        param_stage.id: param_stage.ref(),
        param_lambda_role_arn.id: param_lambda_role_arn.ref()
    },
)
tpl_app_tier.add(app2_stack)
tpl_app_tier.add_nested_stack(app2_stack, tpl_app2)

# Note, data tier is intentionally skipped. Since you can easily repeat
# this pattern for data tier


if __name__ == "__main__":
    # my private aws account session and bucket for testing
    from cottonformation.tests.boto_ses import bsm, bucket

    # define the Parameter.EnvName value
    project_name = "ctf-1-quick-start-3-nested-stack"
    stage = "dev"

    # there's no additional step to deploy a nested stack
    # cottonformation will automatically upload all nested template to s3
    # and update the AWS::CloudFormation::Stack.TemplateUrl property for you.
    env = cf.Env(bsm=bsm)
    env.deploy(
        template=tpl_infra_tier,
        stack_name=project_name,
        stack_parameters={
            param_project_name.id: project_name,
            param_stage.id: stage,
        },
        bucket_name=bucket,
        include_iam=True,
    )

4. Intrinsic Functions

Intrinsic Functions is a CloudFormation mechanism allow you to manipulate string interpolation and variable in JSON.

In 90% of the case, it is unnecessary with cottonformation. Because you are writing CloudFormation in Python, and it is way easier and manageable to do so in Python. In general, you could completely ignore the native CloudFormation Parameter system, and pass in your own Python variable. You can see more about this pattern at cottonformation Styled Parameter

However, cottonformation has the ability to use the native Intrinsic Functions.

# -*- coding: utf-8 -*-

"""
In 90% of the case, it is unnecessary with ``cottonformation``. Because you are
writing CloudFormation in Python, and it is way easier and manageable
to do so in Python.

However, ``cottonformation`` has the ability to use the native Intrinsic Functions.
"""

# First, import cottonformation, I prefer to use cf for a short name
import cottonformation as cf

# import the aws service module you need
from cottonformation.res import s3

# create a ``Template`` object to represent your cloudformation template
tpl = cf.Template(
    Description="Intrinsic Function Example",
)

# create project name ``Parameter`` object
# it is a common prefix for all naming convention
param_project_name = cf.Parameter(
    "ProjectName",
    Type=cf.Parameter.TypeEnum.String,
    Default="cf-example-intrinsic-function",
)
tpl.add(param_project_name)

# create a dummy ``Resource`` object for aws s3 bucket
# this bucket object is just a data container to demonstrate intrinsic function
bucket = s3.Bucket(
    "S3Bucket",
    p_BucketName=cf.Join(
        delimiter="-",
        list_of_values=[
            cf.AWS_ACCOUNT_ID,
            cf.AWS_REGION,
            param_project_name,
        ],
    ),
    # a Ref example
    p_Tags=cf.Tag.make_many(
        Project1=cf.Ref(param_project_name),  # you can Ref
        Project2=param_project_name.ref(),  # or use the oop style Ref
        Project3=param_project_name,  # or just use Parameter itself
    ),
)
tpl.add(bucket)

output_bucket_arn = cf.Output(
    "S3BucketArn",
    # a Fn::GetAttr example
    Value=cf.GetAtt(bucket, "Arn"),  # you can use ``bucket.rv_Arn`` shortcut too
    # an Output.Export example, you can use
    # cf.ImportValue(name="my-project-name-s3-bucket") to reference it later
    Export=cf.Export(
        # a Fn::Sub example
        # cottonformation is smart enough to pass in a parameter object
        # directly without reference
        Name=cf.Sub(
            "${project_name}-s3-bucket",
            dict(project_name=param_project_name),
        )
    ),
)
tpl.add(output_bucket_arn)

if __name__ == "__main__":
    # your private aws account session and bucket for testing
    from cottonformation.tests.boto_ses import bsm

    # create an environment for deployment, it is generally a boto3 session
    # manager and an optional s3 bucket to upload cloudformation template
    env = cf.Env(bsm=bsm)

    # validate the template
    env.validate(tpl)

    # pretty print the template
    print(tpl.to_json())

    # json view of this template
    _ = {
        "AWSTemplateFormatVersion": "2010-09-09",
        "Description": "Intrinsic Function Example",
        "Metadata": {"cottonformation": {"version": "0.0.7"}},
        "Parameters": {
            "ProjectName": {
                "Type": "String",
                "Default": "cf-example-intrinsic-function",
            }
        },
        "Resources": {
            "S3Bucket": {
                "Type": "AWS::S3::Bucket",
                "Properties": {
                    "BucketName": {
                        "Fn::Join": [
                            "-",
                            [
                                {"Ref": "AWS::AccountId"},
                                {"Ref": "AWS::Region"},
                                {"Ref": "ProjectName"},
                            ],
                        ]
                    },
                    "Tags": [
                        {"Key": "Project1", "Value": {"Ref": "ProjectName"}},
                        {"Key": "Project2", "Value": {"Ref": "ProjectName"}},
                        {"Key": "Project3", "Value": {"Ref": "ProjectName"}},
                    ],
                },
            }
        },
        "Outputs": {
            "S3BucketArn": {
                "Value": {"Fn::GetAtt": ["S3Bucket", "Arn"]},
                "Export": {
                    "Name": {
                        "Fn::Sub": [
                            "${project_name}-s3-bucket",
                            {"project_name": {"Ref": "ProjectName"}},
                        ]
                    }
                },
            }
        },
    }

    stack_name = "cottonformation-example-intrinsic-func"
    # deploy this example
    env.deploy(tpl, stack_name=stack_name)

    # clean up AWS resources for this example
    # env.delete(stack_name=stack_name)

Here’s an example of the equivalent template WITHOUT Intrinsic Functions.

# -*- coding: utf-8 -*-

"""
Example of cottonformation without intrinsic function.
"""

import attr
from boto_session_manager import BotoSesManager
import cottonformation as cf
from cottonformation.res import s3


# use Python data class to replace the CloudFormation Parameter System
@attr.s
class Params:
    project_name: str = attr.ib(default="cf-example-intrinsic-function")

# initialize params object
params = Params()

# get value of Pseudo Parameter value from AWS boto3 session
bsm = BotoSesManager()
aws_account_id = bsm.aws_account_id
aws_region = bsm.aws_region

tpl = cf.Template(
    Description="Intrinsic Function Example",
)

bucket = s3.Bucket(
    "S3Bucket",
    p_BucketName="-".join([aws_account_id, aws_region, params.project_name]),
    p_Tags=cf.Tag.make_many(
        Project=params.project_name,
    ),
)
tpl.add(bucket)

output_bucket_arn = cf.Output(
    "S3BucketArn",
    Value=cf.GetAtt(bucket, "Arn"),
    Export=cf.Export(
        # use f-string to replace Sub
        Name=f"{params.project_name}-s3-bucket"
    ),
)
tpl.add(output_bucket_arn)

if __name__ == "__main__":
    print(tpl.to_json())

5. Condition Functions

Condition Functions is a CloudFormation mechanism allow you to declare conditional resource and dynamic value for resource property.

In 90% of the case, it is unnecessary with cottonformation. Because you are writing CloudFormation in Python, and it is way easier and manageable to do so in Python. In general, you could completely ignore the native CloudFormation Parameter system, and pass in your own Python variable. And then use if, else to declare conditional resource and dynamic value. You can see more about this pattern at cottonformation Styled Parameter

However, cottonformation has the ability to use the native Condition Functions.

# -*- coding: utf-8 -*-

"""
This is an example CloudFormation stack to explain the ``Condition Function``
implementation in ``cottonformation``.

Consider the following use case.

1. You have a multi-region stack that will be deployed to many AWS Region in
the same AWS Account. For those globally unique resources like IAM Role,
you only want to create once in the "Main AWS Region". For example, your
main region is us-east-1, you only want to create IAM role in us-east-1
CloudFormation stack.

2. You may have multiple EC2 deployed in different region. You want to create
a tag to indicate that whether if it is in the "Main AWS Region". So you can
execute the same business logic in different ways.
"""

# First, import cottonformation, I prefer to use cf for a short name
import cottonformation as cf

# import the aws service module you need
from cottonformation.res import s3

# create a ``Template`` object to represent your cloudformation template
tpl = cf.Template(
    Description="Conditional Function Example",
)

# create main aws region ``Parameter`` object
param_main_aws_region = cf.Parameter(
    "MainAWSRegion",
    Type=cf.Parameter.TypeEnum.String,
    Default="us-east-1",
)
tpl.add(param_main_aws_region)

# create
condition_is_main_aws_region = cf.Equals(
    "IsMainAWSRegion",
    cf.AWS_REGION,
    param_main_aws_region,
)
tpl.add(condition_is_main_aws_region)

# this is a conditional IAM Role, only exists in the main aws region
# we don't want to create a REAL Role, so we create a S3 Bucket instead for
# demonstration purpose
iam_role_for_ec2 = s3.Bucket(
    "IamRoleForEC2",
    p_BucketName=cf.Join(
        "-", [cf.AWS_ACCOUNT_ID, "cottonformation", "example", "condition", "for-ec2"]
    ),
    ra_Condition=condition_is_main_aws_region,
)
tpl.add(iam_role_for_ec2)

# we don't want to create a REAL Role, so we create a S3 Bucket instead for
# demonstration purpose
ec2_server = s3.Bucket(
    "Ec2Server",
    p_BucketName=cf.Join(
        "-",
        [cf.AWS_ACCOUNT_ID, "cottonformation", "example", "condition", "ec2-server"],
    ),
    p_Tags=cf.Tag.make_many(
        # this is a dynamic value
        IsMainAwsRegion=cf.If(condition_is_main_aws_region, "Y", "N")
    ),
)
tpl.add(ec2_server)

if __name__ == "__main__":
    # your private aws account session and bucket for testing
    from cottonformation.tests.boto_ses import bsm

    # create an environment for deployment, it is generally a boto3 session
    # manager and an optional s3 bucket to upload cloudformation template
    env = cf.Env(bsm=bsm)

    # validate the template
    env.validate(tpl)

    # pretty print the template
    print(tpl.to_json())

    # json view of this template
    _ = {
        "AWSTemplateFormatVersion": "2010-09-09",
        "Description": "Conditional Function Example",
        "Metadata": {"cottonformation": {"version": "0.0.8"}},
        "Parameters": {"MainAWSRegion": {"Type": "String", "Default": "us-east-1"}},
        "Conditions": {
            "IsMainAWSRegion": {
                "Fn::Equals": [{"Ref": "AWS::Region"}, {"Ref": "MainAWSRegion"}]
            }
        },
        "Resources": {
            "IamRoleForEC2": {
                "Type": "AWS::S3::Bucket",
                "Condition": "IsMainAWSRegion",
                "Properties": {
                    "BucketName": {
                        "Fn::Join": [
                            "-",
                            [
                                {"Ref": "AWS::AccountId"},
                                "cottonformation",
                                "example",
                                "condition",
                                "for-ec2",
                            ],
                        ]
                    }
                },
            },
            "Ec2Server": {
                "Type": "AWS::S3::Bucket",
                "Properties": {
                    "BucketName": {
                        "Fn::Join": [
                            "-",
                            [
                                {"Ref": "AWS::AccountId"},
                                "cottonformation",
                                "example",
                                "condition",
                                "ec2-server",
                            ],
                        ]
                    },
                    "Tags": [
                        {
                            "Key": "IsMainAwsRegion",
                            "Value": {"Fn::If": ["IsMainAWSRegion", "Y", "N"]},
                        }
                    ],
                },
            },
        },
    }

    stack_name = "cottonformation-example-condition"
    # deploy this example
    env.deploy(tpl, stack_name=stack_name)

    # clean up AWS resources for this example
    env.delete(stack_name=stack_name)

Here’s an example of the equivalent template WITHOUT Condition Functions.

# -*- coding: utf-8 -*-

"""
Example of cottonformation without condition function.
"""

import attr
from boto_session_manager import BotoSesManager
import cottonformation as cf
from cottonformation.res import s3


# use Python data class to replace the CloudFormation Parameter System
@attr.s
class Params:
    main_aws_region: str = attr.ib(default="us-east-1")


# initialize params object
params = Params()

# get value of Pseudo Parameter value from AWS boto3 session
bsm = BotoSesManager()
aws_account_id = bsm.aws_account_id
aws_region = bsm.aws_region

tpl = cf.Template(
    Description="Conditional Function Example",
)

# use if else logic to declare conditional resource
if params.main_aws_region == aws_region:
    iam_role_for_ec2 = s3.Bucket(
        "IamRoleForEC2",
        p_BucketName=cf.Join(
            "-", [aws_account_id, "cottonformation", "example", "condition", "for-ec2"]
        ),
    )
    tpl.add(iam_role_for_ec2)

ec2_server = s3.Bucket(
    "Ec2Server",
    p_BucketName=cf.Join(
        "-",
        [cf.AWS_ACCOUNT_ID, "cottonformation", "example", "condition", "ec2-server"],
    ),
    p_Tags=cf.Tag.make_many(
        # use if else statement to replace Condition Function If
        IsMainAwsRegion="Y"
        if params.main_aws_region == aws_region
        else "N"
    ),
)
tpl.add(ec2_server)

if __name__ == "__main__":
    tpl.to_json()