source: https://medium.com/@dinh.nt
Recently I took a task to deploy a full-fledged application securely on a virtual network in Azure cloud. I have to use IAC to reduce maintenance time later on.
This is not a easy task for me, since my knowledge of virtual network was limited. But luckily, I managed to get it done in a week, which is quite fast for what I had estimated.
I will share with you everything I did in this post.
The application will be securely contained within a virtual network and only allow traffic from internet to the container app. By that way, we minimize attack surface and secure all internal resources, while allow them to communicate over private connections
The application consists of the following components:
- Azure Container App: Connects to other services and serves as the entry point for internet traffic.
- Azure Redis Cache: Accessed via a private endpoint.
- Azure PostgreSQL Flexible Server: Secured within a dedicated subnet.
Each service resides in its own subnet, connected through a private DNS zone for seamless internal communication.
Our redis cache will be connected through a private endpoint, why ? Actually we can setup redis to sit within a virtual network directly, but that requires Redis Premium Tier which has a minimum size of 6GB and cost ~ $400/m, or more — which is very expensive for a small to medium application.
So we should go with Basic or Standard tier, and the only way to isolate it inside virtual network is through a private endpoint.
All services will be connected using a url, managed by the private DNS zone.
- Basic Terraform and IAC knowledge
- Azure subscription, an available resource group and Azure cli
- Terraform local setup
Everything is bounded by a virtual network so we will start with that
data "azurerm_resource_group" "app_rg" {
name = var.resource_group_name
}
resource "azurerm_virtual_network" "app_vnet" {
name = "app-virtual-network"
location = data.azurerm_resource_group.app_rg.location
resource_group_name = data.azurerm_resource_group.app_rg.name
address_space = ["10.0.0.0/16"]
tags = local.common_tags
}
In order to have PostgreSQL in a virtual network, we need the subnet dedicated for postgreSQL DB resources, a private DNS zone for routing and the DB itself. Notice that this is the DB server, we need to define which database we want to create in the server, too.
resource "azurerm_subnet" "postgresql_subnet" {
name = "postgresql-subnet"
resource_group_name = data.azurerm_resource_group.app_rg.name
virtual_network_name = azurerm_virtual_network.app_vnet.name
address_prefixes = ["10.0.2.0/24"]
service_endpoints = ["Microsoft.Storage"]
delegation {
name = "fs"
service_delegation {
name = "Microsoft.DBforPostgreSQL/flexibleServers"
actions = [
"Microsoft.Network/virtualNetworks/subnets/join/action",
]
}
}
}
resource "azurerm_private_dns_zone" "postgres_zone" {
name = "private.postgres.database.azure.com"
resource_group_name = data.azurerm_resource_group.app_rg.name
}
resource "azurerm_private_dns_zone_virtual_network_link" "postgres_vn_link" {
name = "postgresVnetZone.com"
private_dns_zone_name = azurerm_private_dns_zone.postgres_zone.name
virtual_network_id = azurerm_virtual_network.app_vnet.id
resource_group_name = data.azurerm_resource_group.app_rg.name
}
resource "azurerm_postgresql_flexible_server" "postgres_server" {
name = "app-db-posgresql"
resource_group_name = data.azurerm_resource_group.app_rg.name
location = data.azurerm_resource_group.app_rg.location
version = "16"
delegated_subnet_id = azurerm_subnet.postgresql_subnet.id
private_dns_zone_id = azurerm_private_dns_zone.postgres_zone.id
public_network_access_enabled = false
administrator_login = var.db_admin_username
administrator_password = var.db_admin_password
zone = "1"
storage_mb = var.app_config.postgresql.storage_mb
storage_tier = var.app_config.postgresql.storage_tier
sku_name = var.app_config.postgresql.sku_name
lifecycle {
prevent_destroy = true
}
}
// we will create main-database in the server
resource "azurerm_postgresql_flexible_server_database" "main_database" {
name = "main-database"
server_id = azurerm_postgresql_flexible_server.postgres_server.id
collation = "en_US.utf8"
charset = "utf8"
lifecycle {
prevent_destroy = true
}
}
The next thing will be the Azure Redis Cache with the a private endpoint and a DNS zone
resource "azurerm_redis_cache" "redis" {
name = "app-redis-cache"
location = data.azurerm_resource_group.app_rg.location
resource_group_name = data.azurerm_resource_group.app_rg.name
capacity = var.app_config.redis.capacity
family = var.app_config.redis.family
sku_name = var.app_config.redis.sku_name
public_network_access_enabled = false
non_ssl_port_enabled = false
minimum_tls_version = "1.2"
redis_version = "6"
}
resource "azurerm_private_endpoint" "redis_private_endpoint" {
name = "redis-endpoint"
location = data.azurerm_resource_group.app_rg.location
resource_group_name = data.azurerm_resource_group.app_rg.name
subnet_id = azurerm_subnet.redis_cache_subnet.id
custom_network_interface_name = "private-redis-nic"
private_dns_zone_group {
name = "private.redis.cache.windows.net"
private_dns_zone_ids = [azurerm_private_dns_zone.private_redis_zone.id]
}
private_service_connection {
name = "redis-private"
subresource_names = ["redisCache"]
private_connection_resource_id = azurerm_redis_cache.redis.id
is_manual_connection = false
}
}
resource "azurerm_private_dns_zone" "private_redis_zone" {
name = "redis.cache.windows.net" // must be redis.cache.windows.net
resource_group_name = data.azurerm_resource_group.app_rg.name
}
resource "azurerm_private_dns_zone_virtual_network_link" "private_redis_vn_link" {
name = "redisVnetZone.com"
private_dns_zone_name = azurerm_private_dns_zone.private_redis_zone.name
virtual_network_id = azurerm_virtual_network.app_vnet.id
resource_group_name = data.azurerm_resource_group.app_rg.name
registration_enabled = false
}
The container app will need a registry storage, but you can choose other registry service as well. The container app is only gate accept connection from internet.
The beauty of container app is that it will handle scaling automatically based on the traffic, custom domain, and zero downtime between deployment.
resource "azurerm_container_app_environment" "container_app_environment" {
name = "app-container-environment"
location = data.azurerm_resource_group.app_rg.location
resource_group_name = data.azurerm_resource_group.app_rg.name
infrastructure_subnet_id = azurerm_subnet.container_apps.id
workload_profile {
name = "Consumption"
workload_profile_type = "Consumption"
}
log_analytics_workspace_id = azurerm_log_analytics_workspace.log_analytics.id
}
resource "azurerm_subnet" "container_apps" {
name = "containerapps-subnet"
resource_group_name = data.azurerm_resource_group.app_rg.name
virtual_network_name = azurerm_virtual_network.app_vnet.name
address_prefixes = ["10.0.0.0/23"]
delegation {
name = "Microsoft.App.environments"
service_delegation {
name = "Microsoft.App/environments"
actions = [
"Microsoft.Network/virtualNetworks/subnets/join/action",
]
}
}
}
resource "azurerm_container_app" "container_app" {
name = "container-app"
container_app_environment_id = azurerm_container_app_environment.container_app_environment.id
resource_group_name = data.azurerm_resource_group.app_rg.name
revision_mode = "Single"
template {
container {
name = "container-app"
image = "...container-app:latest" // your image url
cpu = "0.5"
memory = "1Gi"
}
}
}
locals {
databaseUrl = "postgres://${azurerm_postgresql_flexible_server.postgres_server.administrator_login}:${azurerm_postgresql_flexible_server.postgres_server.administrator_password}@${azurerm_postgresql_flexible_server.postgres_server.fqdn}:5432/${azurerm_postgresql_flexible_server_database.main_database.name}?sslmode=require"
redisUrl = "rediss://:${azurerm_redis_cache.redis.primary_access_key}@${azurerm_redis_cache.redis.hostname}:${azurerm_redis_cache.redis.ssl_port}"
}
resource "azurerm_container_app" "container_app" {
template {
container {
env {
name = "DATABASE_URL"
value = local.databaseUrl
}
env {
name = "REDIS_URL"
value = local.redisUrl
}
}
}
}
And your container app can start access to Redis and the db.
Hope you guys find this usefull. If you have questions or improvements, feel free to share them!