Turorial - Using Docker with Terraform
1. Objective
We will be deploying a small containerized application on our local Docker with Terraform.
2. Development setup
2.1. Verify your Terraform and Docker installation
docker --version
terraform version
It should be at least 1.6.0
2.2. Simplify your life
-
Install autocompletion for Terraform CLI.
terraform -install-autocomplete
-
Add an alias into your local
.bashrc
(mandatory).alias tf="terraform"
3. First Terraform resources
We will create our first Terraform resources and inspect the state file.
3.1. Requiring the Docker provider
The Docker provider reference is available at https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs
In order to use it, we must require it in our configuration.
Create a new file main.tf
and copy the following content. Remember, the name of the file doesn’t matter.
terraform { required_providers { docker = { source = "kreuzwerker/docker" version = "~> 3.0.2" } } }
The terraform
block has an embedded block required_providers
that contains a list of arguments.
Each argument key is our identifier for a provider, the value contains source
and version
arguments. The source
is the identifier on the registry registry.terraform.io/
, and the version
is a constraint on the version we require.
Then execute terraform init
on the console, the output should resemble the following:
$ terraform init Initializing the backend... Initializing provider plugins... - Finding kreuzwerker/docker versions matching "~> 3.0.2"... - Installing kreuzwerker/docker v3.0.2... - Installed kreuzwerker/docker v3.0.2 (self-signed, key ID BD080C4571C6104C) 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.
It has downloaded the required docker provider and created a lock file containing the version and hashes of our provider.
To see this more concretly, issue tree -a
to list all files.
$ tree -a . ├── main.tf ├── .terraform │ └── providers │ └── registry.terraform.io │ └── kreuzwerker │ └── docker │ └── 3.0.2 │ └── linux_amd64 │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ └── terraform-provider-docker_v3.0.2 └── .terraform.lock.hcl 8 directories, 6 files
3.2. Create a redis
docker image via Terraform
Bellow the terraform
block, copy the following in order to create a new managed resource with type docker_image
and name redis
:
resource "docker_image" "redis" { name = "docker.io/redis:6.0.5" }
The documentation of the docker_image
resources is available at https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs/resources/image.
In the Schema section, we see that only the name
argument is Required but many other Optional arguments can be configured.
Read-Only attributes are not configurable in .tf
files, their value is determined by the provider itself.
Issue now terraform plan
.
$ terraform plan 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_image.redis will be created + resource "docker_image" "redis" { + id = (known after apply) + image_id = (known after apply) + name = "docker.io/redis:6.0.5" + repo_digest = (known after apply) } Plan: 1 to add, 0 to change, 0 to destroy.
The leading +
sign means this resource will be created. The #
is a comment.
We see here that provider-defined attributes will finally be part of the resource state, but their value is unknown at plan time. This is not always the case, provider-attributes can be absent form the state, or they can have a known value at plan time.
Now execute terraform apply
. A deployment plan will appear as before, with an additional paragraph that prompts confirmation.
Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value:
Enter a plain yes
to effectively execute the current plan.
docker_image.redis: Creating... docker_image.redis: Still creating... [10s elapsed] docker_image.redis: Creation complete after 14s [id=sha256:2355926154447ec75b25666ff5df14d1ab54f8bb4abf731be2fcb818c7a7f145docker.io/redis:6.0.5] Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Check the state of Terraform resources.
$ terraform show # docker_image.redis: resource "docker_image" "redis" { id = "sha256:23559...docker.io/redis:6.0.5" image_id = "sha256:23559..." name = "docker.io/redis:6.0.5" repo_digest = "redis@sha256:800f25..." }
You can now guess why some attributes were computed by the provider itself.
Check that our image was actually created in our local Docker.
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE redis 6.0.5 235592615444 3 years ago 104MB
3.3. Create a docker container running our image
The reference here is https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs/resources/container.
Add the following resource
block to main.tf
.
resource "docker_container" "db" { name = "redis_db" image = docker_image.redis.image_id }
Here, both name
and image
are required. The value of image
refers to our previously declared resource with its type docker_image
and name redis
, and query its image_id
attribute.
Run terraform plan
and terraform apply
, then check with
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 5b3910075b61 235592615444 "docker-entrypoint.s…" 5 seconds ago Up 4 seconds 6379/tcp redis_db
3.4. Inspect the Terraform state
You can look directly in the JSON state file terraform.tfstate
or use terraform show
, but a more idiomatic way is to use the state
command.
Running terraform state
alone will output the help and show the available subcommands.
Let’s list all resources that our state knows about.
$ terraform state list docker_container.db docker_image.redis
Another useful command is terraform state show
to output the whole state of a given resource.
terraform state show docker_container.db
Do not hesitate to abuse those informational commands!
4. Deploy a 2-container application
We will be deploying an simple application that records "guests" (string from a text area) into a database, and show the list of all guests.
4.1. How it works
In addition to the previous redis database container, we have a frontend web server gb-frontend
that needs to know about the database.
It does so by first looking into the environment variable GET_HOSTS_FROM
which gives the access method, its value is either dns
or env
.
We will use the DNS method : the frontend will access the database through redis-leader
and redis-follower
hard-coded domain names.
There is two names because redis can be distributed among multiple nodes organised with a leader and followers. Here we will only have one container that is both leader and follower.
Thus, we must make sure redis-leader
and redis-follower
domain names points to our database container.
To do so, we will configure our frontend by adding a host in /etc/hosts
. Fortunately, the docker_container
resource have a host {}
block that does exactly that. We provide a host name and attach an IP.
Remember that nested blocks creates an array, so we can define multiple host
blocks.
Note
|
Execute terraform state show docker_container.db and seek for an attribute that resemble the IP of the container. We will then refer to this IP by the path of the attribute.
|
Finally, we must expose the container port 80
to our local 8080
in order to access it in the browser with localhost:8080
.
4.2. In practice
Download the archive gb-frontend.zip
of the frontend source code and extract it in your current directory.
Add the following to your main.tf
.
resource "docker_image" "guestbook-frontend" { name = "gb-frontend" build { context = "./gb-frontend/" } } resource "docker_container" "frontend" { name = "guestbook_frontend" image = docker_image.guestbook-frontend.image_id ports { internal = "80" external = "8080" } env = [ "GET_HOSTS_FROM=dns" ] host { host = "redis-leader" ip = docker_container.db.network_data[0].ip_address } host { host = "redis-follower" ip = docker_container.db.network_data[0].ip_address } }
Important
|
Make sure you understand the code. Ask questions if this is not the case. |
Plan and apply if everything seems good.
Access the application and test it : write something and Submit
. It should print your string below.
5. Output variable
It would be nice if our root module (here it is just main.tf
) would show us directly the endpoint of the application, once deployed.
For this purpose, add the following output
block to your configuration.
output "app_endpoint" { value = "http://localhost:${docker_container.frontend.ports[0].external}" description = "The URL endpoint to access the Guestbook application" }
Note the string interpolation with ${ }
marker. An expression inside it will be interpreted by Terraform, here we retrieve the external port of our frontend container.
Plan and apply again.
The result of applying now have an additional section.
Outputs: app_endpoint = "http://localhost:8080"
We can also show the output variables with
terraform output
We will see later how we can use outputs from other parts of our configuration.
6. Clean up
We are now done. Let’s delete our application with all its associated resource.
terraform destroy
Note
|
This command is somehow the reverse of apply . All managed resources in the configuration file (i.e. resource blocks in .tf files) are destroyed and removed from the state.
|
Verify with
docker state list
docker container ls -a
docker image ls -a.