Skip to content

Instantly share code, notes, and snippets.

@tuannvm
Last active December 3, 2022 18:50
Show Gist options
  • Star 38 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save tuannvm/df824b332517eba77d1f6d14e9f16622 to your computer and use it in GitHub Desktop.
Save tuannvm/df824b332517eba77d1f6d14e9f16622 to your computer and use it in GitHub Desktop.
#terraform #hashicorp #cheatsheet #0.12
#### first class expresssion
variable "ami" {}
resource "aws_instance" "example" {
ami = var.ami
}
#### list & map
resource "aws_instance" "example" {
vpc_security_group_ids = var.security_group_id != "" ? [var.security_group_id] : []
}
# In 0.12, HCL resolves this by making an explicit distinction between attributes and blocks. Attributes are single-assigned values using the assignment operator =. Blocks are repeated maps of key/value configuration pairs
resource "aws_security_group" "example" {
# "ingress" is parsed as a block because there is no equals sign
ingress {
# ..
}
# "value" is parsed as a attribute as it has equal sign
value = {
# ..
}
}
### For Expressions for List and Map Transformations
variable "subnet_numbers" {
description = "List of 8-bit numbers of subnets of base_cidr_block that should be granted access."
default = [1, 2, 3]
}
resource "aws_security_group" "example" {
# For each number in subnet_numbers, extend the CIDR prefix of the
# requested VPC to produce a subnet CIDR prefix.
# For the default value of subnet_numbers above and a VPC CIDR prefix
# of 10.1.0.0/16, this would produce:
# ["10.1.1.0/24", "10.1.2.0/24", "10.1.3.0/24"]
cidr_blocks = [
for num in var.subnet_numbers:
cidrsubnet(data.aws_vpc.example.cidr_block, 8, num)
]
}
}
### How to produce a map
output "instance_private_ip_addresses" {
# Result is a map from instance id to private IP address, such as:
# {"i-1234" = "192.168.1.2", "i-5678" = "192.168.1.5"}
value = {
for instance in aws_instance.example:
instance.id => instance.private_ip
# if statement
if instance.associate_public_ip_address
}
}
output "instances_by_availability_zone" {
# Result is a map from availability zone to instance ids, such as:
# {"us-east-1a": ["i-1234", "i-5678"]}
value = {
for instance in aws_instance.example:
# mind the ...
instance.availability_zone => instance.id...
}
}
### Dynamic Nested Blocks
locals {
base_cidr_block = "10.0.0.0/16"
}
variable "subnets" {
default = [
{
name = "a"
number = 1
},
{
name = "b"
number = 2
},
]
}
variable "var_list" {
type = list(string)
default = ["a1", "a2"]
}
resource "azurerm_virtual_network" "example" {
name = "example-network"
resource_group_name = azurerm_resource_group.test.name
address_space = [local.base_cidr_block]
location = "West US"
dynamic "subnet" {
# [{"name": "a", "prefix": "10.1.0.0/24"},
# {"name": "b", "prefix": "10.2.0.0/24"},
for_each = [for s in subnets: {
name = s.name
prefix = cidrsubnet(local.base_cidr_block, 4, s.number)
}]
content {
name = subnet.name
address_prefix = subnet.prefix
}
}
}
resource "sample_dynamic_block_with_list" "example" {
name = "example"
dynamic "block" {
for_each = toset(var.var_list)
content {
name = block.key
}
}
}
### Splat operator
# before only work with resource with "count", now work for any list value
output "instance_ip_addrs" {
value = google_compute_instance.example.network_interface.*.address
}
# full splat operator
# instead of network_interface.*.
output "instance_net_ip_addrs" {
value = google_compute_instance.example.network_interface[*].access_config[0].assigned_nat_ip
}
### Conditional Operator Improvements
locals {
first_id = length(azurerm_virtual_machine.example) > 0 ? azurerm_virtual_machine.example[0].id : ""
buckets = (var.env == "dev" ? [var.build_bucket, var.qa_bucket] : [var.prod_bucket])
}
### Conditionally Omitted Arguments
variable "override_private_ip" {
type = string
default = null
}
resource "aws_instance" "example" {
# By using the new null value, the aws_instance resource can dynamically assign a private IP address when no override is provided,
# but set an explicit IP address if the calling module provides a value for override_private_ip.
private_ip = var.override_private_ip
}
### Complex Values
# complex objects can now be passed to child modules as inputs, and returned to parent modules as outputs
# access with `for network in var.networks`
module "subnets" {
source = "./subnets"
parent_vpc_id = "vpc-abcd1234"
networks = {
production_a = {
network_number = 1
availability_zone = "us-east-1a"
}
production_b = {
network_number = 2
availability_zone = "us-east-1b"
}
staging_a = {
network_number = 1
availability_zone = "us-east-1a"
}
}
}
### Rich types
variable "networks" {
# custom type
# no quote
type = map(object({
network_number = number
availability_zone = string
tags = map(string)
}))
}
### Resources and Modules as Values
# return the whole object
output "vpc" {
value = aws_vpc.example
}
### New Template syntax
locals {
lb_config = <<EOT
%{ for instance in opc_compute_instance.example ~}
server ${instance.label} ${instance.ip_address}:8080
%{ endfor }
EOT
}
### JSON comment
{
"variable": {
"example": {
"#": "This property is ignored",
"default": "foo"
}
}
}
### Validation
variable "word-length" {
validation {
# The condition here identifies if the integer if greater than 1
condition = var.word-length > 1
error_message = "The variable is not greater than 5. Word length has to be at a minimum > 1."
}
}
variable "os" {
validation {
# The condition here identifies if the variable contains the string "linxu" OR "windows".
condition = can(regex("linux|windows", var.os))
error_message = "ERROR: Operating System must be Windows OR Linux."
}
}
locals {
# This variable holds the all the IPs addresses for the S3 service returned by the endpoint. The return value is of the type list OR a string error value.
s3_ips = try(distinct([
for items in jsondecode(data.http.primary-server.body).prefixes:
items.ip_prefix if items.service == "S3"
]), "NO LIST PROVIDED IN LOCALS SERVICES VARIABLE")
}
}

