December 10, 2024
Did you know you could use Terraform to create and manage Docker containers? Yes, there’s Docker Compose, but I wanted a place to practice working with Terraform. And actually, after trying it out with Docker, I might even prefer it over Docker Compose, at least for simple-ish use cases. In this post I’ll walk you through a basic two-container setup that supports systemd, using Terraform and a custom image.
Let’s say we want to create two containers, which will have a systemd service to ping each other. I’m assuming you know Docker, Terraform file syntax, have Docker and Terraform installed, and are comfortable working on the command line; I won’t explain these things here. I’m working on Ubuntu 22.04 LTS, GNU/Linux.
First, let’s create a Dockerfile that installs ping and allows us to use systemd:
FROM robertdebock/debian
RUN apt update && apt install -y inetutils-ping
And now to the Terraform part.
We’ll create and work in the file main.tf
, placed next to the Dockerfile
from above.
Let’s first configure the provider:
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0.1"
}
}
}
provider "docker" {}
This gives our terraform the ability to manage Docker containers.
Let’s define the image to build as a resource:
resource "docker_image" "pinger_systemd" {
name = "pinger-systemd"
build {
context = "."
tag = ["pinger-systemd:latest"]
}
}
This creates a resource with the name pinger_systemd
of type docker_image
(provided by kreuzwerker/docker
).
The image name is pinger-systemd, and we instruct it to build with a Dockerfile in the current directory, tagging it as pinger-systemd:latest
.
Let’s create a network for the containers:
resource "docker_network" "ping_net" {
name = "ping_net"
ipam_config {
subnet = "10.21.21.0/24"
}
}
This creates a docker network resource named ping_net
, the network will have the name ping_net
, and hosts on it will fall in the CIDR range of 10.21.21.0/24
.
And now to create the containers themselves:
resource "docker_container" "pinger_a" {
# use the image we built; get its id from the resource we defined
image = docker_image.pinger_systemd.image_id
# the name of the container
name = "pinger-a"
# the hostname of the container
hostname = "pingera"
# connect it to the bridge network and to our custom network
network_mode = "bridge"
networks_advanced {
name = "bridge"
}
networks_advanced {
# get the network name from the resource we created
name = docker_network.ping_net.name
# use an ip address in the cidr range
ipv4_address = "10.21.21.2"
}
# mount the current directory as a bind mount just in case we want to easily edit things
mounts {
target = "/opt/cwd"
source = "/home/me/Documents/terraform-docker-example"
type = "bind"
}
# enable systemd
privileged = true
volumes {
host_path = "/sys/fs/cgroup"
container_path = "/sys/fs/cgroup"
}
cgroupns_mode = "host"
}
resource "docker_container" "pinger_b" {
# use the image we built; get its id from the resource we defined
image = docker_image.pinger_systemd.image_id
# the name of the container
name = "pinger-b"
# the hostname of the container
hostname = "pingerb"
# connect it to the bridge network and to our custom network
network_mode = "bridge"
networks_advanced {
name = "bridge"
}
networks_advanced {
# get the network name from the resource we created
name = docker_network.ping_net.name
# use an ip address in the cidr range
ipv4_address = "10.21.21.3"
}
# mount the current directory as a bind mount just in case we want to easily edit things
mounts {
target = "/opt/cwd"
source = "/home/me/Documents/terraform-docker-example"
type = "bind"
}
# enable systemd
privileged = true
volumes {
host_path = "/sys/fs/cgroup"
container_path = "/sys/fs/cgroup"
}
cgroupns_mode = "host"
}
Run terraform init
to initialize terraform, creating ./.terraform/
.
Then, terraform apply
, read through the changes, and type yes
to apply.
This will take a bit, because it has to download and create the image.
When that’s done, you can do docker ps
and see that the containers are running.
Let’s try to create a systemd service in one of the containers.
In the current directory, create a service file:
[Unit]
Description=Ping b at 10.21.21.3
[Service]
ExecStart=ping 10.21.21.3
Enter the pinger-a
container by running docker exec -it pinger-a bash
.
Then, copy over the service file from the mounted directory:
cp /opt/cwd/pingb.service /lib/systemd/system/
And start the service:
systemctl daemon-reload
systemctl start pingb
And if you check the status, you’ll see that the service is running:
root@pingera:/# systemctl status pingb
● pingb.service - Ping b at 10.21.21.3
Loaded: loaded (/lib/systemd/system/pingb.service; static)
Active: active (running) since Tue 2024-12-10 14:02:11 UTC; 3s ago
Main PID: 86 (ping)
Tasks: 1 (limit: 2782)
Memory: 236.0K
CPU: 1ms
CGroup: /system.slice/docker-c7fe2a0871f0dd370419e1c246c1d0da529a57d6d8984c7e283448b12cfffbae.scope/system.slice/pingb.service
└─86 ping 10.21.21.3
Dec 10 14:02:11 pingera systemd[1]: Started pingb.service - Ping b at 10.21.21.3.
Dec 10 14:02:11 pingera ping[86]: PING 10.21.21.3 (10.21.21.3): 56 data bytes
Dec 10 14:02:11 pingera ping[86]: 64 bytes from 10.21.21.3: icmp_seq=0 ttl=64 time=0.047 ms
Dec 10 14:02:12 pingera ping[86]: 64 bytes from 10.21.21.3: icmp_seq=1 ttl=64 time=0.070 ms
Dec 10 14:02:13 pingera ping[86]: 64 bytes from 10.21.21.3: icmp_seq=2 ttl=64 time=0.080 ms
If you want to check in more detail, you can install tcpdump in the pinger-b
container, inspect the traffic on the interface connected to the docker network we defined, and you’ll see the incoming pings:
root@pingerb:/# tcpdump -i eth1 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), snapshot length 262144 bytes
14:04:54.385465 IP pinger-a.ping_net > pinger_b: ICMP echo request, id 86, seq 163, length 64
14:04:54.385514 IP pinger_b > pinger-a.ping_net: ICMP echo reply, id 86, seq 163, length 64
14:04:55.386809 IP pinger-a.ping_net > pinger_b: ICMP echo request, id 86, seq 164, length 64
For more information and config options, here’s the documentation for the Terraform Docker provider.