# Interacting with Docker

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 ([available in the Terraform registry](https://registry.terraform.io/browse/providers)). 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.

<figure><img src="https://1797977785-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjrIJ5xrJuOVgeeYdKNB5%2Fuploads%2FMmcMcNEmRZAKhBuygmSU%2Fimage.png?alt=media&#x26;token=e523fdbb-fbfb-442b-8fd1-d95599140f13" alt=""><figcaption><p><a href="https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs">Docker Provider</a></p></figcaption></figure>

{% code title="main.tf" %}

```hcl
// 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"]
}
```

{% endcode %}

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

<figure><img src="https://1797977785-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjrIJ5xrJuOVgeeYdKNB5%2Fuploads%2F66YlJIlhfRlDbYlMn4mQ%2Fimage.png?alt=media&#x26;token=b3f19e62-1002-4bf8-8319-b58bfcdc9309" alt=""><figcaption><p>Docker Resources</p></figcaption></figure>

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

{% code title="main.tf" %}

```hcl
# 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"
}
```

{% endcode %}

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

<figure><img src="https://1797977785-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjrIJ5xrJuOVgeeYdKNB5%2Fuploads%2FdVzZKKfm4ZmyrtKVWzbv%2FDocker%20Ubuntu.svg?alt=media&#x26;token=8f6063aa-e658-428e-a7b0-6d9a944aae09" alt=""><figcaption><p>How the Docker ID gets referenced</p></figcaption></figure>

<figure><img src="https://1797977785-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjrIJ5xrJuOVgeeYdKNB5%2Fuploads%2FkbsfLSR4KZE7OHzogEJ5%2Fimage.png?alt=media&#x26;token=2bf73201-3dd7-4eb4-a1e5-af3892429eb3" alt=""><figcaption><p>Referencing the image ID</p></figcaption></figure>

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

{% code title="main.tf" %}

```hcl
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"
}
```

{% endcode %}

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:

```powershell
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
```

<figure><img src="https://1797977785-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjrIJ5xrJuOVgeeYdKNB5%2Fuploads%2FeQKXQ5Nq0OjHM7PWNEIt%2Fimage.png?alt=media&#x26;token=ad1a332f-71c3-49ef-a350-605537ae238b" alt=""><figcaption><p>Terraform Plan</p></figcaption></figure>

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

<figure><img src="https://1797977785-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjrIJ5xrJuOVgeeYdKNB5%2Fuploads%2Fzsxa7hwi0TFMyUGEplDD%2Fimage.png?alt=media&#x26;token=d548609c-5936-473d-87d9-e7b26ad3bf6d" alt=""><figcaption><p>Terraform Apply - Success</p></figcaption></figure>

<figure><img src="https://1797977785-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjrIJ5xrJuOVgeeYdKNB5%2Fuploads%2FC1MNt2y3nJFJEuPJW8ia%2Fimage.png?alt=media&#x26;token=f6161aee-1137-4538-9ab6-9f580204c088" alt=""><figcaption><p>Docker Processes</p></figcaption></figure>

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 [Docker Registry ](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs/resources/container#ports)to see the required schema:

<figure><img src="https://1797977785-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjrIJ5xrJuOVgeeYdKNB5%2Fuploads%2F00afr2ABmrK28YdxjDBL%2Fimage.png?alt=media&#x26;token=23959a12-0efd-4175-9f63-f9c0673750dd" alt=""><figcaption><p>Ports Schema</p></figcaption></figure>

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

{% code title="main.tf" %}

```hcl
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"
}
```

{% endcode %}

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.*

<figure><img src="https://1797977785-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjrIJ5xrJuOVgeeYdKNB5%2Fuploads%2Fveqk5eYdti1RASynZLTo%2Fimage.png?alt=media&#x26;token=dc6ff737-df81-4653-951f-ab8e76b2fc04" alt=""><figcaption><p>Updated Container</p></figcaption></figure>

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

<figure><img src="https://1797977785-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjrIJ5xrJuOVgeeYdKNB5%2Fuploads%2F3iLDheBtSbyHnV9BDFaA%2Fimage.png?alt=media&#x26;token=53cac939-6200-4aa0-94d5-f912da78c1ad" alt=""><figcaption><p>Nginx Default Page</p></figcaption></figure>

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.

{% code title="main.tf" %}

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

  ports {
    internal = 80
    external = 80
  }
}
```

{% endcode %}

{% code title="resource.tf" %}

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

{% endcode %}

{% code title="provider.tf" %}

```hcl
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"]
}
```

{% endcode %}

*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.

{% code title="vars.tf" overflow="wrap" %}

```hcl
variable "container_name" {
  description = "Docker Container Name"
  type = string
  default = "nginx_container"
}

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

{% endcode %}

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

{% code title="main.tf" %}

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

  ports {
    internal = 80
    external = 80
  }
}
```

{% endcode %}

{% code title="resource.tf" %}

```hcl
resource "docker_image" "nginx" {
  name = var.docker_image
}
```

{% endcode %}

```powershell
> 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