Terraform provider

Source

├── main.go
├── provider.go
├── resource_server.go

provider.go

package main

import (
    "github.com/hashicorp/terraform/helper/schema"
)

func Provider() *schema.Provider {
  return &schema.Provider{
    ResourcesMap: map[string]*schema.Resource{
      "example_server": resourceServer(),
    },
  }
}

main.go

package main

import (
    "github.com/hashicorp/terraform/plugin"
    "github.com/hashicorp/terraform/terraform"
)

func main() {
  plugin.Serve(&plugin.ServeOpts{
    ProviderFunc: func() terraform.ResourceProvider {
      return Provider()
    },
  })
}

As a general convention, Terraform providers put each resource in their own file, named after the resource, prefixed with resource_

resource_server.go

package main

import (
    "github.com/hashicorp/terraform/helper/schema"
)

func resourceServer() *schema.Resource {
  return &schema.Resource{
    Create: resourceServerCreate,
    Read:   resourceServerRead,
    Update: resourceServerUpdate,
    Delete: resourceServerDelete,

    Schema: map[string]*schema.Schema{
      "address": &schema.Schema{
        Type:     schema.TypeString,
        Required: true,
      },
    },
  }
}

func resourceServerCreate(d *schema.ResourceData, m interface{}) error {
  address := d.Get("address").(string)
  d.SetId(address)
  return nil
}

func resourceServerRead(d *schema.ResourceData, m interface{}) error {
    return nil
}

func resourceServerUpdate(d *schema.ResourceData, m interface{}) error {
    return nil
}

func resourceServerDelete(d *schema.ResourceData, m interface{}) error {
    return nil
}

main.tf

resource "example_server" "my-server" {
  address = "1.2.3.4"
}

Cheatsheet

Map Loop

For e.g, we have the following values:

vpc_peering_list = {
  k8s-production = "vpc-111111"
  k8s-service = "vpc-222222"
  k8s-zdata = "vpc-333333"
}

IMPORTANT Map follows alphanumeric sequence.

We can loop through this map with:

resource "aws_vpc_peering_connection" "vpc_peering" {
  count = "${length(keys(var.vpc_peering_list))}"
  vpc_id = "${element(values(var.vpc_peering_list), count.index)}"
  peer_vpc_id   = "${aws_vpc.vpc.id}"

  tags {
    Name = "${element(keys(var.vpc_peering_list), count.index)}-vpc-peering-${var.service_name}"
  }
}

List Loop

variable "bucket_name" {
  description = "Bucket Name"
  type        = "list"
  default     = ["develop", "staging", "prod"]
}

resource "aws_route53_record" "www" {
  count = "${length(var.bucket_name)}"
  name  = "${var.bucket_name[count.index]}"

Default values for external module output

hashicorp/terraform#11566 (comment)

https://github.com/coreos/tectonic-installer/blob/master/modules/aws/vpc/vpc.tf#L24

External module should return output as a list

id = "${var.external_vpc_id == "" ? join(" ", aws_vpc.new_vpc.*.id) : var.external_vpc_id }"

Default value when using conditional check. See this

resource "example" "conditional" {
  count = "${var.enabled ? 1 : 0}"
}

resource "example" "dependent" {
  argument = "${join("", example.conditional.*.attribute)}"
}

Convert yaml to hcl

echo 'yamldecode(file("my-manifest-file.yaml"))' | terraform console

Enable experimental feature

terraform {
  experiments = [variable_validation]
}
@tuannvm
Copy link
Author

tuannvm commented Oct 9, 2017

elasticache import force new resource here

Fixing the state file with

terraform state pull > state.json
Find the elasticache resource
Add "port": "6379"
terraform state push state.json
terraform plan

@tuannvm
Copy link
Author

tuannvm commented Nov 25, 2017

  • Overcome flat list / map limitation:
"${jsonencode(data.github_team.example.*.members[count.index])}"

instead of

"${element(data.github_team.example.*.members, count.index)}"

  • Rename state file to a list of resources, didn't work without quotes:
terraform state mv aws_route53_record.example0 "aws_route53_record.new[0]"
terraform state mv aws_route53_record.example1 "aws_route53_record.new[1]"

@tuannvm
Copy link
Author

tuannvm commented Sep 16, 2020

Get ssh private key with jq

cat state.json | jq -r '.resources[] | select (.type=="tls_private_key") | .instances[0].attributes.private_key_pem'

@tuannvm
Copy link
Author

tuannvm commented Apr 20, 2021

Nested for_each

locals {
  clusters = toset([
    "prod",
    "stage"
  ])

  endpoints = toset([
    "healthz",
    "status"
  ])

  mapping = flatten([
    for cluster in local.clusters : [
      for endpoint in local.endpoints : {
        cluster  = cluster
        endpoint = endpoint
      }
    ]
  ])
}

resource "foo" "bar" {

  count       = length(local.mapping)
  name        = ${local.mapping[count.index]["cluster"]} ${local.mapping[count.index]["endpoint"]}"
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment