Skip to content

Azure MySQL Single Server using Terraform

Step-00: Introduction

Terraform Concepts

  1. Input Variables mysqldb.auto.tfvars
  2. Input Variables secrets.tfvars with -var-file argument

Azure Concepts

  1. Create Azure MySQL Single Server and Sample Schema in it
  2. Create service endpoint policies to allow traffic to specific azure resources from your virtual network over service endpoints
  3. Create Virtual Network Rule to make a connection from Azure Virtual Network Subnet to Azure MySQL Single Server
  4. Create a MySQL Firewall Rule to allow Bastion Host to access MySQL DB. Understand MySQL Firewall rule concept.

Azure Resources

  1. azurerm_mysql_server
  2. azurerm_mysql_database
  3. azurerm_mysql_firewall_rule
  4. azurerm_mysql_virtual_network_rule

Step-01: Azure MySQL Single Server MySQL TF Configs

Step-01-01: c11-01-mysql-servers-input-variables.tf

# Input Variables
# DB Name
variable "mysql_db_name" {
  description = "Azure MySQL Database Name"
  type        = string
}

# DB Username - Enable Sensitive flag
variable "mysql_db_username" {
  description = "Azure MySQL Database Administrator Username"
  type        = string
}
# DB Password - Enable Sensitive flag
variable "mysql_db_password" {
  description = "Azure MySQL Database Administrator Password"
  type        = string
  sensitive   = true
}

# DB Schema Name
variable "mysql_db_schema" {
  description = "Azure MySQL Database Schema Name"
  type        = string
}

Step-01-02: c11-02-mysql-servers-resource.tf

# Resource-1: Azure MySQL Server
resource "azurerm_mysql_server" "mysql_server" {
  name                = "${local.resource_name_prefix}-${var.mysql_db_name}"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name

  administrator_login          = var.mysql_db_username
  administrator_login_password = var.mysql_db_password

  #sku_name   = "B_Gen5_2" # Basic Tier - Azure Virtual Network Rules not supported
  sku_name   = "GP_Gen5_2" # General Purpose Tier - Supports Azure Virtual Network Rules
  storage_mb = 5120
  version    = "8.0"

  auto_grow_enabled                 = true
  backup_retention_days             = 7
  geo_redundant_backup_enabled      = false
  infrastructure_encryption_enabled = false
  public_network_access_enabled     = true
  ssl_enforcement_enabled           = false
  ssl_minimal_tls_version_enforced  = "TLSEnforcementDisabled" 

}

# Resource-2: Azure MySQL Database / Schema
resource "azurerm_mysql_database" "webappdb" {
  name                = var.mysql_db_schema
  resource_group_name = azurerm_resource_group.rg.name
  server_name         = azurerm_mysql_server.mysql_server.name
  charset             = "utf8"
  collation           = "utf8_unicode_ci"
}

# Resource-3: Azure MySQL Firewall Rule - Allow access from Bastion Host Public IP
resource "azurerm_mysql_firewall_rule" "mysql_fw_rule" {
  name                = "allow-access-from-bastionhost-publicip"
  resource_group_name = azurerm_resource_group.rg.name
  server_name         = azurerm_mysql_server.mysql_server.name
  start_ip_address    = azurerm_public_ip.bastion_host_publicip.ip_address
  end_ip_address      = azurerm_public_ip.bastion_host_publicip.ip_address
}

# Resource-4: Azure MySQL Virtual Network Rule
resource "azurerm_mysql_virtual_network_rule" "mysql_virtual_network_rule" {
  name                = "mysql-vnet-rule"
  resource_group_name = azurerm_resource_group.rg.name
  server_name         = azurerm_mysql_server.mysql_server.name
  subnet_id           = azurerm_subnet.websubnet.id
}

Step-01-03: c11-03-mysql-servers-output-values.tf

# Output Values
output "mysql_server_fqdn" {
  description = "MySQL Server FQDN"
  value = azurerm_mysql_server.mysql_server.fqdn
}

Step-01-04: mysqldb.auto.tfvars

# MySQL DB Name
mysql_db_name = "mysql"
mysql_db_username = "dbadmin"
mysql_db_schema = "webappdb"

Step-01-05: secrets.tfvars

# Secret Variables (Should not be checked-in to Github)
mysql_db_password = "H@Sh1CoR3!"

Step-02: Virtual Network Subnet Changes

Step-02-01: c6-03-web-subnet-and-nsg.tf - Web Subnet

  • Create service endpoint policies to allow traffic to specific azure resources from your virtual network over service endpoints.
  • Add service_endpoints = [ "Microsoft.Sql" ] for Web Subnet
    # Resource-1: Create WebTier Subnet
    resource "azurerm_subnet" "websubnet" {
      name                 = "${azurerm_virtual_network.vnet.name}-${var.web_subnet_name}"
      resource_group_name  = azurerm_resource_group.rg.name
      virtual_network_name = azurerm_virtual_network.vnet.name
      address_prefixes     = var.web_subnet_address  
      service_endpoints = [ "Microsoft.Sql" ]
    }
    

