Hooks
Before Hooks, After Hooks and Error Hooks are a feature of terragrunt that make it possible to define custom actions that will be called before/after running an tofu
/terraform
command.
They allow you to orchestrate certain operations around IaC updates so that you have a consistent way to run custom code before or after running OpenTofu/Terraform.
Here’s an example:
terraform { before_hook "before_hook" { commands = ["apply", "plan"] execute = ["echo", "Running OpenTofu"] }
after_hook "after_hook" { commands = ["apply", "plan"] execute = ["echo", "Finished running OpenTofu"] run_on_error = true }
error_hook "import_resource" { commands = ["apply"] execute = ["echo", "Error Hook executed"] on_errors = [ ".*", ] }}
In this example configuration, whenever Terragrunt runs tofu apply
or tofu plan
(or the terraform
equivalent), three things will happen:
- Before Terragrunt runs
tofu
/terraform
, it will outputRunning OpenTofu
to the console. - After Terragrunt runs
tofu
/terraform
, it will outputFinished running OpenTofu
, regardless of whether the command failed. - If an error occurs during the
tofu apply
command, Terragrunt will outputError Hook executed
.
You can learn more about all the various configuration options supported in the reference docs for the terraform block.
Hook Context
All hooks add extra environment variables when executing the hook’s run command:
TG_CTX_TF_PATH
TG_CTX_COMMAND
TG_CTX_HOOK_NAME
For example:
terraform { before_hook "test_hook" { commands = ["apply"] execute = ["hook.sh"] }}
Where hook.sh
is:
echo "TF_PATH=${TG_CTX_TF_PATH} COMMAND=${TG_CTX_COMMAND} HOOK_NAME=${TG_CTX_HOOK_NAME}"
Will result in the following output when Terragrunt runs tofu apply
/terraform apply
:
TF_PATH=tofu COMMAND=apply HOOK_NAME=test_hook
Note that hooks are executed within the working directory where OpenTofu/Terraform would be run.
If using the source
attribute for the terraform
block, this will result in the hook running in
the hidden .terragrunt-cache
directory.
This also means that you can use tofu
/terraform
commands within hooks to access any outputs needed
for hook logic.
For example:
# Get the bucket_name output from OpenTofu/Terraform stateBUCKET_NAME="$("$TG_CTX_TF_PATH" output -raw bucket_name)"
# Use the AWS CLI to list the contents of the bucketaws s3 ls "s3://$BUCKET_NAME"
Note that the TG_CTX_TF_PATH
environment variable is used here to ensure compatibility, regardless of the
value of tf-path. This can be a useful practice
if you are migrating between OpenTofu or Terraform.
You will also have access to all the inputs
set in the terragrunt.hcl
file as environment variables prefixed
by TF_VAR_
, as that’s how the variables are set for use in OpenTofu/Terraform.
For example, if you have the following inputs
block in your terragrunt.hcl
file:
inputs = { bucket_name = "my-bucket"}
You can access the bucket_name
input in your hook as follows:
# Get the bucket_name input from the terragrunt.hcl fileBUCKET_NAME="$TF_VAR_bucket_name"
# Use the AWS CLI to list the contents of the bucketaws s3 ls "s3://$BUCKET_NAME"
Orchestrating execution outside IaC
Hooks can be used to handle operations that need to happen, but are not directly related to the OpenTofu/Terraform.
For example, you may be using Terragrunt to manage an AWS ECS service.
You can use a before_hook
to build and push a new image to the Elastic Container Registry (ECR) before running tofu apply
.
terraform { before_hook "build_and_push_image" { commands = ["plan", "apply"] execute = ["./build_and_push_image.sh"] }}
Where build_and_push_image.sh
is something like:
#!/usr/bin/env bash
set -eou pipefail
ACCOUNT_ID="123456789012"REGION="us-east-1"REPOSITORY="my-repository"TAG="latest"
IMAGE_TAG="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPOSITORY}:${TAG}"
# Build the Docker imagedocker build -t "$IMAGE_TAG" .
# Push the Docker image to ECRaws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.comdocker push "$IMAGE_TAG"
The hard-coding of values in the script could be replaced with context, as shown in the previous section.
Similarly, you may want to smoke-test newly deployed infrastructure after running tofu apply
.
terraform { after_hook "smoke_test" { commands = ["apply"] execute = ["./smoke_test.sh"] run_on_error = true }}
Where smoke_test.sh
is something like:
#!/usr/bin/env bash
set -eou pipefail
# Get the URL for the service from OpenTofu/Terraform stateSERVICE_URL="$("$TG_CTX_TF_PATH" output -raw service_url)"
# Use curl to check the service is upcurl -sSf "$SERVICE_URL"
You might even decide to integrate with a product like Terratest for more complex testing.
Hook Ordering
You can have multiple before and after hooks. Each hook will execute in the order they are defined.
For example:
terraform { before_hook "before_hook_1" { commands = ["apply", "plan"] execute = ["echo", "Will run OpenTofu"] }
before_hook "before_hook_2" { commands = ["apply", "plan"] execute = ["echo", "Running OpenTofu"] }}
This configuration will cause Terragrunt to output Will run OpenTofu
and then Running OpenTofu
before the call
to OpenTofu/Terraform.
Tflint hook
Before Hooks or After Hooks natively support tflint, a linter for OpenTofu/Terraform code. It will validate the OpenTofu/Terraform code used by Terragrunt, and its inputs.
Here’s an example:
terraform { before_hook "before_hook" { commands = ["apply", "plan"] execute = ["tflint"] }}
The .tflint.hcl
should exist in the same folder as terragrunt.hcl
or one of it’s parents. If Terragrunt can’t find
a .tflint.hcl
file, it won’t execute tflint and return an error. All configurations should be in a config
block in this
file, as per Tflint’s docs.
plugin "aws" { enabled = true version = "0.21.0" source = "github.com/terraform-linters/tflint-ruleset-aws"}
config { module = true}
Configuration
By default, tflint
is executed with the internal tflint
built into Terragrunt, which will evaluate parameters passed in.
Any desired extra configuration should be added in the .tflint.hcl
file.
It will work with a .tflint.hcl
file in the current folder or any parent folder.
To utilize an alternative configuration file, use the --config
flag with the path to the configuration file.
If there is a need to run tflint
from the operating system directly, use the extra parameter --external-tflint
.
This will result in usage of the tflint
binary found in the PATH
environment variable.
For example:
terraform { before_hook "tflint" { commands = ["apply", "plan"] execute = ["tflint" , "--external-tflint", "--minimum-failure-severity=error", "--config", "custom.tflint.hcl"] }}
Authentication for tflint rulesets
Public rulesets
tflint
works without any authentication for public rulesets (hosted on public repositories).
Private rulesets
If you want to run the tflint
hook with custom rulesets defined in a private repository, you will need to export a valid GITHUB_TOKEN
token.
Troubleshooting
flag provided but not defined: -act-as-bundled-plugin
error
If you have an .tflint.hcl
file that is empty, or uses the terraform
ruleset without version or source constraint, it can return the following error:
Failed to initialize plugins; Unrecognized remote plugin message: Incorrect Usage. flag provided but not defined: -act-as-bundled-plugin
To fix this, make sure that the configuration for the terraform
ruleset, in the .tflint.hcl
file contains a version constraint:
plugin "terraform" { enabled = true version = "0.2.1" source = "github.com/terraform-linters/tflint-ruleset-terraform"}