Shell script containing Terraform Directives causing an error

I have the terraform file, defining an aws instance using user_data.

resource "aws_instance" "worker" {
  count                       = var.num_workers
  ami                         = data.aws_ami.ubuntu.id
  instance_type               = "t3.micro"
  iam_instance_profile        = aws_iam_instance_profile.boundary.name
  subnet_id                   = data.terraform_remote_state.network.outputs.tokyo_vpc_main.public_subnet_ids[count.index]
  key_name                    = aws_key_pair.boundary.key_name
  vpc_security_group_ids      = [aws_security_group.worker.id]
  associate_public_ip_address = true

  user_data = base64encode(templatefile(
    "${path.module}/userdata_worker.sh", {
      conf_tls_private_key        = tls_private_key.boundary.private_key_pem
      conf_tls_self_signed_cert   = tls_self_signed_cert.boundary.cert_pem
      conf_tls_key_path           = var.tls_key_path
      conf_tls_cert_path          = var.tls_cert_path
      conf_controller_ips         = aws_instance.controller.*.private_ip
      conf_name_suffix            = count.index
      conf_tls_disabled           = var.tls_disabled
      conf_kms_type               = var.kms_type
      conf_kms_worker_auth_key_id = aws_kms_key.worker_auth.id
    }
  ))

  tags = merge(local.common_tags, {
    Name = "boundary-${terraform.workspace}-worker-instance"
  })

  depends_on = [aws_instance.controller]
}

The userdata_worker.sh holds Terraform directives, shown as below.

...
cat > /home/ubuntu/boundary-worker.hcl << EOF
listener "tcp" {
  address = "$BOUNDARY_PRIVATE_IP:9202"
	purpose = "proxy"
%{ if "$BOUNDARY_TLS_DISABLED" == true }
	tls_disable                       = true
%{ else }
  tls_disable   = false
  tls_cert_file = "$BOUNDARY_TLS_CERT_PATH"  
  tls_key_file  = "$BOUNDARY_TLS_KEY_PATH"
%{ endif }

	#proxy_protocol_behavior = "allow_authorized"
	#proxy_protocol_authorized_addrs = "127.0.0.1"
}

worker {
  # Name attr must be unique
	public_addr = "$BOUNDARY_PUBLIC_IP"
	name = "demo-worker-$BOUNDARY_NAME_SUFFIX"
	description = "A default worker created for demonstration"
	controllers = [
%{ for ip in "$conf_controller_ips" ~}
    "$ip",
%{ endfor ~}
  ]
}

%{ if "$BOUNDARY_KMS_TYPE" == "aws" }
kms "awskms" {
	purpose    = "worker-auth"
	key_id     = "global_root"
  kms_key_id = "$BOUNDARY_KMS_WORKER_AUTH_KEY_ID"
}
%{ else }
kms "aead" {
	purpose = "worker-auth"
	aead_type = "aes-gcm"
	key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
	key_id = "global_worker-auth"
}
%{ endif }
EOF

The point where Terraform apply is throwing an error is %{ for ip in "$conf_controller_ips" ~} part. I injected the variable as Terraform List, but the shell script (userdata_worker.sh) seems to convert the List into String.

The error looks like this.
Call to function "templatefile" failed: ./userdata_worker.sh:55,14-40: Iteration over non-iterable value; A value of type string cannot be used as the collection in a 'for' expression..

Any solutions to this problem ?

Hi @neolunar7,

I think you have some Terraform syntax vs. shell syntax confusion there. I’m not sure if I’m catching all of the problems but, when you refer to a Terraform variable in a template directive like if, you need to just use the name alone and not quote it shell-style:

```
%{ if BOUNDARY_TLS_DISABLED }
```

```
%{ for ip in conf_controller_ips ~}
```

Perhaps we can start by fixing those up and then see if that exposes any further errors which we can worth through in subsequent comments if you aren’t sure how to proceed.

