Skip to content

[Az] Day 04: Develop a VNet Hub for Private AKS on Azure.

Published: at 12:00 PM

Introduction

In this tutorial, we will guide you through the development of the initial Hub VNet for a private AKS environment using Pulumi.

We will demonstrate how to seamlessly integrate a VNet with an Azure Firewall, along with configuring outbound public IP addresses.

Security is paramount. Our focus will be on enhancing security by implementing network policies, firewalls, and encryption to safeguard our environment, all while maintaining cost-effectiveness on Azure.


Table of Contents

Open Table of Contents

The Hub VNet modules

The VNet Module

This module facilitates the creation of a Virtual Network, allowing for the specification of subnets as parameters. It also enables VNet encryption by default to enhance security.

View code:

import { getName } from '@az-commons';
import * as network from '@pulumi/azure-native/network';
import * as resources from '@pulumi/azure-native/resources';
import * as inputs from '@pulumi/azure-native/types/input';

export default (
    name: string,
    {
        rsGroup,
        subnets,
    }: {
        rsGroup: resources.ResourceGroup;
        subnets: inputs.network.SubnetArgs[];
    }
) =>
    new network.VirtualNetwork(
        getName(name, 'vnet'),
        {
            // Resource group name
            resourceGroupName: rsGroup.name,
            //Enable VN protection
            enableVmProtection: true,
            //Enable Vnet encryption
            encryption: {
                enabled: true,
                enforcement:
                    network.VirtualNetworkEncryptionEnforcement
                        .AllowUnencrypted,
            },
            addressSpace: {
                addressPrefixes: subnets.map((s) => s.addressPrefix!),
            },
            subnets,
        },
        // Ensure the virtual network depends on the resource group
        {
            dependsOn: rsGroup,
            //Ignore this property as the peering will be manage by instance of VirtualNetworkPeering
            ignoreChanges: ['virtualNetworkPeerings'],
        }
    );

The FirewallPolicy.ts Module

This module is responsible for creating a FirewallPolicy resource, which serves as the root policy for the Azure Firewall. This root policy will be the foundation for linking additional policy groups in subsequent Pulumi projects.

View code:

import * as network from '@pulumi/azure-native/network';
import * as resources from '@pulumi/azure-native/resources';

// Export a function that creates a Firewall Policy
export default (
    name: string,
    {
        rsGroup,
        //This tier should be similar to Firewall tier
        tier = network.FirewallPolicySkuTier.Basic,
    }: {
        rsGroup: resources.ResourceGroup;
        tier?: network.FirewallPolicySkuTier;
    }
) =>
    new network.FirewallPolicy(
        `${name}-fw-policy`,
        {
            resourceGroupName: rsGroup.name,
            sku: { tier },
            snat: { autoLearnPrivateRanges: 'Enabled' },
        },
        { dependsOn: rsGroup }
    );

The Firewall.ts Module

This module is designed to set up an Azure Firewall, including essential parts such as IP addresses and diagnostic settings. It ensures the firewall is connected to the designated subnet within the VNet and is associated with the root policy resources.

View code:

import { getName } from '@az-commons';
import * as azure from '@pulumi/azure-native';
import * as pulumi from '@pulumi/pulumi';

export default (
    name: string,
    {
        vnet,
        rsGroup,
        policy,
        //The Policy tier and Firewall tier must be the same
        tier = azure.network.AzureFirewallSkuTier.Basic,
        logWorkspaceId,
    }: {
        vnet: azure.network.VirtualNetwork;
        rsGroup: azure.resources.ResourceGroup;
        policy: azure.network.FirewallPolicy;
        tier?: azure.network.AzureFirewallSkuTier;
        logWorkspaceId?: pulumi.Output<string>;
    }
) => {
    const firewallSubnetId = vnet.subnets!.apply(
        (s) => s!.find((s) => s!.name === 'AzureFirewallSubnet')!.id!
    );
    const firewallManageSubnetId = vnet.subnets!.apply(
        (s) => s!.find((s) => s!.name === 'AzureFirewallManagementSubnet')!.id!
    );
    // Create Public IP Address for outbound traffic
    const publicIP = new azure.network.PublicIPAddress(
        getName(`${name}-outbound`, 'ip'),
        {
            resourceGroupName: rsGroup.name, // Resource group name
            publicIPAllocationMethod: azure.network.IPAllocationMethod.Static, // Static IP allocation
            sku: {
                name: azure.network.PublicIPAddressSkuName.Standard, // Standard SKU
                tier: azure.network.PublicIPAddressSkuTier.Regional, // Regional tier
            },
        },
        { dependsOn: rsGroup } // Ensure the public IP depends on the resource group
    );

    // Create Management Public IP Address for Firewall "Basic" tier
    const managePublicIP = new azure.network.PublicIPAddress(
        getName(`${name}-manage`, 'ip'),
        {
            resourceGroupName: rsGroup.name, // Resource group name
            publicIPAllocationMethod: azure.network.IPAllocationMethod.Static, // Static IP allocation
            sku: {
                name: azure.network.PublicIPAddressSkuName.Standard, // Standard SKU
                tier: azure.network.PublicIPAddressSkuTier.Regional, // Regional tier
            },
        },
        { dependsOn: rsGroup } // Ensure the management public IP depends on the resource group
    );

    // Create Azure Firewall
    const firewallName = getName(name, 'firewall');
    const firewall = new azure.network.AzureFirewall(
        firewallName,
        {
            resourceGroupName: rsGroup.name,
            firewallPolicy: { id: policy.id },
            ipConfigurations: [
                {
                    name: publicIP.name,
                    publicIPAddress: { id: publicIP.id },
                    subnet: { id: firewallSubnetId },
                },
            ],
            managementIpConfiguration: {
                name: managePublicIP.name,
                publicIPAddress: { id: managePublicIP.id },
                subnet: { id: firewallManageSubnetId },
            },
            sku: {
                name: azure.network.AzureFirewallSkuName.AZFW_VNet,
                tier,
            },
        },
        {
            // Ensure the firewall dependents
            dependsOn: [publicIP, vnet, managePublicIP, policy],
        }
    );

    //create Diagnostic
    if (logWorkspaceId) {
        new azure.insights.DiagnosticSetting(
            firewallName,
            {
                resourceUri: firewall.id,
                workspaceId: logWorkspaceId,

                //Logs
                logs: [
                    'AzureFirewallApplicationRule',
                    'AzureFirewallNetworkRule',
                    'AzureFirewallDnsProxy',
                ].map((c) => ({
                    category: c,
                    retentionPolicy: {
                        enabled: false,
                        //TODO: For demo purpose the log is only 7 day however, it should be 30 days or longer in PRD
                        days: 7,
                    },
                    enabled: true,
                })),
            },
            { dependsOn: firewall }
        );
    }

    //Return Firewall info out
    return { firewall, publicIP };
};

