How to pass IPv4 address to terraform provisioner local-exec - terraform

I was trying to do
terraform apply
but getting below error
1 error(s) occurred:
digitalocean_droplet.testvm[0]: Resource 'digitalocean_droplet.testvm' not found for variable
'digitalocean_droplet.testvm.ipv4_address'
Terraform does not automatically rollback in the face of errors.
Instead, your Terraform state file has been partially updated with any
resources that successfully completed. Please address the error above
and apply again to incrementally change your infrastructure.
How can I pass the public ip of the created droplet to provisioner local-exec command.
Below is my .tf file
provider "digitalocean" {
token = "----TOKEN----"
}
resource "digitalocean_droplet" "testvm" {
count = "10"
name = "do-instance-${count.index}"
image = "ubuntu-16-04-x64"
size = "512mb"
region = "nyc3"
ipv6 = true
private_networking = false
ssh_keys = [
"----SSH KEY----"
]
provisioner "local-exec" {
command = "fab production deploy ${digitalocean_droplet.testvm.ipv4_address}"
}
}
Thanks in advance!

For local-exec provisioner you can make use of the self keyword. In this case it would be {self.ipv4_address}.
My guess is that your snippet would've worked if you don't put count=10 in the testvm droplet. You can also make use of ${count.index}
More info: https://www.terraform.io/docs/provisioners/
Also, found this github issue that might be helpful to you.
Hope it helps

Related

How to destroy a particular resource before deleting other resources in terraform

I am creating a VPN using a script in Terraform as no provider function is available. This VPN also has some other attached resources like security groups.
So when I run terraform destroy it starts deleting the VPN but in parallel, it also starts deleting the security group. The security group deletion fails because those groups are "still" associated with the VPN which is in the process of deletion.
When I run terraform destroy -parallelism=1 it works fine, but due to some limitations, I cannot use this in prod.
Is there a way I can enforce VPN to be deleted first before any other resource deletion starts?
EDIT:
See the security group and VPN Code:
resource "<cloud_provider>_security_group" "sg" {
name = format("%s-%s", local.name, "sg")
vpc = var.vpc_id
resource_group = var.resource_group_id
}
resource "null_resource" "make_vpn" {
triggers = {
vpn_name = var.vpn_name
local_script = local.scripts_location
}
provisioner "local-exec" {
command = "${local.scripts_location}/login.sh"
interpreter = ["/bin/bash", "-c"]
environment = {
API_KEY = var.api_key
}
}
provisioner "local-exec" {
command = local_file.make_vpn.filename
}
provisioner "local-exec" {
when = "destroy"
command = <<EOT
${self.triggers.local_script}/delete_vpn_server.sh ${self.triggers.vpn_name}
EOT
on_failure = continue
}
}

Terraform Vs Packer connection for remote-exec provisioner