Step-02-02: c6-03-web-subnet-and-nsg.tf - Locals Block

  • Add Port 8080 for Web Subnet Inbound Port Rules in Locals Block
    # Resource-4: Create NSG Rules
    ## Locals Block for Security Rules
    locals {
      web_inbound_ports_map = {
        "100" : "80", # If the key starts with a number, you must use the colon syntax ":" instead of "="
        "110" : "443",
        "120" : "22", 
        "130" : "8080"
      } 
    }
    

Step-03: Application Configs

Step-03-01: c7-01-web-linux-vmss-input-variables.tf

# Linux VM Input Variables Placeholder file.
variable "web_vmss_nsg_inbound_ports" {
  description = "Web VMSS NSG Inbound Ports"
  type = list(string)
  default = [22, 80, 443, 8080]
}

Step-03-02: terraform.tfvars

# Add port 8080 for VMSS NSG Inbound Ports
web_vmss_nsg_inbound_ports = [22, 80, 443, 8080]

Step-03-03: c7-03-web-linux-vmss-resource.tf - Locals Block Custom Data

# Locals Block for custom data
locals {
webvm_custom_data = <<CUSTOM_DATA
#!/bin/sh
#sudo yum update -y
# Stop Firewall and Disable it
sudo systemctl stop firewalld
sudo systemctl disable firewalld

# Java App Install
sudo yum -y install java-11-openjdk
sudo yum -y install telnet
sudo yum -y install mysql
mkdir /home/azureuser/app3-usermgmt && cd /home/azureuser/app3-usermgmt
wget https://github.com/stacksimplify/temp1/releases/download/1.0.0/usermgmt-webapp.war -P /home/azureuser/app3-usermgmt 
export DB_HOSTNAME=${azurerm_mysql_server.mysql_server.fqdn}
export DB_PORT=3306
export DB_NAME=${azurerm_mysql_database.webappdb.name}
export DB_USERNAME="${azurerm_mysql_server.mysql_server.administrator_login}@${azurerm_mysql_server.mysql_server.fqdn}"
export DB_PASSWORD=${azurerm_mysql_server.mysql_server.administrator_login_password}
java -jar /home/azureuser/app3-usermgmt/usermgmt-webapp.war > /home/azureuser/app3-usermgmt/ums-start.log &
CUSTOM_DATA  
}

Step-03-04: c7-03-web-linux-vmss-resource.tf - VMSS Resource

# Resource: Azure Linux Virtual Machine Scale Set - App1
resource "azurerm_linux_virtual_machine_scale_set" "web_vmss" {
  # 1. Create VMSS only if Java App related DB Schema "webappdb" is created in MySQL Server
  # 2. Only create VMSS if DB is ready with Virtual Network Rule so connection for Java App can be established to DB
  depends_on = [azurerm_mysql_database.webappdb, azurerm_mysql_virtual_network_rule.mysql_virtual_network_rule] 
  name                = "${local.resource_name_prefix}-web-vmss"
  #computer_name_prefix = "vmss-app1" # if name argument is not valid one for VMs, we can use this for VM Names
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location
  sku                 = "Standard_DS1_v2"
  instances           = 2
  admin_username      = "azureuser"

  admin_ssh_key {
    username   = "azureuser"
    public_key = file("${path.module}/ssh-keys/terraform-azure.pub")
  }

  source_image_reference {
    publisher = "RedHat"
    offer = "RHEL"
    sku = "83-gen2"
    version = "latest"
  }

  os_disk {
    storage_account_type = "Standard_LRS"
    caching              = "ReadWrite"
  }

  upgrade_mode = "Automatic"

  network_interface {
    name    = "web-vmss-nic"
    primary = true
    network_security_group_id = azurerm_network_security_group.web_vmss_nsg.id
    ip_configuration {
      name      = "internal"
      primary   = true
      subnet_id = azurerm_subnet.websubnet.id  
      #load_balancer_backend_address_pool_ids = [azurerm_lb_backend_address_pool.web_lb_backend_address_pool.id]
      application_gateway_backend_address_pool_ids = [azurerm_application_gateway.web_ag.backend_address_pool[0].id]            
    }
  }
  #custom_data = filebase64("${path.module}/app-scripts/redhat-app1-script.sh")      
  custom_data = base64encode(local.webvm_custom_data)  
}