Hi, removing the dollar($) causes the error below.

and also

Call to function "templatefile" failed: ./userdata_worker.sh:15,27-46: Invalid
template interpolation value; Cannot include the given value in a string
template: string required..

Can templatefile receive only string variable?
I believe the problem is that declaring HCL file inside userdata.sh script disables me to use the non-string variables in HCL file.

Hi @neolunar7,

This new error message is good progress! This error message isn’t saying that you can’t pass in a non-string at all, but rather that you can only interpolate a string directly into the template. That makes sense because the result of rendering a template is a string, so you must encode everything you want to include as a string in order to produce a valid result. If you want to include a list then you need to either use the for directive to repeat over it or you’ll need to encode it to a string somehow using one of Terraform’s string manipulation functions.

However, I think now that you’ve changed your template it’s different enough than the one you initially shared that I can’t directly interpret the new error message you’ve shared. Can you re-share the entire template and, if you’ve changed it at all, also the configuration?

1 Like

Hi @apparentlymart , thanks for your reponse! I actually found a workaround for this problem. I removed the for loop inside the .hcl file, and worked on a shell script to replace this part. I’ll attach the whole userdata.sh file for those who will be curious. controllers = [ $NEW_VAR ] is the part that replaced the for loop.

#!/bin/bash

# Installing essential packages
apt update -y
apt install -y \
  unzip

# Values from Terraform
BOUNDARY_TLS_KEY_PATH="/etc/pki/tls/boundary/boundary.key"
BOUNDARY_TLS_CERT_PATH="/etc/pki/tls/boundary/boundary.cert"
BOUNDARY_TLS_DISABLED=${conf_tls_disabled}
BOUNDARY_KMS_TYPE=${conf_kms_type}
BOUNDARY_KMS_WORKER_AUTH_KEY_ID=${conf_kms_worker_auth_key_id}

