How to deploy Nginx with Terraform on AWS
Use Terraform to launch an EC2 instance running Nginx, configure security groups, and set up automatic provisioning with user data.
TL;DR
- You’ll write a Terraform configuration that launches an EC2 instance and installs Nginx automatically with user data.
- Security groups are defined as code, so firewall rules are auditable and consistent across environments.
- An Application Load Balancer and ACM certificate handle HTTPS termination without changes to your Nginx config.
- The same pattern works on GCP and Azure, or with alternative web servers like Apache and Caddy.
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.
- Terraform CLI 1.5 or later. Use HashiCorp’s official Terraform install docs if you need to install or update it.
- AWS CLI configured for an AWS account with permissions to create EC2, security group, ALB, ACM, and optional Route 53 resources.
- An existing EC2 key pair if you want SSH access, plus the matching
.pemfile stored securely. - A working folder or directory for the Terraform files, for example:
nginx-terraform/.
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.
- Cloud provider alternatives – The same structure works on GCP with
google_compute_instanceand on Azure withazurerm_linux_virtual_machine. The provider changes, but the plan, resource graph, and review workflow stay the same. - Alternative web servers – Apache and Caddy are common substitutes for Nginx. Caddy is especially useful when you want automatic TLS through Let’s Encrypt, while HAProxy is better suited to pure load-balancing workloads than static web serving.
- Workflow alternatives – As this grows, move repeated configuration into a Terraform module, bake images instead of using inline user data, or manage changes through pull requests rather than one-off terminal commands.
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.