Patterns of Use¶
The patterns contained in this module support our use cases for services hosted at Thunderbird. At an extremely high
level, this module allows us to standardize certain aspects of the Pulumi resources we’re building by using a custom
ComponentResource
called a tb_pulumi.ThunderbirdComponentResource
. When building your Pulumi project
using this module, you should organize these resources into a tb_pulumi.ThunderbirdPulumiProject
.
See also
Full documentation on the individual resource patterns can be found in the tb_pulumi
pages.
Patterns for managing projects¶
One primary goal of this project is to reduce most infrastructural changes to YAML file tweaks once the initial setup is
done, requiring no code inspection or debugging for most common changes. The
tb_pulumi.ThunderbirdPulumiProject
class brings those configs, information about the cloud environment, and
Pulumi project and stack data into a single context. That context is then made available to any
tb_pulumi.ThunderbirdComponentResource
created within that project.
Note
Some may find it easiest to jump straight to the Quickstart on the Getting Started page, or to review the sample configuration and program used by the quickstart to see how these patterns fit together.
Resource patterns¶
The various classes in tb_pulumi represent commonly used infrastructural patterns. Some of these will depend on the
pre-existence of some other patterns. For example, most services will require some kind of private network space to
operate within. Thus, your infrastructure stack will usually begin with a tb_pulumi.network.MultiCidrVpc
to
establish the network layout.
The resources built by that class will become available as members of its resources
dict. The values in these dicts
are all Pulumi Resource objects, but you’ll have to wait until the component resource is applied to access them. The
simplest way to do this is to create a Pulumi output out of all the resources in the ThunderbirdComponentResource.
In this example, we build a SecretsManagerSecret, which contains both a Secret and a SecretVersion resource. We wait on all resources in the secret to be applied, then return the ARN of the secret.
1secret = tb_pulumi.secrets.SecretsManagerSecret(
2 name='mysecretname',
3 secret_name='app/env/mysecret',
4 secret_value='super duper secret',
5)
6
7secret_arn = pulumi.Output.all(**secret.resources).apply(
8 lambda resources: resources['secret'].arn
9)
The resources produced by each pattern are documented alongside those classes. For example, the
tb_pulumi.secrets.SecretsManagerSecret
documentation lists the 'secret'
resource and links to the
Pulumi documentation for that resource type so you can learn about their properties.
In the above example, we build a single component resource by pulling its config out by name. But in some cases, you may
wish to build multiple instances of one pattern based upon the YAML config. Consider a very frequently appearing
resource such as the AWS EC2 Security Group. We provide the tb_pulumi.network.SecurityGroupWithRules
pattern
for building these resources.
Suppose we must govern traffic for both a backend API service and a separate authentication service. We could define the security groups in YAML:
---
resources:
# ... other resources ...
tb:network:SecurityGroupWithRules:
api:
description: API backend
rules:
egress:
- from_port: 0
to_port: 0
protocol: tcp
description: Allow local egress
cidr_blocks:
- 10.0.0.0/16
ingress:
- from_port: 8080
to_port: 8080
protocol: tcp
description: Allow local ingress
cidr_blocks:
- 10.0.0.0/16
auth_service:
description: Auth service backend
rules:
egress:
- from_port: 0
to_port: 0
protocol: tcp
description: Allow all egress
cidr_blocks:
- 0.0.0.0/0
ingress:
- from_port: 8080
to_port: 8080
protocol: tcp
description: Allow ingress from API
source_security_group_id: sg-abcdefg0123456789
In the __main__.py
code, we need not explicitly extract each member of the tb:network:SecurityGroupWithRules
config because we can iterate over the items quite easily:
security_groups = {
tb_pulumi.network.SecurityGroupWithRules(
name=f'{project.name_prefix}-sg-{sg_name}',
project=project,
**sg_config
)
for sg_name, sg_config in resources['tb:network:SecurityGroupWithRules'].items()
}
Accessing Resources¶
In Pulumi, a Resource can have a number of Outputs, which are pieces of data about a resource that aren’t known until after the resources are “applied” (that is, the real live resources have been altered to match the desired state defined in your code). Pulumi provides the ComponentResource model to aggregate many Resources into a single code object.
Pulumi’s documentation
says you should call the register_outputs
function at the end of a ComponentResource’s constructor. Crucially,
though, unlike plain Pulumi Resources, these outputs do not become accessible after the ComponentResource is fully
applied. The documentation is unclear on the purpose of this, and the Pulumi developers also don’t know why you should call it. Its only purpose is
within the CLI tool, as simple output at the end of the run. As such, we will stop allowing this in a future version,
opting to make the register_outputs
call with an empty dict, as is convention among Pulumi developers.
The good news is that tb_pulumi restores this missing feature through the tb_pulumi.ThunderbirdPulumiProject
object. When you pass your project into a ThunderbirdComponentResource
that subsequently makes a finish
call,
the project adds the resources
dict passed into finish
to its own
tb_pulumi.ThunderbirdPulumiProject.resources
dict, organized by the ThunderbirdComponentResource’s name. It
also stores these resources internally in the tb_pulumi.ThunderbirdComponentResource.resources
dict. This
structure allows us to inspect not only all of the resources in a project after they’ve been applied, but all of the
nested resources in the other component resources.
The contents of the resources
dict in a ThunderbirdComponentResource
are all Pulumi Resources with Outputs that
can be applied. The resources
dict of a ThunderbirdPulumiProject
are either Pulumi Resources or some collection
of them. The full set of allowable entries is defined in the tb_pulumi.Flattenable
type alias.
As an example, the following code (which builds a series of security groups and then tries to print their IDs) will fail
with Calling __str__ on an Output[T] is not supported
because the underlying Pulumi logger wants to print a string
but it’s getting an unresolved Pulumi Output instead.
sgs = {
sg_name: tb_pulumi.network.SecurityGroupWithRules(
name=f'{project.name_prefix}-sg-{sg_name}',
project=project,
vpc_id=vpc.resources['vpc'].id,
opts=pulumi.ResourceOptions(depends_on=[vpc]),
**sg_config,
)
for sg_name, sg_config in resources['tb:network:SecurityGroupWithRules'].items()
}
pulumi.info(f'DEBUG -- {sgs['foo'].resources['sg'].id}')
Instead, wait on the output and then log the ID:
sgs['foo'].resources['sg'].id.apply(
lambda sgid: pulumi.info(f'DEBUG -- {sgid}')
)
This will generate output if you run a pulumi up
to create the resource and generate the ID. It also produces output
on a pulumi preview
if the resource was created on a previous run and the ID has already been generated. It will not
produce output on a preview if the resource does not already exist because the resource has never been applied, but it
will also not throw any errors.
Now suppose you have a component resource which contains other component resources and you need to wait on all of those sub-resources to be applied before acting on their outputs. For example, a PulumiSecretsManager (PSM) creates a list of SecretsManagerSecrets (SMS). If we want to produce a list of the resulting secrets’ ARNs, we could wait on all of the PSMs’ resources to be applied and then try to get at them:
pulumi.Output.all(**psm.resources).apply(lambda resources:
pulumi.info(f'DEBUG -- {[secret.resources['secret'].arn
for secret in resources['secrets']]}'))
This waits on all of the SecretsManagerSecrets’ resources to be applied before accessing the downstream secrets’ ARNs. It doesn’t produce any errors, but it also doesn’t produce ARNs:
DEBUG -- [<pulumi.output.Output object at 0x748bbcfe7a10>, <pulumi.output.Output object at 0x748bbcaa2970>]
That’s because those arn
s are also Outputs, and we still have to wait for those to be applied. Luckily, we can
compile a list of those outputs and then wait on them all to be applied:
pulumi.Output.all(*[
sms.resources['secret'].arn
for sms in psm.resources['secrets']
]).apply(
lambda arns: pulumi.info(f'DEBUG -- {arns}')
)
This produces output similar to this (slightly edited for readability):
DEBUG -- [
'arn:aws:secretsmanager:region_name:account_number:secret:project/stack/secretname1-id',
'arn:aws:secretsmanager:region_name:account_number:secret:project/stack/secretname2-id'
]
The trick here lies in producing a list of those Outputs (the .arn
s), and then using the single-star (*list
)
notation to expand that into a Pulumi Output made of all of those arn
Outputs, and then waiting for them to apply.
Handling secrets¶
Applications often need to operate on values such as database passwords that are considered secrets. You never want to store these values in plaintext, and they should always be protected by policies preventing unauthorized access. Pulumi allows you to store secret values directly in its configuration using hashes only decryptable with a secret passphrase.
To set a secret value, run a command like this:
pulumi config set --secret my-password 'P@$sw0rd'
The first time you set a Pulumi secret, you will be asked to generate this passphrase. When you do, be sure to log it in a safe location. Any other users working with your Pulumi code will need this to manipulate your live resources.
Many AWS configurations will require that secret values come out of their Secrets Manager product. To help bridge the
gap between Pulumi and AWS, we have the tb_pulumi.secrets.PulumiSecretsManager
class. Feed this a list of
secret_names
which match Pulumi secret names. This module will create AWS secrets matching those Pulumi secrets.
Note
AWS Secrets Manager applies a randomly generated suffix to each secret ARN. This value is not predictable. References
to secrets typically require you to use this ARN even though it is not predictable. For this reason, you may have to
run a pulumi up
to generate these secrets before using them as part of, for example, an ECS task definition.