Skip to content
Go back

Introduction to Terraform Actions

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?

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:

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

actions are a good use if you:

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

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.


Share this post on:




Previous Post
Writing a Terraform Action
Next Post
Terraform Providers: Migrating from SDK v2 to the Framework