Terraform

export TF_LOG=TRACE
export TF_LOG_PATH=/path/tf.log
alias tf=terraform

# learn about -target
tf plan -target=aws_acm_certificate.cert -out=tfplanout
tf apply -auto-approve -target module.cert.aws_acm_certificate.cert

tf validate
tf providers
tf refresh
tf graph
tf plan -out=tfplanout
tf show [options] [address]

tf apply -auto-approve
tf apply -auto-approve -no-color |tee "$(date '+%Y-%m-%d-%H%M%S-apply.log')"
tf apply -var-file=domains.tfvars -auto-approve -no-color \
| tee "$(date '+%Y-%m-%d-%H%M%S-apply.log')"

tf output # all
tf output varname

# check formatting
tf fmt -write=false -recursive

# after changing state location in S3
tf init -migrate-state

# state list,mv,pull,rm,show,push
tf state list # to get names
# --- Remove part of the model if needed
tf state rm module.EzFeedFeedWorker

# import existing items
resource "type" "name" {} # must exist
tf import type.name attribute

# workspaces
tf workspace list
tf workspace new aname
tf workspace select aname
# vars based on workspace should be a map with workspace name as key
# use lookup to get values or terraform.workspace to get the name
x = lookup(var.instance_type, terraform.workspace)

alias tf=terraform
terraform apply -input=false -auto-approve
terraform taint -module=pruf_qa.host aws_instance.host
tf show |grep -A2 'aws_iam_access_key.user_file_'
terraform state rm module.EzFeedFeedWorker
tf destroy -force -refresh=false # when bucket is already gone

# only run part of plan
terraform apply -target=aws_instance.myinstance
# combine with no-refresh
# get target list with 
terraform state list

  • Example
    # versions.tf
    terraform {
      required_version = ">= 1.0"
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = ">= 5.0"
        }
      }
    }
    
    # main.tf
    // Use an illegal line to prevent accidental edits.
    // Lines starting with # are illegal but commands like terraform show still work.
    # illegal-comment-this-line-to-make-changes
    
    locals {
      # Flatten all forward_emails from all domains into one map for Lambda
      all_forward_emails = merge([
        for domain_key, domain in var.domains : domain.forward_emails
      ]...)
    
      # Map domain names to their S3 path prefixes
      domain_path_mapping = {
        for domain_key, domain in var.domains : domain.domain => domain.email_path
      }
    }
    
    // ------------------------------------
    provider "aws" {
      region = var.aws_region
      default_tags {
    	  tags = {
    	    Project     = var.project_name
    	    Environment = var.environment
    	    ManagedBy   = "terraform"
    	    Owner       = var.owner
    	  }
    	}
    }
    // ------------------------------------
    // Example of multi region aws access
    provider "aws" {
      profile = "${local.aws_profile}"
      region = "${local.aws_region}"
    }
    provider "aws" {
      alias   = "aws_cert"
      profile  = "${local.aws_profile}"
      region  = "us-east-1"
    }
    
    // ------------------------------------
    terraform {
      backend "s3" {
        bucket = "overton-devops"
        key    = "terraform/setup-mail/terraform.tfstate"
        region = "us-west-2"
        # Enable state locking. All account which use have to be able to see db
        dynamodb_table = "terraform-locks"
        encrypt        = true
      }
    }
    # Data sources
    data "aws_availability_zones" "available" {
      state = "available"
    }
    data "aws_caller_identity" "current" {}
    
    # Reference Network data from another S3 state file
    data "terraform_remote_state" "network" {
      backend = "s3"
      config = {
        bucket = "x-terraform-state"
        key    = "prod/network/terraform.tfstate"
        region = "us-east-1"
      }
    }
    
    ...
    module "mail" {
      for_each = var.domains
      source         = "../lib/ses"
      env            = each.key
      domain         = each.value.domain
      forward_emails = each.value.forward_emails
    }
    
  • Variables
    tf apply -var "name=value" -var "another=val2"
    
    # or as env var
    TF_VAR_name="value3"
    
    # or var file
    terraform.tfvars | terraform.tfvars.json
    *.auto.tfvars    | *.auto.tfvars.json
    
    tf apply -var-file vars.tfvars
    
    # order TF_VAR_env, tf.tfvas, *.auto.tfvars, -var or -var-file
  • for_each loops
    variable "names" {
      type = set
      default = [ "n1", "n2", "n3"]
    }
    
    resource "aws_instance" "cluster" {
      ami = var.ami
      instance_type = var.itype
      for_each = var.names
      tags = {
    	  Name = each.value
      }
    }
  • dynamic block loops
    variable "inports" {
      type = list
      default = [ 80, 443 ]
    }
    
    resource "aws_security_group" "in-sg" {
      name = "aname"
      vpc_id = var.vpc
      dynamic "ingress" {
        iterator = port
        for_each = var.inports
        content {
    	    from_port = port.value
    	    from_port = port.value
    	    protocol = "tcp"
    	    cidr_blocks = [ "0.0.0.0/0" ]
    	  }
    	}
    }
  • Provision
    resource "aws_instance" "server" {
      ami = var.ami
      instance_type = var.itype
      provisioner "remote-exec" {
    	  inline = [ "sudo apt update",
    	             "sudo apt install x" 
    	           ]
      }
      connection {
        type = "ssh"
        host = self.public_ip
        user = "ubuntu"
        private_key = file("/root/.ssh/web")
      }
      key_name = aws_key_pair.web.id
      vpc_security_group_ids = [ aws_security_group.ssh-access.id ]
    }
    
    # can or should use user_data?
    resource "aws_instance" "server" {
      ami = var.ami
      instance_type = var.itype
      user_data = <<-EOF
        #!/bin/bash
        sudo apt update
        sudo apt install nginx -y
        ...
        EOF
    }
  • Funtions
     file()
     length()
     toset(var.v2)
     
     # use terraform console to test
     length(var.name)
     
     max(1,2,3)
     max(var.nums...)
     
     ceil(), floor()
     
     split(",", "a, b, c"), join()
     lower(), upper()
     substr(var.str, 0, 3) # first three
     
     index(var.alist, "findme") # find
     element(var.alist, 1) # get at offset
     contains(var.alist, "findme") # bool
     
     # maps
     keys(var.amap)
     values(var.amap)
     lookup(var.amap, "keystr", "adefault")
  • Clear a lock from dynamodb
    aws dynamodb scan \
    --table-name terraform-lock \
    --query "Items[?contains(LockID.S, 'k4-nodes')]" \
    --region us-west-2
    
    tf force-unlock $UUID
    # may be able to get the uuid with tf force-unlock x
  • Module call with branch
    module "some-mod" {
      source = "git::http://repo.server.com/terraform-mods/repo.git//my-mod?ref=branchname"
    
      # pass a provider
      providers = {
        aws = aws.myalias
      }
    }
  • Convert k/v to key/value
    locals {
      tags_kv_master = [
        for item in keys(local.tags_master) :
        map(
          "key", item,
          "value", element(values(local.tags_master), index(keys(local.tags_master), item))
        )
      ]
    }
    # apply
    ...
      dynamic "tags" {
        for_each = local.tags_kv_master[*]
        content {
          key   = tags.value["key"]
          value = tags.value["value"]
        }
      }
    
  • User data using here file or read a file
      user_data = <<-EOF
    #!/bin/bash
    set -o xtrace
    /etc/eks/bootstrap.sh ${var.cluster_name}
    EOF
    
    userdata = file("userdata.sh")
  • Random string
    resource "random_string" "suffix" {
      length  = 8
      special = false
    }
  • Read json file
    locals {
      json_data = jsondecode(file("~/.aws/myfile.json"))
    }
    provider "xxx" {
      token   = local.json_data.token
      secret = local.json_data.secret
    }
  • Run a script
    resource "null_resource" "script" {
    
      provisioner "local-exec" {
        command = <<EOT
    aws --profile ${var.aws-profile} eks update-cluster-config --name ${data.acluster.cluster.name} \
    --resources-vpc-config endpointPublicAccess=false,endpointPrivateAccess=true
    EOT
      }
    }