The management of cloud infrastructure often involves a tension between the declarative nature of Infrastructure as Code (IaC) and the reality of external mutations. In a perfect declarative world, the code is the single source of truth; however, in production environments, resources are frequently modified by auto-scaling groups, external security scanners, manual emergency interventions, or third-party management tools. When Pulumi performs a deployment, it compares the desired state defined in the program against the last known state stored in the stack. If a discrepancy is found, Pulumi attempts to reconcile it. This behavior, while generally desirable, can lead to "fight" scenarios where Pulumi reverts necessary external changes every time pulumi up is executed. To resolve this, Pulumi provides the ignoreChanges resource option, a powerful mechanism that instructs the engine to disregard specific properties during the diffing process.
The Mechanics of ignoreChanges
The ignoreChanges resource option specifies a precise list of properties that Pulumi should overlook when updating existing resources. Under normal operation, Pulumi examines every property of a resource; if the program says a property should be "A" but the state says it is "B", Pulumi triggers an update. When ignoreChanges is applied to a specific property, Pulumi deviates from this standard logic. Instead of using the value provided in the Pulumi program to determine if an update or replacement is required, it relies on the old value already recorded in the stack state.
This architectural decision has significant implications for the resource lifecycle. During the initial creation of a resource, ignoreChanges has no effect because there is no previous state to reference. Pulumi uses the values provided in the program to instantiate the resource in the cloud. The "ignoring" behavior only activates during subsequent updates. By effectively freezing the state of a specific property for the purpose of comparison, developers can allow external systems to modify those properties without triggering a corrective action from Pulumi.
The impact of this is most evident in environments where "drift" is an intentional part of the operational workflow. For instance, if a security team manages AWS tags via an external enterprise policy engine, Pulumi would normally see those added tags as drift and attempt to remove them to match the code. By adding tags to the ignoreChanges list, the operator ensures that Pulumi treats the current state as correct, regardless of whether the program defines those tags.
Deep Dive into Property Path Targeting
One of the most sophisticated aspects of the ignoreChanges option is its ability to target not just top-level properties, but also deeply nested elements within complex objects and arrays. This granular control prevents the need to ignore an entire object when only a single nested attribute is volatile.
Nested Properties and Property Paths
Pulumi supports the use of property paths to specify exactly which part of a resource's input should be ignored. This is critical for resources like Load Balancer Listeners, where a single property might contain a complex hierarchy of actions, forwards, and target groups.
For example, in an AWS Load Balancer Listener, an operator might want to maintain control over the listener's protocol and port but allow a separate deployment process or an auto-scaling logic to adjust the weights of the target groups for canary deployments. Instead of ignoring the entire defaultActions block, the operator can use a specific path:
defaultActions[*].forward.targetGroups[*].weight
This path tells Pulumi to dive into the defaultActions array, find the forward object, enter the targetGroups array, and specifically ignore changes to the weight property. This ensures that if the protocol changes in the code, Pulumi will still update it, but changes to the weight will be ignored.
Array Handling Logic
The behavior of ignoreChanges when applied to arrays is governed by a specific set of rules designed to handle changes in array length while preserving existing elements. Pulumi does not simply ignore the entire array if it changes; instead, it performs a positional comparison between the old array in the state and the new array provided in the program.
The logic follows these strict parameters:
- Element Matching: Only changes for elements that exist in both the old and new arrays are ignored.
- Expansion: If the new input array is longer than the old array, the additional elements are taken from the new array and added to the resource.
- Contraction: If the new input array is shorter than the old array, Pulumi only takes the number of elements present in the new array from the original array.
To illustrate this logic with concrete examples:
- Case A (Expansion): If the state contains an array
[1, 2]and the program provides a new array[a, b, c], the resulting state will be[1, 2, c]. The first two elements are ignored (retaining the old values), and the third element is accepted because it is new. - Case B (Contraction): If the state contains an array
[1, 2, 3]and the program provides a new array[a, b], the resulting state will be[1, 2]. The first two elements are ignored, and the third element is removed because the new array length is shorter.
Resource Options and Implementation Constraints
While ignoreChanges is a versatile tool, it is subject to specific constraints based on the type of resource it is applied to. Understanding these boundaries is essential for avoiding configuration errors that do not trigger compile-time failures but result in runtime insignificance.
Component Resources vs. Custom Resources
A critical distinction exists between how ignoreChanges interacts with Custom Resources and Component Resources.
- Custom Resources:
ignoreChangesworks directly on Custom Resources (the basic building blocks provided by Pulumi providers, such as anaws.s3.Bucket). It tells the provider plugin exactly which properties to omit from the update request. - Component Resources:
ignoreChangeshas no direct effect on Component Resources. A Component Resource is a logical grouping of other resources created by the user. Because theignoreChangesoption is defined on the baseresource-optionstype in every SDK, the compiler will not throw an error if you pass it to a component. However, at runtime, the component simply ignores this option.
If a developer intends for a component's child resources to ignore certain changes, the component's internal implementation must be explicitly written to pass that option down to the individual Custom Resources it instantiates.
Inputs vs. Outputs
The ignoreChanges option applies exclusively to resource inputs. It cannot be used to ignore changes to resource outputs. This is because outputs are values generated by the cloud provider after the resource is created or updated; they are not "inputs" that Pulumi sends to the provider to request a change. Therefore, attempting to use ignoreChanges on an output property is logically impossible within the Pulumi engine.
Operational Workflow: Drift and the Refresh Cycle
The use of ignoreChanges alters the standard Pulumi deployment loop, introducing a dependency on the pulumi refresh command to maintain accurate state.
The State Dependency
After a resource is created, Pulumi relies on the last recorded state for any property listed in ignoreChanges. During a preview or update, Pulumi performs the following sequence:
- Initial Create: Uses the value from the program because no prior state exists.
- Subsequent Updates: Uses the serialized value from the stack state.
- Live Value Check: Pulumi does not automatically read the live value from the cloud provider for the ignored property.
This creates a scenario where an external system can modify a live resource safely. As long as the state is not updated, Pulumi will continue to use the value stored in the state file.
The Danger of Stale State
If an external system modifies a resource and the operator skips the refresh step, the state file becomes "stale." If the operator later removes the property from the ignoreChanges list or changes other properties that trigger an update, Pulumi will send the stale value from the state to the provider. This will result in the provider overwriting the external changes, effectively erasing the "drift" that the operator intended to preserve.
To prevent this, the following workflow is required:
- Syncing State: Run
pulumi refresh(orpulumi up --refresh) to pull the current live values from the cloud provider into the Pulumi state. - Maintaining Ignored Properties: Even after a
refreshhas updated the state with the new live values, if the property remains in theignoreChangeslist, Pulumi will continue to use that newly updated state value for future updates, rather than the value defined in the program.
Strategic Resource Replacement
The use of ignoreChanges can sometimes create a situation where a resource needs to be updated, but the very option protecting the resource prevents the update from occurring. This is common in scenarios involving Amazon Machine Images (AMIs).
The Bastion Host Scenario
Consider a bastion host managed by Pulumi. To avoid replacing the host every time a new AMI is released in a dynamic lookup, the developer may use:
pulumi.IgnoreChanges([]string{"ami"})
This provides the developer with total control over the replacement cycle. However, when the developer eventually decides that the bastion host must be updated to the latest AMI for security reasons, simply updating the AMI ID in the code will not work, as Pulumi is explicitly ignoring that change.
In this instance, the developer cannot rely on the declarative pulumi up flow alone. Instead, they must use the Pulumi CLI to selectively target the resource for replacement. This allows the operator to force the destruction and recreation of specific resources without tearing down the entire stack, bypassing the ignoreChanges restriction for a single execution cycle.
Advanced Implementation via Resource Transformations
For large-scale infrastructures, manually adding ignoreChanges to every single resource is inefficient and error-prone. Pulumi provides Resource Transformations, which allow developers to globally apply options to resources based on their type, regardless of where they are defined in the codebase.
Transformations act as a middleware layer. When a resource is being registered, the transformation function intercepts the arguments and can modify the properties or the options before the resource is actually created.
Cross-Language Transformation Examples
Transformations are implemented differently across SDKs but achieve the same result: injecting ignoreChanges into specific resource types.
TypeScript Implementation
In TypeScript, transformations are passed as a list of functions within the resource options.
typescript
vpc = new MyVpcComponent("vpc", {}, {
transformations: [args => {
if (args.type === "aws:ec2/vpc:Vpc" || args.type === "aws:ec2/subnet:Subnet") {
return {
props: args.props,
opts: pulumi.mergeOptions(args.opts, { ignoreChanges: ["tags"] }),
};
}
return undefined;
}],
});
Python Implementation
In Python, a transformation function is defined and passed to the ResourceOptions of the component.
```python
def transformation(args: ResourceTransformationArgs):
if args.type_ == "aws:ec2/vpc:Vpc" or args.type_ == "aws:ec2/subnet:Subnet":
return ResourceTransformationResult(
props=args.props,
opts=ResourceOptions.merge(args.opts, ResourceOptions(
ignore_changes=["tags"],
)))
vpc = MyVpcComponent("vpc", opts=ResourceOptions(transformations=[transformation]))
```
Go Implementation
The Go SDK handles transformations by returning a pointer to a ResourceTransformationResult.
go
transformation := func(args *pulumi.ResourceTransformationArgs) *pulumi.ResourceTransformationResult {
if args.Type == "aws:ec2/vpc:Vpc" || args.Type == "aws:ec2/subnet:Subnet" {
return &pulumi.ResourceTransformationResult{
Props: args.Props,
Opts: append(args.Opts, pulumi.IgnoreChanges([]string{"tags"})),
}
}
return nil
}
vpc, err := NewMyVpcComponent(ctx, "vpc", pulumi.Transformations([]pulumi.ResourceTransformation{transformation}))
C# Implementation
In C#, the transformation uses a lambda expression to merge options via CustomResourceOptions.Merge.
csharp
var vpc = new MyVpcComponent("vpc", new ComponentResourceOptions
{
ResourceTransformations =
{
args =>
{
if (args.Resource.GetResourceType() == "aws:ec2/vpc:Vpc" ||
args.Resource.GetResourceType() == "aws:ec2/subnet:Subnet")
{
var options = CustomResourceOptions.Merge(
(CustomResourceOptions) args.Options,
new CustomResourceOptions { IgnoreChanges = {"tags"} });
return new ResourceTransformationResult {
Props = args.Props,
Options = options
};
}
return null;
}
}
});
Comparative Analysis of Resource Options
To properly contextualize ignoreChanges, it must be compared with other lifecycle-management options available in Pulumi. While ignoreChanges manages the update phase, other options manage the destruction or replacement phases.
| Resource Option | Primary Function | Real-World Use Case | Effect on State |
|---|---|---|---|
ignoreChanges |
Skips diffing for specific properties | External tag management or dynamic weights | Uses state value instead of program value |
protect |
Prevents accidental deletion | Critical databases or production VPCs | Blocks pulumi destroy until flag is removed |
retainOnDelete |
Keeps resource alive after code removal | Legacy data migration (S3 buckets) | Removes resource from state but not from cloud |
Detailed Technical Summary and Analysis
The implementation of ignoreChanges represents a pragmatic compromise between strict declarative purity and the operational realities of cloud computing. By allowing the engine to substitute program values with state values, Pulumi enables a "hybrid" management model where some properties are owned by the code and others are owned by the runtime environment.
The power of this feature lies in its precision. The ability to use property paths means that an operator can protect a single integer (like a weight) inside a deeply nested array without sacrificing the ability to update the rest of the configuration. This prevents the "all or nothing" approach to ignoring changes, which would otherwise leave resources vulnerable to unmanaged drift.
However, the reliance on pulumi refresh is the primary architectural vulnerability of this approach. Because Pulumi chooses to trust the state file over the live provider when ignoreChanges is active, the state file becomes the definitive source of truth for those specific properties. If the state file is not synchronized via a refresh, the operator is effectively flying blind, risking the accidental overwriting of production changes.
Ultimately, ignoreChanges is not a tool for ignoring errors or lazy configuration; it is a tool for delegating ownership. When used in conjunction with Resource Transformations, it allows platform engineers to establish global guardrails across an entire organization's infrastructure, ensuring that critical system-level attributes (like security tags or internal routing weights) are not inadvertently reverted by application developers during routine deployments.