Deploying Django Application on AWS with Terraform. Namecheap Domain + SSL


In previous steps, we’ve deployed Django with AWS ECS, connected it to the PostgreSQL RDS, and set up GitLab CI/CD

In this step, we are going to:

  • Connect Namecheap domain to Route53 DNS zone.
  • Create an SSL certificate with Certificate Manager.
  • Reroute HTTP traffic to HTTPS and disable ALB host for Django application.
  • Add /health/ route for Health Checks.

Setting up Namecheap API

I already have a domain name on Namecheap. So I choose to connect the Namecheap domain to an AWS Route53 zone. But you can register domain with AWS Route53.

First, let’s enable API access for Namecheap. Look through this guide to receive APIKey and add your IP to the whitelist.

Second, add the Namecheap provider to Terraform project. Add to the file following code:

terraform {
  required_providers {
    namecheap = {
      source  = "namecheap/namecheap"
      version = ">= 2.0.0"

provider "namecheap" {
  user_name   = var.namecheap_api_username
  api_user    = var.namecheap_api_username
  api_key     = var.namecheap_api_key
  use_sandbox = false

In add:

# Namecheap
variable "namecheap_api_username" {
  description = "Namecheap APIUsername"
variable "namecheap_api_key" {
  description = "Namecheap APIKey"

Also add TF_VAR_namecheap_api_username and TF_VAR_namecheap_api_key variables to .env to provide values to the corresponding Terraform variables.


Import .env variables with export $(cat .env | xargs) and run terraform init to add a Namecheap provider to the project.

Connecting Domain to AWS

Now, let’s create a Route53 zone for the Namecheap domain and set up AWS nameservers. Thus, all DNS queries will be routed to the AWS Route53 nameservers, and we can manage DNS records from the AWS Route53 zone.

Add to the following code:

# Domains
variable "prod_base_domain" {
  description = "Base domain for production"
  default = ""
variable "prod_backend_domain" {
  description = "Backend web domain for production"
  default = ""

Add a file:

resource "aws_route53_zone" "prod" {
  name = var.prod_base_domain

resource "namecheap_domain_records" "prod" {
  domain = var.prod_base_domain
  mode   = "OVERWRITE"

  nameservers = [[0],[1],[2],[3],

Run terraform apply. Check nameservers on Namecheap:

Creating SSL Certificate

Now, let’s create an SSL certificate and set up DNS A record for domain.

Add to the following code:


resource "aws_acm_certificate" "prod_backend" {
  domain_name       = var.prod_backend_domain
  validation_method = "DNS"

resource "aws_route53_record" "prod_backend_certificate_validation" {
  for_each = {
    for dvo in aws_acm_certificate.prod_backend.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type

  allow_overwrite = true
  name            =
  records         = [each.value.record]
  ttl             = 60
  type            = each.value.type
  zone_id         =

resource "aws_acm_certificate_validation" "prod_backend" {
  certificate_arn         = aws_acm_certificate.prod_backend.arn
  validation_record_fqdns = [for record in aws_route53_record.prod_backend_certificate_validation : record.fqdn]

resource "aws_route53_record" "prod_backend_a" {
  zone_id =
  name    = var.prod_backend_domain
  type    = "A"

  alias {
    name                   =
    zone_id                =
    evaluate_target_health = true

Here we are going to create a new SSL certificate for, validate the SSL certificate via DNS CNAME record, and add DNS A record to Load Balancer.

Apply changes with terraform apply and wait for certificate validation. Usually, it takes up to several minutes. But in some cases, it can take several hours. You can check more info here.

Redirecting HTTP to HTTPS

Now let’s use the issued SSL certificate to enable HTTPS. Replace the resource "aws_lb_listener" "prod_http" block in the with the following code:

# Target listener for http:80
resource "aws_lb_listener" "prod_http" {
  load_balancer_arn =
  port              = "80"
  protocol          = "HTTP"
  depends_on        = [aws_lb_target_group.prod_backend]

  default_action {
    type = "redirect"
    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"

# Target listener for https:443
resource "aws_alb_listener" "prod_https" {
  load_balancer_arn =
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"
  depends_on        = [aws_lb_target_group.prod_backend]

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.prod_backend.arn

  certificate_arn = aws_acm_certificate_validation.prod_backend.certificate_arn

Here we redirect unsecured HTTP traffic to HTTPS and add a listener for the HTTPS port. Apply changes and check URL. You should see Django starting page.

Setting up the ALLOWED_HOSTS variable

Now, let’s provide the ALLOWED_HOSTS setting to the Django app. It’s important to prevent HTTP Host header attacks. So, Django Application should only accept our domain in the host header.

Now Django accepts any domain, for example, Load Balancer’s domain. Visit to check this fact. You can ignore the warning about an invalid SSL Certificate and see that Django responds to this host.

Also, let’s disable a Debug mode and remove the SECRET_KEY value from the code to improve security. Add the TF_VAR_prod_backend_secret_key variable with a random generated value to the .env, run export $(cat .env | xargs), and specify this var in

variable "prod_backend_secret_key" {
  description = "production Django's SECRET_KEY"

Next, pass the domain name and SECRET_KEY in, set up SECRET_KEY, DEBUG, and ALLOWED_HOSTS variables in backend_container.json.tpl and apply changes:

locals {
  container_vars = {

    domain = var.prod_backend_domain
    secret_key = var.prod_backend_secret_key
"environment": [
    "name": "SECRET_KEY",
    "value": "${secret_key}"
    "name": "DEBUG",
    "value": "off"
    "name": "ALLOWED_HOSTS",
    "value": "${domain}"

Now we have all necessary environment variables on ECS. Move to the Django app and change

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env("SECRET_KEY", default="ewfi83f2ofee3398fh2ofno24f")

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env("DEBUG", cast=bool, default=True)

ALLOWED_HOSTS = env("ALLOWED_HOSTS", cast=list, default=["*"])

Here we receive SECRET_KEY, DEBUG, and ALLOWED_HOSTS variables from env variables. We provide default SECRET_KEY to allow running the application locally without specifying SECRET_KEY in the .env file.

Health Check

All user’s requests would have Host header But, we also have health check requests from a load balancer.

AWS load balancers can automatically check our container’s health. If the container responds correctly, the load balancer considers that target is healthy. Otherwise, the target will be marked as unhealthy. Load balancer routes traffic to healthy targets only. Thus, user requests wouldn’t hit unhealthy containers.

For HTTP or HTTPS health check requests, the host header contains the IP address of the load balancer node and the listener port, not the IP address of the target and the health check port.

We don’t know the Load Balancer IP address. Also, this IP can be changed after some time. Therefore, we cannot add the Load Balancer host to the ALLOWED_HOSTS.

The solution is to write a custom middleware that returns a successful response before the host checking in the SecurityMiddleware.

First, go to the infrastructure, change the health check URL in to /health/, and apply changes:

resource "aws_lb_target_group" "prod_backend" {
  health_check {
    path = ""

Return to the Django project and create django_aws/

from django.http import HttpResponse
from django.db import connection

def health_check_middleware(get_response):
    def middleware(request):
        # Health-check request
        if request.path == "":
            # Check DB connection is healthy
            with connection.cursor() as cursor:
                cursor.execute("SELECT 1")

            return HttpResponse("Healthy!")

        # Regular requests
        return get_response(request)

    return middleware

Add this middleware to the before the SecurityMiddleware:


Run python runserver and check URL in your browser. You should see the text response Healthy!.

Commit and push changes, wait for the pipeline and check the Load Balancer domain again Now, we get a Bad Request error. Also, we didn’t see a traceback or other debug information, so we can be sure that the debug mode is disabled.

400 Bad Request

Also, navigate to in your browser to check health_check_middleware. We get the Healthy! response. So, the Load Balancer will be able to check containers’ health without providing the correct Host header.

Health Check Success Response

Congratulations! We’ve successfully set up a domain name, created health checks, disabled the debug mode, and removed SECRET_KEY value from the source code. Do not forget to push infrastructure code to GitLab.

You can find the source code of backend and infrastructure projects here and here.

If you need technical consulting on your project, check out our website or connect with me directly on LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

How to Build a Resilient Agile Mindset

Next Post

Facebook Business Manager: How to Use Meta Business Suite in 2022

Related Posts