0

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?

Charlie Joynt
  • 4,411
  • 1
  • 24
  • 46

2 Answers2

1

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.

Martin Atkins
  • 62,420
  • 8
  • 120
  • 138
  • This looks like a plausible solution, but I'm getting an error such as this when trying it: `Error: failed to render : :26,40-27,1: Invalid character; This character is not used within the language.` The character position appears to refer to the line ending of the line preceding the on with `${"%"}${var_name}...`. – Charlie Joynt Mar 03 '23 at 13:44
  • hmmm... I'm not sure how to explain that. It seems like Terraform is objecting to the newline character, but I've never seen it do that before and so I don't know how to explain it. I think it's a separate problem from your original question, though; I wonder if something weird ended up in there when you copy-pasted from that example. – Martin Atkins Mar 06 '23 at 20:38
0

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 [id=is@cloudeteer.de]
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
ishuar
  • 1,193
  • 1
  • 3
  • 6
  • Thanks for the detailed reply. I think this boils down to "cannot reproduce" the issue I am having. I had assumed this problem to be narrowly related to how templates are rendered but it might be a question of how I use them. I had added more detail to my OP. – Charlie Joynt Mar 01 '23 at 11:56
  • as assumed it was using `user_data` argument , when using `templatefile` function `data "template_file" "sudoers"` is unnecessary. Actually, it is the replacement for that. official documented in the data resource [template_file](https://registry.terraform.io/providers/hashicorp/template/latest/docs/data-sources/file). Once you remove this data source please try to adapt my solution as suggested and share your feedback. – ishuar Mar 02 '23 at 23:30