Infrastructure as Code or IAC is one of the core pillars of effective platform engineering. And Terraform is a very popular IAC tool that makes it extremely easy to provision infrastructure.

In this post, you will learn how to use Terraform to provision a Nginx Docker Container.

This will help us explore the usage of Terraform Docker provider to create, modify and destroy Docker-based infrastructure.

1 – What is Terraform?

Before jumping into the code, let us understand some basics about Terraform.

Terraform is an IAC tool that lets you build, modify and version infrastructure resources. These resources can be on the cloud. They can also be on-premises.

To use Terraform, an engineer creates human readable configuration files that describe the various infrastructure pieces. Terraform processes these configuration file, checks whether everything makes sense and provisions the various resources.

Check out the below illustration that showcases the job of Terraform.

terraform infrastructure as code
How Terraform works?

The beauty of this approach is that you can commit the configuration files into a source repository just like your application code. There are several advantages to this:

  • You can review and release infrastructure changes just like normal application code
  • Every detail about the infrastructure is available in a centralized repository. You don’t need to rely on obscure documentation
  • You can create and destroy infrastructure whenever you want without biting off your nails in worry

If interested, you can read more about infrastructure as code vs traditional infrastructure.

2 – Terraform Nginx Docker Container Config File

Let us start with the project.

First, create a project directory for keeping the Terraform code files. A directory also acts as a default Terraform workspace.

$ mkdir terraform-docker-demo
$ cd terraform-docker-demo

Within the folder, create a file named main.tf and paste the below code.

terraform {
    required_providers {
        docker = {
            source = "kreuzwerker/docker"
            version = "~> 2.13.0"
        }
    }
}

provider "docker" {}

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

resource "docker_container" "nginx" {
    image = docker_image.nginx.latest
    name = "terraform-nginx-demo"
    ports {
        internal = 80
        external = 8000
    }
}

Terraform uses a special language known as HCL or Hashicorp Configuration Language. HCL is a unique language designed to work with Hashicorp tools. The language looks quite similar to JSON but is more human readable than JSON.

What is going on inside the file?

You can see several blocks of code. Each block has a specific purpose.

  • The terraform {} block contains Terraform settings. This includes details about the providers needed by Terraform to provision the infrastructure. Think of providers as the glue between Terraform and the actual platform such as Docker, AWS, Azure and so on. For our example, we use the provider for Docker named kreuzwerker/docker. This provider will be downloaded from the Terraform registry.
  • In the provider {} block, we configure the provider. In this case, the provider is Docker and does not contain any specific configuration. For something like the AWS provider, you might want to configure additional properties such as the region and credentials.
  • The next block is the resource {} block. This is where you describe the actual resources that you want to provision. For our example, we have two such blocks. First one defines the nginx Docker image. The second block defines the Docker container to run the image.

3 – Executing the Terraform Configuration File

Once the file is ready, you need to tell Terraform to actually provision the infrastructure.

To do so, execute the command terraform init within the root of the project directory. This command scans the configuration files and installs the Terraform providers from the registry.

Check out the below output from the init command.

Initializing the backend...

Initializing provider plugins...
- Finding kreuzwerker/docker versions matching "~> 2.13.0"...
- Installing kreuzwerker/docker v2.13.0...
- Installed kreuzwerker/docker v2.13.0 (self-signed, key ID 24E54F214569A8A5)

Partner and community providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
<https://www.terraform.io/docs/cli/plugins/signing.html>

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

At this point, only the preparations have been made. No infrastructure is actually provisioned.

The terraform init command only initializes the working directory that contains the configuration files. You should run this command whenever you write a new Terraform configuration or clone an existing one from a source repository. Also, it is safe to run the terraform init command multiple times since it is idempotent.

To create the infrastructure, run the command terraform apply. When you run this command, Terraform describes the activities it is going to perform. It will also ask whether you want to continue.

