Tutorial
Now that we have the introduction out of the way, it's time to learn how to use Takomo. The best way to learn is by doing, so let's get our hands dirty and deploy some stacks.

What are we going to build?

Let's do something more interesting than just a trivial single stack example. Let's create a setup where we have a DynamoDB table, a VPC without internet access, a lambda function inside the VPC, and a VPC endpoint to DynamoDB to make it possible for the lambda function to access the DynamoDB table.
To make our configuration resemble a real-life use case, we'll create two separate environments: dev and prod.
Finally, we choose to deploy our stacks to the eu-west-1 region.

AWS credentials

During this tutorial, you'll deploy some stacks, so you need an AWS account where you can safely try things out.
Create an IAM user with administrator permissions.
Next, create access keys for the IAM user and configure them to your ~/.aws/credentials file. Let's name our profile as takomo-tutorial.
~/.aws/credentials
1
[takomo-tutorial]
2
aws_access_key_id = ENTER_YOUR_ACCESS_KEY_ID_HERE
3
aws_secret_access_key = ENTER_YOUR_SECRET_ACCESS_KEY_HERE
Copied!

Project initialization

We'll start by creating a new directory for your Takomo project:
1
mkdir takomo-tutorial
Copied!
From now on, we'll call the takomo-tutorial directory as project's root directory.
Change to the root directory and initialize a new NPM project:
1
cd takomo-tutorial
2
npm init -y
Copied!
Add Takomo as a development dependency:
1
npm install -D takomo
Copied!

Project file structure

Make sure you are in the project root directory and create two other directories under it:
1
mkdir stacks
2
mkdir templates
Copied!
The stacks directory will contain all configuration files for your stacks, and the templates directory is where you'll place template files for the stacks.

Stack groups

Takomo lets you organize stacks into directories under the stacks directory to group them by the environment, region, or other criteria. These directories are called stack groups. You can use them to provide configuration shared by the stacks that belong under the same stack group.
You can also nest stack groups to build tree-like hierarchies. You identify stack groups by their path, which is the file path to the stack group's directory from the stacks directory.
Let's group our stacks first by the environment and then by region. To do that, create directories under the stacks directory like so:
1
mkdir -p stacks/dev/eu-west-1
2
mkdir -p stacks/prod/eu-west-1
Copied!
Now, you should have the following files in place:
1
.
2
├─ stacks
3
│ ├─ dev
4
│ │ └─ eu-west-1
5
│ └─ prod
6
│ └─ eu-west-1
7
├─ templates
8
└─ package.json
Copied!

DynamoDB stack

It's time to start adding configuration for our stacks. We begin by creating a template file for the DynamoDB table. Go ahead and create a new file for it:
1
touch templates/dynamodb.yml
Copied!
Add the following contents to it:
templates/dynamodb.yml
1
Parameters:
2
Environment:
3
Type: String
4
Description: Application environment
5
AllowedValues:
6
- dev
7
- prod
8
9
Resources:
10
Table:
11
Type: AWS::DynamoDB::Table
12
Properties:
13
TableName: !Sub my-table-${Environment}
14
BillingMode: PAY_PER_REQUEST
15
AttributeDefinitions:
16
- AttributeName: id
17
AttributeType: S
18
KeySchema:
19
- AttributeName: id
20
KeyType: HASH
21
22
Outputs:
23
TableName:
24
Value: !Ref Table
25
TableArn:
26
Value: !GetAtt Table.Arn
Copied!
The template is simple; it has a single parameter for the environment used as a suffix of the DynamoDB table name.
Let's then add configuration for the dev environment's DynamoDB stack.
1
touch stacks/dev/eu-west-1/dynamodb.yml
Copied!
Add the following contents to it:
stacks/dev/eu-west-1/dynamodb.yml
1
regions: eu-west-1
2
template: dynamodb.yml
3
parameters:
4
Environment: dev
Copied!
In the stack configuration file, you can find three important properties.
We instruct Takomo to deploy the stack to the eu-west-1 region using the regions property. It's in plural form because you can deploy a single stack to multiple regions, and therefore the regions property accepts a single region or a list of regions.
The template property takes a file path relative to the templates directory and specifies which template file Takomo uses when it deploys the stack.
Finally, we provide values for the stack parameters using the parameters property.
You should now have the following files in place:
1
.
2
├─ stacks
3
│ ├─ dev
4
│ │ └─ eu-west-1
5
│ │ └─ dynamodb.yml
6
│ └─ prod
7
│ └─ eu-west-1
8
├─ templates
9
│ └─ dynamodb.yml
10
└─ package.json
Copied!

