Terraform template_cloudinit_config multiple part execution order is wrong

I am using the terraform to build my ec2-instances as part of instance bootstrap, added cloud-init config to run multiple userdata scripts. but the content_type = "text/x-shellscript" always executed first. I verified the cat /var/log/cloud-init-output.log file. it shows the shell script is invoked first. How do I config the shell script to run at last?

data "template_cloudinit_config" "myapp_cloudinit_config" {
  gzip = false
  base64_encode = false

  # Main cloud-config configuration file.
  part {
    content_type = "text/cloud-config" 
    content = "${data.template_file.base_bootstrap_file.rendered}" 
    merge_type = "list(append)+dict(recurse_array)+str()"
  }
  part {
    content_type = "text/cloud-config"
    content = "${module.template_file_appsec_init.appsec_user_data_rendered}"
    merge_type = "list(append)+dict(recurse_array)+str()"
  }

  part {
    content_type = "text/x-shellscript"
    content = "${module.template_file_beat_init.beat_user_data_rendered}"
  }
}
Terraform v0.11.8

Hi @balamuruganravi!

Unfortunately I think this is more a question about Cloud-Init than about the Terraform provider, so I don’t have a super-certain answer to give you, but my understanding is that Cloud-Init has its own priority order for processing different settings and that a text/x-shellscript part is a shorthand for populating a particular setting in the cloud-config YAML. Therefore the ordering depends on what order cloud-init processes its “modules” in, not on what order you specify the items in your user_data.

I’m not an expert on the details here by any means, but one reason I know why things might not run in the order you might intend is that Cloud-Init runs in five separate “boot stages”, each of which is responsible for a different subset of the boot process. You can see in the Cloud-Init docs that each of the stages (by default) runs a different subset of modules.

Therefore I think if you want to change the ordering you’d need to either specify the shell script in a different way, such as inclinding it inline some setting in your YAML so that it will be processed by a different module, or you’ll need to change the cloud-init configuration in your base image to run some of the modules during different boot stages.

For example, it might work to embed your script in the runcmd attribute of the Cloud-Init configuration, whereas currently I think (but am not 100% sure) that your script is being interpreted as if it were in the bootcmd attribute. bootcmd runs in the “Network” stage, while runcmd runs in the “Config” stage.

The above has already exhausted all I know about Cloud-Init so I doubt I will be able to answer any follow-up questions but perhaps someone else in the forum can. The Cloud-Init documentation has some general information about these concepts and how it interprets the user_data value.

2 Likes

We solved this by adding a number to the script name:

Files will be copied to the instance in specified order and executed in alphabetical order:
Copying

Feb 19 13:33:02 cloud-init[2016]: util.py[DEBUG]: Writing to /var/lib/cloud/instance/scripts/01-volume.sh - wb: [700] 1476 bytes
Feb 19 13:33:02 cloud-init[2016]: util.py[DEBUG]: Writing to /var/lib/cloud/instance/scripts/02-node.sh - wb: [700] 404 bytes
Feb 19 13:33:02 cloud-init[2016]: util.py[DEBUG]: Writing to /var/lib/cloud/instance/scripts/03-app.sh - wb: [700] 140 bytes

Executing

Feb 19 13:33:17 cloud-init[2433]: util.py[DEBUG]: Running command ['/var/lib/cloud/instance/scripts/01-volume.sh'] with allowed return codes [0] (shell=True, capture=False)
Feb 19 13:33:17 cloud-init[2433]: util.py[DEBUG]: Running command ['/var/lib/cloud/instance/scripts/02-node.sh'] with allowed return codes [0] (shell=True, capture=False)
Feb 19 13:33:49 cloud-init[2433]: util.py[DEBUG]: Running command ['/var/lib/cloud/instance/scripts/03-app.sh'] with allowed return codes [0] (shell=True, capture=False)

Example

# User data
data "cloudinit_config" "user_data" {
  # docker-compose.yml, config.yml
  part {
    content_type = "text/cloud-config"
    content = yamlencode({
      write_files = [
        {
          content = templatefile("${path.module}/user_data/config.yaml", {
            node_public_ip = aws_eip.eip.public_ip
          })
          path = "/opt/config.yaml"
        },
        {
          content = templatefile("${path.module}/user_data/docker-compose.yml", {
            network  = var.network
          })
          path = "/opt/docker-compose.yml"
        }
      ]
    })
  }
  # Volume
  part {
    filename     = "01-volume.sh"
    content_type = "text/x-shellscript"
    content = templatefile("${path.module}/user_data/volume.sh", {
    })
  }
  # Node
  part {
    filename     = "02-node.sh"
    content_type = "text/x-shellscript"
    content = templatefile("${path.module}/user_data/node.sh",{
      node_name = "${var.name}-${var.env}"
    })
  }
  # App
  part {
    filename     = "03-app.sh"
    content_type = "text/x-shellscript"
    content = templatefile("${path.module}/user_data/app.sh",{
      network  = var.network
    })
  }
}