How do I add a variable to a Terraform template heredoc prefixed with a literal percent: %${variable}

523 views Asked by At

I am trying to use a Terraform template file to create a sudoers file on a Linux server. I am passing in the name of a group that should be granted sudo permission as a variable to the template.

As per the syntax of the sudoers file, groups need to be prefixed with a % character. Assuming that the group name I want to use is my-admins the expected content of the file/output of the template will be:

%my-admins ALL=(ALL) NOPASSWD:ALL

I therefore need a literal % character followed immediately by a variable, ${group_name}. My first attempt was as follows, but this seems to give me a corrupt sudoers file:

#!/bin/bash
cat > /etc/sudoers.d/90-${group_name} <<-EOT
    %${group_name} ALL=(ALL) NOPASSWD:ALL
EOT

EDIT: Adding some more implementation details

The template is included as follows:

data "template_file" "sudoers" {
    count           = 1
    template        = file("${path.module}/sudoers.tpl")
    vars = {
        group_name  = var.admins_group
    }
}

resource "openstack_compute_instance_v2" "my_server" {
    count           = 1
    name            = "my_server"
    flavor_name     = var.instance_flavor
    security_groups = var.security_group_ids

    user_data = element(data.template_file.sudoers.*.rendered, 0)
    
    # etc.
}

I have tried a number of different escape sequences, focusing mainly on the % character, for example, with the desirable output hard-coded on the first line:

cat > /etc/sudoers.d/90-${group_name} <<-EOT
    %my-group ALL=(ALL) NOPASSWD:ALL
    %${group_name} ALL=(ALL) NOPASSWD:ALL
    \%${group_name} ALL=(ALL) NOPASSWD:ALL
    %%${group_name} ALL=(ALL) NOPASSWD:ALL
    ${group_name} ALL=(ALL) NOPASSWD:ALL
EOT

This gives the following output; only the first line is valid:

    %my-group ALL=(ALL) NOPASSWD:ALL
    % ALL=(ALL) NOPASSWD:ALL
    \% ALL=(ALL) NOPASSWD:ALL
    %%my-group ALL=(ALL) NOPASSWD:ALL
    my-group ALL=(ALL) NOPASSWD:ALL

As can be seen, the variable appears to be swallowed up on lines 2 and 3, but correctly expanded on lines 4 and 5.

Is there a combination that would allow me to use a variable for the group name with a literal % prefixing it in the output?


And a side note: It seems that my output is being indented; I thought that the hyphen in <<-EOT removed indents?

2

There are 2 answers

2
Martin Atkins On

I'm not 100% sure what's going on here, but if all else fails you can force Terraform to see tokens as separate by splitting them into separate interpolation sequences. For example:

cat > /etc/sudoers.d/90-${group_name} <<-EOT
    ${"%"}${group_name} ALL=(ALL) NOPASSWD:ALL
EOT

The ${"%"} tells Terraform to interpolate a literal %, since there's never any special meaning to % followed by ", so Terraform will always understand that % as literal.

2
ishuar On

I am expecting that you are mostly using the user_data argument of a resource so on that basis I have simulated this issue with aws_instance and got it working with below code using templatefile terraform function.

Terraform Code

resource "aws_instance" "web" {
  ami                         = data.aws_ami.ubuntu.id
  instance_type               = "t3.micro"
  associate_public_ip_address = true ##-NOT RELAVANT HERE-##
  key_name                    = aws_key_pair.deployer.key_name
  user_data_replace_on_change = true ##-NOT RELAVANT HERE-##
  tags = {
    Name = "HelloWorld"
  }
  user_data = templatefile("${path.module}/sudo.sh", { GROUP_NAME = var.group_name })
}
variable "group_name" {
  type        = string
  description = "(optional) describe your variable"
  default     = "stackoverflow"
}

User Data bash script sudo.sh

#!/usr/bin/env bash
cat >> /tmp/"${GROUP_NAME}" <<-EOT
%${GROUP_NAME} ALL=(ALL) NOPASSWD:ALL
EOT
cat >> /etc/sudoers.d/90-"${GROUP_NAME}" <<-EOT
%${GROUP_NAME} ALL=(ALL) NOPASSWD:ALL
EOT

Test with Terraform