First deploy

Alright then, we are now ready to deploy our first stack. Make sure you are in the project's root directory and run the following command:
1
npx tkm stacks deploy --profile takomo-tutorial
Copied!
You should see the deployment plan, and from it that you're about to deploy the DynamoDB stack.
The line printed in green displays the stack's path, which should be /dev/eu-west-1/dynamodb.yml/eu-west-1. The stack path is sort-of a file path to the stack's configuration file under the stacks directory and is used to identify stacks.
From the deployment plan, you can also see the stack's name, which is dev-eu-west-1-dynamodb. We can specify the stack name ourselves by using the name property, but as we didn't do that, Takomo generated the name for us from the stack path.
Choose "continue, but let me review changes to each stack"
You should see a stack-specific deployment plan showing changes about to be performed to the DynamoDB stack.
Choose "continue to deploy the stack, then let me review the remaining stacks"
The deployment should take a few seconds, and after it, you should see a deployment summary.

VPC stack

Let's proceed to the VPC stack. Create a template for it:
1
touch templates/vpc.yml
Copied!
Add the following contents to it:
templates/vpc.yml
1
Parameters:
2
Environment:
3
Type: String
4
Description: Application environment
5
AllowedValues:
6
- dev
7
- prod
8
VpcCidr:
9
Type: String
10
Description: VPC CIDR block
11
12
Resources:
13
Vpc:
14
Type: AWS::EC2::VPC
15
Properties:
16
CidrBlock: !Ref VpcCidr
17
Subnet:
18
Type: AWS::EC2::Subnet
19
Properties:
20
CidrBlock: !Ref VpcCidr
21
VpcId: !Ref Vpc
22
RouteTable:
23
Type: AWS::EC2::RouteTable
24
Properties:
25
VpcId: !Ref Vpc
26
RouteTableAssociation:
27
Type: AWS::EC2::SubnetRouteTableAssociation
28
Properties:
29
SubnetId: !Ref Subnet
30
RouteTableId: !Ref RouteTable
31
32
Outputs:
33
VpcId:
34
Value: !Ref Vpc
35
RouteTableIds:
36
Value: !Ref RouteTable
37
SubnetIds:
38
Value: !Ref Subnet
39
Copied!
Then, create the stack configuration file:
1
touch stacks/dev/eu-west-1/vpc.yml
Copied!
Add the following contents to it:
stacks/dev/eu-west-1/vpc.yml
1
regions: eu-west-1
2
template: vpc.yml
3
parameters:
4
Environment: dev
5
VpcCidr: 10.0.0.0/26
Copied!
You should now have the following files in place:
1
.
2
├─ stacks
3
│ ├─ dev
4
│ │ └─ eu-west-1
5
│ │ ├─ dynamodb.yml
6
│ │ └─ vpc.yml
7
│ └─ prod
8
│ └─ eu-west-1
9
├─ templates
10
│ ├─ dynamodb.yml
11
│ └─ vpc.yml
12
└─ package.json
Copied!

Listing stacks

Let's quickly check what stacks we have configured and what's their current status:
1
npx tkm stacks list --profile takomo-tutorial
Copied!
You should see two stacks: the DynamoDB stack we already deployed and the VPC stack that is still waiting for deployment.

Second deploy

Rerun the deploy command to get also the VPC stack deployed:
1
npx tkm stacks deploy --profile takomo-tutorial
Copied!
This time the deployment plan shows you both of the stacks. The DynamoDB stack already exists, and it is about to be updated. The VPC stack, on the other hand, doesn't exist yet, so Takomo needs to create it.
Choose "continue, but let me review changes to each stack"
The DynamoDB stack contains no changes and you don't need to confirm its deployment. Instead, you'll see the plan for the VPC stack.
Review the changes and choose "continue to deploy the stack, then let me review the remaining stacks".
Like earlier, the deployment takes just a short amount of time, and you'll see the summary once it completes.

Shared configuration

