Automating Local Development Infrastructure with Terraform: Deploying Traefik and Nginx Containers on Docker
Tags: terraform, infrastructureascode, iac, docker
Published: 2024-04-15 05:56:47

## Introduction

[Terraform](https://developer.hashicorp.com/terraform) is a `declarative` powerful `Infrastructure as Code (IaC)` tool used for provisioning on multiple cloud providers as well as locally on your machine (For instance, it can be used to deploy Docker containers for local development purposes).

Using Terraform to provision Traefik and Nginx for local development is a great way to manage and automate the setup of these services. With Terraform, you can define the infrastructure components needed for Traefik and Nginx (used simply for demo purpose), such as virtual machines, networks, and any other dependencies.
Simpler use cases, like local development with Traefik and Nginx, `Docker Compose` can be a more straightforward alternative to Terraform due to its focus on defining and running multi-container Docker applications.

> The [Terraform Registry](https://registry.terraform.io/) is the official repository for `Terraform providers`, `modules`, and `other community-contributed resources`. 
When using Terraform, you typically define providers in your configuration files to interact with various cloud providers, services, or platforms. These providers are often maintained by the community or the respective service providers themselves and are available for use through the Terraform Registry.

## What do i need ?
- Terraform installation (https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli)
- Docker (https://docs.docker.com/engine/install)

## Folder Structure
```
terraform
    └── module
         └── docker/
             └── nginx/
                 └── configs
                     └── html
                         ├── index.html
                     ├── nginx.conf
                 ├── nginx.tf
                 ├── nginx_output.tf
                 ├── variables.tf
             └── traefik/
                 └── configs
                     ├── traefik.yml
                 ├── traefik.tf
                 ├── traefik_outputs.tf
                 ├── variables.tf
        ├── main.tf
        ├── outputs.tf
        ├── providers.tf
        ├── variables.tf
```
## Core
`main.tf`
```terraform
terraform {
  // Need to add explicitly the provider because it is not being picked up by the parent module 
  // It is not from hashicorp/docker as it is used in the parent module
  required_providers {
    docker = {
      version = "~> 3.0.2"
      source  = "kreuzwerker/docker"
    }
  }
}

provider "docker" {
  host = "unix:///var/run/docker.sock"
}

resource "docker_network" "traefik" {
  name   = var.network_name
  driver = "bridge"
}

module "docker-nginx" {
  source = "./module/docker/nginx"

  network_name     = var.network_name
  nginx_name       = var.nginx_name
  nginx_image_name = var.nginx_image_name
}

module "docker-traefik" {
  source = "./module/docker/traefik"

  network_name       = var.network_name
  traefik_name       = var.traefik_name
  traefik_image_name = var.traefik_image_name
}
```

`outputs.tf`
```terraform
output "nginx_ip" {
  value = module.docker-nginx.nginx_ip
}

output "nginx_name" {
  value = module.docker-nginx.nginx_name
}

output "traefik_name" {
  value = module.docker-traefik.traefik_name
}

output "traefik_ip" {
  value = module.docker-traefik.traefik_ip
}
```

`providers.tf`
> **Pessimistic Constraint Operator** (~>): This operator specifies constraints on the versions of a software package that can be used. In this case, it allows only the rightmost version component (typically the patch version) to increment. This means it allows installing newer patch releases within a specific minor release but prevents upgrading to a newer minor or major release.

```terraform
terraform {
  required_version = "~> 1.7.0"
}
```

`variables.tf`
```terraform
variable "network_name" {}
variable "traefik_name" {}
variable "traefik_image_name" {}
variable "nginx_name" {}
variable "nginx_image_name" {}
```

## Modules
Terraform modules are reusable units of infrastructure configuration that encapsulate a set of resources and their configurations. They allow you to organize your Terraform code into smaller, more manageable components, promoting code reusability, modularity, and maintainability.

### Nginx module
`index.html` (webpage) and `nginx.conf` (configuration) are used to configure the nginx.

`nginx.tf`
```terraform
terraform {
  // Need to add explicitly the provider because it is not being picked up by the parent module 
  // It is not from hashicorp/docker as it is used in the parent module
  required_providers {
    docker = {
      version = "~> 3.0.2"
      source  = "kreuzwerker/docker"
    }
  }
}

provider "docker" {
  host = "unix:///var/run/docker.sock"
}

data "docker_registry_image" "nginx" {
  name = "nginx:latest"
}

resource "docker_image" "nginx" {
  name          = var.nginx_image_name
  pull_triggers = [data.docker_registry_image.nginx.sha256_digest]
  keep_locally  = false
}

resource "docker_container" "nginx" {
  name  = var.nginx_name
  image = docker_image.nginx.image_id

  restart               = "unless-stopped"
  destroy_grace_seconds = 30
  must_run              = true
  memory                = 256
  memory_swap           = 512

  networks_advanced {
    name    = var.network_name
    aliases = [var.network_name]
  }

  volumes {
    host_path      = "/Docker/module/docker/nginx/configs/html"
    container_path = "/usr/share/nginx/html"
  }

  volumes {
    host_path   = "/Docker/module/docker/nginx/configs/nginx.conf"
    container_path = "/etc/nginx/nginx.conf"
  }

  env = [
    "PUID=501",
    "PGID=20"
  ]

  ports {
    internal = 80
    external = 90
  }

  labels {
    # You can tell Traefik to consider (or not) the container by setting traefik.enable to true or false.
    # This option overrides the value of exposedByDefault.
    label = "traefik.enable"
    value = true
  }

  labels {
    label = "traefik.http.routers.${var.nginx_name}.rule"
    value = "PathPrefix(`/test`)"
  }

  labels {
    label = "traefik.http.routers.${var.nginx_name}.entrypoints"
    value = "web"
  }

  labels {
    # Overrides the default docker network to use for connections to the container.
    label = "traefik.docker.network"
    value = var.network_name
  }
}
```

`nginx_outputs.tf`
```terraform
output "nginx_ip" {
  value = docker_container.nginx.network_data[0].ip_address
}

output "nginx_name" {
  value = docker_container.nginx.name
}
```

`variables.tf`
```terraform
variable "nginx_name" {
  type    = string
  default = "nginx"
}

variable "nginx_image_name" {
  type    = string
  default = "nginx:latest"
}

variable "network_name" {
  type    = string
  default = "docknet"
}
```
### Traefik module
In the `config` folder, it is the config file for traefik because we have to add the docker providers.

`traefik.tf`
```terraform
terraform {
  // Need to add explicitly the provider because it is not being picked up by the parent module 
  // It is not from hashicorp/docker as it is used in the parent module
  required_providers {
    docker = {
      version = "~> 3.0.2"
      source  = "kreuzwerker/docker"
    }
  }
}

provider "docker" {
  host = "unix:///var/run/docker.sock"
}

data "docker_registry_image" "traefik" {
  name = "traefik:latest"
}

resource "docker_image" "traefik" {
  name          = var.traefik_image_name
  pull_triggers = [data.docker_registry_image.traefik.sha256_digest]
  keep_locally  = false
}

resource "docker_container" "traefik" {
  name  = var.traefik_name
  image = docker_image.traefik.image_id

  restart               = "unless-stopped"
  destroy_grace_seconds = 30
  must_run              = true
  memory                = 256
  memory_swap           = 512

  networks_advanced {
    name    = var.network_name
    aliases = [var.network_name]
  }

  command = [
    "--entrypoints.web.address=:86",
    "--log.level=DEBUG",
    "--entrypoints.websecure.http.tls=false",
    "--providers.docker=true",
    "--providers.docker.exposedbydefault=false",
    "--api"
  ]

  volumes {
    #volume_name    = "traefik_config"
    host_path      = "/Docker/module/docker/traefik/configs"
    container_path = "/etc/traefik"
  }

  volumes {
    host_path      = "/var/run/docker.sock"
    container_path = "/var/run/docker.sock"
  }

  ports {
    internal = 80
    external = 80
  }

  ports {
    internal = 8080
    external = 8080
  }

  ports {
    internal = 443
    external = 443
  }

  labels {
    label = "traefik.enable"
    value = true
  }

  labels {
    label = "traefik.docker.network"
    value = var.network_name
  }
}
```

`traefik_outputs.tf`
```terraform
output "traefik_name" {
  value = docker_container.traefik.name
}

output "traefik_ip" {
  value = docker_container.traefik.network_data[0].ip_address
}
```

`variables.tf`
```terraform
variable "traefik_image_name" {
  type    = string
  default = "traefik:latest"
}

variable "traefik_name" {
  type    = string
  default = "traefik"
}

variable "network_name" {
  type    = string
  default = "docknet"
}
```
## Terraform command
First of all, we need to launch the init command to initialize terraform by downloading the provider plugins and modules specified in your configuration files :
```terraform
terraform init
```

By running `terraform plan` before applying changes to your infrastructure with `terraform apply`, you can preview the changes that Terraform will make and ensure they align with your expectations, helping to prevent unintended or unexpected modifications to your infrastructure.

To apply this environment, you can launch the command (-auto-approve is used to "auto" validate the deployment) :
```terraform
terraform apply -auto-approve
```

Run `terraform destroy` to clean up all infrastructure deployed.


## Github code
Here is the link to the project : [Github page](https://github.com/axeldlv/terraform-docker-nginx-traefik)

## Thank you for reading! 
If you have any questions, feedback, or suggestions, please feel free to leave them in the comments below. I'm eager to hear from you and respond to your thoughts!