DEV Community

Cover image for Deploying Django Application on AWS with Terraform. Namecheap Domain + SSL
Yevhen Bondar for Daiquiri Team

Posted on • Edited on

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 provider.tf 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
}
Enter fullscreen mode Exit fullscreen mode

In variables.tf add:

# Namecheap
variable "namecheap_api_username" {
  description = "Namecheap APIUsername"
}
variable "namecheap_api_key" {
  description = "Namecheap APIKey"
}
Enter fullscreen mode Exit fullscreen mode

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

TF_VAR_namecheap_api_username=YOUR_API_USERNAME
TF_VAR_namecheap_api_key=YOUR_API_KEY
Enter fullscreen mode Exit fullscreen mode

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 variables.tf following code:

# Domains
variable "prod_base_domain" {
  description = "Base domain for production"
  default = "example53.xyz"
}
variable "prod_backend_domain" {
  description = "Backend web domain for production"
  default = "api.example53.xyz"
}
Enter fullscreen mode Exit fullscreen mode

Add a route53.tf file:

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

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

  nameservers = [
    aws_route53_zone.prod.name_servers[0],
    aws_route53_zone.prod.name_servers[1],
    aws_route53_zone.prod.name_servers[2],
    aws_route53_zone.prod.name_servers[3],
  ]
}
Enter fullscreen mode Exit fullscreen mode

Run terraform apply. Check nameservers on Namecheap:

Creating SSL Certificate

Now, let's create an SSL certificate and set up DNS A record for api.example53.xyz domain.

Add to the route53.tf 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            = each.value.name
  records         = [each.value.record]
  ttl             = 60
  type            = each.value.type
  zone_id         = aws_route53_zone.prod.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 = aws_route53_zone.prod.zone_id
  name    = var.prod_backend_domain
  type    = "A"

  alias {
    name                   = aws_lb.prod.dns_name
    zone_id                = aws_lb.prod.zone_id
    evaluate_target_health = true
  }
}
Enter fullscreen mode Exit fullscreen mode

Here we are going to create a new SSL certificate for api.example53.xyz, 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 load_balancer.tf with the following code:

# Target listener for http:80
resource "aws_lb_listener" "prod_http" {
  load_balancer_arn = aws_lb.prod.id
  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 = aws_lb.prod.id
  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
}
Enter fullscreen mode Exit fullscreen mode

Here we redirect unsecured HTTP traffic to HTTPS and add a listener for the HTTPS port. Apply changes and check https://api.example53.xyz 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 api.example53.xyz in the host header.

Now Django accepts any domain, for example, Load Balancer's domain. Visit https://prod-1222631842.us-east-2.elb.amazonaws.com 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 variables.tf:

variable "prod_backend_secret_key" {
  description = "production Django's SECRET_KEY"
}
Enter fullscreen mode Exit fullscreen mode

Next, pass the domain name and SECRET_KEY in ecs.tf, 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
  }
}
Enter fullscreen mode Exit fullscreen mode
"environment": [
  ...
  {
    "name": "SECRET_KEY",
    "value": "${secret_key}"
  },
  {
    "name": "DEBUG",
    "value": "off"
  },
  {
    "name": "ALLOWED_HOSTS",
    "value": "${domain}"
  }
],
Enter fullscreen mode Exit fullscreen mode

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

# 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=["*"])
Enter fullscreen mode Exit fullscreen mode

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 api.example53.xyz. 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 load_balancer.tf to /health/, and apply changes:

resource "aws_lb_target_group" "prod_backend" {
  ...
  health_check {
    path = "/health/"
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

Return to the Django project and create django_aws/middleware.py:

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 == "/health/":
            # 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
Enter fullscreen mode Exit fullscreen mode

Add this middleware to the settings.py before the SecurityMiddleware:

MIDDLEWARE = [
    'django_aws.middleware.health_check_middleware',
    'django.middleware.security.SecurityMiddleware',
    ...
]
Enter fullscreen mode Exit fullscreen mode

Run python manage.py runserver and check 127.0.0.1:8000/health/ 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 https://prod-57218461274.us-east-2.elb.amazonaws.com/. 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 https://prod-57218461274.us-east-2.elb.amazonaws.com/health/ 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.

Top comments (0)