Knowledge Bank
  • 🔭About Us
    • 👋Welcome to Securescape!
  • 👶Fundamentals
    • 🖥️Operating Systems (WIP)
      • Introduction to Operating Systems
      • Processes and Process Management
        • 📠Processes
        • 🧵Threads
        • 📅Scheduling
      • Memory Management
        • Virtual Memory Management
      • Storage Management
      • I/O Management
    • 🐧Linux (WIP)
      • Introduction to Linux
      • Linux System Management
    • ⚡Programming (WIP)
      • 🔗Assembly
      • 💪C(++/#)
      • 🐍Python
      • 👑Nim
      • 🔨Bash
        • Introduction to Bash Scripting
        • Variables, Loops, and Port Scanner
    • 🚩Networking (WIP)
      • 📶Networking 101
        • 🕸️Networking Basics
        • 🛑Protocols
        • 🧓IPv4
        • 🧒IPv6
      • 🪡Packet Tracer
        • Interface
        • Connections
        • Creating Networks
        • Virtual LANS & Trunks
      • 🕸️Subnetting
      • 🕵️Network Security
    • ✍️Report Writing (WIP)
      • 🔍Research Skills
      • 🏗️Structuring a Report
      • 🗃️Creating Templates
  • 🦂General Security
    • 🗒️Methodologies
      • 🇭🇰OSSTMM
      • 🐝OWASP
        • Web Security Testing Guide
        • Mobile Security Testing Guide
      • 🦅NIST
      • 🥢PTES
      • ⛓️Cyber Killchain
    • 🍔Binary Exploitation
      • ir0nstone's Binary Exploitation Notes
    • 🎩Cheat Sheets
      • Enumeration
        • Network Scan
        • Vulnerability Scan
        • Web Scan
      • Exploitation
        • Page 1
        • Payloads & Shells
      • Post Exploitation
        • Lay of The Land
        • Persistence
        • Data Exfiltration
        • Pivoting
      • Command & Control
      • Disassembly
        • ☢️Radare2
        • 🥜GDB
      • CEH Cheatsheet
  • ⚔️Offensive Security
    • 💡Hardware Exploitation
      • Intro to Hardware
    • 🥷Red Team
      • 🦠Malware Development
        • Crow Malware Development
        • 🪡C# Malware
      • 🏭Offensive Development
        • 🔧Offensive DevOps
          • 🏷️GitLab
            • GitLab Setup
            • Simple Calculator Project
            • Making our CI/CD Pipeline
            • Build Artifacts
          • 🌆TeamCity
            • TeamCity Setup
            • Creating TeamCity Projects
            • Obfuscating Payloads
          • 🍷Jenkins (WIP)
            • Jenkins Setup
            • Creating Pipelines
            • Managing Projects
            • API Interaction
        • 🏗️Infrastructure Development (WIP)
          • 🎮Command & Control Infrastructure
            • 🤖Command & Control Anatomy
              • Command & Control Frameworks
              • Ⓜ️Metasploit Framework
                • Installing Metasploit
                • Metasploit Basics
                • Advanced Features
                • Metasploit Documentation
              • 🐲Mythic Framework
                • Installing Mythic
                • Malleable Command & Control
                • All About Agents
                • Services
                • Mythic Documentation
            • 🚥Traffic Redirection
              • Nginx
              • Amazon Web Services
              • Microsoft Azure
              • Google Cloud Platform
              • Cloudflare Workers
            • 🥷Covert Infrastructure
              • Ensuring Resiliency
              • Traffic Masking
              • Network Rules
          • 🎣Phishing Infrastructure
            • 📧Email Anatomy
            • 🐟Phishing Infrastructure Setup
            • 🚚Payload Delivery
            • 🚩Removing Red Flags
          • 🪄Infrastructure as Code
            • 🏝️Terraform
              • Interacting with Docker
              • Going to the Cloud
              • Hybrid Deployment
            • 🧊Pulumi
            • 🎼Ansible
          • ⚙️Infrastructure Automation
            • 🦴Structuring our Project
            • 🏭Automating Server Setups
            • 🎼Orchestrating our Infrastructure
            • 🔧CI/CD Integration
      • 🏙️Active Directory (WIP)
        • Active Directory Overview
        • Authentication
        • AD Lab
      • Red Team Operations - Joas Santos
  • 🛡️Defensive Security
  • 📻Software Defined Radios
    • ⚠️Disclaimer
    • 📡Baofeng
      • Programming
  • 🧑‍🔬Home Lab
    • 💨Virtualisation
      • 🔸Proxmox
  • 🏁Capture The Flag
    • 🧊HackTheBox
      • 👾Cyber Apocalypse
        • Cyber Apocalypse 2023
    • 🐤TryHackMe
      • 🎄Advent of Code
      • 🚪Rooms
        • 🐥Basic Pentesting
        • 👨‍💻Blog
      • 👟Paths
    • 🏳️Competitions
      • Nahamcon
        • Nahamcon 2023
          • Binary Exploitation
            • Open Sesame
      • 👁️Iris CTF
  • 🦺DRAFTS
    • GS
      • 📱Mobile Application Security
      • 👨‍🔬Reverse Engineering
      • 🌐Web Application Security
      • 📌Information Security
      • 🔒Cryptography
      • 🤫Operational Security
    • DS
      • 🧠Threat Intelligence
        • 🦌ELK Stack
          • 🤸Elasticsearch
          • 🏕️Kibana
          • 🦤SELKS
        • 🚓Yara
      • 🏹Threat Hunting
      • 🧬Malware Analysis
        • Fundamentals
      • 🔬Forensics
        • 📶Network Forensics
          • 🦈Wireshark
          • 🥟TCP Dump
        • 💾Memory Forensics
          • ⚡Volatility
        • 💽Disk Forensics
          • 🐕Autopsy
        • 🪟Windows Forensics
        • 🐧Linux Forensics
      • 🌲Security Operations
        • Intrusion Detection & Prevention
          • 🐛Splunk
            • Splunk Basics
            • Integrating Suricata with Splunk
          • 🐗Suricata
            • Intro to Suricata
          • 🐽Snort
            • Snort Basics
        • Security Information and Event Management (SIEM)
        • Security Orchestration, Automation and Response (SOAR)
    • HL
      • 🖥️Hardware
    • OS
      • 📶Network Exploitation
      • 🌩️Cloud Exploitation
Powered by GitBook
On this page
  • First Steps
  • Structuring Terraform Projects
  • Deploying Multiple Containers
  • Scripting Builds

Was this helpful?

Edit on GitHub
  1. Offensive Security
  2. Red Team
  3. Offensive Development
  4. Infrastructure Development (WIP)
  5. Infrastructure as Code
  6. Terraform

Interacting with Docker

PreviousTerraformNextGoing to the Cloud

Last updated 7 months ago

Was this helpful?

In this section, we will be covering some broad basics on how to use Terraform, creating resources, and planning builds. This is by no means comprehensive but aims to give you a general idea on Terraform and how to write HCL code.

First Steps

Terraform works by interacting with a provider such as docker or other supported applications (). To use a provider, we can copy the code block that they provide and paste it in our main.tf file - we will go over how to structure our project later on.

main.tf
// Terraform uses 2 spaces to indent instead of 4
terraform {
  required_providers {
    docker = {
      source = "kreuzwerker/docker"
      version = "~> 3.0.2" # The squiggly line just looks for any version up to the one we want
    }
  }
}

// Extra config options if you are hosting Docker remotely.
provider "docker" {
  host     = "ssh://user@blah:22"
  ssh_opts = ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"]
}

Now that we have a provider specified, we can start adding containers to the mix. In the Terraform Registry, we can see a couple of resources which we can utilise in our code

Let's start with the container resource. Select the docker_container tab then view the example provided:

main.tf
# Start a container
resource "docker_container" "ubuntu" {
  name  = "foo"
  image = docker_image.ubuntu.image_id
}

# Find the latest Ubuntu precise image.
resource "docker_image" "ubuntu" {
  name = "ubuntu:precise"
}

First, Terraform is going to look for the latest image of Ubuntu precise, which we will then reference the ID of when starting the container

To reference an item, we need to specify the resource type (docker_image) and name (ubuntu) then the item that we want to fetch (image_id).

Using this example, we can go to Docker Hub and pick out an application to deploy - below is an example using Nginx

main.tf
terraform {
  required_providers {
    docker = {
      source = "kreuzwerker/docker"
      version = "~> 3.0.2"
    }
  }
}

resource "docker_container" "container_nginx" {
  name  = "foo"
  image = docker_image.nginx.image_id
}

resource "docker_image" "image_nginx" {
  name = "nginx:latest"
}

To make the code cleaner, we can give the resource name a prefix to ensure that it is self documenting (container_ or image_ etc.)

Now that we have our basic Terraform script ready, we can initialise and apply our code by running the following in our terminal:

terraform init
terraform plan -out tf_plan.out # (You can skip this if you run terraform apply)
terraform apply -auto-approve # Auto approve will build without asking for confirmation

If successful, we should be able to see the following output and verify by running docker ps

Let us update our main.tf file to reflect the changes needed

main.tf
terraform {
  required_providers {
    docker = {
      source = "kreuzwerker/docker"
      version = "~> 3.0.2"
    }
  }
}

provider "docker" {
  host     = "ssh://vagrant@192.168.8.129:22"
  ssh_opts = ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"]
}

resource "docker_container" "container_nginx" {
  name  = "Nginx Proxy"
  image = docker_image.nginx.image_id

  ports {
    internal = 80
    external = 80
  }
}

resource "docker_image" "nginx" {
  name = "nginx:latest"
}

We can now run terraform apply -auto-approve to update our container. Note that you do not need to run destroy as Terraform will review the plan and change anything that has been updated.

Now try to visit the web page at the external port you've specified

Success! If you have followed all the steps so far, you should have a working Terraform automation script. Let's tear down our infrastructure by running terraform destroy -auto-approve and look into how we can structure our project.

Structuring Terraform Projects

It is good practice to separate our main.tf file into multiple specialised files so it is easier to read and document. It will also come in handy when we want to modularise our project into a dev and prod version for example. The general files that are needed are:

  • main.tf

  • provider.tf

  • network.tf

  • resource.tf

You do not need to follow the same naming scheme, though it does help to have some form of consistency. Let us create these files, then separate our functions into them.

main.tf
resource "docker_container" "container_nginx" {
  name  = "foo"
  image = docker_image.nginx.image_id

  ports {
    internal = 80
    external = 80
  }
}
resource.tf
resource "docker_image" "nginx" {
  name = "nginx:latest"
}
provider.tf
terraform {
  required_providers {
    docker = {
      source = "kreuzwerker/docker"
      version = "~> 3.0.2"
    }
  }
}

provider "docker" {
  host     = "ssh://blah@127.0.0.1:22"
  ssh_opts = ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"]
}

Make sure to test that your changes worked by running terraform plan.

We can also create a variables file to make it easier to switch out certain parts of our code such as the name of our container or the image version.

vars.tf
variable "container_name" {
  description = "Docker Container Name"
  type = string
  default = "nginx_container"
}

variable "docker_image" {
  description = "Docker Container Image"
  type = string
  default = "nginx:latest"
}

Update main and resource with the new variables and ensure that your codebase is clean by running terraform fmt

main.tf
resource "docker_container" "container_nginx" {
  name  = var.container_name # Variable Here
  image = docker_image.nginx.image_id

  ports {
    internal = 80
    external = 80
  }
}
resource.tf
resource "docker_image" "nginx" {
  name = var.docker_image
}
> terraform fmt # Format your terraform files
provider.tf
vars.tf
> terraform apply -auto-approve # Make sure your code still works

Great! However, the Nginx container variable might not make much sense - instead of specifying the container and its version, we can instead specify the version and change it inline

vars.tf
variable "container_name" {
  description = "Docker Container Name"
  type        = string
  default     = "nginx_container"
}

variable "nginx_image_version" { # Updated the name to reflect our change
  description = "Docker Container Image"
  type        = string
  default     = "1.27"
}
resource.tf
resource "docker_image" "nginx" {
  name = "nginx:${var.nginx_image_version}"
}
# Use ${stuff} to add code inside of a string

Running terraform plan should show you the version number alongside the container you picked

  # docker_image.nginx will be created
  + resource "docker_image" "nginx" {
      + id          = (known after apply)
      + image_id    = (known after apply)
      + name        = "nginx:1.27" # Right here :)
      + repo_digest = (known after apply)
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Shortening our Variable

We can use maps instead of having separate variable names to modify the same container to save on space and keep our code even cleaner - let us examine the required variables on our container

main.tf
resource "docker_container" "container_nginx" {
  name  = var.container_name # Requires a name which we get from our variable
  image = docker_image.nginx.image_id

  ports {
    internal = 80 # Accepts two port numbers for internal and external
    external = 80
  }
}
vars.tf
# Modify the container_name and nginx_image_version variable into a single map object
variable "nginx_container" {
  description = "Nginx Container Variables"
  type        = map(string)
  default = {
    "name"          = "nginx_container"
    "version"       = "1.27"
    "internal_port" = 80
    "external_port" = 80
  }
}

One issue with this is that we specified the map object to be of type string, but we included numbers inside of it. In this case, we can nest an object inside of our map, then specify the data type for each key. Note that you may not run into an issue for this when deploying but it's good for documentation.

vars.tf
variable "nginx_container" {
  description = "Nginx Container Variables"
  type = map(object({
    name          = string,
    version       = string, # We set this as a string because it can either be 'latest' or a number
    internal_port = number,
    external_port = number
    }))
  
  default = {
    "name"          = "nginx_container"
    "version"       = "1.27"
    "internal_port" = 80
    "external_port" = 80
  }
}

Now we can go to our other files and modify their attributes by using var.nginx_container.x

main.tf
resource "docker_container" "container_nginx" {
  name  = var.nginx_container.name
  image = docker_image.nginx.image_id

  ports {
    internal = var.nginx_container.internal_port
    external = var.nginx_container.external_port
  }
}
resource.tf
resource "docker_image" "nginx" {
  name = "nginx:${var.nginx_container.version}"
}

Deploying Multiple Containers

It's cool and good that we have a functional codebase, but it's practically useless right now. So, let us examine how we can use Terraform to deploy a web application using Nginx as the reverse proxy, and Apache Tomcat as the web server.

We can copy the nginx_container variable and re-use it for our Tomcat container

vars.tf
variable "nginx_container" {
  description = "Nginx Container Variables"
  type        = map(string)

  default = {
    "name"          = "nginx_proxy"
    "version"       = "latest"
    "internal_port" = 80
    "external_port" = 80
  }
}

variable "tomcat_container" {
  description = "Tomcat Container Variables"
  type        = map(string)

  default = {
    "name"          = "tomcat_websrv"
    "version"       = "latest"
    "internal_port" = 8080
    "external_port" = 81
  }
}

Next, we'll add Tomcat to our resource and main.tf files

resource.tf
resource "docker_image" "nginx" {
  name = "nginx:${var.nginx_container.version}"
}

resource "docker_image" "tomcat" {
  name = "tomcat:${var.tomcat_container.version}"
}
main.tf
resource "docker_container" "container_nginx" {
  name  = var.nginx_container.name
  image = docker_image.nginx.image_id

  ports {
    internal = var.nginx_container.internal_port
    external = var.nginx_container.external_port
  }
}

resource "docker_container" "container_tomcat" {
  name  = var.tomcat_container.name
  image = docker_image.tomcat.image_id

  ports {
    internal = var.tomcat_container.internal_port
    external = var.tomcat_container.external_port
  }
}

Run terraform apply and check that your containers have deployed before moving the the next step.

Outputs

Finally, we have outputs. You can specify important data to display to the end user such as IP addresses, hosts, or passwords which they might need to use in order to access the resources that have been deployed by creating an outputs.tf file then referencing the Read-Only data generated by your resources.

outputs.tf
output "nginx_url" {
  description = "Nginx URL to access the default page"
  value       = "http://${docker_container.container_nginx.hostname}:${var.nginx_container.external_port}"
}

output "tomcat_url" {
  description = "Nginx URL to access the default page"
  value       = "http://${docker_container.container_tomcat.hostname}:${var.tomcat_container.external_port}"
}

If we run terraform apply -auto-approve now, we should get the output in our terminal window. If you clear this by accident, you can run terraform output to view the outputs. We will see better examples of this later on when we work with cloud resources.

Outputs:

nginx_url = "http://f57596f3665a:80"
tomcat_url = "http://b6b47918df86:81"

Scripting Builds

This is not a lesson per-se, but some quality of life scripts to make deployment easier and faster.

PowerShell/BASH

build.ps1
terraform init # Initialises the directory if it has not been already
terraform fmt
terraform validate
terraform plan -out tfplan.out
terraform apply -auto-approve
destroy.ps1
terraform destroy -auto-approve
rm tfplan.out

Makefile

Makefile
all:
    init
    apply

init:
    terraform init
    terraform fmt

apply:
    terraform validate
    terraform plan -out tfplan.out
    terraform apply -auto-approve

destroy:
    terraform destroy -auto-approve
    rm tfplan.out

We will notice however that if we try to visit the nginx proxy at http://<docker_instance_ip>, we will receive an error. This is because we are not exposing any container ports which are accessible. To do this, we can revisit the to see the required schema:

You can make this as complex or as simple as you'd like. If you want more info on how to use variables, .

⚔️
🥷
🏭
🏗️
🪄
🏝️
Docker Registry
check out this blog
available in the Terraform registry
Docker Resources
How the Docker ID gets referenced
Referencing the image ID
Terraform Plan
Terraform Apply - Success
Docker Processes
Ports Schema
Updated Container
Nginx Default Page
Deployed Nginx and Tomcat containers
Nginx Default Page
Tomcat Error Page (But hey, tomcat :))
Docker Provider