Tutorial

Welcome to the Takomo hands-on tutorial! In this guide, you'll learn how to use Takomo to manage and deploy AWS CloudFormation stacks. We'll walk through building a real-world infrastructure setup, covering best practices and demonstrating Takomo's key features.

What Will You Build?

Instead of a simple single-stack example, we'll create a more realistic environment:

  • A DynamoDB table.
  • A VPC without internet access.
  • A Lambda function running inside the VPC.
  • A VPC endpoint to DynamoDB, so the Lambda can access the table securely.

We'll set up two environments: dev and prod, both deployed in the eu-west-1 region.

Setting Up AWS Credentials

You'll need access to an AWS account where you can safely experiment.

  1. Create an IAM user with administrator permissions.
  2. Generate access keys for this user.
  3. Configure your credentials in your ~/.aws/credentials file, using the profile name takomo-tutorial:
~/.aws/credentials
1[takomo-tutorial]
2aws_access_key_id = ENTER_YOUR_ACCESS_KEY_ID_HERE
3aws_secret_access_key = ENTER_YOUR_SECRET_ACCESS_KEY_HERE

Initializing Your Takomo Project

Let's create a new directory for your Takomo project and set up the required files.

1mkdir takomo-tutorial
2cd takomo-tutorial

From here on, we'll refer to the takomo-tutorial directory as the project's root directory.

Initialize a new NPM project:

1npm init -y

Install Takomo as a development dependency:

1npm install -D takomo

Project File Structure

Within your project root, create directories to organize your stack configurations and templates:

1mkdir stacks
2mkdir templates

The stacks directory contains all stack configuration files, while the templates directory holds the CloudFormation templates.

Organizing with Stack Groups

Takomo lets you organize stacks into stack groups - directories under stacks that group stacks by environment, region, or other criteria. Stack groups can be nested to create a hierarchical structure, and you can provide shared configuration within each group.

Let's group our stacks by environment and then by region:

1mkdir -p stacks/dev/eu-west-1
2mkdir -p stacks/prod/eu-west-1

Your directory tree should now look like this:

1.
2├─ stacks
3│  ├─ dev
4│  │  └─ eu-west-1
5│  └─ prod
6│     └─ eu-west-1
7├─ templates
8└─ package.json

Creating the DynamoDB Stack

Let's start by defining a CloudFormation template for the DynamoDB table.

Create the template file:

1touch templates/dynamodb.yml

Add the following content:

templates/dynamodb.yml
1Parameters:
2  Environment:
3    Type: String
4    Description: Application environment
5    AllowedValues:
6      - dev
7      - prod
8
9Resources:
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
22Outputs:
23  TableName:
24    Value: !Ref Table
25  TableArn:
26    Value: !GetAtt Table.Arn

This template defines a single DynamoDB table, with the environment name as a suffix.

Now, add the stack configuration for the dev environment:

1touch stacks/dev/eu-west-1/dynamodb.yml
stacks/dev/eu-west-1/dynamodb.yml
1regions: eu-west-1
2template: dynamodb.yml
3parameters:
4  Environment: dev

Explanation:

  • regions: Tells Takomo where to deploy the stack. (You can specify one or multiple regions.)
  • template: Path to the template file (relative to templates directory).
  • parameters: Values for the template parameters.

Your structure should now look like:

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

Deploying Your First Stack

Let's deploy the DynamoDB stack!

Make sure you're in the project's root directory and run:

1npx tkm stacks deploy --profile takomo-tutorial

You'll see a deployment plan. The stack's path (e.g., /dev/eu-west-1/dynamodb.yml/eu-west-1) uniquely identifies it. Takomo will generate a stack name (e.g., dev-eu-west-1-dynamodb) unless you specify one.

Follow the prompts:

  1. Choose "continue, but let me review changes to each stack".
  2. Review the stack-specific plan, then choose "continue to deploy the stack, then let me review the remaining stacks".

Deployment should only take a few seconds. You'll see a summary at the end.

Adding the VPC Stack

Next, let's add a VPC stack.

Create the template:

1touch templates/vpc.yml
templates/vpc.yml
1Parameters:
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
12Resources:
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
32Outputs:
33  VpcId:
34    Value: !Ref Vpc
35  RouteTableIds:
36    Value: !Ref RouteTable
37  SubnetIds:
38    Value: !Ref Subnet

Now, add the stack configuration:

1touch stacks/dev/eu-west-1/vpc.yml
stacks/dev/eu-west-1/vpc.yml
1regions: eu-west-1
2template: vpc.yml
3parameters:
4  Environment: dev
5  VpcCidr: 10.0.0.0/26

Your files should now look like:

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

Listing Stacks

To see which stacks are configured and their status, run:

1npx tkm stacks list --profile takomo-tutorial

You should see both the DynamoDB stack (deployed) and the VPC stack (pending deployment).

Deploying the VPC Stack

Run the deploy command again to deploy both stacks:

1npx tkm stacks deploy --profile takomo-tutorial

This time, the plan will show both stacks. The DynamoDB stack will be updated (if needed), and the VPC stack will be created.

Follow the prompts to review and continue the deployment.

Using Shared Configuration with Stack Groups

Notice that both stacks share some configuration, like the environment and region. Let's refactor to use stack groups for shared settings.

TIP

Stack groups allow you to define configuration once and inherit it in all child stacks.

Step 1: Add Shared Configuration for /dev

