Terraform Project 2: Deploying Django Application on AWS ECS using Terraform

Terraform Project 2: Deploying Django Application on AWS ECS using Terraform

Table of Contents

  1. Overview

  2. Project Structure

  3. Project Workflow

  4. File Structure

  5. Creating Parent Module

  6. Creating Child Module

  7. Steps for execution

  8. AWS service console output

  9. Conclusion

  10. Important Note

Overview

The project aims to deploy a Django web application on Amazon Elastic Container Service (ECS) using Terraform for infrastructure provisioning and management. ECS is a fully managed container orchestration service provided by AWS, and Terraform is used to define the AWS resources required for hosting the application and its infrastructure.

Project Structure:

  1. Terraform Configuration:

    • The parent folder contains the main Terraform configuration files: main.tf, variables.tf, and providers.tf.

    • main.tf: This file contains the primary Terraform configuration, including the definition of the AWS resources and their properties.

    • variables.tf: This file defines input variables that allow parameterization of the Terraform configuration.

    • providers.tf: This file specifies the AWS provider and its configurations.

  2. Child Modules:

    • Each child folder in the "child modules" directory represents a specific component of the AWS infrastructure and contains its main.tf, variables.tf, and output.tf files.

    • aws_vpc: Defines the Amazon VPC (Virtual Private Cloud) to isolate the application resources.

    • aws_subnet: Creates the subnets for the VPC where the containers will be deployed.

    • aws_routetables: Configures the route tables for the VPC subnets.

    • aws_iam: Defines the necessary IAM (Identity and Access Management) roles and policies for ECS.

    • aws_sg: Sets up security groups to control inbound and outbound traffic to the containers.

    • aws_lb: Configures an Application Load Balancer to distribute traffic to the containers.

    • aws_policies: Creates any custom IAM policies required for the application.

    • aws_keypair: Sets up an AWS key pair for secure SSH access.

    • aws_autoscaling: Configures auto-scaling rules to adjust the number of containers based on demand.

    • aws_cloudwatch: Sets up CloudWatch for monitoring ECS and application metrics.

  3. Django Application:

    • The project includes a Django web application in the "Django application" folder. This folder contains the necessary Python files for the Django application, such as views, models, and templates.

    • A Dockerfile is provided in the same folder, enabling containerization of the Django application for deployment on ECS.

  4. Deployment Scripts:

    • The "deploy" folder contains the update-ecs Python script, responsible for deploying the Django application to ECS. This script interacts with ECS and the container registry to update the application.

Project Workflow:

  1. Infrastructure Provisioning:

    • Terraform is used to create and configure AWS resources required for the Django application, such as VPC, subnets, security groups, load balancers, and auto-scaling rules.
  2. Dockerizing Django Application:

    • The Django application is containerized using the provided Dockerfile, making it portable and easier to deploy across environments.
  3. ECS Deployment:

    • The update-ecs Python script is executed to deploy the Dockerized Django application to ECS, leveraging the resources created with Terraform

File Structure

overall file structure

app folder

deploy folder

modules folder

Django Application

asgi.py

"""
ASGI config for hello_django project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hello_django.settings')

application = get_asgi_application()

middleware.py

from django.http import HttpResponse
from django.utils.deprecation import MiddlewareMixin


class HealthCheckMiddleware(MiddlewareMixin):
    def process_request(self, request):
        if request.META['PATH_INFO'] == '/ping/':
            return HttpResponse('pong!')

settings.py

"""
Django settings for hello_django project.

Generated by 'django-admin startproject' using Django 3.2.9.

For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-q0_f$kw-#n8-muu5zoka__v8ginvcjii)w7na6xm38yz-t0o%9'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['*']


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'hello_django.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'hello_django.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/

STATIC_URL = '/static/'

# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'


MIDDLEWARE = [
    'hello_django.middleware.HealthCheckMiddleware',  # new
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

urls.py

"""hello_django URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/3.2/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('admin/', admin.site.urls),
]

wsgi.py

"""
WSGI config for hello_django project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hello_django.settings')

application = get_wsgi_application()

manage.py

#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hello_django.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)


if __name__ == '__main__':
    main()

requirements.txt

Django==3.9
gunicorn==20.1.0

update-ecs.py

#python file
import boto3
import click


def get_current_task_definition(client, cluster, service):
    response = client.describe_services(cluster=cluster, services=[service])
    current_task_arn = response["services"][0]["taskDefinition"]

    response = client.describe_task_definition(taskDefinition=current_task_arn)
    return response


@click.command()
@click.option("--cluster", help="Name of the ECS cluster", required=True)
@click.option("--service", help="Name of the ECS service", required=True)
def deploy(cluster, service):
    client = boto3.client("ecs")

    response = get_current_task_definition(client, cluster, service)
    container_definition = response["taskDefinition"]["containerDefinitions"][0].copy()

    response = client.register_task_definition(
        family=response["taskDefinition"]["family"],
        volumes=response["taskDefinition"]["volumes"],
        containerDefinitions=[container_definition],
    )
    new_task_arn = response["taskDefinition"]["taskDefinitionArn"]

    response = client.update_service(
        cluster=cluster, service=service, taskDefinition=new_task_arn,
    )


if __name__ == "__main__":
    deploy()

dockerfile

# pull official base image
FROM python:3.9.0-slim-buster


#FROM python:3.8.6-slim

RUN python3 -m venv venv


# set work directory

WORKDIR /usr/src/app



# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1 #Prevents Python from writing bytecode files (.pyc files).
ENV PYTHONUNBUFFERED 1 #Ensures that Python outputs everything directly to the terminal, avoiding buffering issues



# install dependencies
RUN pip3 install --upgrade pip

#COPY ./requirements.txt .

#RUN pip3 install -r requirements.txt

RUN pip3 install Django --upgrade

RUN pip3 install gunicorn==20.1.0


# copy project
COPY . .

The Dockerfile creates a virtual environment, installs Django and Gunicorn, and copies the Django project files into the image. Once this Dockerfile is used to build the image, the resulting image will be able to run the Django application with Gunicorn as the server inside a container.

Terraform Configuration

Parent Modules

providers.tf

provider "aws" {
    region = var.region
}

main.tf

module "Vpc" {
  source = "./modules/aws_vpc"
  cidr_block = var.cidr_block
  true = var.true
  vpc_tag = var.vpc_tag
}

module "Subnet" {
  source = "./modules/aws_subnet"
  depends_on = [ module.Vpc ]
  public_subnet_1_cidr = var.public_subnet_1_cidr
  public_subnet_2_cidr = var.public_subnet_2_cidr
  private_subnet_1_cidr = var.private_subnet_1_cidr
  private_subnet_2_cidr = var.private_subnet_2_cidr
  availability_zones = var.availability_zones
  vpc_id = module.Vpc.vpc_id
}

module "Security_Group" {
  source = "./modules/aws_sg"
  depends_on = [ module.Vpc ]
  sg_lg_name = var.sg_lg_name
  sg_lb_description = var.sg_lb_description
  sg_ecs_name = var.sg_ecs_name
  sg_ecs_description = var.sg_ecs_description
  vpc_id = module.Vpc.vpc_id
}

module "Route_Table" {
  source = "./modules/aws_rt"
  depends_on = [ module.Vpc, module.Subnet ]
  public_subnet_1_id = module.Subnet.public_subnet_1
  public_subnet_2_id = module.Subnet.public_subnet_2
  private_subnet_1_id = module.Subnet.private_subnet_1
  private_subnet_2_id = module.Subnet.private_subnet_2
  vpc_id = module.Vpc.vpc_id
  true = var.true
  associate_with_private_ip = var.associate_with_private_ip
  destination_cidr_block = var.destination_cidr_block
}

module "Load_balancer" {
  source = "./modules/aws_lb"
  depends_on = [ module.Vpc, module.Subnet, module.Security_Group ]
  ecs_cluster_name = var.ecs_cluster_name
  lbtype = var.lbtype
  false = var.false
  sg_lb_id = module.Security_Group.sg_lb_id
  public_subnet_1_id = module.Subnet.public_subnet_1
  public_subnet_2_id = module.Subnet.public_subnet_2
  port_80 = var.port_80
  HTTP = var.HTTP
  vpc_id = module.Vpc.vpc_id
  health_check_path = var.health_check_path
}

module "Keypair" {
   source = "./modules/aws_keypair"
  ecs_cluster_name = var.ecs_cluster_name
  ssh_pubkey_file = var.ssh_pubkey_file
}

module "IAM" {
  source = "./modules/aws_iam"
  ecs_host_role_name = var.ecs_host_role_name
  ecs_host_role_path = var.ecs_host_role_path
  ecs_instance_role_policy_name = var.ecs_instance_role_policy_name
  ecs_instance_role_policy_path = var.ecs_instance_role_policy_path
  ecs_service_role_name = var.ecs_service_role_name
  ecs_service_role_path = var.ecs_service_role_path
  ecs_service_role_policy_name = var.ecs_service_role_policy_name
  ecs_service_role_policy_path = var.ecs_service_role_policy_path
  ecs_name = var.ecs_cluster_name
}


module "ECS" {
  source = "./modules/aws_ecs"
  depends_on = [ module.IAM, module.Load_balancer ]
  ecs_cluster_name = var.ecs_cluster_name
  ami = var.ami
  instance_type = var.instance_type
  docker_image_url_django = var.docker_image_url_django
  app_count = var.app_count
  region = var.region
  sg_ecs_id = module.Security_Group.sg_ecs_id
  iam_instance_profile_ecs_name = module.IAM.iam_instance_profile_ecs_name
  true = var.true
  keypair_name = module.Keypair.keypair_name
  template_file_app_path = var.template_file_app_path
  app_name = var.app_name
  port_8000 = var.port_8000
  ecs_service_role_arn = module.IAM.ecs_service_role_arn
  alb_target_group_arn = module.Load_balancer.aws_alb_target_group_arn

}

module "Cloudwatch" {
  source = "./modules/aws_cloudwatch"
  django_log_group_name = var.django_log_group_name
  log_retention_in_days = var.log_retention_in_days
  django_log_stream_name = var.django_log_stream_name
}

module "Autoscaling" {
  source = "./modules/aws_autoscaling"
  depends_on = [ module.ECS, module.Subnet ]
  ecs_cluster_name = var.ecs_cluster_name
  autoscale_min = var.autoscale_min
  autoscale_max = var.autoscale_max
  autoscale_desired = var.autoscale_desired
  EC2 = var.EC2
  ecs_launch_configuration_name = module.ECS.ecs_launch_configuration_name
  public_subnet_1_id = module.Subnet.public_subnet_1
  public_subnet_2_id = module.Subnet.public_subnet_2
}

variables.tf

variable "region" {
  description = "this would be the ohio"
  default = "us-east-2"
}
#---------AWS_VPC variables------------------------#
variable "cidr_block"{
  type = string
  default = "10.0.0.0/16"
}

variable "true" {
  type = bool
  default = true
}

variable "vpc_tag" {
  type = map(any)
  default = {
    "Name" = "prod_vpc"
  }
}


#---------AWS_SUBNET variables------------------------#

variable "public_subnet_1_cidr" {
  description = "CIDR Block for Public Subnet 1"
  default     = "10.0.1.0/24"
}
variable "public_subnet_2_cidr" {
  description = "CIDR Block for Public Subnet 2"
  default     = "10.0.2.0/24"
}
variable "private_subnet_1_cidr" {
  description = "CIDR Block for Private Subnet 1"
  default     = "10.0.3.0/24"
}
variable "private_subnet_2_cidr" {
  description = "CIDR Block for Private Subnet 2"
  default     = "10.0.4.0/24"
}

variable "availability_zones" {
  description = "Availability zones"
  type        = list(string)
  default     = ["us-east-2b", "us-east-2c"]
}

variable "vpc_id" {
  type = string
  #as of now we don't have the vpc id
}


#--------------AWS_SG variables-----------------------------#

variable "sg_lg_name" {
  type = string
  default = "load_balancer_security_group"
}

variable "sg_lb_description" {
  type = string
  default = "Controls access to the ALB"
}

variable "sg_ecs_name" {
  type = string
  default = "ecs_security_group"
}

variable "sg_ecs_description" {
  type = string
  default = "Allows inbound access from the ALB only"
}

#--------------AWS_RT variables-----------------------------#

variable "public_subnet_1_id" {
  type = string
  # as we don't have a subnet id
}

variable "public_subnet_2_id" {
  type = string
}

variable "private_subnet_1_id" {
  type = string
}

variable "private_subnet_2_id" {
  type = string
}

variable "associate_with_private_ip" {
  type = string
  default = "10.0.0.5"
}

variable "destination_cidr_block" {
  type = string
  default = "0.0.0.0/0"
}


#--------------AWS_lb variables-----------------------------#

variable "ecs_cluster_name" {
  description = "Name of the ECS cluster"
  default     = "production"
}

variable "lbtype" {
  type = string
  default = "application"
}

variable "false" {
    type = bool
    default = false
}

variable "sg_lb_id" {
  type = string
}

variable "port_80" {
  type = string
  default = "80"
}

variable "HTTP" {
  type = string
  default = "HTTP"
}

variable "health_check_path" {
  description = "Health check path for the default target group"
  default     = "/ping/"
}

#-------------------AWS_IAM variables ----------------#

variable "ecs_host_role_name" {
  type = string 
  default = "ecs_host_role_prod"
}
#copy the role path from your own folder
variable "ecs_host_role_path" {
  type = string
  default = "/Users/kunal/Desktop/Desktop/Devops_Project/Terraform_Project2/Terraform/modules/aws_policies/ecs-role.json"
}

variable "ecs_instance_role_policy_name" {
  type = string 
  default = "ecs_host_role_prod"
}
#copy the role path from your own folder
variable "ecs_instance_role_policy_path" {
  type = string
  default = "/Users/kunal/Desktop/Desktop/Devops_Project/Terraform_Project2/Terraform/modules/aws_policies/ecs-instance-role-policy.json"
}

variable "ecs_service_role_name" {
  type = string 
  default = "ecs_host_role_prod"
}
#copy the role path from your own folder
variable "ecs_service_role_path" {
  type = string
  default = "/Users/kunal/Desktop/Desktop/Devops_Project/Terraform_Project2/Terraform/modules/aws_policies/ecs-role.json"
}

variable "ecs_service_role_policy_name" {
  type = string 
  default = "ecs_host_role_prod"
}
#copy the service policy path from your own folder
variable "ecs_service_role_policy_path" {
  type = string
  default = "/Users/kunal/Desktop/Desktop/Devops_Project/Terraform_Project2/Terraform/modules/aws_policies/ecs-service-role-policy.json"
}

variable "ecs_name" {
  type = string
  default = "ecs_instance_profile_prod"
}


#----------------AWS_ECS variables -----------------------#

variable "ami" {
  description = "Which AMI to spawn."
  default = {
    us-east-2 = "ami-03a0c45ebc70f98ea"
  }
}

variable "instance_type" {
  default = "t2.micro"
}
# please create a docker repo in AWS ecr with the docker file present in the app folder,
# copy the aws ecr path in the default parameter
variable "docker_image_url_django" {
  description = "Docker image to run in the ECS cluster"
  default     = "967427159842.dkr.ecr.us-east-2.amazonaws.com/django-app"
}
variable "app_count" {
  description = "Number of Docker containers to run"
  default     = 2
}

variable "sg_ecs_id" {
  type = string
}

variable "iam_instance_profile_ecs_name" {
  type = string
}

variable "keypair_name" {
  type = string
}
# please copy the path from templates folder present in AWS_ECS child module 
variable "template_file_app_path" {
  type = string
  default = "/Users/kunal/Desktop/Desktop/Devops_Project/Terraform_Project2/Terraform/modules/aws_ecs/templates/django_app.json.tpl"
}

variable "app_name" {
  type = string
  default = "django-app"
}

variable "port_8000" {
  type = string
  default = "8000"
}

variable "ecs_service_role_arn" {
  type = string
}

variable "alb_target_group_arn" {
  type = string
}

#----------AWS_Cloudwatch variables----------------------#

variable "django_log_group_name" {
  type = string
  default = "/ecs/django-app"
}

variable "log_retention_in_days" {
  default = 30
}

variable "django_log_stream_name" {
  type = string
  default = "django-app-log-stream"
}

#----------AWS_Autoscaling variables--------------------

variable "autoscale_min" {
  description = "Minimum autoscale (number of EC2)"
  default     = "1"
}
variable "autoscale_max" {
  description = "Maximum autoscale (number of EC2)"
  default     = "2"
}
variable "autoscale_desired" {
  description = "Desired autoscale (number of EC2)"
  default     = "2"
}

variable "EC2" {
  type = string
  default = "EC2"
}

variable "ecs_launch_configuration_name"  {
  type = string
}

#----------AWS_Keypair variables--------------------#
# generate aws ssh public key and save the file in the aws_keypair and paste the path here
variable "ssh_pubkey_file" {
  description = "Path to an SSH public key"
  default     = "/Users/kunal/Desktop/Desktop/Devops_Project/Terraform_Project2/Terraform/modules/aws_keypair/ohio-region-key-pair.pub"
}

Child Modules

  1. AWS_VPC

    main.tf

     # this block will create an aws vpc
    
     resource "aws_vpc" "prod_vpc" {
       cidr_block = var.cidr_block  #attribute specifies the IPv4 address range for the VPC
       enable_dns_support = var.true #This attribute enables or disables DNS support for the VPC
       enable_dns_hostnames = var.true # This attribute enables or disables DNS hostnames for the VPC
       tags = var.vpc_tag
     }
    

    variables.tf

     # variables for aws vpc
    
     variable "cidr_block"{
       type = string
       default = "10.0.0.0/16"
     }
    
     variable "true" {
       type = bool
       default = true
     }
    
     variable "vpc_tag" {
       type = map(any)
       default = {
         "Name" = "prod_vpc"
       }
     }
    

    output.tf

     #we need the vpc id generated from aws_vpc prog resource block,
     #vpc id will be used by aws_subnet, aws_security_group, aws_route_table, aws load_balancer
    
     output "vpc_id" {
       value = aws_vpc.prod_vpc.id
     }
    
  2. AWS_Subnet

    main.tf

     # this block will create a public subnet 
    
     resource "aws_subnet" "public_subnet_1" {
       cidr_block = var.public_subnet_1_cidr
       vpc_id = var.vpc_id
       availability_zone = var.availability_zones[0]
     }
    
     # this block will create another public subnet 
    
     resource "aws_subnet" "public_subnet_2" {
       cidr_block = var.public_subnet_2_cidr
       vpc_id = var.vpc_id
       availability_zone = var.availability_zones[1]
     }
    
     # this block will create a private subnet 
    
     resource "aws_subnet" "private_subnet_1" {
       cidr_block = var.private_subnet_1_cidr
       vpc_id = var.vpc_id
       availability_zone = var.availability_zones[0]
     }
    
     # this block will create another private subnet 
    
     resource "aws_subnet" "private_subnet_2" {
       cidr_block = var.private_subnet_2_cidr
       vpc_id = var.vpc_id
       availability_zone = var.availability_zones[1]
     }
    

    variables.tf

    ```go

    variables for aws_subnet

    variable "public_subnet_1_cidr" { description = "CIDR Block for Public Subnet 1" default = "10.0.1.0/24" } variable "public_subnet_2_cidr" { description = "CIDR Block for Public Subnet 2" default = "10.0.2.0/24" } variable "private_subnet_1_cidr" { description = "CIDR Block for Private Subnet 1" default = "10.0.3.0/24" } variable "private_subnet_2_cidr" { description = "CIDR Block for Private Subnet 2" default = "10.0.4.0/24" }

    variable "availability_zones" { description = "Availability zones" type = list(string) default = ["us-east-2b", "us-east-2c"] }

variable "vpc_id" { type = string

#as of now we don't have the vpc id, which will be mapped in parent main.tf }


    output.tf

    ```go
    # public and private subnet id's will be used by aws_securitygroup, aws_route_table, aws_load_balancer & aws_autoscaling

    output "public_subnet_1" {
      value = aws_subnet.public_subnet_1.id
    }

    output "public_subnet_2" {
      value = aws_subnet.public_subnet_2.id
    }

    output "private_subnet_1" {
      value = aws_subnet.private_subnet_1.id
    }

    output "private_subnet_2" {
      value = aws_subnet.private_subnet_2.id
    }
  1. AWS_RT

    main.tf

    ```go

    Route tables for the subnets

    resource "aws_route_table" "public_rt" { vpc_id = var.vpc_id }

    resource "aws_route_table" "private_rt" { vpc_id = var.vpc_id }

    Associate the newly created route tables to the subnets

    resource "aws_route_table_association" "public_route-1-association" { route_table_id = aws_route_table.public_rt.id subnet_id = var.public_subnet_1_id }

    resource "aws_route_table_association" "public_route-2-association" { route_table_id = aws_route_table.public_rt.id subnet_id = var.public_subnet_2_id }

    resource "aws_route_table_association" "private_route-1-association" { route_table_id = aws_route_table.private_rt.id subnet_id = var.private_subnet_1_id }

    resource "aws_route_table_association" "private_route-2-association" { route_table_id = aws_route_table.private_rt.id subnet_id = var.private_subnet_2_id }

    Internet Gateway for the public subnet

    resource "aws_internet_gateway" "prod_igw" { vpc_id = var.vpc_id }

    Elastic IP

    resource "aws_eip" "elastic_ip_for_nat_gw" { vpc = var.true associate_with_private_ip = var.associate_with_private_ip depends_on = [ aws_internet_gateway.prod_igw ] }

    NAT gateway

    resource "aws_nat_gateway" "nat_gw" { allocation_id = aws_eip.elastic_ip_for_nat_gw.id subnet_id = var.public_subnet_1_id depends_on = [ aws_eip.elastic_ip_for_nat_gw ] }

    resource "aws_route" "nat_gw_route" { route_table_id = aws_route_table.private_rt.id nat_gateway_id = aws_nat_gateway.nat_gw.id destination_cidr_block = var.destination_cidr_block }

Route the public subnet traffic through the Internet Gateway

resource "aws_route" "public_internet_igw_route" { route_table_id = aws_route_table.public_rt.id gateway_id = aws_internet_gateway.prod_igw.id destination_cidr_block = var.destination_cidr_block }


    variables.tf

    ```go
    # variables for aws rt

    variable "public_subnet_1_id" {
      type = string
      # as we don't have a subnet id
    }

    variable "public_subnet_2_id" {
      type = string
    }

    variable "private_subnet_1_id" {
      type = string
    }

    variable "private_subnet_2_id" {
      type = string
    }

    variable "vpc_id" {
      type = string
      #as of now we don't have the vpc id
    }

    variable "true" {
      type = bool
      default = true
    }

    variable "associate_with_private_ip" {
      type = string
      default = "10.0.0.5"
    }

    variable "destination_cidr_block" {
      type = string
      default = "0.0.0.0/0"
    }
  1. AWS_SG

    main.tf

     # ALB Security Group (Traffic Internet -> ALB), in this have not variablized the ingress and egress
     resource "aws_security_group" "lb" {
       name = var.sg_lg_name
       description = var.sg_lb_description
       vpc_id = var.vpc_id
    
       ingress {
         from_port   = 80
         to_port     = 80
         protocol    = "tcp"
         cidr_blocks = ["0.0.0.0/0"]
       }
    
       ingress {
         from_port   = 443
         to_port     = 443
         protocol    = "tcp"
         cidr_blocks = ["0.0.0.0/0"]
       }
    
       egress {
         from_port   = 0
         to_port     = 0
         protocol    = "-1"
         cidr_blocks = ["0.0.0.0/0"]
       }
     }
    
     # ECS Security group (traffic ALB -> ECS, ssh -> ECS), in this have not variablized the ingress and egress
     resource "aws_security_group" "ecs" {
       name = var.sg_ecs_name
       description = var.sg_ecs_description
       vpc_id = var.vpc_id
    
       ingress {
         from_port       = 0
         to_port         = 0
         protocol        = "-1"
         security_groups = [aws_security_group.lb.id]
       }
    
       ingress {
         from_port   = 22
         to_port     = 22
         protocol    = "tcp"
         cidr_blocks = ["0.0.0.0/0"]
       }
    
       egress {
         from_port   = 0
         to_port     = 0
         protocol    = "-1"
         cidr_blocks = ["0.0.0.0/0"]
       }
     }
    

    variables.tf

     #variables for aws security groups
    
     variable "sg_lg_name" {
       type = string
       default = "load_balancer_security_group"
     }
    
     variable "sg_lb_description" {
       type = string
       default = "Controls access to the ALB"
     }
    
     variable "sg_ecs_name" {
       type = string
       default = "ecs_security_group"
     }
    
     variable "sg_ecs_description" {
       type = string
       default = "Allows inbound access from the ALB only"
     }
    
     variable "vpc_id" {
       type = string
       #as of now we don't have the vpc id
     }
    

    output.tf

     #security group lb & ecs id's will be used by aws_load_balancer module
    
     output "sg_lb_id" {
       value = aws_security_group.lb.id
     }
    
     output "sg_ecs_id" {
       value = aws_security_group.ecs.id
     }
    
  2. AWS_LB

    main.tf

     # have configured loadbalancers, listners and target groups in same file
     resource "aws_lb" "prod" {
       name = "${var.ecs_cluster_name}-alb"
       load_balancer_type = var.lbtype
       internal = var.false
       security_groups = [var.sg_lb_id]
       subnets = [var.public_subnet_1_id, var.public_subnet_2_id]
     }
    
     resource "aws_alb_target_group" "default_tg" {
       name = "${var.ecs_cluster_name}-tg"
       port = var.port_80
       protocol = var.HTTP
       vpc_id = var.vpc_id
    
       health_check {
         path                = var.health_check_path
         port                = "traffic-port"
         healthy_threshold   = 5
         unhealthy_threshold = 2
         timeout             = 2
         interval            = 5
         matcher             = "200"
       }
     }
    
     resource "aws_alb_listener" "ecs_alb_http_listener" {
       load_balancer_arn = aws_lb.prod.id
       port = var.port_80
       protocol = var.HTTP
       depends_on = [aws_alb_target_group.default_tg]
    
       default_action {
         type = "forward"
         target_group_arn = aws_alb_target_group.default_tg.arn
       }
     }
    

    variables.tf

     variable "ecs_cluster_name" {
       description = "Name of the ECS cluster"
       default     = "production"
     }
    
     variable "lbtype" {
       type = string
       default = "application"
     }
    
     variable "false" {
         type = bool
         default = false
     }
    
     variable "sg_lb_id" {
       type = string
     }
    
     variable "public_subnet_1_id" {
       type = string
     }
    
     variable "public_subnet_2_id" {
       type = string
     }
    
     variable "port_80" {
       type = string
       default = "80"
     }
    
     variable "HTTP" {
       type = string
       default = "HTTP"
     }
    
     variable "vpc_id" {
       type = string
       #as of now we don't have the vpc id
     }
    
     variable "health_check_path" {
       description = "Health check path for the default target group"
       default     = "/ping/"
     }
    

    output.tf

     output "alb_hostname" {
       value = aws_lb.prod.dns_name
     }
    
     output "aws_alb_target_group_arn" {
       value = aws_alb_target_group.default_tg.arn
     }
    
  3. AWS_ECS

    template folder- django_app-json.tpl

     [
       {
         "name": "django-app",
         "image": "${docker_image_url_django}",
         "essential": true,
         "cpu": 10,
         "memory": 512,
         "links": [],
         "portMappings": [
           {
             "containerPort": 8000,
             "hostPort": 0,
             "protocol": "tcp"
           }
         ],
         "command": ["gunicorn", "-w", "3", "-b", ":8000", "hello_django.wsgi:application"],
         "environment": [],
         "logConfiguration": {
           "logDriver": "awslogs",
           "options": {
             "awslogs-group": "/ecs/django-app",
             "awslogs-region": "${region}",
             "awslogs-stream-prefix": "django-app-log-stream"
           }
         }
       }
     ]
    

    main.tf

    ```go resource "aws_ecs_cluster" "prod" { name = "${var.ecs_cluster_name}-cluster" }

    resource "aws_launch_configuration" "ecs" { name = "${var.ecs_cluster_name}-cluster" image_id = lookup(var.ami, var.region) instance_type = var.instance_type security_groups = [var.sg_ecs_id] iam_instance_profile = var.iam_instance_profile_ecs_name key_name = var.keypair_name associate_public_ip_address = var.true user_data = "#!/bin/bash\necho ECS_CLUSTER='${var.ecs_cluster_name}-cluster' > /etc/ecs/ecs.config" }

    locals { template_vars = { docker_image_url_django = var.docker_image_url_django region = var.region } } locals { rendered_template = templatefile(var.template_file_app_path, local.template_vars) }

resource "aws_ecs_task_definition" "app" { family = var.app_name container_definitions = local.rendered_template }

resource "aws_ecs_service" "prod" { name = "${var.ecs_cluster_name}-service" cluster = aws_ecs_cluster.prod.id task_definition = aws_ecs_task_definition.app.arn iam_role = var.ecs_service_role_arn desired_count = var.app_count

depends_on = [aws_alb_listener.ecs-alb-http-listener, aws_iam_role_policy.ecs-service-role-policy]

load_balancer {

depends_on = [module.Load_balancer]

target_group_arn = var.alb_target_group_arn container_name = var.app_name container_port = var.port_8000 } }


    variables.tf

    ```go
    variable "ecs_cluster_name" {
      description = "Name of the ECS cluster"
      default     = "production"
    }
    variable "ami" {
      description = "Which AMI to spawn."
      default = {
        us-east-2 = "ami-03a0c45ebc70f98ea"
      }
    }

    variable "instance_type" {
      default = "t2.micro"
    }
    variable "docker_image_url_django" {
      description = "Docker image to run in the ECS cluster"
      default     = "967427159842.dkr.ecr.us-east-2.amazonaws.com/django-app"
    }
    variable "app_count" {
      description = "Number of Docker containers to run"
      default     = 2
    }

    variable "region" {
      description = "this would be the ohio"
      default = "us-east-2"
    }

    variable "sg_ecs_id" {
      type = string
    }

    variable "iam_instance_profile_ecs_name" {
      type = string
    }
    variable "true" {
      type = bool
      default = true
    }

    variable "keypair_name" {
      type = string
    }

    variable "template_file_app_path" {
      type = string
      default = "/Users/kunal/Desktop/Desktop/Devops_Project/Terraform_Project2/Terraform/modules/aws_ecs/templates/django_app.json.tpl"
    }

    variable "app_name" {
      type = string
      default = "django-app"
    }

    variable "port_8000" {
      type = string
      default = "8000"
    }

    variable "ecs_service_role_arn" {
      type = string
    }

    variable "alb_target_group_arn" {
      type = string
    }

output.tf

    output "ecs_launch_configuration_name" {
      value = aws_launch_configuration.ecs.name
    }
  1. AWS_Autoscaling

    main.tf

     resource "aws_autoscaling_group" "ecs-cluster" {
       name = "${var.ecs_cluster_name}_auto_scaling_group"
       min_size = var.autoscale_min
       max_size = var.autoscale_max
       desired_capacity = var.autoscale_desired
       health_check_type = var.EC2
       launch_configuration = var.ecs_launch_configuration_name
       vpc_zone_identifier = [var.public_subnet_1_id, var.public_subnet_2_id]
     }
    

    variables.tf

     variable "ecs_cluster_name" {
       description = "Name of the ECS cluster"
       default     = "production"
     }
    
     variable "autoscale_min" {
       description = "Minimum autoscale (number of EC2)"
       default     = "1"
     }
     variable "autoscale_max" {
       description = "Maximum autoscale (number of EC2)"
       default     = "2"
     }
     variable "autoscale_desired" {
       description = "Desired autoscale (number of EC2)"
       default     = "2"
     }
    
     variable "EC2" {
       type = string
       default = "EC2"
     }
    
     variable "ecs_launch_configuration_name"  {
       type = string
     }
    
     variable "public_subnet_1_id" {
       type = string
       # as we don't have a subnet id
     }
    
     variable "public_subnet_2_id" {
       type = string
     }
    
  2. AWS_Cloudwatch

    main.tf

     resource "aws_cloudwatch_log_group" "django_log_group" {
       name = var.django_log_group_name
       retention_in_days = var.log_retention_in_days
     }
    
     resource "aws_cloudwatch_log_stream" "django_log_stream" {
       name = var.django_log_stream_name
       log_group_name = aws_cloudwatch_log_group.django_log_group.name
     }
    

    variables.tf

     variable "django_log_group_name" {
       type = string
       default = "/ecs/django-app"
     }
    
     variable "log_retention_in_days" {
       default = 30
     }
    
     variable "django_log_stream_name" {
       type = string
       default = "django-app-log-stream"
     }
    
  3. AWS_Policies

    ecs-instance-role-policy.json

     {
         "Version": "2012-10-17",
         "Statement": [
           {
             "Effect": "Allow",
             "Action": [
               "ecs:*",
               "ec2:*",
               "elasticloadbalancing:*",
               "ecr:*",
               "cloudwatch:*",
               "s3:*",
               "rds:*",
               "logs:*"
             ],
             "Resource": "*"
           }
         ]
       }
    

    ecs-role.json

     {
         "Version": "2008-10-17",
         "Statement": [
           {
             "Action": "sts:AssumeRole",
             "Principal": {
               "Service": [
                 "ecs.amazonaws.com",
                 "ec2.amazonaws.com"
               ]
             },
             "Effect": "Allow"
           }
         ]
       }
    

    ecs-service-role-policy.json

     {
         "Version": "2012-10-17",
         "Statement": [
           {
             "Effect": "Allow",
             "Action": [
               "elasticloadbalancing:Describe*",
               "elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
               "elasticloadbalancing:RegisterInstancesWithLoadBalancer",
               "ec2:Describe*",
               "ec2:AuthorizeSecurityGroupIngress",
               "elasticloadbalancing:RegisterTargets",
               "elasticloadbalancing:DeregisterTargets"
             ],
             "Resource": [
               "*"
             ]
           }
         ]
       }
    
  4. AWS_IAM

    main.tf

    resource "aws_iam_role" "ecs_host_role" {
      name = var.ecs_host_role_name
      assume_role_policy = file(var.ecs_host_role_path)
    }
    
    resource "aws_iam_role_policy" "ecs_instance_role_policy" {
      name = var.ecs_instance_role_policy_name
      policy = file(var.ecs_instance_role_policy_path)
      role = aws_iam_role.ecs_host_role.id
    }
    
    resource "aws_iam_role" "ecs_service_role" {
      name = var.ecs_service_role_name
      assume_role_policy = file(var.ecs_service_role_path)
    }
    
    resource "aws_iam_role_policy" "ecs_service_role_policy" {
      name = var.ecs_service_role_policy_name
      policy = file(var.ecs_service_role_policy_path)
      role = aws_iam_role.ecs_service_role.id
    }
    
    resource "aws_iam_instance_profile" "ecs" {
      name = var.ecs_name
      path = "/"
      role = aws_iam_role.ecs_host_role.name
    }
    

    variables.tf

    variable "ecs_host_role_name" {
      type = string 
      default = "ecs_host_role_prod"
    }
    
    variable "ecs_host_role_path" {
      type = string
      default = "/Users/kunal/Desktop/Desktop/Devops_Project/Terraform_Project2/Terraform/modules/aws_policies/ecs-role.json"
    }
    
    variable "ecs_instance_role_policy_name" {
      type = string 
      default = "ecs_host_role_prod"
    }
    
    variable "ecs_instance_role_policy_path" {
      type = string
      default = "/Users/kunal/Desktop/Desktop/Devops_Project/Terraform_Project2/Terraform/modules/aws_policies/ecs-instance-role-policy.json"
    }
    
    variable "ecs_service_role_name" {
      type = string 
      default = "ecs_host_role_prod"
    }
    
    variable "ecs_service_role_path" {
      type = string
      default = "/Users/kunal/Desktop/Desktop/Devops_Project/Terraform_Project2/Terraform/modules/aws_policies/ecs-role.json"
    }
    
    variable "ecs_service_role_policy_name" {
      type = string 
      default = "ecs_host_role_prod"
    }
    
    variable "ecs_service_role_policy_path" {
      type = string
      default = "/Users/kunal/Desktop/Desktop/Devops_Project/Terraform_Project2/Terraform/modules/aws_policies/ecs-service-role-policy.json"
    }
    
    variable "ecs_name" {
      type = string
      default = "ecs_instance_profile_prod"
    }
    

    output.tf

    output "ecs_service_role_arn" {
      value = aws_iam_role.ecs_service_role.arn
    }
    
    output "iam_instance_profile_ecs_name" {
      value = aws_iam_instance_profile.ecs.name
    }
    
  5. AWS_KeyPair

    main.tf

    resource "aws_key_pair" "prod" {
      key_name = "${var.ecs_cluster_name}_key_pair"
      public_key = file(var.ssh_pubkey_file)
    }
    

    variables.tf

    variable "ecs_cluster_name" {
      description = "Name of the ECS cluster"
      default     = "production"
    }
    
    variable "ssh_pubkey_file" {
      description = "Path to an SSH public key"
      default     = "/Users/kunal/Desktop/Desktop/Devops_Project/Terraform_Project2/Terraform/modules/aws_keypair/ohio-region-key-pair.pub"
    }
    

    output.tf

    output "keypair_name" {
      value = aws_key_pair.prod.key_name
    }
    

Steps for execution:

  1. Clone the git hub repo from https://github.com/kunalrepo/Terraform_withAWS.git into your local machine.

  2. Please take care of all prerequisites beforehand, like generating SSH public keypair, copying the correct path in the variable files (both child and parent)etc, wherever required.

  3. Build the docker image from the dockerfile present in the app folder into the AWS ECR

  4. Go to the parent main.tf file and open an integrated terminal from your favourite code editor, here we have used VS code for simplicity.

  5. First, configure the AWS CLI in the integrated terminal by using the command aws configure and provide the access key id and secret key generated for an IAM user/role with sufficient privileges/permissions to deploy services on the AWS console. Keep the region as specified in the provider.tf file "us-east-2".

  6. Execute the command terraform init to initialize the Terraform project in the directory. It prepares the working directory for Terraform and also checks the vulnerability in the code.

  7. Execute the command terraform plan -out=tfplan.out to examine the Terraform configuration and determine what actions need to be taken to achieve the desired state of the infrastructure. Also saving the plan in the tfplan.out file as the best practices.

  8. Execute the command terraform apply tfplan.out to apply the changes specified in the Terraform configuration to create, modify, or delete resources in the infrastructure. It takes the execution plan generated by terraform plan in the output file tfplan.out and performs the necessary actions to achieve the desired state.

  9. Open the integrated terminal of update-ecs.py file and run the command update-ecs.py --cluster=production-cluster --service=production-service

AWS Console Output

  1. AWS VPC, AWS Subnet, AWS route tables & AWS Internet Gateway

After the launch of AWS resource map in VPC, it is amazing to know the flow of traffic between route tables, subnets and Internet gateways. Can notice it is present in two different availability zones (us-east-2b & us-east-2c) to avoid any disaster or glitch.

  1. AWS Network ACL's

  2. AWS Security Group

  3. AWS ECR

  4. AWS EC2

  5. AWS LB

  6. AWS Autoscaling

  7. AWS KeyPairs

  8. AWS EBS

  9. AWS IAM roles

  10. AWS CloudWatch

Conclusion:

This project demonstrates how to use Terraform to define and deploy AWS infrastructure components for hosting a Django application on ECS. The combination of Terraform and ECS offers a powerful and scalable solution for deploying and managing containerized applications on AWS.

Note:

  1. Please note that in the file structure image, .terraform folder, .terraform.lock.hcl file will be automatically generated during terraform execution.

  2. It may take some minutes to configure infrastructure on AWS and mostly depends on your local machine computing power.

  3. Also have variabelized the boolean, which is not recommended and can be used directly in the code.

  4. After running the terraform apply command, it will ask for approx. 12 variables to at terminal interface, please keep the variables blank and press enter till all the variables are input blank, the configuration will run automatically. This is caused because we have created variable files for every child module and the same has been pasted in the parent variables file, also we have not created any config folder to pass the vars.tf values.

  5. Have created the child folders of different resources for simplicity. It helps any person to understand the code and make the necessary changes required without consuming additional time and effort. As per my understanding, no change is required in the Django python program files.

  6. Please use terraform destroy to remove all the infrastructure created to deploy AWS services, otherwise, it may cost unnecessary billing to your AWS account.

  7. Please refer to this document https://registry.terraform.io/providers/hashicorp/aws/latest/docs for any AWS-related syntax.

"Happy learning! Embrace the wonders of technology and let your passion drive you forward. Apologies for any unintended mistakes. Keep exploring and never stop growing. Your tech journey awaits!"