I have a Terraform infrastructure where I have to install a Windows 10 server EC2 instance with an additional volume (D:).
The Terraform configuration is quite easy and well explained in volume_attachment documentation.
To init, attach and format the new volume I found this answer, I tested it (by hands) and it works as expected.
The problem is how to automate everything: the aws_volume_attachment depends on aws_instance so I can't run the script to init, attach and format the new volume in the user_data section of the aws_instance since the aws_volume_attachment is not yet created by Terraform.
I'm trying to execute the script with a null_resource.
To configure the instance image with Packer I'm using WinRM with following configuration:
source "amazon-ebs" "windows" {
ami_name = var.image_name
communicator = "winrm"
instance_type = "t2.micro"
user_data_file = "setup.txt"
winrm_insecure = true
winrm_port = 5986
winrm_use_ssl = true
winrm_username = "Administrator"
so I tried to replicate the same connection for the null_resource in Terraform:
resource "null_resource" "it" {
depends_on = [aws_volume_attachment.it]
triggers = { instance_id = local.jenkins_build_win.id, volume_id = aws_ebs_volume.it.id }
connection {
host = local.jenkins_build_win.fqdn
https = true
insecure = true
password = local.jenkins_build_win.password
port = 5986
type = "winrm"
user = "Administrator"
}
provisioner "remote-exec" {
inline = [
"Initialize-Disk -Number 1 -PartitionStyle MBR",
"$part = New-Partition -DiskNumber 1 -UseMaximumSize -IsActive -AssignDriveLetter",
"Format-Volume -DriveLetter $part.DriveLetter -Confirm:$FALSE"
]
}
}
local.jenkins_build_win.fqdn and local.jenkins_build_win.password resolve correctly (I wrote them with a local_file resource and I can use them to connect to the instance with Remote Desktop), but Terraform can't connect to the instance. :(
Running Terraform with TF_LOG=trace the only detail I can get for the error is:
[DEBUG] connecting to remote shell using WinRM
[ERROR] error creating shell: unknown error Post "https://{fqdn}:5986/wsman": read tcp {local_ip}:44538->{remote_ip}:5986: read: connection reset by peer
while running Packer with PACKER_LOG=1 I can't get any details on WinRM connection: my intention was to compare the calls made by Packer with the ones done by Terraform to try to identify the problem...
I feel I'm stuck. :( Any idea?

Terraform : depends_on argument not creating the specified resource first

I want to push the terraform state file to a github repo. The file function in Terraform fails to read .tfstate files, so I need to change their extension to .txt first. Now to automate it, I created a null resource which has a provisioner to run the command to copy the tfstate file as a txt file in the same directory. I came across this 'depends_on' argument which lets you specify if a particular resource needs to be made first before running the current. However, it is not working and I am straight away getting the error that 'terraform.txt' file doesn't exit when the file function demands it.
provider "github" {
token = "TOKEN"
owner = "USERNAME"
}
resource "null_resource" "tfstate_to_txt" {
provisioner "local-exec" {
command = "copy terraform.tfstate terraform.txt"
}
}
resource "github_repository_file" "state_push" {
repository = "TerraformStates"
file = "terraform.tfstate"
content = file("terraform.txt")
depends_on = [null_resource.tfstate_to_txt]
}
The documentation for the file function explains this behavior:
This function can be used only with files that already exist on disk at the beginning of a Terraform run. Functions do not participate in the dependency graph, so this function cannot be used with files that are generated dynamically during a Terraform operation. We do not recommend using dynamic local files in Terraform configurations, but in rare situations where this is necessary you can use the local_file data source to read files while respecting resource dependencies.
This paragraph also includes a suggestion for how to get the result you wanted: use the local_file data source, from the hashicorp/local provider, to read the file as a resource operation (during the apply phase) rather than as part of configuration loading:
resource "null_resource" "tfstate_to_txt" {
triggers = {
source_file = "terraform.tfstate"
dest_file = "terraform.txt"
}
provisioner "local-exec" {
command = "copy ${self.triggers.source_file} ${self.triggers.dest_file}"
}
}
data "local_file" "state" {
filename = null_resource.tfstate_to_txt.triggers.dest_file
}
resource "github_repository_file" "state_push" {
repository = "TerraformStates"
file = "terraform.tfstate"
content = data.local_file.state.content
}
Please note that although the above should get the order of operations you were asking about, reading the terraform.tfstate file while Terraform running is a very unusual thing to do, and is likely to result in undefined behavior because Terraform can repeatedly update that file at unpredictable moments throughout terraform apply.
If your intent is to have Terraform keep the state in a remote system rather than on local disk, the usual way to achieve that is to configure remote state, which will then cause Terraform to keep the state only remotely, and not use the local terraform.tfstate file at all.
depends_on does not really work with null_resource.provisioner.
here's a workaround that can help you :
resource "null_resource" "tfstate_to_txt" {
provisioner "local-exec" {
command = "copy terraform.tfstate terraform.txt"
}
}
resource "null_resource" "delay" {
provisioner "local-exec" {
command = "sleep 20"
}
triggers = {
"before" = null_resource.tfstate_to_txt.id
}
}
resource "github_repository_file" "state_push" {
repository = "TerraformStates"
file = "terraform.tfstate"
content = file("terraform.txt")
depends_on = ["null_resource.delay"]
}
the delay null resource will make sure the resource 2 runs after the first if the copy command takes more time just change the sleep to higher number

Using function templatefile(path, vars) with a remote-exec provisioner

With terraform 0.12, there is a templatefile function but I haven't figured out the syntax for passing it a non-trivial map as the second argument and using the result to be executed remotely as the newly created instance's provisioning step.
Here's the gist of what I'm trying to do, although it doesn't parse properly because one can't just create a local variable within the resource block named scriptstr.
While I'm really trying to get the output of the templatefile call to be executed on the remote side, once the provisioner can ssh to the machine, I've so far gone down the path of trying to get the templatefile call output written to a local file via the local-exec provisioner. Probably easy, I just haven't found the documentation or examples to understand the syntax necessary. TIA
resource "aws_instance" "server" {
count = "${var.servers}"
ami = "${local.ami}"
instance_type = "${var.instance_type}"
key_name = "${local.key_name}"
subnet_id = "${element(aws_subnet.consul.*.id, count.index)}"
iam_instance_profile = "${aws_iam_instance_profile.consul-join.name}"
vpc_security_group_ids = ["${aws_security_group.consul.id}"]
ebs_block_device {
device_name = "/dev/sda1"
volume_size = 2
}
tags = "${map(
"Name", "${var.namespace}-server-${count.index}",
var.consul_join_tag_key, var.consul_join_tag_value
)}"
scriptstr = templatefile("${path.module}/templates/consul.sh.tpl",
{
consul_version = "${local.consul_version}"
config = <<EOF
"bootstrap_expect": ${var.servers},
"node_name": "${var.namespace}-server-${count.index}",
"retry_join": ["provider=aws tag_key=${var.consul_join_tag_key} tag_value=${var.consul_join_tag_value}"],
"server": true
EOF
})
provisioner "local-exec" {
command = "echo ${scriptstr} > ${var.namespace}-server-${count.index}.init.sh"
}
provisioner "remote-exec" {
script = "${var.namespace}-server-${count.index}.init.sh"
connection {
type = "ssh"
user = "clear"
private_key = file("${local.private_key_file}")
}
}
}
In your question I can see that the higher-level problem you seem to be trying to solve here is creating a pool of HashiCorp Consul servers and then, once they are all booted up, to tell them about each other so that they can form a cluster.
Provisioners are essentially a "last resort" in Terraform, provided out of pragmatism because sometimes logging in to a host and running commands on it is the only way to get a job done. An alternative available in this case is to instead pass the information from Terraform to the server via the aws_instance user_data argument, which will then allow the servers to boot up and form a cluster immediately, rather than being delayed until Terraform is able to connect via SSH.
Either way, I'd generally prefer to have the main body of the script I intend to run already included in the AMI so that Terraform can just run it with some arguments, since that then reduces the problem to just templating the invocation of that script rather than the whole script:
provisioner "remote-exec" {
inline = ["/usr/local/bin/init-consul --expect='${var.servers}' etc, etc"]
connection {
type = "ssh"
user = "clear"
private_key = file("${local.private_key_file}")
}
}
However, if templating an entire script is what you want or need to do, I'd upload it first using the file provisioner and then run it, like this:
provisioner "file" {
destination = "/tmp/consul.sh"
content = templatefile("${path.module}/templates/consul.sh.tpl", {
consul_version = "${local.consul_version}"
config = <<EOF
"bootstrap_expect": ${var.servers},
"node_name": "${var.namespace}-server-${count.index}",
"retry_join": ["provider=aws tag_key=${var.consul_join_tag_key} tag_value=${var.consul_join_tag_value}"],
"server": true
EOF
})
}
provisioner "remote-exec" {
inline = ["sh /tmp/consul.sh"]
}

Referencing Azure ACS Cluster Master VM Public IP?

I'm using this repo to create a kubernetes cluster on Azure using acs-engine.
I am wondering if anyone can help me identify how to reference the master VM's public IP address.
This would be used to ssh into the master VM (ssh user#public-ip), which is important because I want to run local-exec provisioners to configure my cluster with Ansible.
I don't believe that it is the first_master_ip in the below main.tf (this is given a value on the repo's variables.tf), though I also don't know how to reference this IP as well.
One other thing that I have tried is to obtain the master VM public IP address using the azure command line, however I also haven't had any success with this because I don't know how to get the cluster-name, which would be passed in with az acs kubernetes browse -g <resource-group-name> -n <cluster-name>
Any help would be greatly greatly appreciated as I've really hit a road block with this.
provider "azurerm" {
subscription_id = "${var.azure_subscription_id}"
client_id = "${var.azure_client_id}"
client_secret = "${var.azure_client_secret}"
tenant_id = "${var.azure_tenant_id}"
}
# Azure Resource Group
resource "azurerm_resource_group" "default" {
name = "${var.resource_group_name}"
location = "${var.azure_location}"
}
resource "azurerm_public_ip" "test" {
name = "acceptanceTestPublicIp1"
location = "${var.azure_location}"
resource_group_name = "${azurerm_resource_group.default.name}"
public_ip_address_allocation = "static"
}
data "template_file" "acs_engine_config" {
template = "${file(var.acs_engine_config_file)}"
vars {
master_vm_count = "${var.master_vm_count}"
dns_prefix = "${var.dns_prefix}"
vm_size = "${var.vm_size}"
first_master_ip = "${var.first_master_ip}"
worker_vm_count = "${var.worker_vm_count}"
admin_user = "${var.admin_user}"
ssh_key = "${var.ssh_key}"
service_principle_client_id = "${var.azure_client_id}"
service_principle_client_secret = "${var.azure_client_secret}"
}
}
# Locally output the rendered ACS Engine Config (after substitution has been performed)
resource "null_resource" "render_acs_engine_config" {
provisioner "local-exec" {
command = "echo '${data.template_file.acs_engine_config.rendered}' > ${var.acs_engine_config_file_rendered}"
}
depends_on = ["data.template_file.acs_engine_config"]
}
# Locally run the ACS Engine to produce the Azure Resource Template for the K8s cluster
resource "null_resource" "run_acs_engine" {
provisioner "local-exec" {
command = "acs-engine generate ${var.acs_engine_config_file_rendered}"
}
depends_on = ["null_resource.render_acs_engine_config"]
}
I have no experience with terraform but acs-engine sets up a lb with a public ip that goes through your master (or balances across multiple masters). You find the ip of that lb by using <dns_prefix>.<region>.cloudapp.azure.com.
But if you need the ip to provision something extra, this won't be enough when you have multiple masters.

Resources