Disclaimer: I am working at HashiCorp (now IBM) as part of the Terraform
Core team. The postings on this site are my own and don’t necessarily represent
IBM’s positions, strategies or opinions.
Since I am involved in Terraform my opinions can sometimes be (unconsciously)
biased. I hope you enjoy the post anyway.
Content
- What are Terraform Actions?
- Using Actions
- TL;DR for Module Authors
- TL;DR for Provider Developers
- Next Steps
- Please leave a comment
What are Terraform Actions?
Terraform Actions are a new block in the Terraform language that allows you to express non-CRUD operations in your configuration. Some things just don’t fit into the mental model of Resources and Data Sources in Terraform:
- Running a lambda
- Executing a shell script
- Configuring a VM with Ansible
- Restarting a service
- Invalidating a cache
- Triggering a webhook (e.g. sending a Slack message)
All of these things are currently commonly solved by workarounds like local-exec provisioners or pseudo data sources. None of these really fit into the mental model of Terraform; they are ad-hoc actions that need to happen for the infrastructure to be up and running, but that are not really part of the infrastructure itself.
Terraform Actions is here to solve these problems. Let’s see how.
Using Actions
This is a fictitious example of actions. We have a database (not in the code) and an EC2 instance running an application connected to the DB. One common problem is that one needs to initialize the database (migrate the schemas / setup initial data) before it can be used. The application also needs to be deployed whenever the infrastructure is created.
This Terraform config uses actions to solve these problems:
action "aws_lambda_invoke" "db_init" {
config {
lambda_arn = aws_lambda_function.db_migration
db_address = aws_db_instance.db.address
db_name = aws_db_instance.db.db_name
// ...
}
}
action "ansible_playbook" "application" {
config {
playbook_path = "${path.module}/playbook.yml"
host = aws_instance.application.public_ip
ssh_public_key = aws_key_pair.application.public_key
}
}
resource "aws_key_pair" "application" {
key_name = "application"
public_key = "ssh-rsa ..."
}
resource "aws_instance" "application" {
ami = var.ami
instance_type = "t2.micro"
associate_public_ip_address = true
key_name = aws_key_pair.application.key_name
lifecycle {
action_trigger {
events = [after_create]
actions = [
action.aws_lambda_invoke.db_init, // Initialize the database
action.ansible_playbook.application // Provision the server
]
}
}
}
// Trigger the action if the playbook changes
resource "terraform_data" "playbook_changed" {
input = filesha256("${path.module}/playbook.yml")
lifecycle {
action_trigger {
events = [after_update]
actions = [action.ansible_playbook.application]
}
}
}
Now, if you want to e.g. just deploy Ansible you can use the new -invoke
flag to just invoke one action:
terraform apply -invoke action.ansible_playbook.application
just invokes the action that runs the Ansible Playbook.
Where you would previously need to feed the information Terraform holds (ip address / ssh key) into Ansible by hand, you now have the command to be run defined in your Terraform config.
Another neat thing is that what happens is declared within your Terraform config, so there is no need to look through shell scripts or CI pipelines to understand when and how your database gets initialized; it’s right there in the Terraform configuration.
Actions can also be used within modules, so if you take this example and don’t want to always update your application when the Ansible playbook changes, you could use the condition
attribute.
resource "terraform_data" "playbook_changed" {
input = filesha256("${path.module}/playbook.yml")
lifecycle {
action_trigger {
events = [after_update]
condition = var.auto_update_application // Any HCL expression evaluating to bool
actions = [action.ansible_playbook.application]
}
}
}
If you look at action blocks and see squiggly red lines the Terraform Language Server might pick up the wrong Terraform version. Try setting the
terraform { required_version = "1.14.0.beta2" }
attribute (see here).
TL;DR for Module Authors
action
is a new block providers can define. You can invoke an action via terraform apply -invoke action.type.name
, or you can define an action_trigger
block within the lifecycle
block to trigger actions before or after a resource is created or updated.
If you have bash scripts or CI steps that deal with infrastructure you create with Terraform, take a look to see if they can be replaced by actions. But bear in mind that actions are not meant to replace all bash scripts or CI steps; they just remove the need for tight coupling between your infrastructure and your CI pipeline.
TL;DR for Provider Developers
action
s are a good use if you:
- want to trigger a side-effect (e.g. calling a non-CRUD API / triggering a webhook / running a script)
- don’t need any resulting data (e.g. creating a temporary random password is a bad match as you need the data, use an ephemeral resource here instead)
- don’t have any changes to the state based on the side-effect (there might be future additions where we change resource states through actions, stay tuned!)
If you are creating an action, consider adding a boolean flag for waiting on the action result; it might speed up the workflow for your users.
Next Steps
- Writing a Terraform Action walks you through adding your first action to your provider.
- Terraform Action Patterns and Guidelines talks about common patterns and guidelines for creating actions.
- Designing a Terraform language feature like Terraform Actions is more abstract and discusses the design decisions behind the feature.
Please leave a comment
Do you have feedback around actions or do you want to hear about a specific topic around Terraform / Software Development / Language Design / Infrastructure as Code? Please let me know in the comments below.