This script is part of a broader library of utilities that are used in conjunction with Terraform...to make life better/easier for Ops & SRE.
Not everything begins and ends with Kubernetes. Sometimes, you've got things to do directly on an EC2. It (almost) always goes the same way:
- Create an instance
- Wait for that instance to "come online"
- Ensure that all default cloud-init scripts (and any other UserData) have executed to completion
- Do useful work 🫠
As it turns out, it's not very easy or straightforward to cleanly handle that particular case in Terraform.
That's what this script is for.
Let's say that you've got an EC2 instance you want to provision as soon as it's created. Let's also assume that you want to provision it with something like Ansible.
First, we create a null_resource
# Blocks ansible run until new/all hosts are ready
resource "null_resource" "verify_instance_readiness" {
# Run this always...any new instance and/or existing instance should always
# be ready before the terraform run proceeds
triggers = { always_run = timestamp() }
provisioner "local-exec" {
command = "${path.module}/ec2-wait-until-ready.sh ${aws_instance._.id}"
}
}
Next, we create a second null_resource which depends on the verify_instance_readiness
null_resource
resource "null_resource" "ansible" {
# Triggers matter - we need to ensure we trigger on every possible change to
# any relevant data, vars, or files
# ‼️ 👉 This is just ONE *EXAMPLE* trigger, from an existing implementation ‼️
triggers = {
# trigger on changes to ansible vars or instance IDs
instance_id = aws_instance._.id
# ...other triggers
}
#
depends_on = [
null_resource.verify_instance_readiness
# ...other dependencies
]
provisioner "local-exec" {
command = <<-EOT
ansible-playbook \
--connection=aws_ssm \
--inventory ${local_file.ansible_inventory.filename} \
--extra-vars='${jsonencode(local.aspera_ansible_vars)}' \
${local_file.ansible_playbook.filename}
EOT
environment = {
ANSIBLE_REMOTE_TEMP = "/tmp/.ansible/tmp"
ANSIBLE_STDOUT_CALLBACK = "yaml"
AWS_PROFILE = var.aws_cli_profile
OBJC_DISABLE_INITIALIZE_FORK_SAFETY = "YES"
}
}
}
- Your EC2 is created
- Our first
null_resource
namedverify_instance_readiness
runs - It waits until all cloud-init and UserData scripts are executed to completion
- It returns successfully
- Your next
null_resource
then runs, to execute some set of provisioning steps --> BASH scripts, Ansible playbooks, etc.
- The null_resource is set to run every time because of
triggers = { always_run = timestamp() }
- It runs
- It instantly connects to the existing EC2, sees that everything's great, and returns
- It amounts to a totally innocuous NOOP