How to build a lightning fast user signup Application
Part 1 : The Terraform files
Welcome to another CloudBlogger post ! This time we are exploring Azure Service Bus lightning fast messaging with a real world example including Container Apps and Azure Functions.
Azure Service Bus is a fully managed enterprise message broker with message queues and publish-subscribe topics (in a namespace). Service Bus is used to decouple applications and services from each other, providing the following benefits:
- Load-balancing work across competing workers
- Safely routing and transferring data and control across service and application boundaries
- Coordinating transactional work that requires a high-degree of reliability
Αυτό που χρειαζόμαστε:
- An Azure Subscription
- VSCode or your favorite editor
- Terraform
- Docker
- Let’s Encrypt Certificate
First things first, create a Service Principal so Terraform can authenticate to Azure
az ad sp create-for-rbac --n tform --role Contributor --scopes /subscriptions/00000000-0000-0000-0000-000000000000
We are going to need as always our standard files :
- providers.tf
- variables.tf
- terraform.tfvars
- main.tf
So lets create our files to start with, notice we are storing all variables into terraform.tfvars and also label the sp secret as sensitive. Terraform will never show this on the plan or apply tasks :
# providers.tf
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "3.48.0"
}
}
}
provider "azurerm" {
features {
key_vault {
purge_soft_delete_on_destroy = true
recover_soft_deleted_key_vaults = true
}
}
subscription_id = "var.azure_subidid"
tenant_id = "var.azure_tenantid"
client_id = var.azure_clientid
client_secret = var.azure_spsecret
}
# variables.tf
variable "azure_spsecret" {
description = "SP"
type = string
sensitive = true
}
variable "azure_clientid" {
description = "AppID"
type = string
}
variable "azure_subid" {
description = "SubscriptionID"
type = string
}
variable "azure_tenantid" {
description = "TenantID"
type = string
}
variable "azure_user" {
description = "Azure Portal User"
type = "string"
}
# terraform.tfvars
#Azure SP Secret
azure_spsecret = "xxxxx"
#Azure ClientID
azure_clientid = "xxxxxx-xxxx"
#Azure SubsctiptionID
azure_subid = "xxxxxxxxxx-xxx"
#Azure TenantID
azure_tenantid = "xxxxx-xxxxx"
#Azure User
azure_user = "xxx-xxx-xxx"
And of course the main.tf , with all our resources :
# main.tf
resource "azurerm_resource_group" "rgroup" {
name = "rg-app"
location = "West Europe"
}
resource "random_string" "str-name" {
length = 5
upper = false
numeric = false
lower = true
special = false
}
resource "azurerm_storage_account" "storage" {
name = "st${random_string.str-name.result}01"
resource_group_name = azurerm_resource_group.rgroup.name
location = azurerm_resource_group.rgroup.location
account_tier = "Standard"
account_replication_type = "LRS"
}
resource "azurerm_log_analytics_workspace" "logs" {
name = "Logskp"
location = azurerm_resource_group.rgroup.location
resource_group_name = azurerm_resource_group.rgroup.name
sku = "PerGB2018"
retention_in_days = 30
}
resource "azurerm_application_insights" "appinsights" {
name = "funcinsights"
location = azurerm_resource_group.rgroup.location
resource_group_name = azurerm_resource_group.rgroup.name
workspace_id = azurerm_log_analytics_workspace.logs.id
application_type = "Node.JS"
}
output "instrumentation_key" {
value = azurerm_application_insights.appinsights.instrumentation_key
sensitive = true
}
output "connection_string" {
value = azurerm_application_insights.appinsights.connection_string
sensitive = true
}
resource "azurerm_service_plan" "appsrv" {
name = "aplan-${random_string.str-name.result}"
location = azurerm_resource_group.rgroup.location
resource_group_name = azurerm_resource_group.rgroup.name
os_type = "Linux"
sku_name = "B1"
}
resource "azurerm_linux_function_app" "funcapp" {
name = "fnc${random_string.str-name.result}"
location = azurerm_resource_group.rgroup.location
resource_group_name = azurerm_resource_group.rgroup.name
service_plan_id = azurerm_service_plan.appsrv.id
storage_account_name = azurerm_storage_account.storage.name
storage_account_access_key = azurerm_storage_account.storage.primary_access_key
functions_extension_version = "~4"
app_settings = {
"WEBSITE_RUN_FROM_PACKAGE" = "1"
"FUNCTIONS_WORKER_RUNTIME" = "node"
"APPLICATIONINSIGHTS_CONNECTION_STRING" = azurerm_application_insights.appinsights.connection_string
"APPINSIGHTS_INSTRUMENTATIONKEY" = azurerm_application_insights.appinsights.instrumentation_key
}
identity {
type = "SystemAssigned"
}
site_config {
application_stack {
node_version = "18"
}
cors {
allowed_origins = ["*"]
}
}
}
resource "azurerm_resource_group" "rgsbus" {
name = "rg-sbus"
location = "West Europe"
}
resource "azurerm_servicebus_namespace" "sbus" {
name = "kpsbus01"
location = azurerm_resource_group.rgsbus.location
resource_group_name = azurerm_resource_group.rgsbus.name
sku = "Standard"
identity {
type = "SystemAssigned"
}
}
resource "azurerm_servicebus_queue" "squeue" {
name = "sbusqueue"
namespace_id = azurerm_servicebus_namespace.sbus.id
enable_partitioning = true
}
# Create a KeyVault
data "azurerm_client_config" "current" {}
resource "azurerm_key_vault" "kv1" {
name = "kvk${random_string.str-name.result}2"
location = azurerm_resource_group.rgsbus.location
resource_group_name = azurerm_resource_group.rgsbus.name
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
}
resource "azurerm_key_vault_access_policy" "kvpolicy" {
key_vault_id = azurerm_key_vault.kv1.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = azurerm_linux_function_app.funcapp.identity[0].principal_id
secret_permissions = [
"Get",
]
}
resource "azurerm_key_vault_access_policy" "worker_access_policy" {
key_vault_id = azurerm_key_vault.kv1.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = data.azurerm_client_config.current.object_id
key_permissions = [
"Create",
"Get"
]
secret_permissions = [
"Set",
"Get",
"Delete",
"Purge",
"Recover"
]
}
resource "azurerm_key_vault_access_policy" "user_access_policy" {
key_vault_id = azurerm_key_vault.kv1.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = var.azure_user
secret_permissions = [
"List",
"Set",
"Get",
"Purge",
"Recover",
"Delete",
"Backup",
"Restore"
]
}
resource "azurerm_container_app_environment" "cappenv" {
name = "contEnvironment"
location = azurerm_resource_group.rgsbus.location
resource_group_name = azurerm_resource_group.rgsbus.name
log_analytics_workspace_id = azurerm_log_analytics_workspace.logs.id
}
resource "azurerm_container_app" "capp" {
name = "c${random_string.str-name.result}001"
container_app_environment_id = azurerm_container_app_environment.cappenv.id
resource_group_name = azurerm_resource_group.rgsbus.name
revision_mode = "Single"
template {
max_replicas = 5
min_replicas = 1
container {
name = "webreg01"
image = "docker.io/kpassadis/webreg01:v2"
cpu = 1.0
memory = "2Gi"
}
}
ingress {
allow_insecure_connections = "false"
external_enabled = "true"
target_port = 80
traffic_weight {
percentage = "100"
latest_revision = true
}
}
}
Great ! At this point we have created two resource groups with all the required resources :
- Log Analytics Workspace with Application Insights
- Function App with an App Service Plan (Linux) and Storage Account
- Service Bus Queue
- Key Vault
- Container App with a Docker App ( Simple HTML to POST the HTTP Trigger)
Now, i will make quick reference to the Docker Application. All we need is a Docker File and we can push it to Docker Hub and later call it directly from Container Apps, Pretty cool right ?
So here are the HTML container app elements (index.html ,style.css ,Dockerfile) :
index.html
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="style.css">
<style>
#message {
margin-top: 20px;
padding: 10px;
background-color: lightgray;
border-radius: 5px;
text-align: center;
font-weight: bold;
display: none;
}
/* added CSS */
.center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
<script>
async function sendMessage(event) {
event.preventDefault();
const firstname = document.getElementById("firstname").value;
const lastname = document.getElementById("lastname").value;
const nickname = document.getElementById("nickname").value;
const userData = {
firstname: firstname,
lastname: lastname,
nickname: nickname
};
const response = await fetch("https://xxxxx.azurewebsites.net/api/xxxxx", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(userData)
});
const result = await response.json();
document.getElementById('message').innerText = result.message;
}
</script>
</head>
<body>
<div class="container">
<form method="post" action="https://xxxxx.azurewebsites.net/api/submit-form" onsubmit="sendMessage(event)">
<label for="firstname">First Name</label>
<input type="text" id="firstname" name="firstname" required>
<label for="lastname">Last Name</label>
<input type="text" id="lastname" name="lastname" required>
<label for="nickname">Nickname</label>
<input type="text" id="nickname" name="nickname" required>
<button type="submit">Submit</button>
</form>
</div>
<div id="message"></div>
</body>
</html>
style.css
body {
font-family: Arial, sans-serif;
background-color: #cce6ff;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background-color: #0073e6;
padding: 2rem;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
width: 400px;
}
form {
display: flex;
flex-direction: column;
}
label {
font-weight: bold;
margin-bottom: 0.5rem;
}
input {
margin-bottom: 1rem;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 3px;
}
button {
padding: 0.5rem 1rem;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
------------------------------------------------
Dockerfile
FROM nginx:stable-alpine
COPY . /usr/share/nginx/html
It is quite simple to publish the image on Docker Hub via VSCode :
We need NGINX that’s why our Dockerfile is as it is, and 3 simple steps:
- docker build -t myapp:v1 .
- docker login
- docker push myusername/myapp:v1
I won’t dive deeper into Docker but it is that simple! You can validate also with a local run {docker run –name test-container -p 8080:80 -d myusername/myapp:v1}, and all documentation is available at https://docs.docker.com/get-started/.
Now prepare a Let’s Encrypt certificate with you custom Domain, and stay tuned for Part 2! It is coming very very soon!
Pingback: Service Bus with Container Apps : Part 2 – CloudBlogger@2023
Pingback: Azure Vision AI – Object Detection Web App with Docker and Container Registry – CloudBlogger@2023