Organize AWS Object using Resource Group¶
When there are many resources in your template, it becomes difficult to manage and debug. You probably want to put resources into different logic “Resource Group”, and you want to test them separately, even though in most of the case they depend on each other. For debugging, it is always better to deploy resources gradually rather than deploy all of them at once.
Let’s walk through an example to demonstrate how and the benefit of doing this.
Step 1. Declare cottonformation style parameter¶
At begin, we simply declare a CloudFormation stack object. And declared two simple cottonformation style parameters project_name
and stage
. Stack is not associated with a CloudFormation template object yet.
# -*- coding: utf-8 -*-
import attr
import cottonformation as cf
@attr.s
class IamStack(cf.Stack):
project_name: str = attr.ib()
stage: str = attr.ib()
@property
def env_name(self):
"""
A prefix for most of naming convention. Isolate resource from each other.
"""
return f"{self.project_name}-{self.stage}"
@property
def stack_name(self):
"""
CloudFormation stack name.
"""
return f"{self.env_name}-iam-stack"
Step 2. Declare abstract resource group logic¶
Now we believe that we will need two resource groups. For each resource group, we define a method to put into creation logics. And we have a high level method post_hook
to explicitly call resource group creation methods. Of course, you can easily comment them in-and-out to enable/disable those resources. Right now we have nothing in those resource group creation methods.
# -*- coding: utf-8 -*-
import attr
import cottonformation as cf
@attr.s
class IamStack(cf.Stack):
project_name: str = attr.ib()
stage: str = attr.ib()
@property
def env_name(self):
"""
A prefix for most of naming convention. Isolate resource from each other.
"""
return f"{self.project_name}-{self.stage}"
@property
def stack_name(self):
"""
CloudFormation stack name.
"""
return f"{self.env_name}-iam-stack"
#=============================================================================
# New Code starts here
#=============================================================================
def mk_rg1(self):
"""
Make resource group 1
"""
pass
def mk_rg2(self):
"""
Make resource group 2
"""
pass
def mk_rg3(self):
"""
Make resource group 3
"""
pass
def post_hook(self):
"""
A user custom post stack initialization hook function. Will be executed
after object initialization.
We will put all resources in two different resource group.
And there will be a factory method for each resource group. Of course
we have to explicitly call it to create those resources.
"""
self.mk_rg1()
self.mk_rg2()
self.mk_rg3()
Step 3. Declare AWS Resources and Resource Group¶
We declare some AWS Resource for each resource group. Here we use IAM.Group, because it is fast to create and basically has no effect to the AWS Account.
Resource group itself is similar to other AWS Object, you have to pass a unique logic id. Basically you can think that it is just a AWS Object container (Parameter, Resource, Output, Mapping, Condition, Rule …), even for another Resource Group. You can use cottonformation.core.model.ResourceGroup.add
or cottonformation.core.model.ResourceGroup.add_many
API to add other resource to a resource group.
At the end, we create a instance of this stack and a template object for future use. At this moment, stack and template doesn’t know each other yet.
# -*- coding: utf-8 -*-
import attr
import cottonformation as cf
from cottonformation.res import iam
@attr.s
class IamStack(cf.Stack):
project_name: str = attr.ib()
stage: str = attr.ib()
@property
def env_name(self):
"""
A prefix for most of naming convention. Isolate resource from each other.
"""
return f"{self.project_name}-{self.stage}"
@property
def stack_name(self):
"""
CloudFormation stack name.
"""
return f"{self.env_name}-iam-stack"
#=============================================================================
# New Code starts here
#=============================================================================
def mk_rg1(self):
"""
Make resource group 1
"""
# declare a resource group, you can use Stack.rg1 to access it later.
self.rg1 = cf.ResourceGroup("RG1")
# declare a resource
self.iam_group1 = iam.Group(
"IamGroup1",
p_GroupName=f"{self.env_name}-group1",
)
# add resource to resource group
# internally it use "Depends On" mechanism. In this example, we can say
# rg1 depends on iam_group1
self.rg1.add(self.iam_group1)
def mk_rg2(self):
"""
Make resource group 2
"""
self.rg2 = cf.ResourceGroup("RG2")
# you can even add another resource group to it
self.rg2.add(self.rg1)
self.iam_group2 = iam.Group(
"IamGroup2",
p_GroupName=f"{self.env_name}-group2",
)
self.rg2.add(self.iam_group2)
def mk_rg3(self):
"""
Make resource group 3
"""
self.rg3 = cf.ResourceGroup("RG3")
self.rg3.add(self.rg2)
self.iam_group3 = iam.Group(
"IamGroup3",
p_GroupName=f"{self.env_name}-group3",
)
self.rg3.add(self.iam_group3)
def post_hook(self):
"""
A user custom post stack initialization hook function. Will be executed
after object initialization.
We will put all resources in two different resource group.
And there will be a factory method for each resource group. Of course
we have to explicitly call it to create those resources.
"""
self.mk_rg1()
self.mk_rg2()
self.mk_rg3()
#=============================================================================
# New Code starts here
#=============================================================================
iam_stack = IamStack(
project_name="ctf-best-prac-res-group",
stage="dev",
)
tpl = cf.Template(Description="Demo: Resource Group best practice")
Step 4. Manage Resource Group in Template¶
To add resource declared in a stack to a cloudformation Template is easy. You can add resource group in ascending order. Or you can just add the last resource group in dependency chain. All dependency resource will be automatically added. We can easily enable/disable many resources by comment in/out.
For debug, sometime you want to remove resource in descending order.
# -*- coding: utf-8 -*-
import attr
import cottonformation as cf
from cottonformation.res import iam
@attr.s
class IamStack(cf.Stack):
project_name: str = attr.ib()
stage: str = attr.ib()
@property
def env_name(self):
"""
A prefix for most of naming convention. Isolate resource from each other.
"""
return f"{self.project_name}-{self.stage}"
@property
def stack_name(self):
"""
CloudFormation stack name.
"""
return f"{self.env_name}-iam-stack"
def mk_rg1(self):
"""
Make resource group 1
"""
# declare a resource group, you can use Stack.rg1 to access it later.
self.rg1 = cf.ResourceGroup("RG1")
# declare a resource
self.iam_group1 = iam.Group(
"IamGroup1",
p_GroupName=f"{self.env_name}-group1",
)
# add resource to resource group
# internally it use "Depends On" mechanism. In this example, we can say
# rg1 depends on iam_group1
self.rg1.add(self.iam_group1)
def mk_rg2(self):
"""
Make resource group 2
"""
self.rg2 = cf.ResourceGroup("RG2")
# you can even add another resource group to it
self.rg2.add(self.rg1)
self.iam_group2 = iam.Group(
"IamGroup2",
p_GroupName=f"{self.env_name}-group2",
)
self.rg2.add(self.iam_group2)
def mk_rg3(self):
"""
Make resource group 3
"""
self.rg3 = cf.ResourceGroup("RG3")
self.rg3.add(self.rg2)
self.iam_group3 = iam.Group(
"IamGroup3",
p_GroupName=f"{self.env_name}-group3",
)
self.rg3.add(self.iam_group3)
def post_hook(self):
"""
A user custom post stack initialization hook function. Will be executed
after object initialization.
We will put all resources in two different resource group.
And there will be a factory method for each resource group. Of course
we have to explicitly call it to create those resources.
"""
self.mk_rg1()
self.mk_rg2()
self.mk_rg3()
iam_stack = IamStack(
project_name="ctf-best-prac-res-group",
stage="dev",
)
tpl = cf.Template(Description="Demo: Resource Group best practice")
#=============================================================================
# New Code starts here
#=============================================================================
# add resource group from stack to template, in ascending order.
tpl.add(iam_stack.rg1)
tpl.add(iam_stack.rg2)
tpl.add(iam_stack.rg3)
# or you can just do: add(rg3). since we declared that rg3 depends on rg2
# and rg2 depends on rg1, all resource declared in rg2 and rg1 will be
# automatically added.
# tpl.add(iam_stack.rg3) # uncomment this for testing
# For debugging, you could just remove some resource group and comment them
# in and out one by one between cloudformation deployment.
tpl.remove(iam_stack.rg3)
tpl.remove(iam_stack.rg2)
# or you can just do: remove(rg2). since rg3 depends on rg2
# if we remove rg2, rg3 will be automatically removed
# tpl.add(iam_stack.rg2) # uncomment this for testing
Step 5. Play with Deployment¶
Now we can play with the deployment. And feel free to comment in and out either in the template.add(resource_group)
code block, either in the template.remove(resource_group)
code block.
# -*- coding: utf-8 -*-
import attr
import cottonformation as cf
from cottonformation.res import iam
dummy_policy_document = {
"Version": "2012-10-17",
"Statement": [
]
}
@attr.s
class IamStack(cf.Stack):
project_name: str = attr.ib()
stage: str = attr.ib()
@property
def env_name(self):
"""
A prefix for most of naming convention. Isolate resource from each other.
"""
return f"{self.project_name}-{self.stage}"
@property
def stack_name(self):
"""
CloudFormation stack name.
"""
return f"{self.env_name}-iam-stack"
def mk_rg1(self):
"""
Make resource group 1
"""
# declare a resource group, you can use Stack.rg1 to access it later.
self.rg1 = cf.ResourceGroup("RG1")
# declare a resource
self.iam_group1 = iam.Group(
"IamGroup1",
p_GroupName=f"{self.env_name}-group1",
)
# add resource to resource group
# internally it use "Depends On" mechanism. In this example, we can say
# rg1 depends on iam_group1
self.rg1.add(self.iam_group1)
def mk_rg2(self):
"""
Make resource group 2
"""
self.rg2 = cf.ResourceGroup("RG2")
# you can even add another resource group to it
self.rg2.add(self.rg1)
self.iam_group2 = iam.Group(
"IamGroup2",
p_GroupName=f"{self.env_name}-group2",
)
self.rg2.add(self.iam_group2)
def mk_rg3(self):
"""
Make resource group 3
"""
self.rg3 = cf.ResourceGroup("RG3")
self.rg3.add(self.rg2)
self.iam_group3 = iam.Group(
"IamGroup3",
p_GroupName=f"{self.env_name}-group3",
)
self.rg3.add(self.iam_group3)
def post_hook(self):
"""
A user custom post stack initialization hook function. Will be executed
after object initialization.
We will put all resources in two different resource group.
And there will be a factory method for each resource group. Of course
we have to explicitly call it to create those resources.
"""
self.mk_rg1()
self.mk_rg2()
self.mk_rg3()
iam_stack = IamStack(
project_name="ctf-best-prac-res-group",
stage="dev",
)
tpl = cf.Template(Description="Demo: Resource Group best practice")
# add resource group from stack to template, in ascending order.
tpl.add(iam_stack.rg1)
tpl.add(iam_stack.rg2)
tpl.add(iam_stack.rg3)
# or you can just do: add(rg3). since we declared that rg3 depends on rg2
# and rg2 depends on rg1, all resource declared in rg2 and rg1 will be
# automatically added.
# tpl.add(iam_stack.rg3) # uncomment this for testing
# For debugging, you could just remove some resource group and comment them
# in and out one by one between cloudformation deployment.
tpl.remove(iam_stack.rg3)
tpl.remove(iam_stack.rg2)
# or you can just do: remove(rg2). since rg3 depends on rg2
# if we remove rg2, rg3 will be automatically removed
# tpl.add(iam_stack.rg2) # uncomment this for testing
#=============================================================================
# New Code starts here
#=============================================================================
tpl.batch_tagging(dict(ProjectName=iam_stack.project_name, Stage=iam_stack.stage))
if __name__ == "__main__":
# my private aws account session and bucket for testing
from cottonformation.tests.boto_ses import bsm, bucket
env = cf.Env(bsm=bsm)
env.deploy(
template=tpl,
stack_name=iam_stack.stack_name,
bucket_name=bucket,
include_iam=True,
)