Recently I had to learn myself some Terraform for real, and it hit me - Docker (which I have come to use extensively) would be a perfect environment in which to do this.
Before you begin, make sure you have Terraform installed:
$ brew install terraform
Terraform communicates with Docker over a TCP port, enabling it to orchestrate Docker all over the fleet. On OSX, Docker disables this port by default. We will use socat
to expose the Docker UNIX domain socket to the network on port 2375.
Install socat
if you haven't already:
$ brew install socat
And then fire it up to connect the network socket and UNIX domain socket for Docker:
$ socat TCP-LISTEN:2375,reuseaddr,fork UNIX-CONNECT:/var/run/docker.sock
There, you're done and Docker is now reachable from Terraform over the network.
Terraform creates a way for us to describe the network state we want as a series of configuration options. The ones we will use in this super simple proof of concept:
- provider - specifying Docker and how to reach our host
- resource for the Docker image and where to find it in a container registry
- resource for the container itself and what options we want for it
Put all of this in your file config.tf
:
provider "docker" {
host = "tcp://localhost:2375"
}
resource "docker_image" "cyberchef" {
name = "remnux/cyberchef"
}
resource "docker_container" "cyberchef-image" {
name = "cyberchef-image"
image = "${docker_image.cyberchef.latest}"
ports {
internal = 8080
external = 8080
}
}
Make surey you've initialized Terraform:
$ terraform init
You should see something in green that looks like Terraform has been successfully initialized!
Once you have that, ask Terraform to come up with a plan to get from your current state to where you want it to be, and store that in config.tfplan
:
$ terraform plan -out config.tfplan
You should see output like this (I've already fetched the image, so you might see it fetch the image if you don't have it):
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.
docker_image.cyberchef: Refreshing state... (ID: sha256:24a9170bee05c6cac9f61078a241293f...4e3d09650835a0300ba83dremnux/cyberchef)
docker_container.cyberchef-image: Refreshing state... (ID: da4aaf97029f548b7772d724c81d6ec50f293048b898fc515fedde7fb95061d0)
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
+ docker_container.cyberchef-image
id: <computed>
attach: "false"
bridge: <computed>
container_logs: <computed>
exit_code: <computed>
gateway: <computed>
image: "sha256:24a9170bee05c6cac9f61078a241293f55ce1a0dc84e3d09650835a0300ba83d"
ip_address: <computed>
ip_prefix_length: <computed>
log_driver: "json-file"
logs: "false"
must_run: "true"
name: "cyberchef-image"
network_data.#: <computed>
ports.#: "1"
ports.0.external: "8080"
ports.0.internal: "8080"
ports.0.ip: "0.0.0.0"
ports.0.protocol: "tcp"
restart: "no"
rm: "false"
start: "true"
Plan: 1 to add, 0 to change, 0 to destroy.
------------------------------------------------------------------------
This plan was saved to: config.tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "config.tfplan"
And that's a promising result - if we ask Terraform to execute this plan, we'll get a Docker container runinng with those options. Let's apply the plan:
$ terraform apply "config.tfplan"
And you should see output like this:
docker_container.cyberchef-image: Creating...
attach: "" => "false"
bridge: "" => "<computed>"
container_logs: "" => "<computed>"
exit_code: "" => "<computed>"
gateway: "" => "<computed>"
image: "" => "sha256:24a9170bee05c6cac9f61078a241293f55ce1a0dc84e3d09650835a0300ba83d"
ip_address: "" => "<computed>"
ip_prefix_length: "" => "<computed>"
log_driver: "" => "json-file"
logs: "" => "false"
must_run: "" => "true"
name: "" => "cyberchef-image"
network_data.#: "" => "<computed>"
ports.#: "" => "1"
ports.0.external: "" => "8080"
ports.0.internal: "" => "8080"
ports.0.ip: "" => "0.0.0.0"
ports.0.protocol: "" => "tcp"
restart: "" => "no"
rm: "" => "false"
start: "" => "true"
docker_container.cyberchef-image: Creation complete after 2s (ID: a320838c2841d660d77a85ab6459270e1c65d3f9ad70513017aa3cff2e328341)
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
And voila, you should have a running Docker image, in this case of CyberChef:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a320838c2841 24a9170bee05 "grunt dev" 18 seconds ago Up 15 seconds 0.0.0.0:8080->8080/tcp cyberchef-image
Verify that it's accessible over the local network on port 8080 and begin using the awesome CyberChef resource for your analysis needs.
$ open http://localhost:8080