1touch stacks/dev/config.yml
stacks/dev/config.yml
1data:
2  environment: dev

Step 2: Add Shared Configuration for /dev/eu-west-1

1touch stacks/dev/eu-west-1/config.yml
stacks/dev/eu-west-1/config.yml
1regions: eu-west-1

Step 3: Update Stack Configurations to Use Shared Values

Edit stacks/dev/eu-west-1/dynamodb.yml:

stacks/dev/eu-west-1/dynamodb.yml
1template: dynamodb.yml
2parameters:
3  Environment: {{ stackGroup.data.environment }}

Edit stacks/dev/eu-west-1/vpc.yml:

stacks/dev/eu-west-1/vpc.yml
1template: vpc.yml
2parameters:
3  Environment: {{ stackGroup.data.environment }}
4  VpcCidr: 10.0.0.0/26

Now, both stacks inherit the region and environment from their stack groups.

Your file structure should look like:

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

Redeploy to verify that nothing changes (no updates needed):

1npx tkm stacks deploy --profile takomo-tutorial

Adding a VPC Endpoints Stack

To allow your Lambda function to access DynamoDB privately, add a VPC endpoint stack.

Create the template:

1touch templates/vpc-endpoints.yml
templates/vpc-endpoints.yml
1Parameters:
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
15Resources:
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

Now, add the stack configuration:

1touch stacks/dev/eu-west-1/vpc-endpoints.yml
stacks/dev/eu-west-1/vpc-endpoints.yml
1template: vpc-endpoints.yml
2parameters:
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

Parameter Resolvers Explained:
Here, instead of static values, we're using parameter resolvers. The stack-output resolver fetches output values from another stack (in this case, the VPC stack) at deployment time.

Your structure should now look like:

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

Deploying a Single Stack and Its Dependencies

Let's deploy only the VPC endpoints stack (and any dependencies Takomo detects):

1npx tkm stacks deploy /dev/eu-west-1/vpc-endpoints.yml --profile takomo-tutorial
TIP

Takomo automatically detects dependencies between stacks (e.g., via parameter resolvers) and deploys them in the correct order.

Adding the Lambda Function Stack

We're almost done! Now, let's add a Lambda function that will access the DynamoDB table through the VPC endpoint.

First, create a directory to hold code partials and add the Lambda function code:

1mkdir partials
2touch partials/lambda.js
partials/lambda.js
1const AWS = require("aws-sdk")
2const dynamo = new AWS.DynamoDB.DocumentClient()
3
4exports.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}

Now, create the Lambda template:

1touch templates/lambda.yml
templates/lambda.yml
1Parameters:
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
21Resources:
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 }}

Notice how the Lambda code is included in the template file using a partial ({{> lambda.js }}).

Now, add the stack configuration:

1touch stacks/dev/eu-west-1/lambda.yml
stacks/dev/eu-west-1/lambda.yml
1template: lambda.yml
2parameters:
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

Your structure should now be:

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

Deploy the stacks to create the Lambda function:

1npx tkm stacks deploy --profile takomo-tutorial

Testing the Lambda Function

With everything deployed, let's test your Lambda function!

If you have the AWS CLI installed, run:

1aws lambda invoke \
2  --region eu-west-1 \
3  --function-name tutorial-function-dev \
4  --profile takomo-tutorial \
5  response.txt

Each invocation increments the number of items in the DynamoDB table. Check response.txt for the updated count.

Alternatively, you can invoke the function from the AWS Management Console.

Setting Up the Production Environment

Now, let's replicate our setup for the prod environment.

Step 1: Add Shared Configuration for prod

1touch stacks/prod/config.yml
stacks/prod/config.yml
1data:
2  environment: prod

Step 2: Add Shared Configuration for prod/eu-west-1

1touch stacks/prod/eu-west-1/config.yml
stacks/prod/eu-west-1/config.yml
1regions: eu-west-1

Step 3: Create Stack Configuration Files

1touch stacks/prod/eu-west-1/dynamodb.yml
2touch stacks/prod/eu-west-1/lambda.yml
3touch stacks/prod/eu-west-1/vpc.yml
4touch stacks/prod/eu-west-1/vpc-endpoints.yml

Add the following content to each file:

stacks/prod/eu-west-1/dynamodb.yml
1template: dynamodb.yml
2parameters:
3  Environment: {{ stackGroup.data.environment }}
stacks/prod/eu-west-1/lambda.yml
1template: lambda.yml
2parameters:
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
stacks/prod/eu-west-1/vpc.yml
1template: vpc.yml
2parameters:
3  Environment: {{ stackGroup.data.environment }}
4  VpcCidr: 10.0.0.64/26
stacks/prod/eu-west-1/vpc-endpoints.yml
1template: vpc-endpoints.yml
2parameters:
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

Your directory tree should now look like:

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

Listing and Deploying All Stacks

To see all stacks across both environments:

1npx tkm stacks list --profile takomo-tutorial

You should see four new stacks for the prod environment.

Deploy all stacks (using -y to skip plan review and confirm automatically):

1npx tkm stacks deploy --profile takomo-tutorial -y

Cleaning Up

Congratulations! You've completed the Takomo tutorial and deployed real infrastructure to AWS.

To remove all stacks you created, simply run:

1npx tkm stacks undeploy --profile takomo-tutorial

Thank you for following this tutorial. You now have a solid foundation for managing CloudFormation stacks with Takomo!