At this point, we notice that we have specified the same properties in multiple configuration files. Both of our stacks belong to the dev environment and reside in the eu-west-1 region.
Earlier, we learned that we can use stack groups to provide common configuration for multiple stacks. You provide configuration for a stack group by placing a config.yml file in its directory. Stacks that belong to the stack group inherit the stack group's configuration.
Let's start by creating configuration for the /dev stack group.
Create the configuration file:
1
touch stacks/dev/config.yml
Copied!
Add the following contents to it:
stacks/dev/config.yml
1
data:
2
environment: dev
Copied!
We specified the environment under the data property. It's an object that can contain arbitrary values.
Then, create another file for the /dev/eu-west-1 stack group:
1
touch stacks/dev/eu-west-1/config.yml
Copied!
Add the following contents to it:
stacks/dev/eu-west-1/config.yml
1
regions: eu-west-1
Copied!
We can now remove the regions properties from our stack configuration files. We also need to modify the way we give value for the Environment parameter.
Update the stack configuration files to look like this:
stacks/dev/eu-west-1/dynamodb.yml
1
template: dynamodb.yml
2
parameters:
3
Environment: {{ stackGroup.data.environment }}
Copied!
stacks/dev/eu-west-1/vpc.yml
1
template: vpc.yml
2
parameters:
3
Environment: {{ stackGroup.data.environment }}
4
VpcCidr: 10.0.0.0/26
Copied!
Notice how we refer to the values specified in the stack group.
You should now have the following files in place,:
1
.
2
├─ stacks
3
│ ├─ dev
4
│ │ ├─ config.yml
5
│ │ └─ eu-west-1
6
│ │ ├─ config.yml
7
│ │ ├─ dynamodb.yml
8
│ │ └─ vpc.yml
9
│ └─ prod
10
│ └─ eu-west-1
11
├─ templates
12
│ ├─ dynamodb.yml
13
│ └─ vpc.yml
14
└─ package.json
Copied!
Our little configuration restructuring didn't actually change configurations of the stacks. We can verify that by deploying the stacks again. There shouldn't be any updates to the stacks.
1
npx tkm stacks deploy --profile takomo-tutorial
Copied!

VPC endpoints stack

Next, we'll add a stack for the VPC endpoint that makes it possible to use DynamoDB from the VPC without Internet access.
Create a new template file:
1
touch templates/vpc-endpoints.yml
Copied!
Add the following contents to it:
templates/vpc-endpoints.yml
1
Parameters:
2
Environment:
3
Type: String
4
Description: Application environment
5
AllowedValues:
6
- dev
7
- prod
8
VpcId:
9
Type: AWS::EC2::VPC::Id
10
Description: Id of the VPC where the endpoints should be created
11
RouteTableIds:
12
Type: CommaDelimitedList
13
Description: Ids of the route tables where the endpoints should be attached
14
15
Resources:
16
DynamoDbVpcEndpoint:
17
Type: AWS::EC2::VPCEndpoint
18
Properties:
19
RouteTableIds: !Ref RouteTableIds
20
ServiceName: !Sub com.amazonaws.${AWS::Region}.dynamodb
21
VpcEndpointType: Gateway
22
VpcId: !Ref VpcId
Copied!
Then, create the stack configuration file:
1
touch stacks/dev/eu-west-1/vpc-endpoints.yml
Copied!
Add the following contents to it:
stacks/dev/eu-west-1/vpc-endpoints.yml
1
template: vpc-endpoints.yml
2
parameters:
3
Environment: {{ stackGroup.data.environment }}
4
VpcId:
5
resolver: stack-output
6
stack: vpc.yml
7
output: VpcId
8
RouteTableIds:
9
resolver: stack-output
10
stack: vpc.yml
11
output: RouteTableIds
Copied!
The parameters in this stack use a new kind of syntax. Previously, we have used static values for our parameters, but here we are using parameter resolvers that resolve the parameter values at deployment time.
Resolver of type stack-output reads the value for a parameter from another stack's outputs. In this case, we read values from the VPC stack's outputs.
You should now have the following files in place:
1
.
2
├─ stacks
3
│ ├─ dev
4
│ │ ├─ config.yml
5
│ │ └─ eu-west-1
6
│ │ ├─ config.yml
7
│ │ ├─ dynamodb.yml
8
│ │ ├─ vpc.yml
9
│ │ └─ vpc-endpoints.yml
10
│ └─ prod
11
│ └─ eu-west-1
12
├─ templates
13
│ ├─ dynamodb.yml
14
│ ├─ vpc.yml
15
│ └─ vpc-endpoints.yml
16
└─ package.json
Copied!
It's again time to deploy our changes, but this time, let's do something different. Instead of deploying all stacks, let's deploy just the VPC endpoints stack. To achieve that, you need to give the path of the stack you want to deploy to the deploy stacks command:
1
npx tkm stacks deploy \
2
/dev/eu-west-1/vpc-endpoints.yml \
3
--profile takomo-tutorial
Copied!
When you review the deployment plan, you notice something that you might find unexpected. You chose to deploy only the VPC endpoints stack, but the deployment plan indicates that Takomo will deploy the VPC stack as well.
This is because the VPC endpoints stack uses the VPC stack's outputs as inputs to its parameters, making the VPC endpoint stack dependent on the VPC stack. When building the deployment plan, Takomo takes relations between the stacks into account and ensures that it deploys the stacks in the correct order.