resource "null_resource" "test" {
  connection {
    type        = "ssh"
    user        = "ubuntu"
    host        = aws_instance.web.public_ip
    private_key = file("${path.module}/ssh-keys/aws-stackoverflow")
  }
  provisioner "remote-exec" {
    inline = [
      "for i in $(seq 1 10); do echo 'waitiing';sleep 1; done",
      "cat /tmp/${var.group_name}",
      "sudo cat /etc/sudoers.d/90-${var.group_name}",
    ]
  }
}

Test Results (Apply)


Plan: 3 to add, 0 to change, 0 to destroy.
aws_key_pair.deployer: Creating...
aws_key_pair.deployer: Creation complete after 0s [[email protected]]
aws_instance.web: Creating...
aws_instance.web: Still creating... [10s elapsed]
aws_instance.web: Creation complete after 13s [id=i-09fd67ee6d4164f8d]
null_resource.test: Creating...
null_resource.test: Provisioning with 'remote-exec'...
null_resource.test (remote-exec): Connecting to remote host via SSH...
null_resource.test (remote-exec):   Host: 3.73.65.143
null_resource.test (remote-exec):   User: ubuntu
null_resource.test (remote-exec):   Password: false
null_resource.test (remote-exec):   Private key: true
null_resource.test (remote-exec):   Certificate: false
null_resource.test (remote-exec):   SSH Agent: true
null_resource.test (remote-exec):   Checking Host Key: false
null_resource.test (remote-exec):   Target Platform: unix
null_resource.test (remote-exec): Connecting to remote host via SSH...
null_resource.test (remote-exec):   Host: 3.73.65.143
null_resource.test (remote-exec):   User: ubuntu
null_resource.test (remote-exec):   Password: false
null_resource.test (remote-exec):   Private key: true
null_resource.test (remote-exec):   Certificate: false
null_resource.test (remote-exec):   SSH Agent: true
null_resource.test (remote-exec):   Checking Host Key: false
null_resource.test (remote-exec):   Target Platform: unix
null_resource.test (remote-exec): Connecting to remote host via SSH...
null_resource.test (remote-exec):   Host: 3.73.65.143
null_resource.test (remote-exec):   User: ubuntu
null_resource.test (remote-exec):   Password: false
null_resource.test (remote-exec):   Private key: true
null_resource.test (remote-exec):   Certificate: false
null_resource.test (remote-exec):   SSH Agent: true
null_resource.test (remote-exec):   Checking Host Key: false
null_resource.test (remote-exec):   Target Platform: unix
null_resource.test: Still creating... [10s elapsed]
null_resource.test (remote-exec): Connecting to remote host via SSH...
null_resource.test (remote-exec):   Host: 3.73.65.143
null_resource.test (remote-exec):   User: ubuntu
null_resource.test (remote-exec):   Password: false
null_resource.test (remote-exec):   Private key: true
null_resource.test (remote-exec):   Certificate: false
null_resource.test (remote-exec):   SSH Agent: true
null_resource.test (remote-exec):   Checking Host Key: false
null_resource.test (remote-exec):   Target Platform: unix
null_resource.test (remote-exec): Connected!
null_resource.test (remote-exec): waitiing
null_resource.test (remote-exec): waitiing
null_resource.test (remote-exec): waitiing
null_resource.test (remote-exec): waitiing
null_resource.test (remote-exec): waitiing
null_resource.test (remote-exec): waitiing
null_resource.test (remote-exec): waitiing
null_resource.test (remote-exec): waitiing
null_resource.test: Still creating... [20s elapsed]
null_resource.test (remote-exec): waitiing
null_resource.test (remote-exec): waitiing
null_resource.test (remote-exec): %stackoverflow ALL=(ALL) NOPASSWD:ALL
null_resource.test (remote-exec): %stackoverflow ALL=(ALL) NOPASSWD:ALL
null_resource.test: Creation complete after 22s [id=3584233028868119035]

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

This is for the "cat /tmp/${var.group_name}" from remote-exec provisioner.

  • null_resource.test (remote-exec): %stackoverflow ALL=(ALL) NOPASSWD:ALL

And the other is regarding "sudo cat /etc/sudoers.d/90-${var.group_name}".

  • null_resource.test (remote-exec): %stackoverflow ALL=(ALL) NOPASSWD:ALL