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.
Comments