How to prevent data loss in persistent volume when server is recreated - terraform

I am working with terraform and openstack and using a persistent volume to store data. When recreating the server only and reattaching the same vol, this data is sometimes corrupted/lost. How do i prevent this?
I taint the server and then terraform apply to recreate. This detaches the vol , destroys the server, recreates and attaches back the volume. However, sometimes the data in the volume is lost or corrupted. This vol contains postgreSQL DB files.
I tried to use terraform destroy - but that will cause the volume to be destroyed as well.
This is the module
data "template_file" "init-config" {
template = "${file("modules/postgres-server/init-config.tpl")}"
vars {
instance_name = "${var.instance_name}"
tenant_name = "${var.tenant_name}"
}
}
# Define instance properties.
# You should provide the variables in main.tf
resource "openstack_compute_instance_v2" "server" {
name = "${var.instance_name}"
image_name = "${var.image_name}"
flavor_name = "${var.flavor_name}"
key_pair = "${var.key_name}"
security_groups = ["default", "${var.secgroup_name}"]
user_data = "${data.template_file.init-config.rendered}"
stop_before_destroy = "true"
network {
name = "${var.tenant_name}-net"
}
}
# Define a floating ip resoruce
resource "openstack_networking_floatingip_v2" "server_float" {
pool = "net-iaas-external-dev"
}
# Associate the instance and floating ip resources
resource "openstack_compute_floatingip_associate_v2" "server_float_assoc" {
floating_ip = "${openstack_networking_floatingip_v2.server_float.address}"
instance_id = "${openstack_compute_instance_v2.server.id}"
}
# Create persistent vol
resource "openstack_blockstorage_volume_v2" "pgvol" {
name = "postgreSQL-DATA-${var.instance_name}"
description = "Data Vol for ${var.instance_name}"
size = 50
}
# Attach the persistent data to the instance
resource "openstack_compute_volume_attach_v2" "pgvol_attach" {
instance_id = "${openstack_compute_instance_v2.server.id}"
volume_id = "${openstack_blockstorage_volume_v2.pgvol.id}"
device = "/dev/vdc"
}
This is the main.tf
module "postgre-server" {
source = "./modules/postgres-server"
instance_name = "INST_NAME"
image_name = "centos7"
flavor_name = "r1.medium"
key_name = "${module.keypair.output_key_name}"
secgroup_name = "${module.secgroup.output_secgroup_name}"
tenant_name = "${var.tenant_name}"
}
Expected result is that volume data is not lost and when I attached back to the newly re-created server, the filesystems in that volume and all the data is there.
Thanks. Appreciate any insights on how to do this.

