Working with Terraform for managing Docker
- Pre-requisite
- Create an EC2 – Please refer this link
- Install Docker – Please refer this link
- Install Terraform – Please refer this link
- Terraform Commands:
- List the Terraform commands:
terraform
Common commands:
apply
: Builds or changes infrastructure
console
: Interactive console for Terraform interpolations
destroy
: Destroys Terraform-managed infrastructure
fmt
: Rewrites configuration files to canonical format
get
: Downloads and installs modules for the configuration
graph
: Creates a visual graph of Terraform resources
import
: Imports existing infrastructure into Terraform
init
: Initializes a new or existing Terraform configuration
output
: Reads an output from a state file
plan
: Generates and shows an execution plan
providers
: Prints a tree of the providers used in the configuration
push
: Uploads this Terraform module to Terraform Enterprise to run
refresh
: Updates local state file against real resources
show
: Inspects Terraform state or plan
taint
: Manually marks a resource for recreation
untaint
: Manually unmarks a resource as tainted
validate
: Validates the Terraform files
version
: Prints the Terraform version
workspace
: Workspace management - Set up the environment:
mkdir -p terraform/basics cd terraform/basics
- Create a Terraform script:
vi main.tf
main.tf contents:
# Download the latest Ghost image resource "docker_image" "image_id" { name = "ghost:latest" } # Start the Container resource "docker_container" "container_id" { name = "ghost_blog" image = "${docker_image.image_id.latest}" ports { internal = "2368" external = "80" } }
- Initialize Terraform:
terraform init
- Validate the Terraform file:
terraform validate
- List providers in the folder:
ls .terraform/plugins/linux_amd64/
- List providers used in the configuration:
terraform providers
- Terraform Plan:
terraform plan
Useful flags for
plan
:
-out=path
: Writes a plan file to the given path. This can be used as input to the “apply” command.
-var 'foo=bar'
: Set a variable in the Terraform configuration. This flag can be set multiple times. - Terraform Apply:
terraform apply
Useful flags for
apply
:
-auto-approve
: This skips interactive approval of plan before applying.
-var 'foo=bar'
: This sets a variable in the Terraform configuration. It can be set multiple times.Confirm your apply by typing
yes
. The apply will take a bit to complete. - List the Docker images:
docker image ls
- Terraform Show:
terraform show
- Terraform Destroy:
terraform destroy
Useful flags for destroys:
-auto-approve
: Skip interactive approval of plan before applying.
Confirm your destroy by typingyes
. - Re-list the Docker images:
docker image ls
- Using a plan:
terraform plan -out=tfplan
- Applying a plan:
terraform apply tfplan
- Show the Docker Image resource:
terraform show
- Destroy the resource once again:
terraform destroy
- Open Ghost blog using Docker IP
- List the Terraform commands:
- Tainting and Updating Resources
- Tainting a resource:
terraform taint docker_container.container_id
Check tainted resource which will be recreated using
terraform plan
- Untainting a resource:
terraform untaint docker_container.container_id
- Let’s edit
main.tf
and change the image toghost:alpine
.vi main.tf
replace the existing content with below one
# Download the latest Ghost image resource "docker_image" "image_id" { name = "ghost:alpine" } # Start the Container resource "docker_container" "container_id" { name = "ghost_blog" image = "${docker_image.image_id.latest}" ports { internal = "2368" external = "80" } }
- Now do a terraform validate, plan & apply
terraform validate terraform plan terraform apply
- Check the new image id by using
docker images ls
- Reset the environment:
terraform destroy
Confirm the
destroy
by typing yes.
List the Docker images:docker image ls
Remove the Ghost blog image:
docker image rm ghost:latest
Reset
main.tf
:vi main.tf
main.tf
contents:# Download the latest Ghost image resource "docker_image" "image_id" { name = "ghost:latest" } # Start the Container resource "docker_container" "container_id" { name = "ghost_blog" image = "${docker_image.image_id.latest}" ports { internal = "2368" external = "80" } }
- Tainting a resource:
- Terraform Console and Output
- Redeploy the Ghost image and container:
terraform apply
- Show the Terraform resources:
terraform show
- Start the Terraform console:
terraform console
- Type the following in the console to get the container’s name:
docker_container.container_id.name
- Type the following in the console to get the container’s IP:
docker_container.container_id.ip_address
Break out of the Terraform console by using Ctrl+C.
- Destroy the environment:
terraform destroy
- Edit
main.tf
:vi main.tf
main.tf
contents:# Download the latest Ghost Image resource "docker_image" "image_id" { name = "ghost:latest" } # Start the Container resource "docker_container" "container_id" { name = "blog" image = "${docker_image.image_id.latest}" ports { internal = "2368" external = "80" } } #Output the IP Address of the Container output "ip_address" { value = "${docker_container.container_id.ip_address}" description = "The IP for the container." } #Output the Name of the Container output "container_name" { value = "${docker_container.container_id.name}" description = "The name of the container." }
- Validate & Apply changes:
terraform validate terraform apply
- Reset the environment:
terraform destroy
- Redeploy the Ghost image and container:
- Input Variables
- Edit
main.tf
:vi main.tf
main.tf
contents:#Define variables variable "image_name" { description = "Image for container." default = "ghost:latest" } variable "container_name" { description = "Name of the container." default = "blog" } variable "int_port" { description = "Internal port for container." default = "2368" } variable "ext_port" { description = "External port for container." default = "80" } # Download the latest Ghost Image resource "docker_image" "image_id" { name = "${var.image_name}" } # Start the Container resource "docker_container" "container_id" { name = "${var.container_name}" image = "${docker_image.image_id.latest}" ports { internal = "${var.int_port}" external = "${var.ext_port}" } } #Output the IP Address of the Container output "ip_address" { value = "${docker_container.container_id.ip_address}" description = "The IP for the container." } output "container_name" { value = "${docker_container.container_id.name}" description = "The name of the container." }
- Validate the changes:
terraform validate
- Plan the changes:
terraform plan
- Apply the changes using a variable:
terraform apply -var 'ext_port=8080'
- Change the container name:
terraform apply -var 'container_name=ghost_blog' -var 'ext_port=8080'
- Reset the environment:
terraform destroy -var 'ext_port=8080'
- Edit
- Breaking Out Our Variables and Outputs
- Edit
variables.tf
:vi variables.tf
variables.tf
contents:#Define variables variable "container_name" { description = "Name of the container." default = "blog" } variable "image_name" { description = "Image for container." default = "ghost:latest" } variable "int_port" { description = "Internal port for container." default = "2368" } variable "ext_port" { description = "External port for container." default = "80" }
- Edit
main.tf
:vi main.tf
main.tf
contents:# Download the latest Ghost Image resource "docker_image" "image_id" { name = "${var.image_name}" } # Start the Container resource "docker_container" "container_id" { name = "${var.container_name}" image = "${docker_image.image_id.latest}" ports { internal = "${var.int_port}" external = "${var.ext_port}" } }
- Edit
outputs.tf
:vi outputs.tf
outputs.tf
contents:#Output the IP Address of the Container output "ip_address" { value = "${docker_container.container_id.ip_address}" description = "The IP for the container." } output "container_name" { value = "${docker_container.container_id.name}" description = "The name of the container." }
- Validate the changes:
terraform validate
- Plan the changes:
terraform plan -out=tfplan -var container_name=ghost_blog
- Apply the changes:
terraform apply tfplan
- Destroy deployment:
terraform destroy -auto-approve -var container_name=ghost_blog
- Edit
- Maps and Lookups
- Edit
variables.tf
:vi variables.tf
variables.tf
contents:#Define variables variable "env" { description = "env: dev or prod" } variable "image_name" { type = "map" description = "Image for container." default = { dev = "ghost:latest" prod = "ghost:alpine" } } variable "container_name" { type = "map" description = "Name of the container." default = { dev = "blog_dev" prod = "blog_prod" } } variable "int_port" { description = "Internal port for container." default = "2368" } variable "ext_port" { type = "map" description = "External port for container." default = { dev = "8081" prod = "80" } }
- Validate the change:
terraform validate
- Edit
main.tf
:vi main.tf
main.tf
contents:# Download the latest Ghost Image resource "docker_image" "image_id" { name = "${lookup(var.image_name, var.env)}" } # Start the Container resource "docker_container" "container_id" { name = "${lookup(var.container_name, var.env)}" image = "${docker_image.image_id.latest}" ports { internal = "${var.int_port}" external = "${lookup(var.ext_port, var.env)}" } }
- Plan the
dev
deploy:terraform plan -out=tfdev_plan -var env=dev
- Apply the
dev
plan:terraform apply tfdev_plan
- Plan the
prod
deploy:terraform plan -out=tfprod_plan -var env=prod
- Apply the
prod
plan:terraform apply tfprod_plan
- Destroy
prod
deployment:terraform destroy -var env=prod -auto-approve
- Use environment variables:
export TF_VAR_env=prod
- Open the Terraform console:
terraform console
- Execute a lookup:
lookup(var.ext_port, var.env)
- Exit the console:
unset TF_VAR_env
- Edit
- Workspaces
- Create a
dev
workspace:terraform workspace new dev
- Plan the
dev
deployment:terraform plan -out=tfdev_plan -var env=dev
- Apply the
dev
deployment:terraform apply tfdev_plan
- Change workspaces:
terraform workspace new prod
- Plan the
prod
deployment:terraform plan -out=tfprod_plan -var env=prod
- Apply the
prod
deployment:terraform apply tfprod_plan
- Select the default workspace:
terraform workspace select default
- Find what workspace we are using:
terraform workspace show
- Select the
dev
workspace:terraform workspace select dev
- Destroy the
dev
deployment:terraform destroy -var env=dev
- Select the
prod
workspace:terraform workspace select prod
- Destroy the
prod
deployment:terraform destroy -var env=prod
- Create a
- Null Resources and Local-exec
- Edit
main.tf
:vi main.tf
main.tf
contents:# Download the latest Ghost Image resource "docker_image" "image_id" { name = "${lookup(var.image_name, var.env)}" } # Start the Container resource "docker_container" "container_id" { name = "${lookup(var.container_name, var.env)}" image = "${docker_image.image_id.latest}" ports { internal = "${var.int_port}" external = "${lookup(var.ext_port, var.env)}" } } resource "null_resource" "null_id" { provisioner "local-exec" { command = "echo ${docker_container.container_id.name}:${docker_container.container_id.ip_address} >> container.txt" } }
- Reinitialize Terraform:
terraform init
- Validate the changes:
terraform validate
- Plan the changes:
terraform plan -out=tfplan -var env=dev
- Apply the changes:
terraform apply tfplan
- View the contents of
container.txt
:cat container.txt
- Destroy the deployment:
terraform destroy -auto-approve -var env=dev
- Edit
- Terraform Modules
- Basic Module Directory Setup
- Set up the environment:
mkdir -p modules/image mkdir -p modules/container
- Create files for the image:
cd ~/terraform/basics/modules/image touch main.tf variables.tf outputs.tf
- Create files for container:
cd ~/terraform/basics/modules/contain
- Set up the environment:
- The Image Module
- Go to the image directory:
cd ~/terraform/basics/modules/image
- Edit
main.tf
:vi main.tf
main.tf
contents:# Download the Image resource "docker_image" "image_id" { name = "${var.image_name}" }
- Edit
variables.tf
:vi variables.tf
variables.tf
contents:variable "image_name" { description = "Name of the image" }
- Edit
outputs.tf
:vi outputs.tf
outputs.tf:
contents:output "image_out" { value = "${docker_image.image_id.latest}" }
- Initialize Terraform:
terraform init
- Create the image plan:
terraform plan -out=tfplan -var 'image_name=ghost:alpine'
- Deploy the image using the plan:
terraform apply -auto-approve tfplan
- Destroy the image:
terraform destroy -auto-approve -var 'image_name=ghost:alpine'
- Go to the image directory:
- The Container Module
- Go to the container directory:
cd ~/terraform/basics/modules/container
- Edit
main.tf
:vi main.tf
main.tf
contents:# Start the Container resource "docker_container" "container_id" { name = "${var.container_name}" image = "${var.image}" ports { internal = "${var.int_port}" external = "${var.ext_port}" } }
- Edit
variables.tf
:vi variables.tf
variables.tf
contents:#Define variables variable "container_name" {} variable "image" {} variable "int_port" {} variable "ext_port" {}
- Edit
outputs.tf
:vi outputs.tf
outputs.tf
contents:#Output the IP Address of the Container output "ip" { value = "${docker_container.container_id.ip_address}" } output "container_name" { value = "${docker_container.container_id.name}" }
- Initialize:
terraform init
- Create the image plan:
terraform plan -out=tfplan -var 'container_name=blog' -var 'image=ghost:alpine' -var 'int_port=2368' -var 'ext_port=80'
- Deploy container using the plan:
terraform apply tfplan
- Go to the container directory:
- The Root Module
- Go to the module directory:
cd ~/terraform/basics/modules/ touch {main.tf,variables.tf,outputs.tf}
- Edit
main.tf
:vi main.tf
main.tf
contents:# Download the image module "image" { source = "./image" image_name = "${var.image_name}" } # Start the container module "container" { source = "./container" image = "${module.image.image_out}" container_name = "${var.container_name}" int_port = "${var.int_port}" ext_port = "${var.ext_port}" }
- Edit
variables.tf
:vi variables.tf
variables.tf
contents:#Define variables variable "container_name" { description = "Name of the container." default = "blog" } variable "image_name" { description = "Image for container." default = "ghost:latest" } variable "int_port" { description = "Internal port for container." default = "2368" } variable "ext_port" { description = "External port for container." default = "80" }
- Edit
outputs.tf
:vi outputs.tf
outputs.tf
contents:#Output the IP Address of the Container output "ip" { value = "${module.container.ip}" } output "container_name" { value = "${module.container.container_name}" }
- Initialize Terraform:
terraform init
- Create the image plan:
terraform plan -out=tfplan
- Deploy the container using the plan:
terraform apply tfplan
- Destroy the deployment:
terraform destroy -auto-approve
- Go to the module directory:
- Basic Module Directory Setup
- Managing Docker Networks
- Set up the environment:
mkdir -p ~/terraform/docker/networks cd terraform/docker/networks
- Create the files:
touch {variables.tf,image.tf,network.tf,main.tf}
- Edit
variables.tf
:vi variables.tf
variables.tf
contents:variable "mysql_root_password" { description = "The MySQL root password." default = "P4sSw0rd0!" } variable "ghost_db_username" { description = "Ghost blog database username." default = "root" } variable "ghost_db_name" { description = "Ghost blog database name." default = "ghost" } variable "mysql_network_alias" { description = "The network alias for MySQL." default = "db" } variable "ghost_network_alias" { description = "The network alias for Ghost" default = "ghost" } variable "ext_port" { description = "Public port for Ghost" default = "8080" }
- Edit
image.tf
:vi image.tf
image.tf
contents:resource "docker_image" "ghost_image" { name = "ghost:alpine" } resource "docker_image" "mysql_image" { name = "mysql:5.7" }
- Edit
network.tf
:vi network.tf
network.tf
contents:resource "docker_network" "public_bridge_network" { name = "public_ghost_network" driver = "bridge" } resource "docker_network" "private_bridge_network" { name = "ghost_mysql_internal" driver = "bridge" internal = true }
- Edit
main.tf
:vi main.tf
main.tf
contents:resource "docker_container" "blog_container" { name = "ghost_blog" image = "${docker_image.ghost_image.name}" env = [ "database__client=mysql", "database__connection__host=${var.mysql_network_alias}", "database__connection__user=${var.ghost_db_username}", "database__connection__password=${var.mysql_root_password}", "database__connection__database=${var.ghost_db_name}" ] ports { internal = "2368" external = "${var.ext_port}" } networks_advanced { name = "${docker_network.public_bridge_network.name}" aliases = ["${var.ghost_network_alias}"] } networks_advanced { name = "${docker_network.private_bridge_network.name}" aliases = ["${var.ghost_network_alias}"] } } resource "docker_container" "mysql_container" { name = "ghost_database" image = "${docker_image.mysql_image.name}" env = [ "MYSQL_ROOT_PASSWORD=${var.mysql_root_password}" ] networks_advanced { name = "${docker_network.private_bridge_network.name}" aliases = ["${var.mysql_network_alias}"] } }
- Initialize Terraform:
terraform init
- Validate the files:
terraform validate
- Build a plan:
terraform plan -out=tfplan -var 'ext_port=8082'
- Apply the plan:
terraform apply tfplan
- Destroy the environment:
terraform destroy -auto-approve -var 'ext_port=8082'
-
Fixing
main.tf
main.tf
contents:resource "docker_container" "mysql_container" { name = "ghost_database" image = "${docker_image.mysql_image.name}" env = [ "MYSQL_ROOT_PASSWORD=${var.mysql_root_password}" ] networks_advanced { name = "${docker_network.private_bridge_network.name}" aliases = ["${var.mysql_network_alias}"] } } resource "null_resource" "sleep" { depends_on = ["docker_container.mysql_container"] provisioner "local-exec" { command = "sleep 15s" } } resource "docker_container" "blog_container" { name = "ghost_blog" image = "${docker_image.ghost_image.name}" depends_on = ["null_resource.sleep", "docker_container.mysql_container"] env = [ "database__client=mysql", "database__connection__host=${var.mysql_network_alias}", "database__connection__user=${var.ghost_db_username}", "database__connection__password=${var.mysql_root_password}", "database__connection__database=${var.ghost_db_name}" ] ports { internal = "2368" external = "${var.ext_port}" } networks_advanced { name = "${docker_network.public_bridge_network.name}" aliases = ["${var.ghost_network_alias}"] } networks_advanced { name = "${docker_network.private_bridge_network.name}" aliases = ["${var.ghost_network_alias}"] } }
- Build a plan:
terraform plan -out=tfplan -var 'ext_port=8082'
- Apply the plan:
terraform apply tfplan
- Set up the environment:
- Managing Docker Volumes
- Setup an environment:
cp -r ~/terraform/docker/networks ~/terraform/docker/volumes cd ../volumes/
- Create
volumes.tf
:vi volumes.tf
volumes.tf
contents:resource "docker_volume" "mysql_data_volume" { name = "mysql_data" }
- Edit
main.tf
:vi main.tf
main.tf
contents:resource "docker_container" "mysql_container" { name = "ghost_database" image = "${docker_image.mysql_image.name}" env = [ "MYSQL_ROOT_PASSWORD=${var.mysql_root_password}" ] volumes { volume_name = "${docker_volume.mysql_data_volume.name}" container_path = "/var/lib/mysql" } networks_advanced { name = "${docker_network.private_bridge_network.name}" aliases = ["${var.mysql_network_alias}"] } } resource "null_resource" "sleep" { depends_on = ["docker_container.mysql_container"] provisioner "local-exec" { command = "sleep 15s" } } resource "docker_container" "blog_container" { name = "ghost_blog" image = "${docker_image.ghost_image.name}" depends_on = ["null_resource.sleep", "docker_container.mysql_container"] env = [ "database__client=mysql", "database__connection__host=${var.mysql_network_alias}", "database__connection__user=${var.ghost_db_username}", "database__connection__password=${var.mysql_root_password}", "database__connection__database=${var.ghost_db_name}" ] ports { internal = "2368" external = "${var.ext_port}" } networks_advanced { name = "${docker_network.public_bridge_network.name}" aliases = ["${var.ghost_network_alias}"] } networks_advanced { name = "${docker_network.private_bridge_network.name}" aliases = ["${var.ghost_network_alias}"] } }
- Initialize Terraform:
terraform init
- Validate the files:
terraform validate
- Build a plan:
terraform plan -out=tfplan -var 'ext_port=8082'
- Apply the plan:
terraform apply tfplan
- List Docker volumes:
docker volume inspect mysql_data
- List the data in
mysql_data
:sudo ls /var/lib/docker/volumes/mysql_data/_data
- Destroy the environment:
terraform destroy -auto-approve -var 'ext_port=8082'
- Setup an environment:
- Using Secrets
- Setup the environment:
mkdir secrets cd secrets
- Encode the password with Base64:
echo 'p4sSWoRd0!' | base64
- Create
variables.tf
:vi variables.tf
variables.tf
contents:variable "mysql_root_password" { default = "cDRzU1dvUmQwIQo=" } variable "mysql_db_password" { default = "cDRzU1dvUmQwIQo=" }
- Create
image.tf
:vi image.tf
image.tf
contents:resource "docker_image" "mysql_image" { name = "mysql:5.7" }
- Create
secrets.tf
:vi secrets.tf
secrets.tf
contents:resource "docker_secret" "mysql_root_password" { name = "root_password" data = "${var.mysql_root_password}" } resource "docker_secret" "mysql_db_password" { name = "db_password" data = "${var.mysql_db_password}" }
- Create
networks.tf
:vi networks.tf
networks.tf
contents:resource "docker_network" "private_overlay_network" { name = "mysql_internal" driver = "overlay" internal = true }
- Create
volumes.tf
:vi volumes.tf
volumes.tf
contents:resource "docker_volume" "mysql_data_volume" { name = "mysql_data" }
- Create
main.tf
:vi main.tf
main.tf
contents:resource "docker_service" "mysql-service" { name = "mysql_db" task_spec { container_spec { image = "${docker_image.mysql_image.name}" secrets = [ { secret_id = "${docker_secret.mysql_root_password.id}" secret_name = "${docker_secret.mysql_root_password.name}" file_name = "/run/secrets/${docker_secret.mysql_root_password.name}" }, { secret_id = "${docker_secret.mysql_db_password.id}" secret_name = "${docker_secret.mysql_db_password.name}" file_name = "/run/secrets/${docker_secret.mysql_db_password.name}" } ] env { MYSQL_ROOT_PASSWORD_FILE = "/run/secrets/${docker_secret.mysql_root_password.name}" MYSQL_DATABASE = "mydb" MYSQL_PASSWORD_FILE = "/run/secrets/${docker_secret.mysql_db_password.name}" } mounts = [ { target = "/var/lib/mysql" source = "${docker_volume.mysql_data_volume.name}" type = "volume" } ] } networks = [ "${docker_network.private_overlay_network.name}" ] } }
- Initialize Terraform:
terraform init
- Validate the files:
terraform validate
- Build a plan:
terraform plan -out=tfplan
- Apply the plan:
terraform apply tfplan
- Find the MySQL container:
docker container ls
- Use the
exec
command to log into the MySQL container:docker container exec -it [CONTAINER_ID] /bin/bash
- Access MySQL:
mysql -u root -p
- Destroy the environment:
terraform destroy -auto-approve
- Setup the environment:
- Terraform Remote State
-
Create an S3 Bucket
-
Search for S3 in Find Services.
-
Click Create Bucket.
-
Enter a Bucket name. The bucket name must be unique.
-
Make sure the Region is US East (N. Virginia) Click Next.
-
Click Next again on the Configure options page.
-
Click Next again on the Set permissions page.
-
Click Create bucket on the Review page.
-
-
Add the Terraform Folder to the Bucket
-
Click on the bucket name.
-
Click Create folder.
-
Enter terraform-aws for the folder name.
-
Click Save.
-
-
Add Backend to Your Scripts
-
From the Docker Swarm Manager navigate to the AWS directory:
cd ~/terraform/AWS
-
Set the Environment Variables
export AWS_ACCESS_KEY_ID="[ACCESS_KEY]" export AWS_SECRET_ACCESS_KEY="[SECRET_KEY]]" export AWS_DEFAULT_REGION="us-east-1"
- Create
terraform.tf
:vi terraform.tf
terraform.tf
contents:terraform { backend "s3" { key = "terraform-aws/terraform.tfstate" } }
- Initialize Terraform:
terraform init -backend-config "bucket=[BUCKET_NAME]"
- Validate changes:
terraform validate
- Plan the changes:
terraform plan
- Apply the changes:
terraform apply -auto-approve
- Destroy environment:
terraform destroy -auto-approve
-
-