terraform lifecycle prevent destroy - terraform

I am working with Terraform V11 and AWS provider; I am looking for a way to prevent destroying few resources during the destroy phase. So I used the following approach.
lifecycle {
prevent_destroy = true
}
When I run a "terraform plan" I get the following error.
the plan would destroy this resource, but it currently has
lifecycle.preven_destroy set to true. to avoid this error and continue with the plan.
either disable or adjust the scope.
All that I am looking for is a way to avoid destroying one of the resources and its dependencies during the destroy command.

AFAIK This feature is not yet supported
You need to remove that resource from state file and then reimport it
terraform plan | grep <resource> | grep id
terraform state rm <resource>
terraform destroy
terraform import <resource> <ID>

The easiest way to do this would be to comment out all of the the resources that you want to destroy and then do a terraform apply.

I've found the most practical way to manage this is through a combination of variables that allow the resource in question to be conditionally created or not on via the use of count, alongside having all other resources depend on the associated Data Source instead of the conditionally created resource.
A good example of this is a Route 53 Hosted Zone which can be a pain to destroy and recreate if you manage your domain outside of AWS and need to update your nameservers, waiting for DNS propagation each time you spin it up.
1. By specifying some variable
variable "should_create_r53_hosted_zone" {
type = bool
description = "Determines whether or not a new hosted zone should be created on apply."
}
2. you can use it alongside count on the resource to conditionally create it.
resource "aws_route53_zone" "new" {
count = var.should_create_r53_hosted_zone ? 1 : 0
name = "my.domain.com"
}
3. Then, by following up with a call to the associated Data Source
data "aws_route53_zone" "existing" {
name = "my.domain.com"
depends_on = [
aws_route53_zone.new
]
}
4. you can give all other resources consistent access to the resource's attributes regardless of whether or not your flag has been set.
resource "aws_route53_record" "rds_reader_endpoint" {
zone_id = data.aws_route53_zone.existing.zone_id
# ...
}
This approach is only slightly better than commenting / uncommenting resources during apply, but at least gives some consistent, documented way of working around it.

Related

Shall we introduce deletion_protection attribute to "important" Terraform resources to protect against deletion on top of the prevent_destroy?

Context: I'm developing a TF Provider using Terraform SDKv2.
I've noticed there's prevent_destroy attribute that should protect against accidental deletions of important resources (that should only be deleted in exceptional cases).
However it looks like it won't prevent from issues where the resource is accidentally removed from the TF configuration or some other silly developer error, since the prevent_destroy attribute will be lost as well and not applied.
And then we noticed there's deletion_protection attribute for aws_rds_cluster resource of AWS TF Provider. Is it a thing or there're any other built in mechanism to address it?
For example, GCP's Best practices for using Terraform mentions:
resource "google_sql_database_instance" "main" {
name = "primary-instance"
settings {
tier = "D0"
}
lifecycle {
prevent_destroy = true
}
}
so it sounds like prevent_destroy might be good enough. That said, there's deletion_protection attribute for sql_database_instance resource too. What's even more interesting, for google_bigquery_table deletion_protection attribute defaults to true and there's a note:
: On newer versions of the provider, you must explicitly set deletion_protection=false (and run terraform apply to write the field to state) in order to destroy an instance. It is recommended to not set this field (or set it to true) until you're ready to destroy.

What's the common pattern for sensitive attributes of Terraform resource that are only required on creation?