Lambda function stack

The infrastructure for the dev environment is almost complete. We still need to add the Lambda function that accesses the DynamoDB table through the VPC endpoint.
Let's start by creating a file that holds the Lambda function body:
1
mkdir partials
2
touch partials/lambda.js
Copied!
Add the following contents to it:
partials/lambda.js
1
const AWS = require("aws-sdk")
2
const dynamo = new AWS.DynamoDB.DocumentClient()
3
4
exports.handler = async (event, context) => {
5
console.log("EVENT: \n" + JSON.stringify(event, null, 2))
6
await dynamo.put({
7
TableName: process.env.TABLE_NAME,
8
Item: {
9
id: Date.now().toString()
10
}
11
}).promise()
12
13
const { Count } = await dynamo.scan({ TableName: process.env.TABLE_NAME }).promise()
14
return Count
15
}
Copied!
Create a new template file:
1
touch templates/lambda.yml
Copied!
Add the following contents to it:
templates/lambda.yml
1
Parameters:
2
Environment:
3
Type: String
4
Description: Application environment
5
AllowedValues:
6
- dev
7
- prod
8
VpcId:
9
Type: AWS::EC2::VPC::Id
10
Description: Id of the VPC where the endpoints should be created
11
SubnetIds:
12
Type: CommaDelimitedList
13
Description: Ids of the subnets where the function should be created
14
TableName:
15
Type: String
16
Description: Name of the DynamoDB table
17
TableArn:
18
Type: String
19
Description: ARN of the DynamoDB table
20
21
Resources:
22
FunctionSecurityGroup:
23
Type: AWS::EC2::SecurityGroup
24
Properties:
25
GroupDescription: !Sub tutorial-function-${Environment}
26
VpcId: !Ref VpcId
27
28
FunctionRole:
29
Type: AWS::IAM::Role
30
Properties:
31
AssumeRolePolicyDocument:
32
Version: 2012-10-17
33
Statement:
34
- Effect: Allow
35
Principal:
36
Service: lambda.amazonaws.com
37
Action: sts:AssumeRole
38
ManagedPolicyArns:
39
- arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
40
Policies:
41
- PolicyName: DynamoDB
42
PolicyDocument:
43
Version: 2012-10-17
44
Statement:
45
- Effect: Allow
46
Action:
47
- dynamodb:PutItem
48
- dynamodb:Scan
49
Resource: !Ref TableArn
50
51
Function:
52
Type: AWS::Lambda::Function
53
Properties:
54
FunctionName: !Sub tutorial-function-${Environment}
55
Handler: index.handler
56
MemorySize: 128
57
Role: !GetAtt FunctionRole.Arn
58
Runtime: nodejs12.x
59
Timeout: 10
60
Environment:
61
Variables:
62
TABLE_NAME: !Ref TableName
63
VpcConfig:
64
SecurityGroupIds:
65
- !Ref FunctionSecurityGroup
66
SubnetIds: !Ref SubnetIds
67
Code:
68
ZipFile: |
69
{{> lambda.js }}
Copied!
Notice how the lambda code is included in the template file (line 69).
Then, create the stack configuration file:
1
touch stacks/dev/eu-west-1/lambda.yml
Copied!
Add the following contents to it:
stacks/dev/eu-west-1/lambda.yml
1
template: lambda.yml
2
parameters:
3
Environment: {{ stackGroup.data.environment }}
4
VpcId:
5
resolver: stack-output
6
stack: vpc.yml
7
output: VpcId
8
SubnetIds:
9
resolver: stack-output
10
stack: vpc.yml
11
output: SubnetIds
12
TableName:
13
resolver: stack-output
14
stack: dynamodb.yml
15
output: TableName
16
TableArn:
17
resolver: stack-output
18
stack: dynamodb.yml
19
output: TableArn
Copied!
You should now have the following files in place:
1
.
2
├─ stacks
3
│ ├─ dev
4
│ │ ├─ config.yml
5
│ │ └─ eu-west-1
6
│ │ ├─ config.yml
7
│ │ ├─ dynamodb.yml
8
│ │ ├─ lambda.yml
9
│ │ ├─ vpc.yml
10
│ │ └─ vpc-endpoints.yml
11
│ └─ prod
12
│ └─ eu-west-1
13
├─ templates
14
│ ├─ dynamodb.yml
15
│ ├─ vpc.yml
16
│ └─ vpc-endpoints.yml
17
└─ package.json
Copied!
It's time to deploy the stacks again to get the lambda function stack created.
1
npx tkm stacks deploy --profile takomo-tutorial
Copied!

