Terraform Guide - Docker Containers & AWS ECR(elastic container registry)?

Terraform is the most popular IaC(Infrastructure as Code) tool for provisioning your cloud infrastructure on AWS, GCP and Azure.

Terraform capabilities are not limited to cloud infrastructure instead you can use Terraform for other container infrastructure provisioning also. In this blog post, we will take a look at "How to Use Terraform for provisioning(setup/start/stop) of Docker container?"

Objective-

  1. Setup Application- Setup a simple server application using Node.js
  2. Create Dockerfile- Create Dockerfile for Node.js application
  3. Build Docker Image- Build Docker Image of Node.js application
  4. Test Docker Image- Start, Run and Stop Node.js docker container using Docker commands
  5. Create Terraform Manifest for Docker- Create Terraform Manifest for the Node.js Docker image
  6. Start and Stop Docker image using Terraform- Finally we will only use terraform init, terraform plan and terraform apply command to start and stop the Docker container of our Node.js application.
  7. How to Push Docker Image to ECR(elastic container registry) on AWS- A detailed instructions on how to push Docker image to ECR
  8. Conclusion

Prerequisites

Here are the prerequisites for this blog post. Since this is a little advanced topic of Terraform where you need to have the following software component already installed onto your laptop/desktop -

  1. Docker- As this article is focused on Docker, so you must have Docker Desktop and [Docker engine] 22 installed onto your working laptop/desktop. Here are the installation guide provided by docker based on the operating system -

    Docker Desktop & Docker Engine

  2. Terraform- The second important software package you need to have is Terraform installed. Here is a a detailed guide on installation of Terraform based on the different operating systems (macOS, MacOS Silicon chip, Windows, Linux) -

  3. NodeJS- The third component which you need is NodeJS because we will set up a very basic NodeJS server application.

  4. IDE - At last you need a good IDE(Integrated Development environment). Any of the following would work -

1. Create a Node.js app

Alright now we are starting from scratch so let's set up a very basic Node.js application for this tutorial.

1. package.json- Create a directory named terrafrom-docker and inside that directory create a file package. json. This file will be used for putting the required dependencies and also the information about the app -

 1 <!-- package.json-->
 2
 3 {
 4 "name": "docker_node_js_web_app",
 5     "version": "1.0.0",
 6     "description": "Node.js on Docker manged by Terrafrom",
 7     "author": "Rahul Wagh <rahul.wagh@jhooq.com>",
 8     "main": "server.js",
 9     "scripts": {
10     "start": "node server.js"
11 },
12 "dependencies": {
13     "express": "^4.16.1"
14 }
15}

2. server.js- Now we will create server.js in which we will write a very basic HTTP server code running on port 8080.

 1<!-- server.js-->
 2
 3   'use strict';
 4   
 5   const express = require('express');
 6   
 7   // Constants
 8   const PORT = 8080;
 9   const HOST = '0.0.0.0';
10   
11   // App
12   const app = express();
13   app.get('/', (req, res) => {
14         res.send('Hello World');
15   });
16   
17   app.listen(PORT, HOST);
18   console.log(`Running on http://${HOST}:${PORT}`); 

3.npm install- After creating package.json and server.js now we need to run $ npm install command. The npm install command will generate the package-lock.json file and in the later steps we will use the same inside the Dockerfile

1# run the following command /terraform-docker
2
3 npm install 

You should see the following output at your console -

run the npm install

Here is the directory structure of the project -

Project directory for terraform docker

4. Verify Node.js application- Now we have successfully installed all the node js dependencies let's first verify the application -

Run the following command to start the node js application-

1# following command will start the node.js application. In the next step, we will create a docker file
2
3 node server.js

start node js appication

Verify the node.js application with the help of curl command -

1curl localhost:8080

start node js appication

Now our node js application is ready and we need to create a Docker file for the application.



2. Create Dockerfile

In the previous step we have seen how to create a very simple Node JS application running on port 8080. Now we will create a Dockerfile, so that we wrap the application and run it inside a Docker container.

Here is the Dockerfile which you need to create and save inside your project -

 1FROM node:16
 2
 3# Create an app directory
 4WORKDIR /usr/src/app
 5
 6# Install app dependencies
 7# A wildcard is used to ensure both package.json AND package-lock.json are copied
 8# where available (npm@5+)
 9COPY package*.json ./
10
11RUN npm install
12# If you are building your code for production
13# RUN npm ci --only=production
14
15# Bundle app source
16COPY . .
17
18EXPOSE 8080
19CMD [ "node", "server.js" ] 


3. Build Docker Image by Tagging it with a suitable name

Now we have Node JS application ready and we also have Dockerfile ready with us.

Let's run some docker command to create a docker image -

1docker build . -t ubuntu/docker-node-terraform-app 

docker build-

Here is the docker build output which you should see once you build and tag the image -

Docker build and tag image for the Node js Application

List docker image-

After building and tagging the image let's verify the docker image by running the command $ docker images ls

1docker image ls

Here is the output after building the image-

List docker image after building it



4. Test Docker Image

Now we are at a point where we can run and verify the docker image for the Node JS application which we have built.

To verify the docker image, we need to start the docker container, and to start the docker container we need the following info -

  1. Docker image name - ubuntu/docker-node-terraform-app
  2. Port on Local machine - 8080
  3. Port on host machine - 49160

Here is the command to run and test the docker image -

1docker run -p 49160:8080 -d ubuntu/docker-node-terraform-app

After running the docker image you should follow output where it would spit out the docker container id for you-

Run docker image

As we have started the docker container on the port 41960 on the local machine so we should be able to access it by curl command -

1curl localhost:41960 

Here is the screenshot-

curl localhost on port 41960

So till now, we have finished -

  1. Setting up Node JS application
  2. Creating Dockerfile
  3. Running and verifying Docker container

In the next step, we will create a Terraform Configuration for the Docker container.



5. Create Terraform Manifest for Docker

All the previous steps are pretty much prerequisites and now we are ready to write the Terraform configuration to provision/start the docker container.

5.1 Define Docker provider

As we are working with Docker so the provider should be docker -

 1## Step 1 - Define provider 
 2
 3terraform {
 4  required_providers {
 5    docker = {
 6      source  = "kreuzwerker/docker"
 7      version = "2.21.0"
 8    }
 9  }
10}

5.2 Pull the docker image

Now we need to provide a reference to the Docker Image(ubuntu/docker-node-terraform-app) which we have just built -

1## Step 2 - Docker image reference so that it can pull the local or remote docker image
2
3resource "docker_image" "node-js-app" {
4  name = "ubuntu/docker-node-terraform-app"
5  keep_locally = false
6}

5.3 Define the docker ports for running the application

In the final part of terraforming configuration we are going to mention the internal as well as external port references -

 1## Step 3 - Mention the ports for localhost as well as for external host
 2
 3resource "docker_container" "foo" {
 4  image = docker_image.node-js-app.latest
 5  name  = "tutorial"
 6  ports {
 7    internal = 8080     ## Host machine
 8    external = 49160    ## Local machine
 9  }
10}

5.4 Complete terraform code -

 1 terraform {
 2  required_providers {
 3    docker = {
 4      source  = "kreuzwerker/docker"
 5      version = "2.21.0"
 6    }
 7  }
 8}
 9
10provider "docker" {
11}
12
13# Pulls the image
14resource "docker_image" "node-js-app" {
15  name = "ubuntu/docker-node-terraform-app"
16  keep_locally = false
17}
18
19
20resource "docker_container" "foo" {
21  image = docker_image.node-js-app.latest
22  name  = "tutorial"
23  ports {
24    internal = 8080
25    external = 49160
26  }
27}

6. Start and Stop Docker image using Terraform

6.1 Run terraform init command

Alright now we have everything ready with us and it is time for us to run the first $ terraform init command.

Go to your project directory and run the following command -

1terraform init 

After running the command you should see a successful message stating - "Terraform has been successfully initialized"

Run terraform init

6.2 Run terraform plan command

After running the terraform init command successfully the next command we are going to run is $ terraform plan.

Here is the command -

1terrafrom plan 

Run terraform plan command

After running the command you should observe the following messages on your console where it says -

  • "docker_image.node-js-app will be created"
  • Plan: 2 to add, 0 to change, 0 to destroy
    Run terraform plan command

6.3 Run terraform apply command

Finally, you need to run $ terraform apply command.

Here is the command -

1terraform apply 

Run terraform apply command to start docker container
After running the $ terraform apply command you should following message onto your console -

  • "docker_image.node-js-app will be created"
  • "Docker container ID"
    Run terraform apply command to start docker container

You can also do a final verification by listing all the running docker container $ docker ps

1docker ps 

Here is the output you should see -

Execute docker ps to verify the docker container

6.4 Verify the application by using the curl

At last, you can verify the application with the help of curl command -

1 curl localhost:41960

curl localhost on port 41960



7. How to Push Docker Image to ECR(elastic container registry) on AWS

Terraform is great tool for provisioning the infrastructure but when it comes to pushing docker image to AWS ECR(elastic container registry) then I would say Terraform is not recommended for performing operations such as docker push or docker pull

Although you can achieve the desired functionality of docker push, docker pull with the help of Terrafrom local-exec provisioner. But still it would be bad practice for manging the container infrastructure.

In this section we will take a look on both Recommended as well as Not Recommended ways of pushing docker image to ECR -

As I have mentioned, Terraform is not recommended for performing the docker pull, docker push operation but instead you can use the Terraform in the following ways to improve your container infrastructure provisioning -

1. Authenticate to AWS ECR- The first step would be to authenticate yourself with AWS ECR using the following commands_(Note- Please do change the repository URI based on your )_

1aws ecr get-login-password --region aws_region | \
2docker login --username AWS --password-stdin account_id.dkr.ecr.us-east-1.amazonaws.com 

2. Create ECR Repository using Terraform- Use the following Terraform Code where we will use the "aws_ecr_repository" and "aws_ecr_repository_policy" for creating the ECR repository.

 1resource "aws_ecr_repository" "docker_ecr_repo" {
 2  name                 = "docker_ecr_repo"
 3  image_tag_mutability = "IMMUTABLE"
 4}
 5
 6resource "aws_ecr_repository_policy" "demo-repo-policy" {
 7  repository = aws_ecr_repository.docker_ecr_repo.name
 8  policy     = <<EOF
 9  {
10    "Version": "2008-10-17",
11    "Statement": [
12      {
13        "Sid": "Set the permission for ECR",
14        "Effect": "Allow",
15        "Principal": "*",
16        "Action": [
17          "ecr:BatchCheckLayerAvailability",
18          "ecr:BatchGetImage",
19          "ecr:CompleteLayerUpload",
20          "ecr:GetDownloadUrlForLayer",
21          "ecr:GetLifecyclePolicy",
22          "ecr:InitiateLayerUpload",
23          "ecr:PutImage",
24          "ecr:UploadLayerPart"
25        ]
26      }
27    ]
28  }
29  EOF
30} 

3. Push Docker Image to ECR- After creating the ECR repository use the following docker command to build and push the image -

1# Step-1:  Tag docker image.
2# Note: Replace the repositoryURI based on the ECR repository which you have created
3docker tag my-image repositoryURI/repository-name:myimage
4
5# Step-2: Push Docker image
6docker push repositoryURI/repository-name:myimage

Here is how you need to implement local-exec provisioner -

  1. Create a bash file(push_docker_image)- Write down all the commands related to aws ecr login as well as docker login and docker push

     1# Step-1: AWS Authentication so that you can push the docker image later
     2
     3aws ecr get-login-password --region aws_region | \
     4docker login --username AWS --password-stdin account_id.dkr.ecr.us-east-1.amazonaws.com
     5
     6# Step-2:  Tag docker image.
     7# Note: Replace the repositoryURI based on the ECR repository which you have created
     8docker tag my-image repositoryURI/repository-name:myimage
     9
    10# Step-3: Push Docker image
    11docker push repositoryURI/repository-name:myimage
    
  2. Use local-exec provisioner- Now need to write the Terraform file main.tf and need to implement local-exec provisioner so that we can execute the bash script containing docker push commands -

     1provider "aws" {
     2   region     = "eu-central-1"
     3   access_key = "<YOUR_ACCESS_KEY>"
     4   secret_key = "<YOUR_SECRET_KEY>"
     5
     6}
     7
     8resource "aws_instance" "ec2_example" {
     9
    10    ami = "ami-0767046d1677be5a0"  
    11    instance_type = "t2.micro" 
    12    key_name= "aws_key"
    13    vpc_security_group_ids = [aws_security_group.main.id]
    14
    15    provisioner "file" {
    16        # Name of the bash file .i.e. push_docker_image
    17        source      = "push_docker_image"
    18
    19        # Update the destination where you want to put your bash file
    20        destination = "/home/ubuntu/push_docker_image"
    21    }
    22
    23   provisioner "remote-exec" {
    24   inline = [
    25
    26         # Change the mode of bash file to make it executable
    27         "chmod +x push_docker_image",
    28
    29         "/home/ubuntu/push_docker_image",
    30       ]
    31   }
    32  connection {
    33      type        = "ssh"
    34      host        = self.public_ip
    35      user        = "ubuntu"
    36      private_key = file("/home/rahul/Jhooq/keys/aws/aws_key")
    37      timeout     = "4m"
    38   }
    39}
    40
    41resource "aws_security_group" "main" {
    42  egress = [
    43    {
    44      cidr_blocks      = [ "0.0.0.0/0", ]
    45      description      = ""
    46      from_port        = 0
    47      ipv6_cidr_blocks = []
    48      prefix_list_ids  = []
    49      protocol         = "-1"
    50      security_groups  = []
    51      self             = false
    52      to_port          = 0
    53    }
    54  ]
    55 ingress                = [
    56   {
    57     cidr_blocks      = [ "0.0.0.0/0", ]
    58     description      = ""
    59     from_port        = 22
    60     ipv6_cidr_blocks = []
    61     prefix_list_ids  = []
    62     protocol         = "tcp"
    63     security_groups  = []
    64     self             = false
    65     to_port          = 22
    66  }
    67  ]
    68}
    
  3. Finally you need to run the terraform init, terraform plan and terraform apply and you should be able to push docker image using the Terraform.


8. Conclusion

This blog post will help you get yourself started with the Terraform && Docker Container and How you can create your own docker image and provision a docker container with the help of Terraform

Also here are key changes that you can do to customize the instructions as per your need -

  1. Instead of using Node JS you use any other application such as Spring Boot, Python, Angular, ReactJS, GoLang. The only primary requirement is you need to create a Docker container.
  2. You should always use "aws_ecr_repository" and "aws_ecr_repository_policy" for setting up the ECR container repository then use the docker build, docker push command for pushing docker image to ECR.

Posts in this Series