My intentions of blogging more frequently have somewhat fallen by the wayside! Partly due to work/life busyness, partly due to a period of time catching up on Netflix series! I spotted some of the recent HashiConf announcements but I haven’t had a chance to explore the new features. So, I figured, what better way than to kill two birds with one stone.
In this article, I’m going to take an introductory look at one of the new features of Terraform - Terraform Actions.
What Challenge are Actions Helping Solve?
Terraform is a declarative language - you tell it what you want, and it makes it so. It does this through create, read, update and delete (CRUD) operations. This is great and works well, but there are often times you want to perform certain imperative or one-time actions outside of this usual process, such as:
- Provisioning OS settings on a VM
- Initialising a database
- Running a script
- Powering on/off a VM
Historically, we would have achieved these sorts of tasks using either separate scripts/pipelines, or through the use of Terraform provisioners, perhaps in combination with null_resource or terraform_data.
With Terraform actions, some of these non-provisioning tasks are now available as a first-class citizens directly within Terraform.
Terraform Actions in Code
Terraform actions are part of the provider binary, and as such you can only use what has been defined by the developers - the same as resources and data calls. This means that initially, actions are likely to be pretty sparse but will grow over time.
Like resource blocks, actions have two labels - the provider defined name, and a user-defined label. The usual rules around uniqueness apply.
action "some_provider_name" "some_user_name" {
somesetting = "some setting goes here"
}
Using Actions
You can trigger actions in one of two ways:
- Using the
-invokeflag - When running Terraform plan, you can include the optional invoke flag and the address of the action you want to trigger. This will then execute once the plan is applied. - Using a resource
lifecycleblock - Under thelifecycleblock, you now have anaction_triggersub-block. This sub-block then defines what resource actions trigger the action (such asafter_createorafter_update), and which actions to invoke.
Let’s have a look at some examples in Azure!
Invoke Example
Prerequisites
In order to utilise Terraform actions, you’ll need to be running at least 1.14.0, which at the time of writing is still in beta.
curl -LO https://releases.hashicorp.com/terraform/1.14.0-beta2/terraform_1.14.0-beta2_darwin_amd64.zip && unzip terraform_1.14.0-beta2_darwin_amd64.zip
The Code
As mentioned earlier, we can only use actions that are available as part of the provider. For this example, we are going to use the azurerm_virtual_machine_power action that is part of the azurerm provider - https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/actions/virtual_machine_power.
Let’s create a simple Linux VM (not best practice settings!) and a couple of azurerm_virtual_machine_power actions that will trigger a power on and power off.
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "4.47.0"
}
}
}
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "example" {
name = "rg-terraform-actions-demo"
location = "uksouth"
}
resource "azurerm_virtual_network" "example" {
name = "vnet-demo"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
}
resource "azurerm_subnet" "example" {
name = "subnet-demo"
resource_group_name = azurerm_resource_group.example.name
virtual_network_name = azurerm_virtual_network.example.name
address_prefixes = ["10.0.1.0/24"]
}
resource "azurerm_network_interface" "example" {
name = "nic-demo-vm"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.example.id
private_ip_address_allocation = "Dynamic"
}
}
resource "azurerm_linux_virtual_machine" "example" {
name = "vm-demo-linux"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
size = "Standard_B1s"
admin_username = "azureuser"
admin_password = "P@ssw0rd1234!"
disable_password_authentication = false
network_interface_ids = [
azurerm_network_interface.example.id,
]
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts-gen2"
version = "latest"
}
}
action "azurerm_virtual_machine_power" "off" {
config {
virtual_machine_id = azurerm_linux_virtual_machine.example.id
power_action = "power_off"
}
}
action "azurerm_virtual_machine_power" "on" {
config {
virtual_machine_id = azurerm_linux_virtual_machine.example.id
power_action = "power_on"
}
}
After running a terraform init and terraform apply, nothing out of the ordinary has happened. We’ve got all the relevant resources provisioned and the VM is running. If we try and run a plan or apply again, nothing is being flagged, as nothing has changed, nor are we explicitly invoking the action.
Invoking the Actions
Let’s invoke the actions using the -invoke flag. As the VM is running, we will start with powering if off with the command:
terraform apply -auto-approve -invoke="action.azurerm_virtual_machine_power.off"
As you can see, the plan included “Actions: 1 to invoke” with a reference to our “off” action. If we want to turn it back on, we simply do the same, but with the “on” action.
terraform apply -auto-approve -invoke="action.azurerm_virtual_machine_power.on"
Lifecycle Actions Example
Tweaking the Code
Let’s say we want to restart the VM when the disk size is changed. Whilst we can’t do this directly with the virtual machine resource, we can use our good friend terraform_data to trigger the action.
Let’s start by changing the action to restart and add a terraform_data resource ready to trigger the action. I’ll leave the lifecycle block commented out for now, to prevent an unnecessary restart on the first apply.
resource "terraform_data" "disk_size_change" {
triggers_replace = [azurerm_linux_virtual_machine.example.os_disk.0.disk_size_gb]
# lifecycle {
# action_trigger {
# events = [after_create]
# actions = [action.azurerm_virtual_machine_power.restart]
# }
# }
}
action "azurerm_virtual_machine_power" "restart" {
config {
virtual_machine_id = azurerm_linux_virtual_machine.example.id
power_action = "restart"
}
}
Running a terraform apply creates the terraform_data resource, which is simply an entry in state.
If I now uncomment the lifecycle block and run a plan again, we shouldn’t see any changes yet as the disk size has remained the same (the default of 30GB)…
Great! Finally, let’s change the disk size to 50GB under the azurerm_linux_virtual_machine resource and see what happens…
resource "azurerm_linux_virtual_machine" "example" {
name = "vm-demo-linux"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
size = "Standard_B1s"
admin_username = "azureuser"
admin_password = "P@ssw0rd1234!"
disable_password_authentication = false
network_interface_ids = [
azurerm_network_interface.example.id,
]
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
disk_size_gb = 50 # This has changed!
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts-gen2"
version = "latest"
}
}
The disk_size_gb attribute has changed from 30 to 50 in the plan. This in turn has caused the terraform_data.disk_size_change resource to be marked for replacement, which in turn triggers the azurerm_virtual_machine_power.restart action due to the use of the lifecycle block! Let’s apply the changes…
In the words of Louis Balfour…
Conclusion
This is only a brief look at Terraform actions, but hopefully it has given you a bit of an idea of what they are and how you can use them. I’m sure as time goes on, we will see many more useful actions being added to the providers!
I still need to play with some of the other announcements, so perhaps the next article will look at Terraform Search!