Step-04: Application Gateway Configs - c9-02-application-gateway-resource.tf

  • We will update the backend_http_settings block
  • cookie_based_affinity = "Enabled"
  • affinity_cookie_name = "ApplicationGatewayAffinity"
  • port = 8080
  • We will update the probe block
  • port = 8080
  • path = "/login"
  • body = "Username"
    # Resource-2: Azure Application Gateway - Standard
    resource "azurerm_application_gateway" "web_ag" {
      name                = "${local.resource_name_prefix}-web-ag"
      resource_group_name = azurerm_resource_group.rg.name
      location            = azurerm_resource_group.rg.location
    # START: --------------------------------------- #
    # SKU: Standard_v2 (New Version )
      sku {
        name     = "Standard_v2"
        tier     = "Standard_v2"
        #capacity = 2
      }
      autoscale_configuration {
        min_capacity = 0
        max_capacity = 10
      }  
    # END: --------------------------------------- #
    
      gateway_ip_configuration {
        name      = "my-gateway-ip-configuration"
        subnet_id = azurerm_subnet.agsubnet.id
      }
    
    # Frontend Port  - HTTP Port 80
      frontend_port {
        name = local.frontend_port_name_http 
        port = 80    
      }
    
    # Frontend Port  - HTTP Port 443
      frontend_port {
        name = local.frontend_port_name_https
        port = 443    
      }  
    
    # Frontend IP Configuration
      frontend_ip_configuration {
        name                 = local.frontend_ip_configuration_name
        public_ip_address_id = azurerm_public_ip.web_ag_publicip.id    
      }
    
      # App1 Configs
      backend_address_pool {
        name = local.backend_address_pool_name_app1
      }
      backend_http_settings {
        name                  = local.http_setting_name_app1
        #cookie_based_affinity = "Disabled"
        cookie_based_affinity = "Enabled"
        affinity_cookie_name = "ApplicationGatewayAffinity"
        #path                  = "/app1/"
        port                  = 8080
        protocol              = "Http"
        request_timeout       = 60
        probe_name            = local.probe_name_app1
      }
      probe {
        name                = local.probe_name_app1
        host                = "127.0.0.1"
        interval            = 30
        timeout             = 30
        unhealthy_threshold = 3
        protocol            = "Http"
        port                = 8080
        path                = "/login"
        match { # Optional
          body              = "Username"
          status_code       = ["200"]
        }
      }   
    
    # HTTP Listener - Port 80
      http_listener {
        name                           = local.listener_name_http
        frontend_ip_configuration_name = local.frontend_ip_configuration_name
        frontend_port_name             = local.frontend_port_name_http
        protocol                       = "Http"    
      }
    # HTTP Routing Rule - HTTP to HTTPS Redirect
      request_routing_rule {
        name                       = local.request_routing_rule_name_http
        rule_type                  = "Basic"
        http_listener_name         = local.listener_name_http 
        redirect_configuration_name = local.redirect_configuration_name
      }
    # Redirect Config for HTTP to HTTPS Redirect  
      redirect_configuration {
        name = local.redirect_configuration_name
        redirect_type = "Permanent"
        target_listener_name = local.listener_name_https
        include_path = true
        include_query_string = true
      }  
    
    
    # SSL Certificate Block
      ssl_certificate {
        name = local.ssl_certificate_name
        password = "kalyan"
        data = filebase64("${path.module}/ssl-self-signed/httpd.pfx")
      }
    
    # HTTPS Listener - Port 443  
      http_listener {
        name                           = local.listener_name_https
        frontend_ip_configuration_name = local.frontend_ip_configuration_name
        frontend_port_name             = local.frontend_port_name_https
        protocol                       = "Https"    
        ssl_certificate_name           = local.ssl_certificate_name    
        custom_error_configuration {
          custom_error_page_url = "${azurerm_storage_account.storage_account.primary_web_endpoint}502.html"
          status_code = "HttpStatus502"
        }
        custom_error_configuration {
          custom_error_page_url = "${azurerm_storage_account.storage_account.primary_web_endpoint}403.html"
          status_code = "HttpStatus403"
        }    
      }
    
    # HTTPS Routing Rule - Port 443
      request_routing_rule {
        name                       = local.request_routing_rule_name_https
        rule_type                  = "Basic"
        http_listener_name         = local.listener_name_https
        backend_address_pool_name  = local.backend_address_pool_name_app1
        backend_http_settings_name = local.http_setting_name_app1    
      }
    
    
    }
    

Step-05: Enable Bastion Host

Step-05-01: c8-02-bastion-host-linuxvm.tf

  • Enable all configs from file c8-02-bastion-host-linuxvm.tf

Step-05-02: c8-03-move-ssh-key-to-bastion-host.tf

  • Enable all configs from file c8-03-move-ssh-key-to-bastion-host.tf

Step-05-03: c8-05-bastion-outputs.tf

  • Enable all configs from file c8-05-bastion-outputs.tf

