Author: Karen
Categories: TECH
Tags: Terraform Docker AWS

In previous posts we explored how to create an EC2 instance using Terraform. However, to make the application production-ready, a few more important steps are still needed. We need to:

  • Create an Elastic IP and attach to the instance
  • Register a domain (Route 53 preferrably) and point to the Elastic IP
  • Create Route 53 records, such as yourdomain.com www.yourdomain.com 
  • Launch the instance with a predefined script for installations (optional)
  • Configure HTTPS
  • Update the nginx container and default.conf to handle HTTPS traffic

Elastic IP

Elastic IP can be thought of  as a static IP and it is better to point your domain to the elastic IP rather than to the public IP because when the instance restarts, public IP can change. Using an Elastic IP ensures that your application remains accessible at the same address even if the EC2 instance is stopped and started again.

In Terraform you can create and attach an elastic IP to your instance like this:

resource "aws_eip" "app_eip" {
  domain = "vpc"

  tags = {
    Name = "app-eip"
  }
}

resource "aws_eip_association" "app_eip_assoc" {
  instance_id   = aws_instance.web.id
  allocation_id = aws_eip.app_eip.id
}

Now when the Elastic IP is created and attached to the instance, it is time to get a domain. Assuming the domain is purchased using AWS Route 53, you can reference it in Terraform and point it to your Elastic IP. Managing DNS records through Terraform also allows your infrastructure and configuration to remain fully reproducible.

Domain Handling

variable "domain_name" {
  description = "Root domain name"
  type        = string
}

data "aws_route53_zone" "zone" {
  name = var.domain_name
  private_zone = false
}

resource "aws_route53_record" "root" {
  zone_id = data.aws_route53_zone.zone.zone_id
  name    = var.domain_name
  type    = "A"
  ttl     = 300
  records = [aws_eip.app_eip.public_ip]
}

resource "aws_route53_record" "www_root" {
  zone_id = data.aws_route53_zone.zone.zone_id
  name    = "www.${var.domain_name}"
  type    = "A"
  ttl     = 300
  records = [aws_eip.app_eip.public_ip]
}
var.domain_name is simply a variable referenced in the terraform.tfvars, for example 
domain_name = "yourdomain.com"
This approach keeps your configuration flexible across environments. If your domain is unlikely to change, you can also hardcode it directly inside the Terraform file instead of using terraform.tfvars.
 
HTPPS Configuration
 
Once the EC2 instance is up and running (you can refer to Creating an EC2 Instance in the Default VPC Using Terraform for the complete Terraform configuration) it is time to configure HTTPS. Securing your application with HTTPS is critical, as it encrypts traffic between the client and the server and is expected by modern browsers.
On the instance, run the following commands:
  • sudo yum install certbot python3-certbot-nginx
  • sudo certbot certonly --standalone -d yourdomain.com -d www.yourdomain.com

After the certificates are generated, verify that /etc/letsencrypt/live/ exists on the host and contains the expected certificate files. These certificates will later be mounted into the nginx container.

Now we need to modify nginx's default.conf and later the container to properly handle HTTPS traffic.

In default.conf we define two server blocks. Port 80 now redirects traffic to port 443, ensuring that all requests use HTTPS. Port 443 is configured to handle secure traffic using the certificates generated by Certbot.

Previously created ssl_certificate files are referenced. 

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    client_max_body_size 10M;
    server_name yourdomain.com www.yourdomain.com;

     # SSL certificate files
     ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
     ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

    location /static/ {
        alias /app/static/;
        expires 1d;
        add_header Cache-Control "public";
    }

    location / {
        limit_req zone=mylimit burst=10 nodelay;
        proxy_pass http://web:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

This setup is similar to the one described in Gunicorn and Nginx Setup for Serving the Web App, with the key difference being the addition of redirecting, HTTPS support and certificate handling.

Updating the Nginx Container

Now it time to modify the nginx container. Since the SSL certificates are generated on the host, they must be mounted into the container so Nginx can access them.

In Gunicorn and Nginx Setup for Serving the Web App post's setup, the container only exposed port 80. We now expose port 443 as well and mount the /etc/letsencrypt directory as read-only. The updated Docker Compose configuration looks like this:

nginx:
    image: nginx:latest
    container_name: nginx
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - web
    volumes:
      - /etc/letsencrypt:/etc/letsencrypt:ro
      - ./src/nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/logs:/var/log/nginx
      - ./src/staticfiles:/app/static
    restart: always
    networks:
      - myproject-net

Assuming your web container is running on port 8000 and the nginx container is also successfully running, opening  yourdomain.com or www.yourdomain.com in the browser should now securely serve the web app over HTTPS.