RSS

How to deploy Nginx with Terraform on AWS

Terraform AWS Infrastructure Load Balancer

Use Terraform to launch an EC2 instance running Nginx, configure security groups, and set up automatic provisioning with user data.

TL;DR

Nginx remains one of the most widely used web servers in the world. W3Techs reports that Nginx is used by 32.7% of websites whose web server is known. Terraform is built for exactly this kind of task: defining cloud infrastructure in configuration files so it can be reviewed, versioned, changed, and destroyed safely.

Manually deploying an Nginx web server through the AWS console is fine once. Doing it repeatedly across dev, staging, and production is where errors creep in. Using Terraform, you can create the instance, security group, boot scripts, and load balancer from a Git repository instead.

This guide walks through a complete Nginx-Terraform deployment on AWS: provisioning an EC2 instance, locking down access with a security group, using user data to install Nginx, and adding TLS termination at an Application Load Balancer.

Prerequisites

Before writing the provider configuration, make sure your local environment can authenticate to AWS and run Terraform commands.

With those prerequisites in place, the next task is to initialize Terraform with the AWS provider.

Set up the Terraform AWS provider

After preparing your tools and AWS account, create a main.tf file and define the Terraform and provider blocks.

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

variable "aws_region" {
  type    = string
  default = "eu-west-2"
}

provider "aws" {
  region = var.aws_region
}

Pinning the provider version prevents unexpected drift when the AWS provider changes, which is important as the AWS provider is one of the most-used Terraform providers.

Initialize the working directory:

terraform init

Once Terraform has downloaded the provider, you can define the EC2 instance that will run the Nginx web server.

Define the EC2 instance

Now that the provider is initialized, add the compute resource that Terraform will create in AWS.

A current Amazon Linux 2023 AMI is a good default for this walkthrough, but AMI IDs are region-specific. Instead of hard-coding an AMI ID that might fail when someone changes var.aws_region, use an aws_ami data source.

data "aws_ami" "al2023" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-2023.*-x86_64"]
  }

  filter {
    name   = "architecture"
    values = ["x86_64"]
  }
}

variable "key_name" {
  type        = string
  description = "Name of an existing EC2 key pair for SSH access"
}

resource "aws_instance" "nginx" {
  ami           = data.aws_ami.al2023.id
  instance_type = "t3.micro"
  key_name      = var.key_name

  tags = {
    Name = "terraform-nginx"
  }
}

The most important settings in this EC2 resource are ami, instance_type, key_name, and tags.

At this point, Terraform can create an instance, but it still needs network rules before you can reach it in a browser or over SSH.

Configure security groups

With the EC2 instance defined, add a security group so network access is explicit, reviewed, and stored in the same Terraform configuration file.

variable "vpc_id" {
  type        = string
  description = "VPC ID where the instance will be created"
}

variable "allowed_ssh_cidr" {
  type        = string
  description = "CIDR block allowed to SSH into the instance"
}

resource "aws_security_group" "nginx" {
  name        = "nginx-web-sg"
  description = "Allow HTTP, HTTPS, and restricted SSH"
  vpc_id      = var.vpc_id
}

resource "aws_vpc_security_group_ingress_rule" "nginx_http" {
  security_group_id = aws_security_group.nginx.id
  description       = "HTTP from the world"
  from_port         = 80
  to_port           = 80
  ip_protocol       = "tcp"
  cidr_ipv4         = "0.0.0.0/0"
}

resource "aws_vpc_security_group_ingress_rule" "nginx_https" {
  security_group_id = aws_security_group.nginx.id
  description       = "HTTPS from the world"
  from_port         = 443
  to_port           = 443
  ip_protocol       = "tcp"
  cidr_ipv4         = "0.0.0.0/0"
}

resource "aws_vpc_security_group_ingress_rule" "nginx_ssh" {
  security_group_id = aws_security_group.nginx.id
  description       = "SSH from a trusted IP range"
  from_port         = 22
  to_port           = 22
  ip_protocol       = "tcp"
  cidr_ipv4         = var.allowed_ssh_cidr
}

resource "aws_vpc_security_group_egress_rule" "nginx_all" {
  security_group_id = aws_security_group.nginx.id
  description       = "Allow all outbound traffic"
  ip_protocol       = "-1"
  cidr_ipv4         = "0.0.0.0/0"
}

Attach it to the EC2 resource:

resource "aws_instance" "nginx" {
  ami                    = data.aws_ami.al2023.id
  instance_type          = "t3.micro"
  key_name               = var.key_name
  vpc_security_group_ids = [aws_security_group.nginx.id]

  tags = {
    Name = "terraform-nginx"
  }
}

Port 22 is a common target, so it’s important to keep SSH locked down by default. Defining ingress and egress rules in Terraform also means a future terraform plan can show when someone modifies access, rather than leaving changes hidden in the AWS console.

Now that the server can receive traffic, the next step is to install Nginx automatically at boot.

Automate provisioning with user data

After the security group allows traffic, use EC2 user data to install Nginx when the instance starts for the first time.

resource "aws_instance" "nginx" {
  ami                    = data.aws_ami.al2023.id
  instance_type          = "t3.micro"
  key_name               = var.key_name
  vpc_security_group_ids = [aws_security_group.nginx.id]

  user_data = <<-EOF
  #!/bin/bash
  dnf update -y
  dnf install -y nginx
  systemctl enable --now nginx
  echo "Hello world from nginx terraform" > /usr/share/nginx/html/index.html
  EOF

  tags = {
    Name = "terraform-nginx"
  }
}

For Ubuntu, replace the package commands with apt-get update -y and apt-get install -y nginx.

User data is a good fit here because it runs during the instance’s first boot and keeps the setup tied to the EC2 lifecycle. You may have seen examples that use a null_resource, remote-exec, or local bash scripts to install software after provisioning. That pattern can work, as we explain in our article about null_resource use cases, but for basic bootstrapping, user data is usually simpler and easier to reason about.

At this stage, you can run terraform plan, then terraform apply, and test the instance’s public DNS name in a browser. For a production-style deployment, though, you usually want a load balancer in front of the instance.

Add a Load Balancer and TLS Termination

Once Nginx is installed automatically on the instance, the natural next step is to stop sending users directly to that instance and put an Application Load Balancer in front of it.

Start with an ALB security group and the load balancer:

variable "public_subnet_ids" {
  type        = list(string)
  description = "Public subnet IDs for the load balancer"
}

resource "aws_security_group" "alb" {
  name        = "nginx-alb-sg"
  description = "Allow web traffic to the ALB"
  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"]
  }
}

resource "aws_lb" "nginx" {
  name               = "nginx-alb"
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = var.public_subnet_ids
}

Then, create a target group and register the EC2 instance on port 80:

resource "aws_lb_target_group" "nginx" {
  name     = "nginx-targets"
  port     = 80
  protocol = "HTTP"
  vpc_id   = var.vpc_id

  health_check {
    path    = "/"
    matcher = "200-399"
  }
}

resource "aws_lb_target_group_attachment" "nginx" {
  target_group_arn = aws_lb_target_group.nginx.arn
  target_id        = aws_instance.nginx.id
  port             = 80
}

Finally, request an ACM certificate and attach it to an HTTPS listener:

variable "domain_name" {
  type        = string
  description = "Domain name for the TLS certificate"
}

resource "aws_acm_certificate" "nginx" {
  domain_name       = var.domain_name
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.nginx.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = aws_acm_certificate_validation.nginx.certificate_arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.nginx.arn
  }
}

AWS requires a certificate on an HTTPS listener, and the load balancer uses that certificate to terminate the front-end TLS connection before forwarding requests to the target. ACM DNS validation requires adding CNAME records that prove domain ownership. If the domain is in Route 53, you can automate that with aws_route53_record and aws_acm_certificate_validation.

Terminating TLS at the load balancer keeps certificate management out of the Nginx configuration file. Nginx can continue serving HTTP on port 80 inside the VPC, while the ALB handles HTTPS for users. As a hardening step, you can modify the instance security group later so that port 80 only accepts traffic from the ALB security group.

With the AWS path complete, let’s explore which parts of this setup are specific to AWS and which parts transfer to other tools.

Alternative technology options

The AWS implementation above is one version of a broader Terraform pattern: define infrastructure, bootstrap the server, and expose the app through a controlled entry point.

Conclusion

You now have a Terraform-managed Nginx deployment on AWS: an EC2 instance, a security group defined as code, automated Nginx provisioning through user data, and HTTPS termination at an Application Load Balancer.

Because the configuration is stored in Terraform and tracked in state, you can promote the same setup across environments, modify it through pull requests, tear it down, or rebuild it without retracing manual steps in the AWS console.

For teams, the next step is making sure changes to this Nginx-Terraform configuration go through review before they hit production. Terrateam brings Terraform workflows into pull requests, automatically running terraform plan and commenting results where reviewers can see them.