Multiple user_data File use in Terraform - terraform

I am trying to have a common user_data file for common tasks such as folder creation and certain package install and a separate user_data file for application specific configuration
I am trying the below -
user_data = "${data.template_file.userdata_common.rendered}", "${data.template_file.userdata_master.rendered}"
With these configs -
Common User Data Template
data "template_file" "userdata_common" {
template = "${file("${path.module}/userdata_common.sh")}"
vars {
"ALBTarget" = "${var.ALBTarget}"
"s3bucket" = "${var.s3bucket}"
"centrifydomain" = "${lookup(var.centrifydomain, format("%s-%s", lower(var.env),var.region))}"
"centrifyadgroup" = "${lookup(var.centrifyadgroup, format("%s-%s", lower(var.env),var.region))}"
}
}
Application Specific Config
data "template_file" "userdata_master" {
template = "${file("${path.module}/userdata_master.sh")}"
vars {
"ALBTarget" = "${var.ALBTarget}"
"s3bucket" = "${var.s3bucket}"
"centrifydomain" = "${lookup(var.centrifydomain, format("%s-%s", lower(var.env),var.region))}"
"centrifyadgroup" = "${lookup(var.centrifyadgroup, format("%s-%s", lower(var.env),var.region))}"
}
}
I get the below Error when i do Plan -
Failed to load root config module: Error parsing /terraform/main.tf: key ${data.template_file.userdata_common.rendered}"' expected start of object ('{') or assignment ('=')
Is this possible using Terraform (0.9.3)?
If not, what's the best way to do this with Terraform?

Did you try template_cloudinit_config?
Add below codes.
data "template_cloudinit_config" "master" {
gzip = true
base64_encode = true
# get common user_data
part {
filename = "common.cfg"
content_type = "text/part-handler"
content = "${data.template_file.userdata_common.rendered}"
}
# get master user_data
part {
filename = "master.cfg"
content_type = "text/part-handler"
content = "${data.template_file.userdata_master.rendered}"
}
}
# sample code to use it.
resource "aws_instance" "web" {
ami = "ami-d05e75b8"
instance_type = "t2.micro"
user_data = "${data.template_cloudinit_config.master.rendered}"
}
Let me know if it works.

You can use "provisioner" to modify the infrastructure you are creating using the Terraform, Here is the example from them https://www.terraform.io/intro/getting-started/provision.html

Related

Cloudinit file inside terraform config file not working