A quick way is to split code into two stacks, one stack (module #1) manages the storage only, the other (module #2) manage the rest.
After split, you can change module #2 anytime, whatever apply or destroy.
Between two stacks, you can reference storage resource with several ways.
Way one:
reference from data source terraform_remote_state, which you need set an output as below
output "persistant_storage_id" {
value = "${openstack_blockstorage_volume_v2.pgvol.id}"
}
then use below code in module 2 to reference the persistent storage.
data "terraform_remote_state" "persistent_storage" {
backend = "xxx"
config {
name = "hashicorp/persistent-storage"
}
}
so module #2 can reference it as ${data.terraform_remote_state.persistent_storage.persistent_storage_id}"
Way two:
reference persistent storage volume id with data source openstack_blockstorage_availability_zones_v3 directly
way three:
way #3 is similar as #1.
you need output the value "${openstack_blockstorage_volume_v2.pgvol.id}" in module #1,
output "persistant_storage_id" {
value = "${openstack_blockstorage_volume_v2.pgvol.id}"
}
call the module #1
module "persistent_storage" {
...
}
then reference it as ${module.persistent_storage.persistent_storage_id}"

This is working when I unmount the filesystems in the vol prior to using terraform to recreate the instance. I thought the stop_before_destroy = "true" would have gracefully stopped the instance and detach vol, but it didn't work in my case :)

Related

Is there a way to get the Terraform output for only the added resources?

I'm creating an azurerm_windows_virtual_machine resource that has a count which changes overtime.
resource "azurerm_windows_virtual_machine" "this" {
# The count variable could change
count = var.virtual_machine_count[terraform.workspace]
name = "vmt${count.index}"
...
tags = local.tags
}
After creating the VM resource, I'm creating a local_file that I would need to use as an Ansible inventory to configure the VMs.
resource "local_file" "ansible_inventory" {
depends_on = [azurerm_windows_virtual_machine.this]
content = templatefile(var.template_file_name,
{
private-ips = azurerm_windows_virtual_machine.this.*.private_ip_address,
ansible-user = var.ansible_user,
ansible-password = var.ansible_password,
ansible-ssh-port = var.ansible_ssh_port,
ansible-connection = var.ansible_connection,
ansible-winrm-transport = var.ansible_winrm_transport,
ansible-winrm-server-cert-validation = var.ansible_winrm_server_cert_validation
}
)
filename = var.template_output_file_name
}
The current behavior of this code is whenever I add another VM to the VM count, the local file resource adds the IP addresses of all VMs, including existing ones to the template file.
What I was hoping to do was to add the IP address of just the newly added VM resource to the template file.
Not sure if this is possible with Terraform only or if I need an external script to keep track of the existing and new IP addresses. Thanks.

Terraform check if resource exists before creating it

Is there a way in Terraform to check if a resource in Google Cloud exists prior to trying to create it?
I want to check if the following resources below exist in my CircleCI CI/CD pipeline during a job. I have access to terminal commands, bash, and gcloud commands. If the resources do exist, I want to use them. If they do not exist, I want to create them. I am doing this logic in CircleCI's config.yml as steps where I have access to terminal commands and bash. My goal is to create my necessary infrastructure (resources) in GCP when they are needed, otherwise use them if they are created, without getting Terraform errors in my CI/CD builds.
If I try to create a resource that already exists, Terraform apply will result in an error saying something like, "you already own this resource," and now my CI/CD job fails.
Below is pseudo code describing the resources I am trying to get.
resource "google_artifact_registry_repository" "main" {
# this is the repo for hosting my Docker images
# it does not have a data source afaik because it is beta
}
For my google_artifact_registry_repository resource. One approach I have is to do a Terraform apply using a data source block and see if a value is returned. The problem with this is that the google_artifact_registry_repository does not have a data source block. Therefore, I must create this resource once using a resource block and every CI/CD build thereafter can rely on it being there. Is there a work-around to read that it exists?
resource "google_storage_bucket" "bucket" {
# bucket containing the folder below
}
resource "google_storage_bucket_object" "content_folder" {
# folder containing Terraform default.tfstate for my Cloud Run Service
}
For my google_storage_bucket and google_storage_bucket_object resources. If I do a Terraform apply using a data source block to see if these exist, one issue I run into is when the resources are not found, Terraform takes forever to return that status. It would be great if I could determine if a resource exists within like 10-15 seconds or something, and if not assume these resources do not exist.
data "google_storage_bucket" "bucket" {
# bucket containing the folder below
}
output bucket {
value = data.google_storage_bucket.bucket
}
When the resource exists, I can use Terraform output bucket to get that value. If it does not exist, Terraform takes too long to return a response. Any ideas on this?
Thanks to the advice of Marcin, I have a working example of how to solve my problem of checking if a resource exists in GCP using Terraform's external data sources. This is one way that works. I am sure there are other approaches.
I have a CircleCI config.yml where I have a job that uses run commands and bash. From bash, I will init/apply a Terraform script that checks if my resource exists, like so below.
data "external" "get_bucket" {
program = ["bash","gcp.sh"]
query = {
bucket_name = var.bucket_name
}
}
output "bucket" {
value = data.external.get_bucket.result.name
}
Then in my gcp.sh, I use gsutil to get my bucket if it exists.
#!/bin/bash
eval "$(jq -r '#sh "BUCKET_NAME=\(.bucket_name)"')"
bucket=$(gsutil ls gs://$BUCKET_NAME)
if [[ ${#bucket} -gt 0 ]]; then
jq -n --arg name "" '{name:"'$BUCKET_NAME'"}'
else
jq -n --arg name "" '{name:""}'
fi
Then in my CircleCI config.yml, I put it all together.
terraform init
terraform apply -auto-approve -var bucket_name=my-bucket
bucket=$(terraform output bucket)
At this point I check if the bucket name is returned and determine how to proceed based on that.
TF does not have any build in tools for checking if there are pre-existing resources, as this is not what TF is meant to do. However, you can create your own custom data source.
Using the custom data source you can program any logic you want, including checking for pre-existing resources and return that information to TF for future use.
There is a way to check if a resource already exists before creating the resource. But you should be aware of whether it exists. Using this approach, you need to know if the resource exists. If the resource does not exist, it'll give you an error.
I will demonstrate it by create/reading data from an Azure Resource Group. First, create a boolean variable azurerm_create_resource_group. You can set the value to true if you need to create the resource; otherwise, if you just want to read data from an existing resource, you can set it to false.
variable "azurerm_create_resource_group" {
type = bool
}
Next up, get data about the resource using the ternary operator supplying it to count, next do the same for creating the resource:
data "azurerm_resource_group" "rg" {
count = var.azurerm_create_resource_group == false ? 1 : 0
name = var.azurerm_resource_group
}
resource "azurerm_resource_group" "rg" {
count = var.azurerm_create_resource_group ? 1 : 0
name = var.azurerm_resource_group
location = var.azurerm_location
}
The code will create or read data from the resource group based on the value of the var.azurerm_resource_group. Next, combine the data from both the data and resource sections into a locals.
locals {
resource_group_name = element(coalescelist(data.azurerm_resource_group.rg.*.name, azurerm_resource_group.rg.*.name, [""]), 0)
location = element(coalescelist(data.azurerm_resource_group.rg.*.location, azurerm_resource_group.rg.*.location, [""]), 0)
}
Another way of doing it might be using terraformer to import the infra code.
I hope this helps.
This work for me:
Create data
data "gitlab_user" "user" {
for_each = local.users
username = each.value.user_name
}
Create resource
resource "gitlab_user" "user" {
for_each = local.users
name = each.key
username = data.gitlab_user.user[each.key].username != null ? data.gitlab_user.user[each.key].username : split("#", each.value.user_email)[0]
email = each.value.user_email
reset_password = data.gitlab_user.user[each.key].username != null ? false : true
}
P.S.
Variable
variable "users_info" {
type = list(
object(
{
name = string
user_name = string
user_email = string
access_level = string
expires_at = string
group_name = string
}
)
)
description = "List of users and their access to team's groups for newcommers"
}
Locals
locals {
users = { for user in var.users_info : user.name => user }
}

Terraform: What is the simplest way to Incrementally add servers to a deployment?

I am a newbie with terraform so donĀ“t laugh :) I want to deploy a number of instances of a server, then add their IPs to a Route53 hosted zone. I will be using Terraform v0.12.24 no chance of 0.14 at the moment.
So far, I have working the "easy", spaghetti approach:
module server: buys and creates a list of servers
module route53: adds route53 records, parameter=aray of ips
main.tf
module "hostedzone" {
source = "./route53"
ncs_domain = var.ncs_domain
}
module "server" {
source = "./server"
name = "${var.ncs_hostname}-${var.ncs_id}"
hcloud_token = var.server_htk
servers = [
{
type = "cx11",
location = "fsn1",
},
{
type = "cx11",
location = "fsn1",
}
]
}
resource "aws_route53_record" "server1-record" {
zone_id = module.hostedzone.zone.zone_id
name = "${var.ncs_hostname}.${var.ncs_domain}"
type = "A"
ttl = "300"
records = module.server.server.*.ipv4_address
}
and the relevant server resource array:
resource "hcloud_server" "apiserver" {
count = length(var.servers)
# Create a server
name = "${var.name}-${count.index}"
# Name server
image = var.image
# Basic image
server_type = var.servers[count.index].type
# Instance type
location = var.servers[count.index].location
}
So if I run terraform apply, I get the server array created. Cool !
Now, I would like to be able to run this module to create and destroy specific servers on demand, like:
initially deploy the platform with one or two servers.
remove one of the initial servers in the array
add new servers
So, how could I use this incrementally, that is, without providing the whole array of servers everytime? Like just adding one to the existing list, or remove other.

Attach Leaf interface to EPG on Cisco ACI with Terraform

I'm trying to create an EPG on Cisco ACI using Terraform. EPG is created but Leaf's interface isn't attached.
The terraform synthax to attach Leaf interface is :
resource "aci_application_epg" "VLAN-616-EPG" {
...
relation_fv_rs_path_att = ["topology/pod-1/paths-103/pathep-[eth1/1]"]
...
}
It works when I do it manually through ACI web interface or REST API
I don't believe that this has been implemented yet. If you look in the code for the provider there is no test for that attribute, and I find this line in the examples for the EPGs. Both things lead me to believe it's not completed. Also, that particular item requires an encapsulation with VLAN/VXLAN, or QinQ, so that would need to be included if this was to work.
relation_fv_rs_path_att = ["testpathatt"]
Probably the best you could do is either make a direct REST call (act_rest in the terraform provider), or use an Ansible provider to create it (I'm investigating this now).
I ask to Cisco support and they send me this solution :
resource "aci_application_epg" "terraform-epg" {
application_profile_dn = "${aci_application_profile.terraform-app.id}"
name = "TerraformEPG1"
}
resource "aci_rest" "epg_path_relation" {
path = "api/node/mo/${aci_application_epg.terraform-epg.id}.json"
class_name = "fvRsPathAtt"
content = {
"encap":"vlan-907"
"tDn":"topology/pod-1/paths-101/pathep-[eth1/1]"
}
}
The solution with latest provider version is to do this:
data "aci_physical_domain" "physdom" {
name = "phys"
}
resource "aci_application_epg" "on_prem_epg" {
application_profile_dn = aci_application_profile.on_prem_app.id
name = "db"
relation_fv_rs_dom_att = [data.aci_physical_domain.physdom.id]
}
resource "aci_epg_to_domain" "rs_on_prem_epg_to_physdom" {
application_epg_dn = aci_application_epg.on_prem_epg.id
tdn = data.aci_physical_domain.physdom.id
}
resource "aci_epg_to_static_path" "leaf_101_eth1_23" {
application_epg_dn = aci_application_epg.on_prem_epg.id
tdn = "topology/pod-1/paths-101/pathep-[eth1/23]"
encap = "vlan-1100"
}

How can I overcome "Error: Cycle" on digitalocean droplet

I am sure this is a simple problem that I don't know how to interpret it at the moment.
I use 3 droplets(called rs) and have a template file which configures each.
[..]
data "template_file" "rsdata" {
template = file("files/rsdata.tmpl")
count = var.count_rs_nodes
vars = {
docker_version = var.docker_version
private_ip_rs = digitalocean_droplet.rs[count.index].ipv4_address_private
private_ip_mysql = digitalocean_droplet.mysql.ipv4_address_private
}
}
resource "digitalocean_droplet" "rs" {
count = var.count_rs_nodes
image = var.image
name = "${var.prefix}-rs-${count.index}"
region = var.region
size = var.rs_size
private_networking = true
user_data = data.template_file.rsdata.rendered
ssh_keys = var.ssh_keys
depends_on = ["digitalocean_droplet.mysql"]
}
[..]
When I do a terraform apply I get:
Error: Cycle: digitalocean_droplet.rs, data.template_file.rsdata
Note this is terraform 0.12
What am I doing wrong and how can I overcome this please?
This error is returned because the data.template_file.rsdata resource refers to the digitalocean_droplet.rs resource and vice-versa. That creates an impossible situation for Terraform: there is no ordering Terraform could use to process these that would ensure that all of the necessary data is available at each step.
The key problem is that the private IPv4 address for a droplet is allocated as part of creating it, but the user_data is used as part of that creation and so it cannot include the IPv4 address that is yet to be allocated.
The most straightforward way to deal with this would be to not include the droplet's IP address as part of its user_data and instead arrange for whatever software is processing that user_data to fetch the IP address of the host directly from the network interface at runtime. The kernel running in that droplet will know what the IP address is, so you can retrieve it from there in principle.
If for some reason including the IP addresses in the user_data is unavoidable (this can occur, for example, if there are a set of virtual machines that all need to be aware of each other) then a more complex alternative is to separate the allocation of the IP addresses from the creation of the instances. DigitalOcean doesn't have a mechanism to create private network interfaces separately from the droplets they belong to, so in this case it would be necessary to use public IP addresses via digitalocean_floating_ip, which may not be appropriate for all situations:
resource "digitalocean_floating_ip" "rs" {
count = var.count_rs_nodes
region = var.region
}
resource "digitalocean_droplet" "rs" {
count = var.count_rs_nodes
image = var.image
name = "${var.prefix}-rs-${count.index}"
region = var.region
size = var.rs_size
private_networking = true
ssh_keys = var.ssh_keys
user_data = templatefile("${path.module}/files/rsdata.tmpl", {
docker_version = var.docker_version
private_ip_rs = digitalocean_floating_ip.rs[count.index].ip_address
private_ip_mysql = digitalocean_droplet.mysql.ipv4_address_private
})
}
resource "digitalocean_floating_ip_assignment" "rs" {
count = var.count_rs_nodes
ip_address = digitalocean_floating_ip.rs[count.index].ip_address
droplet_id = digitalocean_droplet.rs[count.index].id
}
Because the "floating IP assignment" is created as a separate step after the droplet is launched, there will be some delay before the floating IP is associated with the instance and so whatever software is relying on that IP address must be resilient to running before the association is created.
Note that I also switched from using data "template_file" to the templatefile function because the data source is offered only for backward-compatibility with Terraform 0.11 configurations; the built-in function is the recommended way to render external template files in Terraform 0.12, and avoids the need for a separate configuration block.

Resources