Introduction
In general you would want to deploy infrastructure using terraform, build and push docker images in CI/CD phase. But let's say, just in case, someone pointed a gun at your head asking you to build and push docker images from within Terraform, how would you do it? Fear not, I am here, let me save you today.
Action Plan
It's simple, we will just wait check for file change - where a specific Dockerfile is kept, if any changes are detected we will build the image and push it.
Prerequisites
- You have to have aws cli configured (v2 is prefered but v1 will just do)
- Terraform installed (I am using 1.5.5)
Get into action, we don't have much time
The directory Structure
So this will be our directory structure -
.
├── src
│ └── frontend
│ ├── Dockerfile
│ └── index.html
└── Terraform
├── ecr.tf
├── main.tf
└── variables.tf
Here, we have a very basic nginx app (code below) serving a html page. And inside the Terraform directory we will store the Terraform specific files.
The sample app
You can skip this if you already have an app and Dockerfile.
├── src
│ └── frontend
│ ├── Dockerfile
│ └── index.html
For this demo I built this src/frontend/index.html -
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Service Connect Test</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
input, button {
padding: 10px;
width: 100%;
margin: 5px 0;
font-size: 1rem;
}
pre {
background: #f4f4f4;
padding: 10px;
border-radius: 5px;
overflow-x: auto;
}
</style>
</head>
<body>
<h2>ECS Service Connect Tester</h2>
<input type="text" id="urlInput" placeholder="Enter internal service URL (e.g., http://service-name:8080)">
<button id="connectBtn">Connect</button>
<h3>Response:</h3>
<pre id="responseBox">No data yet.</pre>
<script>
document.getElementById('connectBtn').addEventListener('click', async () => {
const url = document.getElementById('urlInput').value;
const responseBox = document.getElementById('responseBox');
responseBox.textContent = "Connecting...";
try {
const res = await fetch(url);
const text = await res.text();
responseBox.textContent = text;
} catch (err) {
responseBox.textContent = "Error: " + err.message;
}
});
</script>
</body>
</html>
Like I mentioned, this is not important, use any basic html or your complex app, your wish.
If you are interested, this is to check other APIs.
Now the Dockerfile - src/frontend/Dockerfile -
FROM nginx:alpine-slim
COPY ./index.html /usr/share/nginx/html/index.html
EXPOSE 80
Terraform Scripts
└── Terraform
├── ecr.tf
├── main.tf
└── variables.tf
You you love bad practice, you can put all files in one directory. But I am the opposite so let's maintain multiple files -
1. variables.tf
variable "aws_region" {
default = "ap-southeast-1"
}
variable "frontend_dir" {
default = "../src/frontend"
}
variable "frontend_repo_name" {
default = "frontend"
}
2. main.tf
provider "aws" {
region = var.aws_region
}
3. ecr.tf
Here's the main game (full code below). We will create a repository first, where our image will be pushed -
resource "aws_ecr_repository" "frontend_repo" {
name = var.frontend_repo_name
image_tag_mutability = "MUTABLE" # Or "IMMUTABLE" based on your needs
image_scanning_configuration {
scan_on_push = false # Set to true if you want to scan images as they are pushed
}
}
Now to track changes, we will generate hash of the Dockerfile (or any other file you wish to track -
locals {
frontend_hash = filebase64sha256("${path.module}/${var.frontend_dir}/Dockerfile")
}
Based on that hash, we will generate a random tag for the image. byte_length = 2 generates 4 char long random string, increase to 4,8 or 12 as you please.
resource "random_id" "frontend_rand_tag" {
keepers = {
# Generate a new tag each time there's change in Dockerfile hash
frontend_img_tag = local.frontend_hash
}
byte_length = 2
}
output "image_tag" {
value = {
frontend_tag = random_id.frontend_rand_tag.hex
}
}
Finally, we build and push the image. Just like we would do using the CLI. We are utilizing terraform null_resource and local-exec provisioner to run the commands -
resource "null_resource" "build_and_push_frontend" {
triggers = {
# Trigger a rebuild if the Dockerfile or source code changes
dockerfile_hash = local.frontend_hash
}
provisioner "local-exec" {
command = <<-EOT
aws ecr get-login-password --region ${var.aws_region} | docker login --username AWS --password-stdin ${aws_ecr_repository.frontend_repo.repository_url}
docker build -t ${aws_ecr_repository.frontend_repo.repository_url}:${random_id.frontend_rand_tag.hex} ${path.module}/${var.frontend_dir}
docker push ${aws_ecr_repository.frontend_repo.repository_url}:${random_id.frontend_rand_tag.hex}
EOT
}
}
Now, combining all -
Full ecr.tf
resource "aws_ecr_repository" "frontend_repo" {
name = var.frontend_repo_name
image_tag_mutability = "MUTABLE" # Or "IMMUTABLE" based on your needs
image_scanning_configuration {
scan_on_push = false # Set to true if you want to scan images as they are pushed
}
}
locals {
frontend_hash = filebase64sha256("${path.module}/${var.frontend_dir}/Dockerfile")
}
resource "random_id" "frontend_rand_tag" {
keepers = {
# Generate a new tag each time there's change in Dockerfile hash
frontend_img_tag = local.frontend_hash
}
byte_length = 2
}
resource "null_resource" "build_and_push_frontend" {
triggers = {
# Trigger a rebuild if the Dockerfile or source code changes
dockerfile_hash = local.frontend_hash
}
provisioner "local-exec" {
command = <<-EOT
aws ecr get-login-password --region ${var.aws_region} | docker login --username AWS --password-stdin ${aws_ecr_repository.frontend_repo.repository_url}
docker build -t ${aws_ecr_repository.frontend_repo.repository_url}:${random_id.frontend_rand_tag.hex} ${path.module}/${var.frontend_dir}
docker push ${aws_ecr_repository.frontend_repo.repository_url}:${random_id.frontend_rand_tag.hex}
EOT
}
}
output "image_tag" {
value = {
frontend_tag = random_id.frontend_rand_tag.hex
}
}
You can download the full code and script from this git repo.
Now if we init, plan, apply -
terraform init
terraform plan
terraform apply --auto-approve
You will see that after creating the repository it ran docker-login command and started building the image -
After the build process is complete, it pushed the image to ECR -
Let's verify. Based on the output the image tag should be 87ed . Let's see on the console.
Yeah looks like we have successfully done it!
Conclusion
Today we learnt how we can automate Docker image building and pushing using Terraform. But it's not production ready, use this only when using minor tasks, or specific needs - like dodging a bullet. Congratulations! you are saved today.
: what do we say to the god of manual labour?
: Not today, we have Terraform.
Happy Coding!




Top comments (0)