I was working on a new Terraform module for deploying a Nessus appliance in AWS and ran into a an interesting problem. Nessus provides two different types of AMI images that can be deployed in AWS. One is pre-authorized and the other is bring your own license (BYOL). Each of these require different information in their user_data when they deploy.

When I created my ECS Module, I created two different template_file data blocks for my user_data to deploy the cluster with or without an EFS cluster and used a variable to determine with one to use.

data "template_file" "user_data-default" {
  count    = var.attach_efs ? 0 : 1
  template = <<EOF
Content-Type: multipart/mixed; boundary="==BOUNDARY=="
MIME-Version: 1.0
--==BOUNDARY==
Content-Type: text/x-shellscript; charset="us-ascii"
#!/bin/bash
# Set any ECS agent configuration options
echo "ECS_CLUSTER=$${ecs_cluster_name}" >> /etc/ecs/ecs.config
--==BOUNDARY==--
EOF

  vars = {
    ecs_cluster_name = aws_ecs_cluster.this.name
  }
}

data "template_file" "user_data-efs" {
  count    = var.attach_efs ? 1 : 0
  template = <<EOF
Content-Type: multipart/mixed; boundary="==BOUNDARY=="
MIME-Version: 1.0
--==BOUNDARY==
Content-Type: text/cloud-boothook; charset="us-ascii"
# Install amazon-efs-utils
cloud-init-per once yum_update yum update -y
cloud-init-per once install_amazon-efs-utils yum install -y amazon-efs-utils
# Create /efs folder
cloud-init-per once mkdir_efs mkdir /efs
# Mount /efs
cloud-init-per once mount_efs echo -e '$${efs_id}:/ /efs efs defaults,_netdev 0 0' >> /etc/fstab
mount -a
--==BOUNDARY==
Content-Type: text/x-shellscript; charset="us-ascii"
#!/bin/bash
# Set any ECS agent configuration options
echo "ECS_CLUSTER=$${ecs_cluster_name}" >> /etc/ecs/ecs.config
--==BOUNDARY==--
EOF

  vars = {
    ecs_cluster_name = aws_ecs_cluster.this.name
    efs_id           = var.efs_id
    depends_on       = join("", var.depends_on_efs)
  }
}

I planned on following this approach for my nessus module until I learned that the pre-authorization image user_data included 3 required parameters and 2 optional ones. I couldn’t very well have 5 different iterations of the template_file data block in my code. Not only would it take up a lot of space, the Terraform count would get kind of ugly. I also needed to switch to templatefile since the template_file data block has been deprecated.

Switching to templatefile also allowed me to move from multiple user_data related blocks to one by creating a template file and using the Jinja templating language to create conditionals within the template file.

First, I created a variable to define whether we wanted to build a preauth, a BYOL machine, or a BYOL machine for Taenable.sc.

variable "license_type" {
  description = "The type of Nessus License to use: byob or preauth"
  type        = string
  default     = "byol"
  validation {
    condition     = var.license_type == "byol" || var.license_type == "byol-sc" || var.license_type == "preauth"
    error_message = "Sorry, type must be either 'byob' or 'preauth'."
  }
}

Next, I created files/user_data.tpl template for my userdata.

%{ if license == "preauth" }{
    "name": "${name}",
    "key": "${key}",
    %{ if proxy != "" }"proxy": ${proxy},%{ endif }
    %{ if proxy_port != "" }"proxy": ${proxy_port},%{ endif }
    "iam_role": "${role}"
}%{ endif }%{ if license == "byol" }#!/bin/bash
yum update -y
service nessusd stop
/opt/nessus/sbin/nessuscli managed link --key=${key} --cloud --name=${name}
service nessusd start
%{ endif }%{ if license == "byol-sc" }#!/bin/bash
yum update -y
yum install -y expect jq

cat << EOF > /tmp/nessuscli_adduser.expect 
#!/usr/bin/expect -f
set timeout -1

set nessuscli_path [lindex \$argv 0];
set username [lindex \$argv 1];
set password [lindex \$argv 2];
set is_admin [lindex \$argv 3];

# update with path to nessuscli
spawn \$nessuscli_path adduser \$username

expect "Login password:"
send -- "\$password\n"

expect "(again)"
send -- "\$password\n"

expect "Do you want this user to be a Nessus"
send -- "\$is_admin\n" 


expect "the user can have an empty rules set"
send -- "\n"


expect "Is that ok"
send -- "y\n"

expect "User added"
EOF

chmod 700 /tmp/nessuscli_adduser.expect

${nessus_credentials}


service nessusd stop
/opt/nessus/sbin/nessuscli fetch --security-center
/tmp/nessuscli_adduser.expect /opt/nessus/sbin/nessuscli $NESSUS_USER $NESSUS_PASS y
service nessusd start
%{ endif }

With the template file created, I add the following local directive to my main.tf file to create the template file.

locals {
  userdata = templatefile("${path.module}/files//user_data.tpl",
    {
      license            = var.license_type
      key                = var.nessus_key
      name               = var.nessus_scanner_name
      role               = aws_iam_role.this.name
      proxy              = var.nessus_proxy
      proxy_port         = var.nessus_proxy_port
      nessus_credentials = var.nessus_credentials
    }
  )
}

Finally, I create the resource, using local.userdata in the user_data section.

resource "aws_instance" "this" {
  ami                    = data.aws_ami.this.id
  instance_type          = var.instance_type
  key_name               = var.key_name
  iam_instance_profile   = aws_iam_instance_profile.this.name
  subnet_id              = var.subnet_id
  vpc_security_group_ids = [aws_security_group.this.id]
  user_data              = local.userdata
  tags = merge(
    {
      "Name" = var.name
    },
    var.tags
  )
  volume_tags = merge(
    {
      "Name" = var.name
    },
    var.tags
  )
  lifecycle {
    ignore_changes = [volume_tags]
  }
}

This has made my code much cleaner and easier to read. I even went back and updated my ECS Module to use the same pattern.