I'm trying to run a cloudinit file by passing it in the terraform config file. The terraform apply command creates all the resources. But when i spin up the VM, none of the changes from the cloudinit are seen in the VM.
Here is the Cloudinit file with .tpl extension:
users:
- name: ansible
gecos: Ansible
sudo: ALL=(ALL) NOPASSWD:ALL
groups: [users, admin]
shell: /bin/bash
ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1.......
And here is the main.tf file:
data "template_file" "users_data" {
template = file("./sshPass.tpl")
}
data "template_cloudinit_config" "config" {
gzip = true
base64_encode = true
part {
content_type = "text/cloud-config"
content = data.template_file.users_data.rendered
}
resource "azurerm_linux_virtual_machine" "poc-vm" {
name = var.vm_name
resource_group_name = azurerm_resource_group.poc_rg.name
location = azurerm_resource_group.poc_rg.location
size = var.virtual_machine_size
admin_username = var.vm_username
network_interface_ids = [azurerm_network_interface.poc_nic_1.id]
admin_ssh_key {
username = var.vm_username
public_key = tls_private_key.poc_key.public_key_openssh
}
os_disk {
caching = var.disk_caching
storage_account_type = var.storage_type
}
source_image_reference {
publisher = var.image_publisher
offer = var.image_offer
sku = var.image_sku
version = var.image_version
}
user_data = data.template_cloudinit_config.config.rendered
}
Try this:
data "template_cloudinit_config" "config" {
gzip = true
base64_encode = true
part {
content_type = "text/cloud-config"
content = "${data.template_file.users_data.rendered}"
}
In this example I change this line content = data.template_file.users_data.rendered for this one content = "${data.template_file.users_data.rendered}"
Hope this helps!
Found the errors. Changed the extension to '.cfg.'. Added 'custom_data' instead of user_data.
Added '#cloud-config' to the 1st line of the file.
Made sure I removed any spaces at end of my ssh key.
And also felt like I was using the wrong ssh key to login the whole time.
But anyways those things helped me.

Terraform - can source modules use different datasources for each instance OS?

I have an "aws_instance" resource (in the source module) that worked fine to bootstrap either centos, coreos or ubuntu instances via user_data = data.template_file.user-data.rendered by using the following data block;
data "template_file" "user-data" {
template = file("${path.module}/bootstrap-${var.os_distro}.sh")
vars = {
access_port = var.access_port
service_port1 = var.service_port1
docker_api_port = var.docker_api_port
}
}
The associated file (e.g. bootstrap-centos.sh) was then loaded and rendered depending on the value for the $os_distro variable in the root module.
All was well until i switched from coreos for fedora coreos... the issue being that I now need to call a different datasource first (ct_config) to transpile my bootstrap-fcos.yaml file for ignition.
Is there any logic I can use in the source module to use a different datasource when i want to deploy a fedora coreos AMI? Seems totally against the power of terraform modules to take the easy way and create a new source module just for this new OS.
The salient parts from the source and root modules are;
SOURCE MODULE
resource ` "my-ec2-instance" {
count = var.node_count
availability_zone = element(var.azs, count.index)
subnet_id = var.aws_subnet_id
private_ip = length(var.private_ips) > 0 ? element(var.private_ips, count.index) : var.private_ip
ami = var.machine_ami
instance_type = var.aws_instance_type
vpc_security_group_ids = [aws_security_group.my-sg-group.id]
key_name = var.key_name
user_data = data.template_file.user-data.rendered
monitoring = false
ebs_optimized = false
associate_public_ip_address = var.public_ip
root_block_device {
volume_type = var.root_volume_type
volume_size = var.root_volume_size
delete_on_termination = true
}
}
data "template_file" "user-data" {
template = file("${path.module}/bootstrap-${var.os_distro}.sh")
vars = {
access_port = var.access_port
service_port1 = var.service_port1
docker_api_port = var.docker_api_port
}
}
variable "user_data" {
type = string
description = "userdata used to bootstrap the node"
}
variable "os_distro" {
type = string
description = "choose centos coreos or ubuntu to load either bootstrap-centos.sh, bootstrap-ubuntu.sh or bootstrap-coreos.sh from this module"
}
ROOT MODULE
module "demo_coreos_stg_ec2" {
source = ".../aws/ec2" # as per source module code above
node_count = local.node_count
azs = local.azs
aws_subnet_id = "subnet-c18c0fbb"
private_ips = ["172.31.16.20"]
machine_ami = data.aws_ami.fcos-stable-latest.id # latest stable fedora coreos release
aws_instance_type = "t2.micro"
key_name = "keys-2020"
user_data = data.ct_config.boot_config.rendered # convert the boot config in yaml to the ignition config in json via ct (config transpiler)
os_distro = var.os_distro # enables either bootstrap-centos.sh, bootstrap-ubuntu.sh or bootstrap-coreos.sh from this module
data "ct_config" "boot_config" {
content = data.template_file.fcos.rendered
strict = true
pretty_print = true
}
data "template_file" "fcos" {
template = file("${path.module}/bootstrap-fcos.yaml")
vars = {
access_port = var.access_port
service_port1 = var.service_port1
docker_api_port = var.docker_api_port
}
}
Notice that the root module needs to be able to first use the ct_config datasource before using the template_file datasource for loading the bootstrap-fcos.yaml for interpolation. Previously all 3 OS's could use template_file to load their .sh file.

How to iterate resources in terraform?

How to iterate resources for different values of a parameter
For example, in my below terraform file I have one data block and one resource. If I pass value DB_NAME=test then its working fine. But what if I have multiple values of DB_NAME and I want it to run multiple time DB_NAME=test, app. How will I iterate over data and resource block? :
data "template_file" "search-index" {
template = "${file("search-index/search-index.sh")}"
vars {
DB_NAME = "${var.DB_NAME}"
}
}
resource "null_resource" "script" {
triggers = {
DB_NAME = "${var.DB_NAME}"
script_sha = "${sha256(file("search-index/search-index.sh"))}"
}
provisioner "local-exec" {
command = "${data.template_file.search-index.rendered}"
interpreter = ["/bin/bash", "-c"]
}
}
I'm not sure if I fully understood what you are trying to achieve but you can create multiple DB by defining a variables.tf like this:
variable "DB_NAME" {
description = "A list of databases"
type = list(string)
default = ["db1", "db2", "db3"]
}
And then using the for_each functionality in your terraform file:
data "template_file" "search-index" {
for_each = toset(var.DB_NAME)
template = "${file("search-index/search-index.sh")}"
vars = {
DB_NAME = each.value
}
}
resource "null_resource" "script" {
for_each = toset(var.DB_NAME)
triggers = {
DB_NAME = each.value
script_sha = "${sha256(file("search-index/search-index.sh"))}"
}
provisioner "local-exec" {
command = "${data.template_file.search-index[each.value]}"
interpreter = ["/bin/bash", "-c"]
}
}

How do I make terraform skip that block while creating multiple resources in loop from a CSV file?

Hi I am trying to create a Terraform script which will take inputs from the user in the form of a CSV file and create multiple Azure resources.
For example if the user wants to create: ResourceGroup>Vnet>Subnet in bulk, he will provide input in CSV format as below:
resourcegroup,RG_location,RG_tag,domainname,DNS_Zone_tag,virtualnetwork,VNET_location,addressspace
csvrg1,eastus2,Terraform RG,test.sd,Terraform RG,csvvnet1,eastus2,10.0.0.0/16,Terraform VNET,subnet1,10.0.0.0/24
csvrg2,westus,Terraform RG2,test2.sd,Terraform RG2,csvvnet2,westus,172.0.0.0/8,Terraform VNET2,subnet1,171.0.0.0/24
I have written the following working main.tf file:
# Configure the Microsoft Azure Provider
provider "azurerm" {
version = "=1.43.0"
subscription_id = var.subscription
tenant_id = var.tenant
client_id = var.client
client_secret = var.secret
}
#Decoding the csv file
locals {
vmcsv = csvdecode(file("${path.module}/computelanding.csv"))
}
# Create a resource group if it doesn’t exist
resource "azurerm_resource_group" "myterraformgroup" {
count = length(local.vmcsv)
name = local.vmcsv[count.index].resourcegroup
location = local.vmcsv[count.index].RG_location
tags = {
environment = local.vmcsv[count.index].RG_tag
}
}
# Create a DNS Zone
resource "azurerm_dns_zone" "dnsp-private" {
count = 1
name = local.vmcsv[count.index].domainname
resource_group_name = local.vmcsv[count.index].resourcegroup
depends_on = [azurerm_resource_group.myterraformgroup]
tags = {
environment = local.vmcsv[count.index].DNS_Zone_tag
}
}
To be continued....
The issue I am facing here what is in the second resource group, the user don't want a resource type, suppose the user want to skip the DNS zone in the resource group csvrg2. How do I make terraform skip that block ?
Edit: What I am trying to achieve is "based on some condition in the CSV file, not to create azurerm_dns_zone resource for the resource group csvrg2"
I have provided an example of the CSV file, how it may look like below:
resourcegroup,RG_location,RG_tag,DNS_required,domainname,DNS_Zone_tag,virtualnetwork,VNET_location,addressspace
csvrg1,eastus2,Terraform RG,1,test.sd,Terraform RG,csvvnet1,eastus2,10.0.0.0/16,Terraform VNET,subnet1,10.0.0.0/24
csvrg2,westus,Terraform RG2,0,test2.sd,Terraform RG2,csvvnet2,westus,172.0.0.0/8,Terraform VNET2,subnet1,171.0.0.0/24
you had already the right thought in your mind using the depends_on function. Although, you're using a count inside, which causes from my understanding, that once the first resource[0] is created, Terraform sees the dependency as solved and goes ahead as well.
I found this post with a workaround which you might be able to try:
https://github.com/hashicorp/terraform/issues/15285#issuecomment-447971852
That basically tells us to create a null_resource like in that example:
variable "instance_count" {
default = 0
}
resource "null_resource" "a" {
count = var.instance_count
}
resource "null_resource" "b" {
depends_on = [null_resource.a]
}
In your example, it might look like this:
# Create a resource group if it doesn’t exist
resource "azurerm_resource_group" "myterraformgroup" {
count = length(local.vmcsv)
name = local.vmcsv[count.index].resourcegroup
location = local.vmcsv[count.index].RG_location
tags = {
environment = local.vmcsv[count.index].RG_tag
}
}
# Create a DNS Zone
resource "azurerm_dns_zone" "dnsp-private" {
count = 1
name = local.vmcsv[count.index].domainname
resource_group_name = local.vmcsv[count.index].resourcegroup
depends_on = null_resource.example
tags = {
environment = local.vmcsv[count.index].DNS_Zone_tag
}
}
resource "null_resource" "example" {
...
depends_on = [azurerm_resource_group.myterraformgroup[length(local.vmcsv)]]
}
or depending on your Terraform version (0.12+ which you're using guessing your syntax)
# Create a resource group if it doesn’t exist
resource "azurerm_resource_group" "myterraformgroup" {
count = length(local.vmcsv)
name = local.vmcsv[count.index].resourcegroup
location = local.vmcsv[count.index].RG_location
tags = {
environment = local.vmcsv[count.index].RG_tag
}
}
# Create a DNS Zone
resource "azurerm_dns_zone" "dnsp-private" {
count = 1
name = local.vmcsv[count.index].domainname
resource_group_name = local.vmcsv[count.index].resourcegroup
depends_on = [azurerm_resource_group.myterraformgroup[length(local.vmcsv)]]
tags = {
environment = local.vmcsv[count.index].DNS_Zone_tag
}
}
I hope that helps.
Greetings

Terraform Resource does not have attribute for variable

Running Terraform 0.11.7 and getting the following error:
module.frontend_cfg.var.web_acl: Resource 'data.terraform_remote_state.waf' does not have attribute 'waf_nonprod_id' for variable 'data.terraform_remote_state.waf.waf_nonprod_id'
Below is the terraform file:
module "frontend_cfg"
{
source = "../../../../modules/s3_fe/developers"
region = "us-east-1"
dev_shortname = "cfg"
web_acl = "${data.terraform_remote_state.waf.waf_nonprod_id}"
}
data "terraform_remote_state" "waf" {
backend = "local"
config = {
name = "../../../global/waf/terraform.tfstate"
}
}
The file which creates the tfstate file referenced above is below. This file has had no issues building.
resource "aws_waf_web_acl" "waf_fe_nonprod"
{
name = "fe_nonprod_waf"
metric_name = "fenonprodwaf"
default_action
{
type = "ALLOW"
}
}
output waf_nonprod_id
{
value = "${aws_waf_web_acl.waf_fe_nonprod.id}"
}
I will spare the full output of the cloudfront file, however, the following covers the text:
resource "aws_cloudfront_distribution" "fe_distribution"
{
web_acl_id = "${var.web_acl}"
}
If I put the ID of the waf ID into the web_acl variable, it works just fine, so I suspect the issue is something to do with the way I am calling data. This appears to match documentation though.
Use path instead of name in terraform_remote_state,
https://www.terraform.io/docs/backends/types/local.html
data "terraform_remote_state" "waf" {
backend = "local"
config = {
path = "../../../global/waf/terraform.tfstate"
}
}
or
data "terraform_remote_state" "waf" {
backend = "local"
config = {
path = "${path.module}/../../../global/waf/terraform.tfstate"
}
}
I tested it with terraform version 0.11.7 and 0.11.14
If you upgrade terraform to version 0.12.x, syntax using remote_state ouput has changed.
So change
web_acl = "${data.terraform_remote_state.waf.waf_nonprod_id}"
to
web_acl = data.terraform_remote_state.waf.outputs.waf_nonprod_id
or
web_acl = "${data.terraform_remote_state.waf.outputs.waf_nonprod_id}"

Resources