Azure MySQL Single Server using Terraform
Step-00: Introduction
- Input Variables
mysqldb.auto.tfvars
- Input Variables
secrets.tfvars
with -var-file
argument
Azure Concepts
- Create Azure MySQL Single Server and Sample Schema in it
- Create
service endpoint policies
to allow traffic to specific azure resources from your virtual network over service endpoints
- Create
Virtual Network Rule
to make a connection from Azure Virtual Network Subnet to Azure MySQL Single Server
- Create a
MySQL Firewall Rule
to allow Bastion Host to access MySQL DB. Understand MySQL Firewall rule concept.
Azure Resources
- azurerm_mysql_server
- azurerm_mysql_database
- azurerm_mysql_firewall_rule
- azurerm_mysql_virtual_network_rule
Step-01: Azure MySQL Single Server MySQL TF Configs
# 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
# 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]
}
# 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
- c1-versions.tf
- c2-generic-input-variables.tf
- c3-locals.tf
- c4-random-resources.tf
- c5-resource-group.tf
- c6-01-vnet-input-variables.tf
- c6-02-virtual-network.tf
- c6-04-app-subnet-and-nsg.tf
- c6-05-db-subnet-and-nsg.tf
- c6-06-bastion-subnet-and-nsg.tf
- c6-07-ag-subnet-and-nsg.tf
- c6-08-vnet-outputs.tf
- c7-02-web-linux-vmss-nsg-inline-basic.tf
- c7-04-web-linux-vmss-outputs.tf
- c7-05-web-linux-vmss-autoscaling-default-profile.tf
- c7-06-web-linux-vmss-autoscaling-default-and-recurrence-profiles.tf
- c7-07-web-linux-vmss-autoscaling-default-recurrence-fixed-profiles.tf
- c8-01-bastion-host-input-variables.tf
- c8-04-AzureBastionService.tf
- c9-01-application-gateway-input-variables.tf
- c9-03-application-gateway-outputs.tf
- c10-01-storage-account-input-variables.tf
- c10-02-storage-account.tf
- c10-03-storage-account-outputs.tf
# 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