Terraform Project 2: Deploying Django Application on AWS ECS using Terraform
Table of Contents
Overview
Project Structure
Project Workflow
File Structure
Creating Parent Module
Creating Child Module
Steps for execution
AWS service console output
Conclusion
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:
Terraform Configuration:
The parent folder contains the main Terraform configuration files:
main.tf
,variables.tf
, andproviders.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.
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
, andoutput.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.
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.
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.
- The "deploy" folder contains the
Project Workflow:
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.
Dockerizing Django Application:
- The Django application is containerized using the provided Dockerfile, making it portable and easier to deploy across environments.
ECS Deployment:
- The
update-ecs
Python script is executed to deploy the Dockerized Django application to ECS, leveraging the resources created with Terraform
- The
File Structure
overall file structure
app folder
deploy folder
modules folder
Django Application
"""
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()
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!')
"""
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',
]
"""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 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()
#!/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
#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
provider "aws" {
region = var.region
}
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
}
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
AWS_VPC
# 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 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" } }
#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 }
AWS_Subnet
# 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] }
```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
}
AWS_RT
```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"
}
AWS_SG
# 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 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 }
#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 }
AWS_LB
# 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 } }
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 "alb_hostname" { value = aws_lb.prod.dns_name } output "aws_alb_target_group_arn" { value = aws_alb_target_group.default_tg.arn }
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" } } } ]
```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 "ecs_launch_configuration_name" {
value = aws_launch_configuration.ecs.name
}
AWS_Autoscaling
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] }
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 }
AWS_Cloudwatch
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 }
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_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": [ "*" ] } ] }
AWS_IAM
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 }
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 "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 }
AWS_KeyPair
resource "aws_key_pair" "prod" { key_name = "${var.ecs_cluster_name}_key_pair" public_key = file(var.ssh_pubkey_file) }
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 "keypair_name" { value = aws_key_pair.prod.key_name }
Steps for execution:
Clone the git hub repo from https://github.com/kunalrepo/Terraform_withAWS.git into your local machine.
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.
Build the docker image from the dockerfile present in the app folder into the AWS ECR
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.
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".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.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.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 byterraform plan
in the output file tfplan.out and performs the necessary actions to achieve the desired state.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
- 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.
AWS Network ACL's
AWS Security Group
AWS ECR
AWS EC2
AWS LB
AWS Autoscaling
AWS KeyPairs
AWS EBS
AWS IAM roles
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:
Please note that in the file structure image, .terraform folder, .terraform.lock.hcl file will be automatically generated during terraform execution.
It may take some minutes to configure infrastructure on AWS and mostly depends on your local machine computing power.
Also have variabelized the boolean, which is not recommended and can be used directly in the code.
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.
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.
Please use
terraform destroy
to remove all the infrastructure created to deploy AWS services, otherwise, it may cost unnecessary billing to your AWS account.Please refer to this document https://registry.terraform.io/providers/hashicorp/aws/latest/docs for any AWS-related syntax.