I'm building a Terraform plugin/provider (link) which will help users manage their cloud resources e.g. cloud instances, Kubernetes clusters & etc on a cloud platform.
The cloud platform at this moment does not support Kubernetes nodes size change after it gets created. If user wants to change the nodes size, they need to create a new node pool with the new nodes size.
So I'm adding this block in my plugin code, specifically in the Kubernetes cluster update method (link):
if d.HasChange("target_nodes_size") {
errMsg := []string{
"[ERR] Unable to update 'target_nodes_size' after creation.",
"Please create a new node pool with the new node size.",
}
return fmt.Errorf(strings.Join(errMsg, " "))
}
The problem is, the error only appears when I run terraform apply
command. What I want is, I want it to show when user runs terraform plan
command so they know it early that it's not possible to change the nodes size without creating a new node pool.
How do I make that target_nodes_size
field immutable and show the error early in terraform plan
output?
The correct thing to do here is to tell Terraform that changes to the resource cannot be done in place and instead requires the recreation of the resource (normally destroy followed by creation but you can reverse that with lifecycle.create_before_destroy
).
When creating a provider you can do this with the ForceNew
parameter on a schema's attribute.
As an example, the aws_launch_configuration
resource is considered immutable from AWS' API side so every non computed attribute in the schema is marked with ForceNew: true
.:
func resourceAwsLaunchConfiguration() *schema.Resource {
return &schema.Resource{
Create: resourceAwsLaunchConfigurationCreate,
Read: resourceAwsLaunchConfigurationRead,
Delete: resourceAwsLaunchConfigurationDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"arn": {
Type: schema.TypeString,
Computed: true,
},
"name": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ConflictsWith: []string{"name_prefix"},
ValidateFunc: validation.StringLenBetween(1, 255),
},
// ...
If you then attempt to modify any of the ForceNew: true
fields then Terraform's plan will show that it needs to replace the resource and at apply time it will automatically do that as long as the user accepts the plan.
For a more complicated example, the aws_elasticsearch_domain
resource allows in place version changes but only for specific version upgrade paths (so you can't eg go from 5.4
to 7.8
directly and instead have to go to 5.4 -> 5.6 -> 6.8 -> 7.8
. This is done by using the CustomizeDiff
attribute on the schema which allows you to use logic at plan time to give a different result than would normally be found from static configuration.
The CustomizeDiff
for the aws_elasticsearch_domain
elasticsearch_version
attribute looks like this:
func resourceAwsElasticSearchDomain() *schema.Resource {
return &schema.Resource{
Create: resourceAwsElasticSearchDomainCreate,
Read: resourceAwsElasticSearchDomainRead,
Update: resourceAwsElasticSearchDomainUpdate,
Delete: resourceAwsElasticSearchDomainDelete,
Importer: &schema.ResourceImporter{
State: resourceAwsElasticSearchDomainImport,
},
Timeouts: &schema.ResourceTimeout{
Update: schema.DefaultTimeout(60 * time.Minute),
},
CustomizeDiff: customdiff.Sequence(
customdiff.ForceNewIf("elasticsearch_version", func(_ context.Context, d *schema.ResourceDiff, meta interface{}) bool {
newVersion := d.Get("elasticsearch_version").(string)
domainName := d.Get("domain_name").(string)
conn := meta.(*AWSClient).esconn
resp, err := conn.GetCompatibleElasticsearchVersions(&elasticsearch.GetCompatibleElasticsearchVersionsInput{
DomainName: aws.String(domainName),
})
if err != nil {
log.Printf("[ERROR] Failed to get compatible ElasticSearch versions %s", domainName)
return false
}
if len(resp.CompatibleElasticsearchVersions) != 1 {
return true
}
for _, targetVersion := range resp.CompatibleElasticsearchVersions[0].TargetVersions {
if aws.StringValue(targetVersion) == newVersion {
return false
}
}
return true
}),
SetTagsDiff,
),
Attempting to upgrade an aws_elasticsearch_domain
's elasticsearch_version
on an accepted upgrade path (eg 7.4 -> 7.8
) will show that it's an in place upgrade in the plan and apply that at apply time. On the other hand if you tried to upgrade via a path that isn't allowed directly (eg 5.4 -> 7.8
directly) then Terraform's plan will show that it needs to destroy the existing Elasticsearch domain and create a new one.