Step-06: TF Configs Untouched

  1. c1-versions.tf
  2. c2-generic-input-variables.tf
  3. c3-locals.tf
  4. c4-random-resources.tf
  5. c5-resource-group.tf
  6. c6-01-vnet-input-variables.tf
  7. c6-02-virtual-network.tf
  8. c6-04-app-subnet-and-nsg.tf
  9. c6-05-db-subnet-and-nsg.tf
  10. c6-06-bastion-subnet-and-nsg.tf
  11. c6-07-ag-subnet-and-nsg.tf
  12. c6-08-vnet-outputs.tf
  13. c7-02-web-linux-vmss-nsg-inline-basic.tf
  14. c7-04-web-linux-vmss-outputs.tf
  15. c7-05-web-linux-vmss-autoscaling-default-profile.tf
  16. c7-06-web-linux-vmss-autoscaling-default-and-recurrence-profiles.tf
  17. c7-07-web-linux-vmss-autoscaling-default-recurrence-fixed-profiles.tf
  18. c8-01-bastion-host-input-variables.tf
  19. c8-04-AzureBastionService.tf
  20. c9-01-application-gateway-input-variables.tf
  21. c9-03-application-gateway-outputs.tf
  22. c10-01-storage-account-input-variables.tf
  23. c10-02-storage-account.tf
  24. c10-03-storage-account-outputs.tf

Step-07: Execute Terraform Commands

# Terraform Initialize
terraform init

# Terraform Validate
terraform validate

# Terraform Plan
terraform plan -var-file=secrets.tfvars

# Terraform Apply 
terraform apply -var-file=secrets.tfvars

Step-08: Connect to MySQL DB from Bastion Host VM and VMSS VM

  • Test from Bastion Host which confirms our Azure MySQL firewall rule test
  • Test from VMSS VM1 or VM2 confirms that our Azure MySQL Virtual Network rule and Web Subnet Service Endpoint Configs we have enabled private communication from Web Subnet hosted VM's to Azure MySQL Single Server
    # SSH to Bastion Host
    ssh -i ssh-keys/terraform-azure.pem azureuser@<Bastion-Public-IP>
    sudo su - 
    
    # Connect to MySQL DB
    mysql -h hr-dev-mysql.mysql.database.azure.com -u dbadmin@hr-dev-mysql -p 
    
    # DB Password to use
    mysql_db_password = "H@Sh1CoR3!"
    
    # SSH to Web VMSS VM1 or VM2
    ssh -i /tmp/terraform-azure.pem azureuser@<VMSS-VM1-Private-IP>
    ssh -i /tmp/terraform-azure.pem azureuser@10.1.1.6
    
    # Connect to MySQL DB from Web VMSS VM1 or VM2 (This happens via Virtual Network we created from Web Subnet to MySQL Server)
    mysql -h hr-dev-mysql.mysql.database.azure.com -u dbadmin@hr-dev-mysql -p 
    

Step-09: Verify VMSS VM1 or VM2 Custom Data installed Apps

  • Verify /var/log/cloud-init-output.log
  • Verify User Management Web Application (UMS App) startup log /home/azureuser/app3-usermgmt/ums-start.log
# Verify VMSS VM1 or VM2  cloud-init-output.log
cd /var/log
tail -100f cloud-init-output.log

# Verify User Management UMS App in VMSS VM1 or VM2
cd /home/azureuser/app3-usermgmt
ls
tail -100f /home/azureuser/app3-usermgmt/ums-start.log
more /home/azureuser/app3-usermgmt/ums-start.log

Step-10: Verify Application Gateway Health

  • Go to Services -> Application Gateways -> hr-dev-web-ag -> Monitoring -> Backend Health
  • Backend Health
  • Health Probes -> Test Probe
  • Insights

Step-11: Verify by accessing Application

# Update Host Entry
sudo vi /etc/hosts
<APP-GW-PublicIP> terraformguru.com 

# Custom Host Entries
20.85.193.158  terraformguru.com

# Access Application
http://terraformguru.com # Should redirect to https URL
https://terraformguru.com 
Username: admin101
Password: password101
- Test List User
- Test Create User
- Test login with newly created user

# Important Notes
1. User Management Web Application (UMS Web App) is coded in such a way during the startup of the application, it will create a default admin user in MySQL Database connected to it. 
2. If connection to MySQL Server fails when UMS Web App is starting, it will come online.  

Step-12: Clean-up

# Destroy Resources
terraform destroy -var-file=secrets.tfvars -auto-approve

# Delete Files
rm -rf .terraform*
rm -rf terraform.tfstate*

Additional Reference

🎉 New Course
Ultimate DevOps Real-World Project Implementation on AWS
$15.99 $84.99 81% OFF
MARCH2026
Enroll Now on Udemy →
🎉 Offer