Check out the below output:

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # docker_container.nginx will be created
  + resource "docker_container" "nginx" {
      + attach           = false
      + bridge           = (known after apply)
      + command          = (known after apply)
      + container_logs   = (known after apply)
      + entrypoint       = (known after apply)
      + env              = (known after apply)
      + exit_code        = (known after apply)
      + gateway          = (known after apply)
      + hostname         = (known after apply)
      + id               = (known after apply)
      + image            = (known after apply)
      + init             = (known after apply)
      + ip_address       = (known after apply)
      + ip_prefix_length = (known after apply)
      + ipc_mode         = (known after apply)
      + log_driver       = "json-file"
      + logs             = false
      + must_run         = true
      + name             = "terraform-nginx-demo"
      + network_data     = (known after apply)
      + read_only        = false
      + remove_volumes   = true
      + restart          = "no"
      + rm               = false
      + security_opts    = (known after apply)
      + shm_size         = (known after apply)
      + start            = true
      + stdin_open       = false
      + tty              = false

      + healthcheck {
          + interval     = (known after apply)
          + retries      = (known after apply)
          + start_period = (known after apply)
          + test         = (known after apply)
          + timeout      = (known after apply)
        }

      + labels {
          + label = (known after apply)
          + value = (known after apply)
        }

      + ports {
          + external = 8000
          + internal = 80
          + ip       = "0.0.0.0"
          + protocol = "tcp"
        }
    }

  # docker_image.nginx will be created
  + resource "docker_image" "nginx" {
      + id           = (known after apply)
      + keep_locally = false
      + latest       = (known after apply)
      + name         = "nginx:latest"
      + output       = (known after apply)
      + repo_digest  = (known after apply)
    }

Plan: 2 to add, 0 to change, 0 to destroy.
╷
│ Warning: Deprecated attribute
│ 
│   on main.tf line 18, in resource "docker_container" "nginx":
│   18:     image = docker_image.nginx.latest
│ 
│ The attribute "latest" is deprecated. Refer to the provider documentation for
│ details.
│ 
│ (and one more similar warning elsewhere)
╵

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

When we enter “yes” to the prompt, Terraform will go ahead and provision the infrastructure. See below output:

docker_image.nginx: Creating...
docker_image.nginx: Still creating... [10s elapsed]
docker_image.nginx: Creation complete after 15s [id=sha256:e7a312b21448316ac5bdc7f4b1ee5ab9e06ac7638bac879263b750d0bedb3633nginx:latest]
docker_container.nginx: Creating...
docker_container.nginx: Creation complete after 0s [id=cecec711162626f4655fcf92d7b2e3e4cee128bbc3dbc5c469f8976706688dc5]
╷
│ Warning: Deprecated attribute
│ 
│   on main.tf line 18, in resource "docker_container" "nginx":
│   18:     image = docker_image.nginx.latest
│ 
│ The attribute "latest" is deprecated. Refer to the provider documentation for
│ details.
│ 
│ (and one more similar warning elsewhere)
╵

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

You can also confirm that Terraform did its job by executing docker ps command to get the list of running containers.

CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                  NAMES
cecec7111626   e7a312b21448   "/docker-entrypoint.…"   30 seconds ago   Up 29 seconds   0.0.0.0:8000->80/tcp   terraform-nginx-demo

As you can see, the Nginx Docker Container was provisioned successfully. You can also visit http://localhost:8000 to make sure that the container is serving requests.

terraform nginx docker container

4 – Modifying the Terraform Managed Infrastructure

Creating infrastructure with Terraform works like a charm.

But how about modifying the infrastructure? After all, it is reasonable to assume that you will be modifying stuff more than creating.

Assume you want to update the port on which Nginx is listening from 8000 to 8080. You can simply make the appropriate change in the main.tf file as below:

resource "docker_container" "nginx" {
    image = docker_image.nginx.latest
    name = "terraform-nginx-demo"
    ports {
        internal = 80
        external = 8080
    }
}

The external port is now set to 8080.

Since you did not change the provider, you don’t need to run the terraform init command. Go ahead and directly execute terraform apply.

Check out the below output:

