unfriendlygrinch.info
Open in
urlscan Pro
176.118.186.121
Public Scan
Submitted URL: https://view.hashicorp.com/ODQ1LVpMRi0xOTEAAAGLzdCfe5cEBc11sC8K-KqOEKDY0pjaltPZL0BalAePqqexi8elm75DHonNfH77LSGavE8a7GI=
Effective URL: https://unfriendlygrinch.info/posts/terraform-check-block/?mkt_tok=ODQ1LVpMRi0xOTEAAAGLzdCfe7D9F1APqBZPN64NKMi60TGngC4wzK3iFrw...
Submission: On May 18 via api from US — Scanned from DE
Effective URL: https://unfriendlygrinch.info/posts/terraform-check-block/?mkt_tok=ODQ1LVpMRi0xOTEAAAGLzdCfe7D9F1APqBZPN64NKMi60TGngC4wzK3iFrw...
Submission: On May 18 via api from US — Scanned from DE
Form analysis
0 forms found in the DOMText Content
Unfriendly Grinch Posts Authors Tags TERRAFORM CHECK{} BLOCK Posted on Apr 10, 2023 | By Elif Samedin, Andrei Buzoianu | 15 minutes read * Azure * Cloud * Infrastructure as Code * Hashicorp * Terraform * Use Cases * tfenv * tfenv install * Examples * Setup * Checks * UP or DOWN * VM & Public IP Association * Unrestricted SSH Access * Stopping the VM * Starting the VM * Final Thoughts The check{} block has been introduced in the latest pre-release of Terraform (v1.5.0-alpha20230405). This allows practitioners to define assertions based on data source values to verify the state of the infrastructure on an ongoing basis. These blocks must contain at least one assert block with a condition expression and an error message expression aligning with current Custom Condition Checks. The flexibility of the check{} block lies in its ability to refer to any other Terraform resource, as well as newly defined data sources to perform a set of assertions. USE CASES * Check the health of the infrastructure. By adding such checks to modules, practitioners would take advantage by default from post-apply assertions. * Drift Detection TFENV tfenv is a nifty little project, also referred to as Terraform version manager. It allows you to easily switch between different versions of Terraform on your development machine. This can be useful when working on projects that require specific versions of Terraform, or such as our current case, to test new features. tfenv works by installing each version of Terraform into a separate directory. Then, using tfenv use <desired version> allows you to switch between Terraform versions quickly and easily without having to (re-)install the binary each time. TFENV INSTALL In order to get tfenv, you should follow the installation instructions on the tfenv GitHub page and then use the tfenv install and tfenv use commands to manage your Terraform versions. Check out tfenv into ~/.tfenv path: $ git clone --depth=1 https://github.com/tfutils/tfenv.git ~/.tfenv If shasum is present in the path, tfenv will verify the download against Hashicorp’s published sha256 hash. If keybase is available in the path it will also verify the signature for those published hashes using Hashicorp’s published public key. We can opt-in to using GnuPG tools for PGP signature verification if keybase is not available. If the tfenv install directory is ~/.tfenv: $ echo 'trust-tfenv: yes' > ~/.tfenv/use-gpgv The trust-tfenv directive means that verification uses a copy of the Hashicorp OpenPGP key found in the tfenv repository. For example, to install version 1.5.0-alpha20230405 of Terraform, you run tfenv install 1.5.0-alpha20230405, and to switch to that version, you run tfenv use 1.5.0-alpha20230405. Lets start by installing the latest stable (that would be 1.4.4 as of this writing): $ tfenv install latest or $ tfenv install 1.4.4 Then, install terraform version 1.5.0-alpha20230405: $ tfenv install 1.5.0-alpha20230405 Installing Terraform v1.5.0-alpha20230405 Downloading release tarball from https://releases.hashicorp.com/terraform/1.5.0-alpha20230405/terraform_1.5.0-alpha20230405_linux_amd64.zip ############################################################################################################################################################################################################ 100.0% Downloading SHA hash file from https://releases.hashicorp.com/terraform/1.5.0-alpha20230405/terraform_1.5.0-alpha20230405_SHA256SUMS Downloading SHA hash signature file from https://releases.hashicorp.com/terraform/1.5.0-alpha20230405/terraform_1.5.0-alpha20230405_SHA256SUMS.72D7468F.sig gpgv: Signature made Wed 05 Apr 2023 07:08:02 PM EEST gpgv: using RSA key 374EC75B485913604A831CC7C820C6D5CD27AB87 gpgv: Good signature from "HashiCorp Security (hashicorp.com/security) <security@hashicorp.com>" Archive: /tmp/tfenv_download.Ax7wpD/terraform_1.5.0-alpha20230405_linux_amd64.zip inflating: /home/check/.tfenv/versions/1.5.0-alpha20230405/terraform Installation of terraform v1.5.0-alpha20230405 successful. To make this your default version, run 'tfenv use 1.5.0-alpha20230405' $ tfenv use 1.5.0-alpha20230405 Switching default version to v1.5.0-alpha20230405 Default version (when not overridden by .terraform-version or TFENV_TERRAFORM_VERSION) is now: 1.5.0-alpha20230405 We can inspect the currently used version: $ cat .tfenv/version 1.5.0-alpha20230405 $ terraform version Terraform v1.5.0-alpha20230405 on linux_amd64 EXAMPLES We are now going to take a look at a few examples using the check{} block. SETUP Firstly, we are going to deploy a Linux Virtual Machine on Azure with a public IP address, attached to the CHECK-VNET Virtual Network and CHECK-SNET Subnet, and secured by a network security group (NSG) which allows ICMP and SSH to the VM. data "azurerm_resource_group" "this" { name = "CHECK" } data "azurerm_virtual_network" "this" { name = "CHECK-VNET" resource_group_name = data.azurerm_resource_group.this.name } data "azurerm_subnet" "this" { name = "CHECK-SNET" virtual_network_name = data.azurerm_virtual_network.this.name resource_group_name = data.azurerm_resource_group.this.name } resource "azurerm_public_ip" "this" { name = "check-PublicIP" resource_group_name = data.azurerm_resource_group.this.name location = data.azurerm_resource_group.this.location allocation_method = "Dynamic" } resource "azurerm_network_interface" "this" { name = "check-NIC" resource_group_name = data.azurerm_resource_group.this.name location = data.azurerm_resource_group.this.location ip_configuration { name = "check-NIC-CONFIG" subnet_id = data.azurerm_subnet.this.id private_ip_address_allocation = "Static" private_ip_address = "172.16.1.10" public_ip_address_id = azurerm_public_ip.this.id } } resource "azurerm_network_security_group" "this" { name = "check-NSG" resource_group_name = data.azurerm_resource_group.this.name location = data.azurerm_resource_group.this.location security_rule { name = "ICMP" priority = 1000 direction = "Inbound" access = "Allow" protocol = "Icmp" source_port_range = "*" destination_port_range = "*" source_address_prefixes = ["198.51.100.10/32"] destination_address_prefixes = ["0.0.0.0/0"] } security_rule { name = "SSH" priority = 1001 direction = "Inbound" access = "Allow" protocol = "Tcp" source_port_range = "*" destination_port_range = "22" source_address_prefixes = ["198.51.100.10/32"] destination_address_prefixes = ["0.0.0.0/0"] } } resource "azurerm_network_interface_security_group_association" "this" { network_interface_id = azurerm_network_interface.this.id network_security_group_id = azurerm_network_security_group.this.id } resource "azurerm_linux_virtual_machine" "this" { name = "check" resource_group_name = data.azurerm_resource_group.this.name location = data.azurerm_resource_group.this.location size = "Standard_B2s" network_interface_ids = [ azurerm_network_interface.this.id, ] admin_username = "check" admin_ssh_key { username = "check" public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCsjvtAsk/E3wkqxpBnujZPTZ1kjQSrssww0SV8YixBUE+iBTVw4WhkOs36gYiZVHqApHLlyFZags8J6NfmFEWY644wBw//MXXY9EbY+aDhrfNGj+SqbFQs+357ldEmM8U/iSk9OfaM3St5URJ867RI1LfeLmGo8L/yAJzUBjxQ7OneDohChhszbDqV2Vl8Bh0hyGROfFuTA9lMlU9dfudfMCve4kvdxh5mAso4pr74lR3Q+WBNNGf/i6B4I74qhzxSV6jjKPsKArnUPMhdqKXEfOnLkhZjRZAxqQgr5GzzqpfO+LB2Z6ogOS84cutgm6nx/m7eSYAbomlEAjeukW0sCMKA6+GBpPeYhYK7w/1IOa7/JcXDJlC2eRsZBnF2IqkGNq1n3mF7rnfMXXhogy/WsUW7RPWrbwXiEtAFqQoPPB1PejU5wGW6e/2zTKdqdB6f8ACyTJj489KBv+Sc7tAbc6sdN5JkefcciR7yKstmeXctuRFNlsB598lZbQb/mf8= grinch@unfriendlygrinch.info" } os_disk { name = "check-DISK-OS" caching = "ReadWrite" storage_account_type = "StandardSSD_LRS" } source_image_reference { publisher = "Canonical" offer = "0001-com-ubuntu-minimal-jammy" sku = "minimal-22_04-lts" version = "22.04.202303080" } } CHECKS Having now the setup in place, let’s check whether the provisioned Virtual Machine is up or down, whether this is associated with a the public IP, and whether the SSH access is unrestricted. UP OR DOWN We are going to define a check block which makes use of an external data source to query the status of the Virtual Machine in Azure and assert whether this is running. check "vm_up_down" { data "external" "this" { program = ["python", "./ping.py", "${azurerm_linux_virtual_machine.this.public_ip_address}"] query = { ip_address = azurerm_linux_virtual_machine.this.public_ip_address } } assert { condition = data.external.this.result.status == "up" error_message = format("The Virtual Machine %s is down.", azurerm_linux_virtual_machine.this.name ) } } The external data source executes a Python script to check the connectivity to the Virtual Machine. The command to execute is specified by the program argument, and the variables to pass to the script are specified by the query argument. In this case, the IP address of the Virtual Machine is passed to the script as a variable. The assert block includes a condition determines if the status returned by the external data source is up or not. Should the condition not be met, then a error_message is displayed. import subprocess import json import sys ip_address = sys.argv[1] if len(sys.argv) > 1 else "" if ip_address: ping_output = subprocess.check_output(["ping", "-c", "3", "-q", ip_address], shell=False, text=True) status = "up" if "0% packet loss" in ping_output else "down" else: status = "down" ping_data = { "ip_address": ip_address, "status": status, } json_data = json.dumps(ping_data) print(json_data) ping.py is a rather simple Python script which calls upon the ping command to check the connectivity to a certain IP address and prints to standard output the JSON-encoded string. In case the Virtual Machine is stopped, it will continue to be associated with the public IP resource. However, it will no longer be allocated a public IP address. This is why we need to check beforehand whether or not an IP address has been provided and set the ip_address to the first command line argument or an empty string if not. VM & PUBLIC IP ASSOCIATION Next we are going to assert whether the public IP has been dissociated from the Virtual Machine. check "vm_has_public_ip" { data "azurerm_public_ip" "this" { name = "check-PublicIP" resource_group_name = data.azurerm_resource_group.this.name } assert { condition = data.azurerm_public_ip.this.ip_address == azurerm_linux_virtual_machine.this.public_ip_address error_message = format("The Virtual Machine %s is no longer associated with the public IP %s.", azurerm_linux_virtual_machine.this.name, data.azurerm_public_ip.this.name ) } } The condition in the check block compares the ip_address attribute of a public IP address resource to the public_ip_address attribute of the Virtual Machine resource. In case these do not match, then an error message is displayed. This condition covers as well the case when the Virtual Machine is stopped, because the public IP address queried by the data source, as well as the public_ip_address attribute of the Virtual Machine will be empty strings UNRESTRICTED SSH ACCESS And one more check, we are going to ensure that any NSG associated with the Virtual Machine’s NIC allows incoming SSH traffic only from certain IP address or IP address ranges. locals { ssh_security_rule = tolist(azurerm_network_security_group.this.security_rule)[1] } check "unrestricted_ssh_access" { assert { condition = local.ssh_security_rule.source_address_prefix != "*" && setintersection(local.ssh_security_rule.source_address_prefixes, ["0.0.0.0/0"]) != toset([]) error_message = "SSH access is allowed from anywhere." } } STOPPING THE VM We have stopped the Virtual Machine. Let’s see what happens. $ terraform apply data.azurerm_resource_group.this: Reading... data.azurerm_resource_group.this: Read complete after 0s [id=/subscriptions/************************************/resourceGroups/CHECK] data.azurerm_virtual_network.this: Reading... data.azurerm_public_ip.this: Reading... azurerm_public_ip.this: Refreshing state... [id=/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/publicIPAddresses/check-PublicIP] azurerm_network_security_group.this: Refreshing state... [id=/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/networkSecurityGroups/check-NSG] data.azurerm_virtual_network.this: Read complete after 1s [id=/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/virtualNetworks/CHECK-VNET] data.azurerm_subnet.this: Reading... data.azurerm_public_ip.this: Read complete after 1s [id=/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/publicIPAddresses/check-PublicIP] data.azurerm_subnet.this: Read complete after 1s [id=/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/virtualNetworks/CHECK-VNET/subnets/CHECK-SNET] azurerm_network_interface.this: Refreshing state... [id=/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/networkInterfaces/check-NIC] azurerm_network_interface_security_group_association.this: Refreshing state... [id=/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/networkInterfaces/check-NIC|/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/networkSecurityGroups/check-NSG] azurerm_linux_virtual_machine.this: Refreshing state... [id=/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Compute/virtualMachines/check] data.external.this: Reading... data.external.this: Read complete after 0s [id=-] Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: <= read (data resources) Terraform will perform the following actions: # data.azurerm_public_ip.this will be read during apply # (config will be reloaded to verify a check block) <= data "azurerm_public_ip" "this" { + allocation_method = "Dynamic" + ddos_protection_mode = "VirtualNetworkInherited" + id = "/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/publicIPAddresses/check-PublicIP" + idle_timeout_in_minutes = 4 + ip_tags = {} + ip_version = "IPv4" + location = "northeurope" + name = "check-PublicIP" + resource_group_name = "CHECK" + sku = "Basic" + tags = {} + zones = [] } # data.external.this will be read during apply # (config will be reloaded to verify a check block) <= data "external" "this" { + id = "-" + program = [ + "python", + "./ping.py", + "", ] + query = { + "ip_address" = "" } + result = { + "ip_address" = "" + "status" = "down" } } Plan: 0 to add, 0 to change, 0 to destroy. ╷ │ Warning: Check block assertion failed │ │ on check.tf line 11, in check "vm_up_down": │ 11: condition = data.external.this.result.status == "up" │ ├──────────────── │ │ data.external.this.result.status is "down" │ │ The Virtual Machine check is down. ╵ Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes data.azurerm_public_ip.this: Reading... data.external.this: Reading... data.external.this: Read complete after 0s [id=-] data.azurerm_public_ip.this: Read complete after 0s [id=/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/publicIPAddresses/check-PublicIP] ╷ │ Warning: Check block assertion failed │ │ on check.tf line 11, in check "vm_up_down": │ 11: condition = data.external.this.result.status == "up" │ ├──────────────── │ │ data.external.this.result.status is "down" │ │ The Virtual Machine check is down. ╵ Apply complete! Resources: 0 added, 0 changed, 0 destroyed. The ip_address parameter is an empty string, as seen above. As this indicates, the vm_up_down check block fails. Checking tfstate: "check_results": [ { "object_kind": "check", "config_addr": "check.vm_has_public_ip", "status": "pass", "objects": [ { "object_addr": "check.vm_has_public_ip", "status": "pass" } ] }, { "object_kind": "check", "config_addr": "check.unrestricted_ssh_access", "status": "pass", "objects": [ { "object_addr": "check.unrestricted_ssh_access", "status": "pass" } ] }, { "object_kind": "check", "config_addr": "check.vm_up_down", "status": "fail", "objects": [ { "object_addr": "check.vm_up_down", "status": "fail", "failure_messages": [ "The Virtual Machine check is down." ] } ] } ] Despite the fact that the Virtual Machine is halted, the vm_has_public_ip check has passed. This implies that the Virtual Machine is still associated with the public IP address, and because the public IP address is dynamically assigned, a new one will be allocated to the Virtual Machine upon start-up. STARTING THE VM $ terraform apply data.azurerm_resource_group.this: Reading... data.azurerm_resource_group.this: Read complete after 1s [id=/subscriptions/************************************/resourceGroups/CHECK] data.azurerm_public_ip.this: Reading... data.azurerm_virtual_network.this: Reading... azurerm_public_ip.this: Refreshing state... [id=/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/publicIPAddresses/check-PublicIP] azurerm_network_security_group.this: Refreshing state... [id=/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/networkSecurityGroups/check-NSG] data.azurerm_public_ip.this: Read complete after 0s [id=/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/publicIPAddresses/check-PublicIP] data.azurerm_virtual_network.this: Read complete after 0s [id=/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/virtualNetworks/CHECK-VNET] data.azurerm_subnet.this: Reading... data.azurerm_subnet.this: Read complete after 1s [id=/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/virtualNetworks/CHECK-VNET/subnets/CHECK-SNET] azurerm_network_interface.this: Refreshing state... [id=/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/networkInterfaces/check-NIC] azurerm_network_interface_security_group_association.this: Refreshing state... [id=/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/networkInterfaces/check-NIC|/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/networkSecurityGroups/check-NSG] azurerm_linux_virtual_machine.this: Refreshing state... [id=/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Compute/virtualMachines/check] data.external.this: Reading... data.external.this: Read complete after 2s [id=-] Note: Objects have changed outside of Terraform Terraform detected the following changes made outside of Terraform since the last "terraform apply" which may have affected this plan: # azurerm_linux_virtual_machine.this has changed ~ resource "azurerm_linux_virtual_machine" "this" { id = "/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Compute/virtualMachines/check" name = "check" + public_ip_address = "203.0.113.20" tags = {} # (22 unchanged attributes hidden) # (3 unchanged blocks hidden) } Unless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes. ─────────────────────────────────────────────────────────────────────────────────────────────── Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: <= read (data resources) Terraform will perform the following actions: # data.azurerm_public_ip.this will be read during apply # (config will be reloaded to verify a check block) <= data "azurerm_public_ip" "this" { + allocation_method = "Dynamic" + ddos_protection_mode = "VirtualNetworkInherited" + id = "/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/publicIPAddresses/check-PublicIP" + idle_timeout_in_minutes = 4 + ip_address = "203.0.113.20" + ip_tags = {} + ip_version = "IPv4" + location = "northeurope" + name = "check-PublicIP" + resource_group_name = "CHECK" + sku = "Basic" + tags = {} + zones = [] } # data.external.this will be read during apply # (config will be reloaded to verify a check block) <= data "external" "this" { + id = "-" + program = [ + "python", + "./ping.py", + "203.0.113.20", ] + query = { + "ip_address" = "203.0.113.20" } + result = { + "ip_address" = "203.0.113.20" + "status" = "up" } } Plan: 0 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes data.azurerm_public_ip.this: Reading... data.external.this: Reading... data.azurerm_public_ip.this: Read complete after 1s [id=/subscriptions/************************************/resourceGroups/CHECK/providers/Microsoft.Network/publicIPAddresses/check-PublicIP] data.external.this: Read complete after 2s [id=-] Apply complete! Resources: 0 added, 0 changed, 0 destroyed. All the checks we have defined successfully passed. FINAL THOUGHTS By using check{} blocks, we are able to continuously assert the health of our infrastructure. These, in our view, address the need for an improved framework capable of confirming a certain condition once post-apply is accomplished. How do we ensure that our infrastructure components are doing exactly what they were meant to? This new feature will undoubtedly add functional and security testing as one of the most reliable approaches of identifying infrastructure issues and internal security vulnerabilities using Terraform itself. We’re looking forward to seeing how the community embraces check{} blocks and how authors use it into Terraform modules to provide a Continuous Validation strategy so that infrastructure works as expected. -------------------------------------------------------------------------------- | Copyright © Unfriendly Grinch 2023 | Archie Theme | Rendered by Hugo