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)