BOUNDARY_CONTROLLER_IPS=${conf_controller_ips}
BOUNDARY_NAME_SUFFIX=${conf_name_suffix}
BOUNDARY_PUBLIC_IP=$(curl http://169.254.169.254/latest/meta-data/public-ipv4)
BOUNDARY_PRIVATE_IP=$(curl http://169.254.169.254/latest/meta-data/local-ipv4)

IFS="," read -a controller_ips <<< $BOUNDARY_CONTROLLER_IPS
NEW_VAR=$(printf "\"%s\",,\n" $${controller_ips[@]} | sed 's/.$//')

sudo mkdir -p /etc/pki/tls/boundary
cat > $BOUNDARY_TLS_KEY_PATH << EOF
${conf_tls_private_key}
EOF
cat > $BOUNDARY_TLS_CERT_PATH << EOF
${conf_tls_self_signed_cert}
EOF

curl https://releases.hashicorp.com/boundary/0.1.2/boundary_0.1.2_linux_amd64.zip --output /home/ubuntu/boundary.zip && unzip /home/ubuntu/boundary.zip -d /home/ubuntu
sudo mv /home/ubuntu/boundary /usr/local/bin/boundary
sudo chmod 0755 /usr/local/bin/boundary

cat > /home/ubuntu/boundary-worker.hcl << EOF
listener "tcp" {
  address = "$BOUNDARY_PRIVATE_IP:9202"
	purpose = "proxy"
%{ if "$BOUNDARY_TLS_DISABLED" == true }
	tls_disable                       = true
%{ else }
  tls_disable   = false
  tls_cert_file = "$BOUNDARY_TLS_CERT_PATH"  
  tls_key_file  = "$BOUNDARY_TLS_KEY_PATH"
%{ endif }

	#proxy_protocol_behavior = "allow_authorized"
	#proxy_protocol_authorized_addrs = "127.0.0.1"
}

worker {
  # Name attr must be unique
	public_addr = "$BOUNDARY_PUBLIC_IP"
	name = "demo-worker-$BOUNDARY_NAME_SUFFIX"
	description = "A default worker created for demonstration"
	controllers = [
	$NEW_VAR
  ]
}

%{ if "$BOUNDARY_KMS_TYPE" == "aws" }
kms "awskms" {
	purpose    = "worker-auth"
	key_id     = "global_root"
  kms_key_id = "$BOUNDARY_KMS_WORKER_AUTH_KEY_ID"
}
%{ else }
kms "aead" {
	purpose = "worker-auth"
	aead_type = "aes-gcm"
	key = "8fZBjCUfN0TzjEGLQldGY4+iE9AkOvCfjh7+p0GtRBQ="
	key_id = "global_worker-auth"
}
%{ endif }
EOF

sudo mv /home/ubuntu/boundary-worker.hcl /etc/boundary-worker.hcl

TYPE=worker
NAME=boundary

sudo cat << EOF > /etc/systemd/system/$NAME-$TYPE.service
[Unit]
Description=$NAME $TYPE
[Service]
ExecStart=/usr/local/bin/$NAME server -config /etc/$NAME-$TYPE.hcl
User=boundary
Group=boundary
LimitMEMLOCK=infinity
Capabilities=CAP_IPC_LOCK+ep
CapabilityBoundingSet=CAP_SYSLOG CAP_IPC_LOCK
[Install]
WantedBy=multi-user.target
EOF

# Add the boundary system user and group to ensure we have a no-login
# user capable of owning and running Boundary
sudo adduser --system --group boundary || true
sudo chown boundary:boundary /etc/$NAME-$TYPE.hcl
sudo chown boundary:boundary /usr/local/bin/boundary

# Make sure to initialize the DB before starting the service. This will result in
# a database already initizalized warning if another controller or worker has done this 
# already, making it a lazy, best effort initialization
if [ "$TYPE" = "controller" ]; then
  sudo /usr/local/bin/boundary database init -skip-auth-method-creation -skip-host-resources-creation -skip-scopes-creation -skip-target-creation -config /etc/$NAME-$TYPE.hcl || true
fi

sudo chmod 664 /etc/systemd/system/$NAME-$TYPE.service
sudo systemctl daemon-reload
sudo systemctl enable $NAME-$TYPE
sudo systemctl start $NAME-$TYPE

The ec2.tf that is using the userdata script above is as below.

resource "aws_instance" "worker" {
  count                       = var.num_workers
  ami                         = data.aws_ami.ubuntu.id
  instance_type               = "t3.micro"
  iam_instance_profile        = aws_iam_instance_profile.boundary.name
  subnet_id                   = data.terraform_remote_state.network.outputs.tokyo_vpc_main.public_subnet_ids[count.index]
  key_name                    = aws_key_pair.boundary.key_name
  vpc_security_group_ids      = [aws_security_group.worker.id]
  associate_public_ip_address = true

  user_data = base64encode(templatefile(
    "${path.module}/userdata_worker.sh", {
      conf_tls_private_key        = tls_private_key.boundary.private_key_pem
      conf_tls_self_signed_cert   = tls_self_signed_cert.boundary.cert_pem
      conf_tls_key_path           = var.tls_key_path
      conf_tls_cert_path          = var.tls_cert_path
      conf_controller_ips         = join(",", aws_instance.controller.*.private_ip)
      conf_name_suffix            = count.index
      conf_tls_disabled           = var.tls_disabled
      conf_kms_type               = var.kms_type
      conf_kms_worker_auth_key_id = aws_kms_key.worker_auth.id
    }
  ))

  tags = merge(local.common_tags, {
    Name = "boundary-${terraform.workspace}-worker-instance"
  })

  depends_on = [aws_instance.controller]
}

Though I found a workaround to the for loop problem in this post, I’m now having trouble with if statement inside .hcl file, where the problem was hidden due to the for loop.

I posted this new problem on a separate issue, HCL in userdata.sh unable to interpret shell script variables

Thank you very much ! :smile: