Concepts¶
There are concepts and assumptions made about the intended use case for Cally, this page will attempt to cover them.
Python Namespaces¶
Cally relies heavily on Python namespace packages
The tool comes with the following namespaces built in:
cally.cli - Command line, and configuration tools
cally.cdk - A collection of utilities for building CDK for Terraform stacks
cally.testing - Base testing harnesses for testing the command line + stacks
A big focus for Cally is the generation of Terraform templates via the CDK for Terraform. All providers built by Cally, and installed will be availble under the following namespace:
cally.providers
When building your IDP tool on top of Cally, it’s expected that your code reside in the following namespace:
cally.idp
Further detail for each of the following sub namespaces will be covered in this document, but they are listed here
cally.idp.commands - All your internal commands
cally.idp.resources - Where you configure the defaults of each of your terraform resources
cally.idp.stacks - This is where you will build your Terraform ‘Stacks’
cally.idp.defaults - Where the defaults for your tool exist, ie default region for your google provider
CLI¶
Cally’s commmand line interface is built on the shoulders of click, which is an incredibly powerful library, and has provided the foundation of the tool.
Commands¶
Any command group added to your cally.idp.commands, namespace will be available as a cally subcommand. For example:
import click
@click.group()
def example() -> None:
pass
@click.command()
@click.argument('name')
def hello(name: str):
click.secho(f'Hello {name}')
example.add_command(hello)
Would be availble as:
✗ cally example hello world
Hello world
Configuration¶
Cally’s configuration is built on top of dynaconf with a custom loader to provide a layered, consitent builder to provide a strong starting point for building your infrastructure from a very small amount of configuration. Further details can be found in the Configuration section.
cally.yaml¶
Cally will look for a cally.yaml in your working directory and load it. The expected layout is as follows:
defaults:
# All defaults here
development:
services:
test-service:
stack_type: CallyStack
stack_vars:
example: variable
The CDK for Terraform¶
One of the nuances of the CDK for Terraform, is that (IMHO) very boilerplate heavy, and feels a lot like writing typescript or HCL, that happens to look like python. Though you can use constructs and other functions, being able to focus purely on the building blocks for your infrastructure and tooling, while allowing the CDK for Terraform to produce extremely consistent and strongly typed templates.
Note
As jsii is integral to the CDK for Terraform, Node.js must be available in your path.
Resources¶
Cally provides a wrapper for the resources, this is a good place to set defaults. Which can be of Any type, including resource attributes, dicts, etc. On instation, these defaults will be copied using deepcopy, avoiding issue related to memory refs being shared across child classes.
provider - This is the namespace of the resource built by the provider builder.
- resource - All cdk providers built after 0.7.0, have a coresponding module that
matches the resource listed in the documentation/HCL spec.
defaults - This is a dictionary, where you can define the defaults for that resource type.
Example:
class Pet(CallyResource):
provider = 'random'
resource = 'pet'
defaults = {
'length': 3,
'separator': ' ',
}
When consumed in a stack, would have an output like this:
{
"random_pet": {
"random-pet": {
"//": {
"metadata": {
"path": "pets/random-pet",
"uniqueId": "random-pet"
}
},
"length": 3,
"provider": "random.foo",
"separator": " "
}
}
}
Stacks¶
The goal of a CallyStack is to abstract away all of the boiler plate of setting up CDK for Terraform stack. Along with taking care of configuring the providers as per the service and defaults defined, it also configures the backend. You are free to construct this class, using all the python tools at your disposal. All resources that are added using the add_resource(resource) or add_resources([resource, another]) commands, will be included in the resulting Terraform JSON.
class RandomPets(CallyStack):
def __init__(self, service: CallyStackService) -> None:
super().__init__(service)
random_pet = Pet('random-pet')
self.add_resource(random_pet)
When print called, this would be the output
{
"//": {
"metadata": {
"backend": "local",
"stackName": "pets",
"version": "0.20.5"
},
"outputs": {
}
},
"provider": {
"random": [
{
"alias": "foo"
}
]
},
"resource": {
"random_pet": {
"random-pet": {
"//": {
"metadata": {
"path": "pets/random-pet",
"uniqueId": "random-pet"
}
},
"length": 3,
"provider": "random.foo",
"separator": " "
}
}
},
"terraform": {
"backend": {
"local": {
"path": "state/dev/pets"
}
},
"required_providers": {
"random": {
"source": "hashicorp/random",
"version": "3.6.0"
}
}
}
}