How to deploy an Nginx App on AWS with Terraform and GitHub Actions

How to deploy an Nginx App on AWS with Terraform and GitHub Actions


7 min read


This is a step-by-step guide on how to create a web server on AWS using Terraform. In this tutorial, we will walk through the process of provisioning a virtual private cloud (VPC) with public and private subnets, setting up an internet gateway, and deploying an Nginx web server in the public subnet. Additionally, we will configure the necessary security groups, route tables, and a NAT gateway to ensure secure and efficient communication within the VPC.

This guide is designed to provide you with a solid foundation in deploying infrastructure using Terraform. It serves as a practical example to help you understand key concepts and best practices in infrastructure as code. If you're looking to expand your knowledge further and explore more advanced topics in infrastructure as code, I encourage you to check out my other article titled "Learn Infrastructure as Code by Building a Custom Machine Image in AWS" on FreeCodeCamp. It delves deeper into Terraform and demonstrates how to build custom machine images in the AWS environment.


Before you begin, ensure you have the following:

How to configure terraform backend state

To store the Terraform state, we'll configure a backend to use an S3 bucket. Add the following configuration to your Terraform file:

terraform {
  backend "s3" {
    bucket  = "team-warriors-web-server-backend-state"
    key     = "team-warriors-web-server/development/terraform.tfstate"
    region  = "eu-west-1"
    encrypt = true
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"

How to Set Up VPC and Networking Topology

How to Create AWS VPC with Terraform

A Virtual Private Cloud (VPC) is a logically isolated network within AWS. Let's create a VPC with the following configuration:

resource "aws_vpc" "main_vpc" {
  cidr_block           = var.cidr_block
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = "${var.env_prefix}-main_vpc"

How to Create AWS Internet Gateway with Terraform

An internet gateway enables communication between instances in your VPC and the internet. Let's create an internet gateway:

"aws_internet_gateway" "igw" {
  vpc_id = var.vpc_id

  tags = {
    Name = "${var.env_prefix}-web-server-igw"

How to Create AWS Public Subnets with Terraform

Public subnets are used to launch resources that require a public IP address. Let's create public subnets:

"aws_subnet" "public_subnets" {
  vpc_id                  = var.vpc_id
  cidr_block              = cidrsubnet(var.vpc_cidr_block, 8, 2 + count.index)
  availability_zone       = element(var.availability_zones, count.index)
  map_public_ip_on_launch = "true"
  count                   = var.public_subnets_count

  tags = {
    Name = "${var.env_prefix}-web-server-public-subnet"

How to Create AWS Private Subnets with Terraform

Private subnets are used for resources that don't require a public IP address. Let's create private subnets:

"aws_subnet" "private_subnets" {
  vpc_id                  = var.vpc_id
  cidr_block              = cidrsubnet(var.vpc_cidr_block, 8, count.index)
  availability_zone       = element(var.availability_zones, count.index)
  map_public_ip_on_launch = false
  count                   = var.private_subnets_count

  tags = {
    Name = "${var.env_prefix}-web-server-private-subnet"

How to Create a route table and Associate Subnets with Terraform

A route table is needed to allow subnets to access the internet. Let's create a route table and associate the subnets:

"aws_route_table" "public_rt" {
  vpc_id = var.vpc_id

  route {
    cidr_block = ""
    gateway_id = var.internet_gateway_id

  tags = {
    Name = "${var.env_prefix}-web-server-public-rt"

resource "aws_route_table_association" "public" {
  count          = var.public_subnets_count
  subnet_id      = element(var.public_subnets.*.id, count.index)
  route_table_id =

How to Create AWS NAT Gateway with Terraform

A NAT gateway allows instances in private subnets to access the internet. Let's create a NAT gateway:

"aws_eip" "eip_for_the_nat_gateway" {
  vpc = true

  tags = {
    Name = "${var.env_prefix}-web-server-eip-for-the-nat-gateway"

resource "aws_nat_gateway" "nat_gateway" {
  allocation_id =
  subnet_id     = element(var.public_subnets.*.id, 0)

  tags = {
    Name = "${var.env_prefix}-web-server-nat-gateway"

How to Provision an AWS Compute Service

How to Create AWS Security Group with Terraform

A security group allows traffic to reach the web server. Let's create a security group to allow ports 22 (SSH), 80 (HTTP), and 443 (HTTPS):

"aws_security_group" "allow-web-security-group" {
  vpc_id      = var.vpc_id
  description = "Allow web inbound traffic"
  name        = "allows_web_traffic"

  ingress {
    from_port       = "22"
    to_port         = "22"
    protocol        = "tcp"
    security_groups = []

  ingress {
    description = "HTTPS in public subnet"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = [""]

  ingress {
    description = "HTTPS in public subnet"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = [""]

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1" # all protocols
    cidr_blocks = [""]

  tags = {
    Name = "allow-web"

How to Create AWS key pair with Terraform

To establish SSH access to the instances, we'll create a key pair:

"aws_key_pair" "bastion" {
  key_name   = "bastion-key"
  public_key = file(var.public_key_path)

How to Create Compute Instances (EC2) with Terraform

Finally, let's create the EC2 instances for the web server and bastion:

hashicorpCopy codedata "aws_ami" "latest-amazon-linux-image" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn-ami-hvm-*-x86_64-gp2"]

resource "aws_instance" "nginx-instance" {
  ami                    = data.aws_ami.latest-amazon-linux-image.value
  instance_type          = "t2.micro"
  key_name               = aws_key_pair.ngnix-server-key-pair.key_name
  vpc_security_group_ids = [var.security_group_allow_web]
  subnet_id              = element(var.public_subnets, 0).id

  user_data = file("./userdata/")

  tags = {
    Name = "${var.env_prefix}-nginx-instance"

How to Create CICD with Github Actions

Furthermore, to enhance your deployment workflow and automate the infrastructure provisioning process, we will be using GitHub Actions. GitHub Actions is a powerful CI/CD platform that allows you to define workflows and automate tasks directly within your GitHub repository. The code snippet below showcases the GitHub Actions workflow for Terraform:

name: Terraform

      - main
      - Github-actions

    name: Terraform
    runs-on: ubuntu-latest
      AWS_REGION: "eu-west-1"

      - name: Checkout
        uses: actions/checkout@v2

      - name: Terraform Format
        id: fmt
        run: terraform fmt -check

      - name: Terraform Init
        id: init
        run: terraform init

      - name: Terraform Validate
        id: validate
        run: terraform validate -no-color

      - name: Terraform Plan
        id: plan
        if: github.event_name == 'pull_request'
        run: terraform plan -no-color
        continue-on-error: true

      - uses: actions/github-script@0.9.0
        if: github.event_name == 'pull_request'
          PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const output = `#### Terraform Format and Style ๐Ÿ–Œ\`${{ steps.fmt.outcome }}\`
            #### Terraform Initialization โš™๏ธ\`${{ steps.init.outcome }}\`
            #### Terraform Validation ๐Ÿค–\`${{ steps.validate.outcome }}\`
            #### Terraform Plan ๐Ÿ“–\`${{ steps.plan.outcome }}\`

            <details><summary>Show Plan</summary>



            *Pusher: @${{ }}, Action: \`${{ github.event_name }}\`*`;

              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output

      - name: Terraform Plan Status
        if: steps.plan.outcome == 'failure'
        run: exit 1

      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve

Feel free to incorporate this workflow into your project, which will enable you to automate the provisioning process of your infrastructure using Terraform.

For a comprehensive understanding of CI/CD pipelines and to explore different deployment strategies, including continuous integration, continuous delivery, and continuous deployment, you can refer to my blog post titled "How to Create a CI/CD Pipeline". It provides valuable insights into building robust and efficient CI/CD workflows.

Server test


In this article, we walk through the process of setting up an Nginx web server on AWS using Terraform. We covered the creation of a VPC, subnets, internet and NAT gateways, security groups, and EC2 instances. Also automating it with GitHub actions. With this setup, you now have a scalable and secure web server infrastructure on AWS.

Remember to destroy the resources when they are no longer needed to avoid incurring unnecessary costs.

Project Source

This article is based on a project developed as part of the NexaScale cloud mentorship program. The program provided hands-on guidance and training on cloud technologies, including AWS and Terraform. The project aimed to create a scalable and secure web server infrastructure using the concepts covered during the program.

I would like to express our gratitude to the NexaScale team for their support and guidance throughout this project. It has been an invaluable learning experience, and we encourage others to explore similar mentorship opportunities in their cloud journey.

Source code

Check out the source code here: aws-nginx-terraform

Additional Resources