Context: I'm working on a new TF Provider using SDKv2.
I'm adding a new data plane resource which has a very weird API. Namely, there're some sensitive attributes (that are specific to this resource so they can't be set under provider block -- think about DataDog / Slack API secrets that this resource needs to interact with under the hood) I need to pass on creation that are not necessary later on (for example, even for update operation). My minimal code sample:
resource "foo" "bar" {
name = "abc"
sensitive_creds = {
"datadog_api_secret" = "abc..."
// might pass "slack_api_secret" instead
}
...
}
How can I implement it in Terraform to avoid state drifts etc?
So far I can see 3 options:
Make a user pass it first, don't save "sensitive_creds" to TF state. Make a user set it to sensitive_creds = {} to avoid a state drift for the next terraform plan run.
Make a user pass it first, don't save "sensitive_creds" to TF state. Make a user add ignore_changes = [sensitive_creds] } to their Terraform configuration.
Save "sensitive_creds" to TF state and live with it since users are likely to encrypt TF state anyways.
The most typical compromise is for the provider to still save the user's specified value to the state during create and then to leave it unchanged in the "read" operation that would normally update the state to match the remote system.
The result of this compromise is that Terraform can still detect when the user has intentionally changed the secret value in the configuration, but Terraform will not be able to detect changes made to the value outside of Terraform.
This is essentially your option 3. The Terraform provider protocol requires that the values saved to the state after create exactly match anything the user has specified in the configuration, so your first two options would violate the expected protocol and thus be declared invalid by Terraform Core.
Since you are using SDKv2, you can potentially "get away with it" because Terraform Core permits that older SDK to violate some of the rules as a pragmatic way to deal with the fact that SDKv2 was designed for older versions of Terraform and therefore doesn't implement the type system correctly, but Terraform Core will still emit warnings into its own logs noting that your provider produced an invalid result, and there may be error messages raised against downstream resources if they have configuration derived from the value of your sensitive_creds argument.

How to prevent the instance getting deleted when creating new instance in terraform in a single code?

I have tried creating an instance using terraform by getting values either windows ami or linux ami using parameters in jenkins pipeline.
The loophole is :-
When I choose windows instance ,it creates instance in AWS, next time I choose linux ,windows gets deleted and newly Linux is created.
Expected Output
The old instance should not get deleted when creating the new one
I have tried using the following codes:-
provider "aws"{
region="us-east-1"
}
resource "aws_instance" "my-instance" {
ami = lookup(var.ami,var.name)
instance_type = "t2.micro"
count=1
key_name = "nits"
tags = {
Name = var.name
}
}
variable "ami" {
default = {
"Linux" = "ami-08e4e35cccc6189f4"
"Windows" = "ami-0d43d465e2051057f"
}
}
variable "name" {
default ="Linux"
}
pipeline{
agent any
tools{
terraform 'terra'
}
parameters{
choice(name:'ami', choices: ['Linux','Windows'])
choice(name:'Actions', choices:['apply','destroy'])
}
stages{
stage('Git checkout'){
steps{
//
}
}
stage('Terraform Init'){
steps{
sh label: '', script:'terraform init'
}
}
stage('Terraform apply'){
steps{
sh label:'',script:'terraform ${Actions} -var name="${ami}" --auto-approve'
}
}
}
}
If I can try with modules, kindly suggest me the ways.
The Terraform language is declarative, meaning what you described using your terraform configuration is the intended goal rather than the steps to reach that goal. No matter how many times you run the pipeline terraform will make sure that only one resource get provisioned (because in your configuration you have only placed one aws_instance) and then maintain the infrastructure information in terraform.tf state file
For your expected output there are quite a few ways:
Add a stage in the pipeline before the terraform commands which will add a example.tf file with the content :
resource "aws_instance" "my-other-instance" {
/......
}
Make sure to add example.tf in the same directory as the previous tf files.
So every time you run the pipeline a new instance will be created without others getting deleted.
But here you need to adjust the variables so that every new instance gets different ami and name.
Add a stage in the pipeline before the terraform commands which deletes the terraform.tf state file so that terrform forgets the infrastructure it built the previous time and then init and apply.
That's not how Terraform works. You are trying to use Terraform as a scripting language, but it is not a scripting language. Terraform templates are not scripts, they are a declarative template to specify what infrastructure you want to exist, which you then pass to Terraform, which does what it needs to do in order to make that infrastructure exist. If you run it again with changes, then you are telling it: "I want you to change what currently exists, to match this new template".
If you want both resources to exist, then you have to declare them both separately in Terraform.
If you want to build some sort of interactive pipeline that creates EC2 instances on demand, then you may want to build that with a scripting language like Python + Boto3, instead of Terraform.

Terraform provisioner trigger only for new instances / only run once

I have conditional provision steps I want to run for all compute instances created, but only run once.
I know I can put the provisioning within the compute resource, but then it cannot be conditional.
If I put it in a null_resource, I need a trigger, and I don't know how to trigger on only the newly created resources (i.e. if I already have 1 instance, and want to scale to 2, I want to only run provisioning on the 2nd being created, not run again on the 1st which is already provisioned).
How can I get a variable that only gives me the id or ip of the instance just created, as opposed to all of them?
Below an example of the provisioner.
resource "null_resource" "provisioning" {
count = var.condition ? length(var.instance_ips) : 0
triggers = {
instance_ids = join(",", var.instance_ips)
}
connection {
agent = false
timeout = "4m"
host = var.instance_ips[count.index]
user = "user"
private_key = var.ssh_private_key
}
provisioner "remote-exec" {
inline = [ do something, then remove the public key from authorized_keys ]
}
}
PS: the reason I only can run once (as opposed to run again and do nothing if already provisioned) is that I want to destroy the provisioning public key after I'm done, since it is using a tf generated key pair and the private key ends up in the state file, I want to make sure someone who gets access to the key pair still cannot access the instance.
Once the public key is removed from the authorized_keys the provisioner running a second time will just fail to connect, timeout and fail.
I found that I can use the on_failure: continue key, but then if it actual fails for legitimate reasons it would continue too.
I also could use a key pair that is generated locally with a local-exec provisioner so it doesn't show in the state file, but then the key is a file, which is not much different if someone get access to it; the file needs to stay on the machine, which may not work well with a cloud resource manager env that is recreated on a need to run basis.
And then I'm sure there are other ways to provision a file or script, but in this case it contains instance dependency data generated by TF, that I don't want left in a cloud-init.
So, I come down to needing to figure a way to use a trigger that only contains the new instance(s)
Any ideas how to do this?
https://www.terraform.io/docs/provisioners/
This documentation lists provisioners as a last resource and provides some suggestions on how to avoid having to use it, for various common resources.
Execute the script from the user_data, which is specifically designed for provisional, run-once actions. Since defining the user_data supports all regular Terraform interpolation, you can use that opportunity to pass environment variables or selectively include/exclude parts of a script, if you need conditional logic.
The downside is that any change in user_data results in recreating the instances, or creating a new launch configuration/template.

How to use multiple Terraform Providers sequentially

How can I get Terraform 0.10.1 to support two different providers without having to run 'terraform init' every time for each provider?
I am trying to use Terraform to
1) Provision an API server with the 'DigitalOcean' provider
2) Subsequently use the 'Docker' provider to spin up my containers
Any suggestions? Do I need to write an orchestrating script that wraps Terraform?
Terraform's current design struggles with creating "multi-layer" architectures in a single configuration, due to the need to pass dynamic settings from one provider to another:
resource "digitalocean_droplet" "example" {
# (settings for a machine running docker)
}
provider "docker" {
host = "tcp://${digitalocean_droplet.example.ipv4_address_private}:2376/"
}
As you saw in the documentation, passing dynamic values into provider configuration doesn't fully work. It does actually partially work if you use it with care, so one way to get this done is to use a config like the above and then solve the "chicken-and-egg" problem by forcing Terraform to create the droplet first:
$ terraform plan -out=tfplan -target=digitalocean_droplet.example
The above will create a plan that only deals with the droplet and any of its dependencies, ignoring the docker resources. Once the Docker droplet is up and running, you can then re-run Terraform as normal to complete the setup, which should then work as expected because the Droplet's ipv4_address_private attribute will then be known. As long as the droplet is never replaced, Terraform can be used as normal after this.
Using -target is fiddly, and so the current recommendation is to split such systems up into multiple configurations, with one for each conceptual "layer". This does, however, require initializing two separate working directories, which you indicated in your question that you didn't want to do. This -target trick allows you to get it done within a single configuration, at the expense of an unconventional workflow to get it initially bootstrapped.
Maybe you can use a provider instance within your resources/module to set up various resources with various providers.
https://www.terraform.io/docs/configuration/providers.html#multiple-provider-instances
The doc talks about multiple instances of same provider but I believe the same should be doable with distinct providers as well.
A little bit late...
Well, got the same Problem. My workaround is to create modules.
First you need a module for your docker Provider with an ip variable:
# File: ./docker/main.tf
variable "ip" {}
provider "docker" {
host = "tcp://${var.ip}:2375/"
}
resource "docker_container" "www" {
provider = "docker"
name = "www"
}
Next one is to load that modul in your root configuration:
# File: .main.tf
module "docker01" {
source = "./docker"
ip = "192.169.10.12"
}
module "docker02" {
source = "./docker"
ip = "192.169.10.12"
}
True, you will create on every node the same container, but in my case that's what i wanted. I currently haven't found a way to configure the hosts with an individual configuration. Maybe nested modules, but that didn't work in the first tries.

Resources