# docker_container.nginx must be replaced
-/+ resource "docker_container" "nginx" {
      + bridge            = (known after apply)
      ~ command           = [
          - "nginx",
          - "-g",
          - "daemon off;",
        ] -> (known after apply)
      + container_logs    = (known after apply)
      - cpu_shares        = 0 -> null
      - dns               = [] -> null
      - dns_opts          = [] -> null
      - dns_search        = [] -> null
      ~ entrypoint        = [
          - "/docker-entrypoint.sh",
        ] -> (known after apply)
      ~ env               = [] -> (known after apply)
      + exit_code         = (known after apply)
      ~ gateway           = "172.17.0.1" -> (known after apply)
      - group_add         = [] -> null
      ~ hostname          = "635558eaf250" -> (known after apply)
      ~ id                = "635558eaf250ae922fb98e736cad1075d98d1f3f031b7d5d0c99979fdebf0b0f" -> (known after apply)
      ~ init              = false -> (known after apply)
      ~ ip_address        = "172.17.0.2" -> (known after apply)
      ~ ip_prefix_length  = 16 -> (known after apply)
      ~ ipc_mode          = "private" -> (known after apply)
      - links             = [] -> null
      - log_opts          = {} -> null
      - max_retry_count   = 0 -> null
      - memory            = 0 -> null
      - memory_swap       = 0 -> null
        name              = "terraform-nginx-demo"

      ~ ports {
          ~ external = 8000 -> 8080 # forces replacement
            # (3 unchanged attributes hidden)
        }
    }

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

Notice the -/+ resource "docker_container" "nginx" statement in the beginning. It means that Terraform concludes that the required change can be performed only by destroying and re-creating the container. Of course, all of this is handled seamlessly by the Docker provider based on the resource type it is dealing with.

On answering “yes” to the prompt, Terraform will go ahead and destroy the Nginx container and immediately spawn a new one to take its place. See below output:

docker_container.nginx: Destroying... [id=635558eaf250ae922fb98e736cad1075d98d1f3f031b7d5d0c99979fdebf0b0f]
docker_container.nginx: Destruction complete after 0s
docker_container.nginx: Creating...
docker_container.nginx: Creation complete after 0s [id=68045124d1c6648ee623046960f403a9605c410684ce7faae262b579dcc72275]
╷
│ Warning: Deprecated attribute
│ 
│   on main.tf line 18, in resource "docker_container" "nginx":
│   18:     image = docker_image.nginx.latest
│ 
│ The attribute "latest" is deprecated. Refer to the provider documentation for details.
│ 
│ (and one more similar warning elsewhere)
╵

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

5 – Destroying the Terraform Managed Infrastructure

You can also permanently destroy the infrastructure provisioned by Terraform.

To do so, use the command terraform destroy. Check out the below output generated by the command.

docker_container.nginx: Destroying... [id=cecec711162626f4655fcf92d7b2e3e4cee128bbc3dbc5c469f8976706688dc5]
docker_container.nginx: Destruction complete after 0s
docker_image.nginx: Destroying... [id=sha256:e7a312b21448316ac5bdc7f4b1ee5ab9e06ac7638bac879263b750d0bedb3633nginx:latest]
docker_image.nginx: Destruction complete after 0s

Destroy complete! Resources: 2 destroyed.

After prompting for an approval, Terraform proceeds to remove the Nginx container as well as the image.

Conclusion

Terraform is a complete solution to manage the infrastructure of our application.

The Hashicorp Configuration Language lets us declare our infrastructure needs in a programmatic manner. Then, you can use Terraform to manage the entire lifecycle of your infrastructure.

The code for Terraform Nginx Docker Container is available on Github for reference.

In the next post, we will look at provisioning an AWS EC2 instance using Terraform configuration files.

Also, if you want to deploy Nginx on EC2 instance, check out this post on using Terraform to deploy Nginx webserver on EC2 instance.

If you found this post to be useful, consider sharing it with friends and colleagues.


Saurabh Dashora

Saurabh is a Software Architect with over 12 years of experience. He has worked on large-scale distributed systems across various domains and organizations. He is also a passionate Technical Writer and loves sharing knowledge in the community.

0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *