Hosting a Static Website on Azure VM
Making use of Terraform to create Azure resources
Table of contents
Introduction
You have embarked on your journey toward becoming a DevOps Engineer and have been tasked with deploying a static website.
While manually creating your infrastructure on the Cloud provider's management console may seem the preferred option, it's not always practical.
This is where infrastructure as code comes into play, one of the game changers in the tech industry that has benefited DevOps engineers for years.
Infrastructure as code refers to provisioning and managing computing infrastructure through code rather than manual configurations and processes.
Terraform is an Infrastructure as Code (IaC) tool that enables engineers to define their software infrastructure using code.
This article is for beginners and it explains in detail how to deploy a static website on an Azure VM using Terraform.
Pre-requisites
An Azure account. If you don't have one, sign up here
Install Terraform on your local terminal.
Install Azure CLI on your local terminal
What is a static website?
A static website is a type of website that displays the same content to every visitor, without any dynamic elements or user interaction. In other words, the content of a static website is fixed and does not change based on user input or interactions.
These websites are typically coded in HyperText Markup Language (HTML) and Cascading Style Sheets (CSS), with perhaps some JavaScript for interactivity.
The content is pre-defined and does not rely on server-side processing or databases to generate pages.
Static websites are often used for simple websites, blogs, portfolios, or informational sites where content doesn't need to be frequently updated or personalized for each user.
If you're unfamiliar with creating a static website, explore free website templates available here.
Terraform Modules
In Terraform, root modules, and child modules are used to structure and manage your infrastructure code.
Root modules are the top-level directories in your Terraform project. They typically contain the main configuration files used to define the infrastructure for various environments such as production, development, and staging.
Child modules are Terraform project directories or collections of files that include certain resources or customizations.
By identifying and isolating smaller, independent components of your infrastructure, they encourage modularity and reusability.
You can assemble complicated infrastructures from smaller, more manageable components by referencing child modules from either root modules or other child modules.
know your .tf
files
Terraform scripts use the .tf
Extension. In any Terraform directory, you will typically find these seven files:
main.tf: The primary configuration file that defines the resources to be created. It uses the resource block.
variable.tf: The file that makes it possible for terraform scripts to be reusable. It uses the variable block.
locals.tf: Defines local values that simplify and reduce repetition in the configuration. It uses the local block.
data. tf: Retrieves information about existing resources to be used elsewhere in the configuration. It uses the data block.
terraform. tfvars: Assigns values to the variables declared in
`variable.tf
.output.tf: The files that specify the output values of the resources created. It uses the output block.
provider.tf: Configures the provider (e.g., Azure, AWS) and sets up necessary credentials and settings. It uses the provider block.
Visualize your solution architecture.
Creating a solution architecture diagram helps identify the necessary resources and understand their interactions. It provides a clear overview of the components, their connections, and the overall infrastructure layout.
Visualize your directory structure.
Visualizing your directory structure can assist you in understanding how to organize your code. One of the reasons why IaC is a game changer is its capability to provision multiple cloud resources across various regions using a single script.
This is why these scripts are preferred to be modularized, where each piece of code required to create a specific resource is organized within its directory.
Each module typically serves a specific function or handles a distinct set of tasks, making the overall system easier to manage, understand, and maintain.
When visualizing your directory structure, begin by creating a structure with main.tf, variable.tf, and output.tf files in your child-modules directory. Create additional files as needed.
Define your cloud provider
The first step is to create a file named provider.tf
. This is where you define your cloud provider. In this case, the cloud provider is Azure. To see a list of cloud providers in Terraform check here
To get a terraform configuration for your desired cloud provider:
Click on the link above.
Click on your desired cloud provider.
select your version
click on the button Use Provider
Copy the configuration and paste it to the provider.tf file.
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "3.77.0"
}
}
}
provider "azurerm" {
subscription_id = var.subscription_id
features {
}
}
Note: Every Azure account by default is assigned a subscription ID. Terraform requires this ID to access and create infrastructure within your specific account.
After Provider.tf, What Next?
"Ever heard of a nesting doll? If you haven't, see the picture below."
Before you start writing Terraform scripts, think of it like a nesting doll. Begin by writing the code for the foundational components of your infrastructure (the outer layer) like resource groups, and then proceed to define the more specific resources and configurations (the inner layers)like the virtual machine.
Child modules at work
This project has three sub-directories in the child modules directory.
Resource group creation
An Azure Resource Group is a container that stores resources for an Azure solution. It consists of resources such as virtual computers, storage accounts, virtual networks, and databases that are controlled collaboratively.
# Create a resource group for the project
resource "azurerm_resource_group" "rg" {
name = "${var.name}-rg"
location = var.region
}
variable "name" {}
variable "region" {}
# Output the name of the resource group
output "rg-name" {
value = azurerm_resource_group.rg.name
}
# Ouput the location of the resource group
output "rg-location" {
value = azurerm_resource_group.rg.location
}
Virtual Network Creation
# create the virtual network for the application
resource "azurerm_virtual_network" "vnet" {
name = "${var.name}-network"
location = var.rg-location
resource_group_name = var.rg-name
address_space = var.cidr_block
}
############################################################################################
###########################################################################################
# Create the subnets
resource "azurerm_subnet" "subnet" {
count = length(var.subnets)
name = "${var.name}-subnet-${count.index}"
resource_group_name = var.rg-name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = [var.subnets[count.index]]
private_link_service_network_policies_enabled = false
}
variable "name" {}
variable "rg-name" {}
variable "rg-location" {}
variable "cidr_block" {}
variable "subnets" {}
# Output the subnet ids
output "subnet_ids" {
value = azurerm_subnet.subnet[*].id
}
Virtual Machines
# Create a public Ip address that will be attached to the virtual machine
resource "azurerm_public_ip" "public-ip" {
name = "${var.name}-publicip"
resource_group_name = var.rg-name
location = var.rg-location
allocation_method = var.allocation_method
}
######################################################################
######################################################################
# Create a network interface to manage the VM's network
resource "azurerm_network_interface" "nic" {
name = "${var.name}-nic"
location = var.rg-location
resource_group_name = var.rg-name
ip_configuration {
name = "internal"
subnet_id = var.subnet_ids[1]
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.public-ip.id
}
}
########################################################################
########################################################################
# Create network security group
resource "azurerm_network_security_group" "sg" {
name = "${var.name}-sg"
location = var.rg-location
resource_group_name = var.rg-name
security_rule {
name = "ssh"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "*"
destination_address_prefix = azurerm_network_interface.nic.private_ip_address
}
security_rule {
name = "http"
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "*"
destination_address_prefix = azurerm_network_interface.nic.private_ip_address
}
}
########################################################################################
########################################################################################
# Create a network interface security group association
resource "azurerm_network_interface_security_group_association" "nic-sg" {
network_interface_id = azurerm_network_interface.nic.id
network_security_group_id = azurerm_network_security_group.sg.id
}
##########################################################################################
#########################################################################################
# Create the azure VM
resource "azurerm_linux_virtual_machine" "linuxVm" {
name = "${var.name}-linuxVm"
resource_group_name = var.rg-name
location = var.rg-location
size = "Standard_F2"
admin_username = var.admin_username
admin_password = var.admin_password
network_interface_ids = [
azurerm_network_interface.nic.id,
]
disable_password_authentication = false
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
}
##############################################################################
##############################################################################
# Copy files to desired path an install nginx
resource "null_resource" "web" {
provisioner "file" {
source = var.source_file_path
destination = var.destination_file_path
}
connection {
type = "ssh"
user = var.admin_username
password = var.admin_password
host = azurerm_public_ip.public-ip.ip_address
}
provisioner "remote-exec" {
inline = [
"sudo apt-get update -y",
"sudo apt-get install -y apache2",
"sudo cp -r ${var.destination_file_path}/* /var/www/html/"
]
connection {
type = "ssh"
user = var.admin_username
password = var.admin_password
host = azurerm_public_ip.public-ip.ip_address
}
}
depends_on = [ azurerm_linux_virtual_machine.linuxVm ]
}
variable "name" {}
variable "rg-name" {}
variable "rg-location" {}
variable "allocation_method" {}
variable "subnet_ids" {}
variable "source_file_path" {}
variable "destination_file_path" {}
variable "admin_username" {}
variable "admin_password" {}
The output files were referenced in the main.tf
of other resources. The static files were managed within the main.tf
configuration of the virtual machine.
The location of these files was specified in the Terraform configuration. Additionally, Apache2 was configured and installed on the virtual machine.
A command was issued to transfer the files to /var/www/html/index.html
on the Apache2 server, as this directory serves static content.
Root modules at work
This directory contains files that can be used to build infrastructure in your work environment
Main.tf: This is where the child modules are referenced
module "rg"{
source = "../terraform-child-modules/rg"
region = var.region
name = var.name
}
module "network" {
source = "../terraform-child-modules/network"
name = var.name
rg-location = module.rg.rg-location
rg-name = module.rg.rg-name
cidr_block = var.cidr_block
subnets = var.subnets
}
module "VM" {
source = "../terraform-child-modules/VM"
name = var.name
rg-location = module.rg.rg-location
rg-name = module.rg.rg-name
allocation_method = var.allocation_method
source_file_path = var.source_file_path
destination_file_path = var.destination_file_path
subnet_ids = module.network.subnet_ids
admin_username = var.admin_username
admin_password = var.admin_password
}
Variable.tf: This file holds all the variables in the child modules directory and also in the provider.tf
variable "name" {}
variable "region" {}
variable "cidr_block" {}
variable "subnets" {}
variable "allocation_method" {}
variable "source_file_path" {}
variable "destination_file_path" {}
variable "subscription_id" {}
variable "admin_username" {}
variable "admin_password" {}
Terraform.tfvars: This is a private file. It's not good practice to push this file to Git Hub. The values in this file determine the shape your infrastructure will take.
name = "static-web"
region = "West Europe"
cidr_block = ["10.0.0.0/16"]
subnets = ["10.0.1.0/24", "10.0.2.0/24"]
allocation_method = "Static"
source_file_path = "../fashion"
destination_file_path = "/home/adminuser/fashion/"
subscription_id = "************" #input your own id
admin_username = "adminuser"
admin_password = "techChic123%"
Documentation is a necessary tool
It is essential to comprehend Terraform's extensive documentation to fully utilize it. Terraform's documentation is a vital resource for anyone embarking on a journey. It provides valuable insights, industry best practices, and customized solutions to optimize infrastructure management.
A simple Google search 'terraform azure virtual machine' can help you access the documentation to get details on this resource and how to structure your code.
Conclusion
Terraform's module-based structure and declarative configuration allow developers to provide and manage resources across many environments with ease.
Whether establishing new deployments or growing already existing infrastructures, Terraform gives teams the flexibility to standardize and automate processes, guaranteeing dependability and consistency.