{% code title="vars.tf" %}

```hcl
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"
}
```

{% endcode %}

{% code title="resource.tf" %}

```hcl
resource "docker_image" "nginx" {
  name = "nginx:${var.nginx_image_version}"
}
# Use ${stuff} to add code inside of a string
```

{% endcode %}

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

```hcl
  # 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

{% code title="main.tf" %}

```hcl
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
  }
}
```

{% endcode %}

{% code title="vars.tf" %}

```hcl
# 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
  }
}
```

{% endcode %}

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.*

{% code title="vars.tf" %}

```hcl
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
  }
}
```

{% endcode %}

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

{% code title="main.tf" %}

```hcl
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
  }
}
```

{% endcode %}

{% code title="resource.tf" %}

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

{% endcode %}

*You can make this as complex or as simple as you'd like. If you want more info on how to use variables,* [*check out this blog*](https://spacelift.io/blog/how-to-use-terraform-variables)*.*

### 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

{% code title="vars.tf" %}

```hcl
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
  }
}
```

{% endcode %}

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

{% code title="resource.tf" %}

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

resource "docker_image" "tomcat" {
  name = "tomcat:${var.tomcat_container.version}"
}
```

{% endcode %}

{% code title="main.tf" %}

```hcl
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
  }
}
```

{% endcode %}

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

<figure><img src="https://1797977785-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjrIJ5xrJuOVgeeYdKNB5%2Fuploads%2FtJJsehuUxATVNHuBdZOQ%2Fimage.png?alt=media&#x26;token=7ef0118b-2478-42cc-a0c2-413d714c50b5" alt=""><figcaption><p>Deployed Nginx and Tomcat containers</p></figcaption></figure>

<figure><img src="https://1797977785-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjrIJ5xrJuOVgeeYdKNB5%2Fuploads%2FlWvdrYzbMTDavzK3QZXU%2Fimage.png?alt=media&#x26;token=47be4fde-1955-42e5-bcef-ebde5dd3b4f6" alt=""><figcaption><p>Nginx Default Page</p></figcaption></figure>

<figure><img src="https://1797977785-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FjrIJ5xrJuOVgeeYdKNB5%2Fuploads%2F9dKp4kee0VH6ng6VXRk3%2Fimage.png?alt=media&#x26;token=93c1e5bc-ef16-4f45-a972-e90c95bf77bb" alt=""><figcaption><p>Tomcat Error Page (But hey, tomcat :))</p></figcaption></figure>

#### 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.

{% code title="outputs.tf" %}

```hcl
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}"
}
```

{% endcode %}

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.

```sh
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

{% code title="build.ps1" %}

```powershell
terraform init # Initialises the directory if it has not been already
terraform fmt
terraform validate
terraform plan -out tfplan.out
terraform apply -auto-approve
```

{% endcode %}

{% code title="destroy.ps1" %}

```powershell
terraform destroy -auto-approve
rm tfplan.out
```

{% endcode %}

#### Makefile

<pre class="language-makefile" data-title="Makefile"><code class="lang-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
<strong>    rm tfplan.out
</strong></code></pre>