Developing a Hub VNET

Our goal is to set up the main parts required for the Hub VNet, which include:

  1. Resource Group: A container for managing related Azure resources.
  2. Virtual Network (VNet): The main network that hosts our subnets.
  3. Subnets: Segments within the VNet to isolate and organize resources.
  4. Public IP Addresses: For outbound internet connectivity and firewall management.
  5. Firewall Policy: Defines rules to control network traffic.
  6. Azure Firewall: A managed firewall service to protect our network.
View code:

import { getGroupName, StackReference } from '@az-commons';
import * as network from '@pulumi/azure-native/network';
import * as resources from '@pulumi/azure-native/resources';
import * as config from '../config';
import Firewall from './Firewall';
import FirewallPolicy from './FirewallPolicy';
import VNet from './VNet';

//Reference to the output of `az-01-shared` and link workspace to firewall for log monitoring.
const sharedStack = StackReference<config.SharedStackOutput>('az-01-shared');

// Create Hub Resource Group
const rsGroup = new resources.ResourceGroup(getGroupName(config.azGroups.hub));

// Create Virtual Network with Subnets
const vnet = VNet(config.azGroups.hub, {
    rsGroup,
    subnets: [
        {
            // Azure Firewall subnet name must be `AzureFirewallSubnet`
            name: 'AzureFirewallSubnet',
            addressPrefix: config.subnetSpaces.firewall,
        },
        {
            // Azure Firewall Management subnet name must be `AzureFirewallManagementSubnet`
            name: 'AzureFirewallManagementSubnet',
            addressPrefix: config.subnetSpaces.firewallManage,
        },
        {
            name: 'general',
            addressPrefix: config.subnetSpaces.general,
            // Allows Azure Resources Private Link
            privateEndpointNetworkPolicies:
                network.VirtualNetworkPrivateEndpointNetworkPolicies.Enabled,
        },
    ],
});

//Firewall Policy
const policy = FirewallPolicy(config.azGroups.hub, {
    rsGroup,
    //The Policy tier and Firewall tier must be the same
    tier: network.FirewallPolicySkuTier.Basic,
});

const firewallInfo = Firewall(config.azGroups.hub, {
    policy,
    rsGroup,
    vnet,
    //The Policy tier and Firewall tier must be the same
    tier: network.AzureFirewallSkuTier.Basic,
    logWorkspaceId: sharedStack.logWorkspace.id,
});

// Export the information that will be used in the other projects
export default {
    rsGroup: { name: rsGroup.name, id: rsGroup.id },
    hubVnet: { name: vnet.name, id: vnet.id },
    ipAddress: {
        name: firewallInfo.publicIP.name,
        address: firewallInfo.publicIP.ipAddress,
        id: firewallInfo.publicIP.id,
    },
    firewallPolicy: { name: policy.name, id: policy.id },
    firewall: {
        name: firewallInfo.firewall.name,
        address: firewallInfo.firewall.ipConfigurations.apply(
            (c) => c![0]!.privateIPAddress!
        ),
        id: firewallInfo.firewall.id,
    },
};

Note:

  • Properly setting the dependsOn property ensures that resources are created and destroyed in the correct sequence.
  • The code above demonstrates how to reuse the log workspace from the az-01-shared project for Firewall diagnostics, enabling effective tracing and monitoring of firewall rules.

Deployment and Cleanup

Deploying the Stack

To deploy the stack, execute the pnpm run up command. This provisions the necessary Azure resources. We can verify the deployment as follows:

Cleaning Up the Stack

To remove the stack and clean up all associated Azure resources, run the pnpm run destroy command. This ensures that any resources no longer needed are properly deleted.


Conclusion

In this guide, we have successfully constructed a Hub Virtual Network (VNet) for our private AKS environment using Pulumi. This Hub VNet serves as a crucial element in managing and securing access to all resources within our infrastructure, ensuring robust control and enhanced security measures.


References


Next

Day 05: Implementing a Private AKS Cluster with Pulumi

In the next tutorial, We’ll build a private AKS cluster with advanced networking features. We’ll explore how to integrate the AKS cluster with the Hub VNet and apply the firewall policies we’ve created.


Thank You

Thank you for taking the time to read this guide! I hope it has been helpful, feel free to explore further, and happy coding! 🌟✨

Steven | GitHub