Pulumi is a great IaC (Infrastructure as Code) tool, but sometimes things by default don’t work as we expect them to work. Here’s where ResourceOptions comes to help us impact how Pulumi will work on the more low-level side. This guide will try to explain useful options available for the Pulumi resources.
Let’s begin with the explanation of the ResourceOptions and where they could be found. ResourceOptions are additional attributes that could be defined for every Pulumi resource. Pulumi documentation shows a such example of the general construction of creating a resource’s desired state:
res = Resource(name, args, options)
And documentation also states:
optionsargument is optional, but lets you control certain aspects of the resource. For example, you can show explicit dependencies, use a custom provider configuration, or import an existing infrastructure.
So options could be defined for every Pulumi resource. There are a bunch of options available. Let’s go through each of them.
additionalSecretOutputsresource option specifies a list of named output properties that should be treated as secrets, which means they will be encrypted. It augments the list of values that Pulumi detects, based on secret inputs to the resource.
Let’s check what it means. Say we are creating an S3 Bucket in Pulumi and exporting a bucket name:
When previewing/performing an update we’ll see the bucket name in the plain text:
And we have the same plaintext value in the pulumi stack file.
But we don’t want this to happen. Let’s try setting AdditionalSecretOutputs.
Much better, now the output is not displayed on the preview.
And the stack output is also encrypted.
The Python Pulumi documentation gives a small little explanation which I like:
An optional list of aliases to treat this resource as matching.
Pulumi documentation gives an example of Aliases usage: when you say create a database named “database-original” and you then want to change the name to “database-new”, but you don’t want resources that use this database to be deleted and new resources created but replaced in place.
In this example, we’ll use another option for passing a custom provider for the resource. So we’ll create an S3 Bucket in another region.
Now we will change the name of the provider and see what happens.
Pulumi will create a new provider and will try to replace the bucket. The bucket will not be replaced, as Pulumi will be crashed with an error
* Error creating S3 bucket: BucketAlreadyOwnedByYou: Your previous request to create the named bucket succeeded and you already own it.
This behavior is pretty weird, but only if you add Aliases to the provider resources basically saying that it’s the same resource.
It’ll work leaving all the resources unchanged. So if you’ll face a problem with changing the resource name and then the wrong recreation of dependent resources — try using Aliases.
This option is pretty simple. If you want to override the default timeout for the resource create, update or delete operations — you can do it with this attribute.
Quite a useful option. In order to understand it, you should understand how replacement operation works in Pulumi. So in order to minimize downtime, Pulumi first creates a new resource and only then deletes the old one. By setting DeleteBeforeReplace to True, Pulumi will first Delete the original resource, then create the resource and replace references if there are such.
Remember this issue we had with replacing the bucket when changing the name of the provider? We could also solve this issue by setting the DeleteBeforeReplace option. Because we explicitly set the bucket name to some value, Pulumi will try to create a bucket with the same name and this request will obviously fail. Changing the order of the replacement operation will help. Or don’t provide an explicit name for your resources :P
Cool option allowing you to create a list of explicit dependencies between resources. Pulumi actually tracks these dependencies when you pass the resource’s outputs to the resource’s inputs. But sometimes Pulumi doesn’t know about some of the dependencies — as Pulumi states in the documentation:
if a dependency is external to the infrastructure itself — such as an application dependency — or is implied due to an ordering or eventual consistency requirement
Let’s look at the example. We are creating a bucket, creating some objects inside it, and listing them with the pulumi_command module. Let’s assume we should (for any reason) hardcode the name of the bucket in the command.
As you know Pulumi creates the resources in its own way (parallelly when it can)— if the order is not defined the overall behavior could vary. In our case command could fail (if will be executed before the bucket gets created) or list the different results every time. That’s why we need to set dependsOn option:
opts = pulumi.ResourceOptions(depends_on=bucket)
Notice how the graph will look like after we specify the dependsOn option
Notice that dependsOn relation is different from parent-child relation. DependsOn is only defining the order, while in the parent-child relation, the child inherits some of the attributes from the parent. DependsOn accepts many dependencies, while a child could only have one parent.
IgnoreChanges option could be used if you want to ignore list of properties during Pulumi update.
res = MyResource("res",
Import option could be extremely useful when you want to import resources to your Pulumi stack which has already been created. Import will warn you if any of the inputs do not match the existing resource.
After resources were imported to pulumi, the import option could be deleted and you could change the resource as you want.
IMO parent is the most interesting attribute which specifies the parent of the resource. Do you know that every resource in Pulumi has its own parent — and it’s stack! Notice the top-level resource on the tree when you update your stack.
These relations could also be viewed in the stack graph, which could be exported via command:
pulumi stack graph
If you want to really dive into how these relationships work, I advise you to start from here. This line of code explicitly set the parent to be the root resource, if no other parent were provided.
Parent is similar to dependsOn options in the sense that it allows explicitly defining the order of creating resources, but it also allows inheriting default values of other options from the parent: provider, aliases, protect, and transformations. And as mentioned before, the child can only have one parent.
Use this if the resource couldn’t be directly deleted. Pulumi will fail if the protected resources will be deleted unless you set Protect to False or directly unprotect the resource with Pulumi.
error: unable to delete resource "urn:pulumi:dev::pulumi-advanced::aws:sns/topic:Topic::topic"
as it is currently marked for protection. To unprotect the resource, either remove the `protect` flag from the resource in your Pulumi program and run `pulumi up` or use the command:
`pulumi state unprotect 'urn:pulumi:dev::pulumi-advanced::aws:sns/topic:Topic::topic'`
With this option, you could explicitly pass the provider. This could be useful when you want to use other than inherited from the parent resource provider (the default one is defined by stack) e.g. when you want to create a resource in the region different from the default one defined in pulumi config.
The same as the single provider, except you provide a map of providers for the resource and its children.
This option will define the set of the attributes that will require Pulumi to replace (create a new one, delete the old one, and change the references), instead of changing the resource in place.
replaceOnChangesresource option can be combined with the
deleteBeforeReplaceresource option to trigger a resource to be deleted before it is replaced whenever a given input has changes.
If you want to keep the resource on the cloud provider when executing the Delete operation in pulumi — use this option. The resource will be deleted from the Pulumi state, but still will be available in the place you’ve deployed it.
topic = sns.Topic("topic", opts=ResourceOptions(retain_on_delete=True))
The further deletion could be done manually or by importing the resource into Pulumi, unsetting the RetaionOnDelete option, and deleting it.
transformationsresource option provides a list of transformations to apply to a resource and all of its children. This option is used to override or modify the inputs to the child resources of a component resource.
This option is extremely powerful because with it you can define any transformation for the resource’s inputs and its children. And remember that stack is also the resource, so you could define the transformation for the whole stack! I really like that because you could e.g. add/change the tags for all of your AWS resources in just a couple of lines of code
The example above uses register_stack_transformation which is unique method for registering transformation for the whole Pulumi stack, but you could obviously use the transformations option with any resource by providing the ResourceOption.
Pulumi states that this option should be used rarely. Indeed, this option changes the version of the provider to use. I could think of some scenarios where it could be used — either when migrating from the old version to the newer and keeping some of the resources in the older version or when finding a bug in the provider version, so you have no choice but to choose another version.
One more thing
ResourceOptions actually have one cool method allowing you to merge two instances of options.
merge(opts1:ResourceOptions, opts2: ResourceOptions)
The opts2 will be merged over opts1 attributes. Pretty much the same as in dict.update(other_dict) Python method.
As you could see, Pulumi allow you to customize a lot in how resources are created, deleted and updated. Knowing the advantages of all of these options will facilitate the management of existing infrastructure, its refactoring and all kinds of migrations.
Wanna talk about IaC or need help creating infrastructure for your project? Contact me!