Testing

We now have everything ready for the development environment, and it's time to test the lambda function.
If you have the AWS CLI installed, you can test the function from command-line:
1
aws lambda invoke \
2
--region eu-west-1 \
3
--function-name tutorial-function-dev \
4
--profile takomo-tutorial \
5
response.txt
Copied!
The lambda returns the number of items in the DynamoDB table, so each invocation should increase the number in response.txt by one.
You can also invoke the function from AWS management console.

Production environment

Now that we have the dev environment ready, it's time to set up the prod environment.
Create shared configuration for the prod environment.
1
touch stacks/prod/config.yml
Copied!
Add the following contents to it:
stacks/prod/config.yml
1
data:
2
environment: prod
Copied!
Then, create configuration shared by all stacks located in the eu-west-1 region.
1
touch stacks/prod/eu-west-1/config.yml
Copied!
Add the following contents to it:
stacks/prod/eu-west-1/config.yml
1
regions: eu-west-1
Copied!
Next, create configuration files for the stacks.
1
touch stacks/prod/eu-west-1/dynamodb.yml
2
touch stacks/prod/eu-west-1/lambda.yml
3
touch stacks/prod/eu-west-1/vpc.yml
4
touch stacks/prod/eu-west-1/vpc-endpoints.yml
Copied!
Then, add the following contents to them:
stacks/prod/eu-west-1/dynamodb.yml
1
template: dynamodb.yml
2
parameters:
3
Environment: {{ stackGroup.data.environment }}
Copied!
stacks/prod/eu-west-1/lambda.yml
1
template: lambda.yml
2
parameters:
3
Environment: {{ stackGroup.data.environment }}
4
VpcId:
5
resolver: stack-output
6
stack: vpc.yml
7
output: VpcId
8
SubnetIds:
9
resolver: stack-output
10
stack: vpc.yml
11
output: SubnetIds
12
TableName:
13
resolver: stack-output
14
stack: dynamodb.yml
15
output: TableName
16
TableArn:
17
resolver: stack-output
18
stack: dynamodb.yml
19
output: TableArn
Copied!
stacks/prod/eu-west-1/vpc.yml
1
template: vpc.yml
2
parameters:
3
Environment: {{ stackGroup.data.environment }}
4
VpcCidr: 10.0.0.64/26
Copied!
stacks/prod/eu-west-1/vpc-endpoints.yml
1
template: vpc-endpoints.yml
2
parameters:
3
Environment: {{ stackGroup.data.environment }}
4
VpcId:
5
resolver: stack-output
6
stack: vpc.yml
7
output: VpcId
8
RouteTableIds:
9
resolver: stack-output
10
stack: vpc.yml
11
output: RouteTableIds
Copied!
Your file structure should now look like this:
1
.
2
├─ stacks
3
│ ├─ dev
4
│ │ ├─ config.yml
5
│ │ └─ eu-west-1
6
│ │ ├─ config.yml
7
│ │ ├─ dynamodb.yml
8
│ │ ├─ lambda.yml
9
│ │ ├─ vpc.yml
10
│ │ └─ vpc-endpoints.yml
11
│ └─ prod
12
│ ├─ config.yml
13
│ └─ eu-west-1
14
│ ├─ config.yml
15
│ ├─ dynamodb.yml
16
│ ├─ lambda.yml
17
│ ├─ vpc.yml
18
│ └─ vpc-endpoints.yml
19
├─ templates
20
│ ├─ dynamodb.yml
21
│ ├─ vpc.yml
22
│ └─ vpc-endpoints.yml
23
└─ package.json
Copied!
Let's quickly check how our stacks look like now:
1
npx tkm stacks list --profile takomo-tutorial
Copied!
You should see four more stacks in addition to the existing dev stacks.
Deploy the stacks like earlier but this time use -y option to skip the plan review and confirm step.
1
npx tkm stacks deploy --profile takomo-tutorial -y
Copied!

Clean up

You have reached the end of this tutorial. Hopefully, you now have a better understanding of how to configure and deploy CloudFormation stacks with Takomo.
To remove the stacks you have created, run the next command:
1
npx tkm stacks undeploy --profile takomo-tutorial
Copied!
Last modified 9mo ago