Overview: How the code is structured
Before diving into the details of individual resources, it’s useful to understand the overall structure of this Pulumi program and how it leverages functions to define and provision infrastructure. This project uses Pulumi with Go to describe Azure resources programmatically. Instead of defining each resource directly within the main function, the program organizes logic into helper functions, which serve as reusable templates specifying the configuration and properties of a resource, and function calls in main, which instruct the program to create new instances of these resources with the deployment-specific values. This layered structure promotes modularity, readability, and maintainability, making it easier to reuse and modify the deployment.
At a high level, the program is divided into three conceptual layers:
- Program Entry Point (main)
- function call (tell the program that new vnet, database, etc. should be created)
- The helper function (blueprint for an Azure resource)
Entry Point
Every Pulumi Go prgram starts exectuion inside the main function. Inside the run section is where the function calls will happen.
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
// infrastructure definitions
return nil
})
}
Caller Logic
The job of the function call is to invoke the helper function and pass in the unique information required to create a new Azure resource. The caller logic does not define how the resource is created. It simply declares that the resource should exist and supplies the necessary inputs. Parameters allow the same helper function to create different instances of a resource with unique names, locations, or settings, without duplicating code. For example, when creating a resource group, the caller tells the program that a new resource group should be created and provides values such as the resource group name and its location.
//Resource Group
rg, err := createResourceGroup(ctx, resourceGroupName, resourceGroupLocation)
Helper Functions
Helper functions define the blueprint for each Azure resource. It takes in the parameters in the same order as the function call parameters defining variables that can be used within the function. The body of the function determines what the resource should look like and the settings options. You can think of the helper function as a template that can be reused at any time to create a new resource group in the instance of this example.
Each helper function receives the Pulumi context (ctx) from the main function. This context manages resource lifecycle, outputs, and dependencies within the Pulumi program
func createResourceGroup(ctx *pulumi.Context, name string, location string) (*resources.ResourceGroup, error) {
}
Starting Project Resource Deployment
Package Imports and Variables
The import block tells Go which external packages the program depends on. Packages are a way for our program to integrate the code that they have written into our program.
A rundown of what the packages are used for in this program:
strings – Standard Go library for string manipulation (e.g., trimming, splitting, replacing).
github.com/google/uuid – Generates unique identifiers, which is useful for creating resource names that don’t conflict.
authorization – for role assignments and permissions.
containerregistry – for Azure Container Registry.
containerservice – for AKS (Azure Kubernetes Service) clusters.
keyvault – for Azure Key Vault.
network – for VNets, subnets, and networking resources.
resources – for resource groups and other basic resources.
github.com/pulumi/pulumi/sdk/v3/go/pulumi – The core Pulumi Go SDK, required to create resources, manage context, and run the Pulumi program.
package main
import (
"strings"
"github.com/google/uuid"
"github.com/pulumi/pulumi-azure-native/sdk/go/azure/authorization"
"github.com/pulumi/pulumi-azure-native/sdk/go/azure/containerregistry"
"github.com/pulumi/pulumi-azure-native/sdk/go/azure/containerservice"
"github.com/pulumi/pulumi-azure-native/sdk/go/azure/keyvault"
"github.com/pulumi/pulumi-azure-native/sdk/go/azure/network"
"github.com/pulumi/pulumi-azure-native/sdk/go/azure/resources"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
// Resource Group Variables
var resourceGroupName string = "rg1"
var resourceGroupLocation string = "westus"
Resource Group
A resource group in Azure serves as a container that organizes all related project resources. Since networking, security, and compute resources all require a resource group, it must be created first to establish the correct dependency chain. Inside the main function, a function call invokes the helper function that defines the configuration and settings for the resource group. The if err != nil statement is a standard Go statement used to catch and handle any errors that occur during resource creation.
rg, err := createResourceGroup(ctx, resourceGroupName, resourceGroupLocation)
if err != nil {
return err
}
Helper Function – As explained previously, the helper function defines the blueprint for creating a resource group. The function maps the input parameters like the name used for the resource group and the location and adds them to the resource configuration.
How this functions works:
func createResourceGroup: Declares a new Go function named createResourceGroup
ctx *pulumi.Context: Pulumi passes a “context” object to every resource creation function. It contains information about the current deployment, configuration, and resource state.
name string: The name you want to assign to your resource group.
location string: The Azure region where the resource group will be created
*resources.ResourceGroup: Returns a pointer to the newly created Azure Resource Group object.
error: Returns an error if something goes wrong during creation.
rg, err := resources.NewResourceGroup(ctx, name, &resources.ResourceGroupArgs{
resources.NewResourceGroup: This is a Pulumi function that actually creates a new Azure Resource Group.
// Function for Creating Resource Group
func createResourceGroup(ctx *pulumi.Context, name string, location string) (*resources.ResourceGroup, error) {
rg, err := resources.NewResourceGroup(ctx, name, &resources.ResourceGroupArgs{
Location: pulumi.String(location),
ResourceGroupName: pulumi.String(name),
})
if err != nil {
return nil, err
}
return rg, nil
}
Virtual Network
The function call tells the program to create a new virtual network using the blueprint defined in the helper function.
Parameters passed in:
ctx – The Pulumi execution context, required for resource creation.
NodeVnet – The name for this VNet.
rg.Name – The resource group where the VNet will be deployed.
resourceGroupLocation – The Azure region.
10.0.0.0/16 – The address space for the VNet.
Error handling – if err != nil { return err } ensures that the program stops and reports an error if creating the VNet fails.
//Networking
vnet, err := NewVNet(ctx, "NodeVnet", rg.Name, resourceGroupLocation, "10.0.0.0/16")
if err != nil {
return err
}
Helper Function – The reusable blueprint for creating a VNet. The network.NewVirtualNetwork function is part of the Pulumi Azure SDK and tells Azure to create a new virtual network with the settings defined in the function (such as the resource group, address space, and location). When this function runs, Pulumi communicates with Azure’s API to provision the network. The return statement passes the resulting *network.VirtualNetwork object back to the caller, allowing the program to reference the newly created network in later resources, such as subnets.
// New Virtual Network Function
func NewVNet(
ctx *pulumi.Context,
name string,
rgName pulumi.StringInput,
location string,
addressPrefix string,
) (*network.VirtualNetwork, error) {
return network.NewVirtualNetwork(ctx, name, &network.VirtualNetworkArgs{
ResourceGroupName: rgName,
VirtualNetworkName: pulumi.String(name),
Location: pulumi.String(location),
AddressSpace: &network.AddressSpaceArgs{
AddressPrefixes: pulumi.StringArray{
pulumi.String(addressPrefix),
},
},
})
}
Network Subnets
The below lines tell the program to create two subnets inside the previously created virtual network.
Parameters passed in:
ctx – Pulumi context for resource tracking.
aksSubnet | appGwSubnet – unique name for the subnet in Azure.
rg.Name – the Resource Group where the subnet will be deployed.
vnet – reference to the parent Virtual Network.
10.0.1.0/24 | 10.0.2.0/24 – IP address ranges for the subnet.
aksSubnet, err := NewSubnet(ctx, "aksSubnet", rg.Name, vnet, "10.0.1.0/24")
if err != nil {
return err
}
appGwSubnet, err := NewSubnet(ctx, "appGwSubnet", rg.Name, vnet, "10.0.2.0/24")
if err != nil {
return err
}
Helper Function – Inside the helper function, network.NewSubnet is the Pulumi SDK function that instructs Azure to create a new subnet. The values defined in the function specify the settings that the subnet will use. The pulumi.Parent(vnet) parameter tells Pulumi that this subnet belongs to the previously defined vnet.
func NewSubnet(
ctx *pulumi.Context,
name string,
rgName pulumi.StringInput,
vnet *network.VirtualNetwork,
prefix string,
) (*network.Subnet, error) {
return network.NewSubnet(ctx, name, &network.SubnetArgs{
ResourceGroupName: rgName,
VirtualNetworkName: vnet.Name,
AddressPrefix: pulumi.String(prefix),
}, pulumi.Parent(vnet))
}
Azure Keyvault
Before creating the Key Vault, the program retrives information aboute the currently authenticated Azure identity
- authorization.GetClientConfig queries Azure for the tenant ID and object ID of the identity running the Pulumi deployment.
- tenantID Automatically gets the Azure Active Directory tenant from the logged in Azure User
- objectID represents the user or service principal that will be granted access to the Key Vault.
Parameters passed in:
ctx – The Pulumi execution context, required for resource creation.
rg.Name – the Resource Group where the subnet will be deployed.
resourceGroupLocation –
tenantID & objectID – Passing in the variables from above into the function call.
//KeyVault
cfg, err := authorization.GetClientConfig(ctx, nil)
if err != nil {
return err
}
tenantID := pulumi.String(cfg.TenantId)
objectID := pulumi.String(cfg.ObjectId)
_, err = createKeyVault(ctx, "vault90348503485", rg.Name, resourceGroupLocation, tenantID, objectID, aksSubnet.ID())
if err != nil {
return err
}
Helper Function – Defines the blueprint for creating a Key Vault.
The keyvault name must be unique across Azure and the uuidSufix section below appends a randomized suffix to the name.
uuidSuffix := strings.ReplaceAll(uuid.New().String(), “-“, “”)[:8]
fullvaultName := strings.ToLower(vaultName + uuidSuffix)
The vault, err := keyvault.NewVault section calls the Pulumi SDK function and instructs Azure to create a new Key Vault Resource.
The body of the function under keyvault.NewVault defines the properties and permissions of the key vault.
- Location and resource group
- SKU
- Access Policies (Who can manage keys, secrets, and certificates)
- Network rules
// Function creates new KeyVault
func createKeyVault(ctx *pulumi.Context, vaultName string, resourceGroupName pulumi.StringInput, location string, tenantID pulumi.StringInput, objectID pulumi.StringInput, subnetID pulumi.StringInput) (*keyvault.Vault, error) {
uuidSuffix := strings.ReplaceAll(uuid.New().String(), "-", "")[:8]
fullvaultName := strings.ToLower(vaultName + uuidSuffix)
vault, err := keyvault.NewVault(ctx, fullvaultName, &keyvault.VaultArgs{
ResourceGroupName: resourceGroupName,
VaultName: pulumi.String(fullvaultName),
Location: pulumi.String(location),
Properties: &keyvault.VaultPropertiesArgs{
TenantId: tenantID,
Sku: &keyvault.SkuArgs{
Name: keyvault.SkuNameStandard,
Family: pulumi.String("A"),
},
AccessPolicies: keyvault.AccessPolicyEntryArray{
&keyvault.AccessPolicyEntryArgs{
TenantId: tenantID,
ObjectId: objectID,
Permissions: &keyvault.PermissionsArgs{
Keys: pulumi.StringArray{
pulumi.String("get"),
pulumi.String("list"),
pulumi.String("create"),
pulumi.String("delete"),
},
Secrets: pulumi.StringArray{
pulumi.String("get"),
pulumi.String("list"),
pulumi.String("set"),
pulumi.String("delete"),
},
Certificates: pulumi.StringArray{
pulumi.String("get"),
pulumi.String("list"),
pulumi.String("create"),
pulumi.String("delete"),
},
},
},
},
NetworkAcls: &keyvault.NetworkRuleSetArgs{
DefaultAction: pulumi.String("Deny"),
Bypass: pulumi.String("AzureServices"),
},
},
})
if err != nil {
return nil, err
}
Key Vault Endpoint
This block calls the Pulumi Azure SDK to create a Private Endpoint for the Key Vault. The name uses the KeyVault name and adds -pe to the end to identify it as the endpoint. The endpoint is only created in Azure after the Keyvault creation is successful.
ResourceGroupName and Location values determine the resource group and which region it should be deployed to. The Private Endpoint must exist in the same region as the virtual network subnet.
Subnet: &network.SubnetTypeArgs{
Id: subnetID: This section creates a network interface and places the NIC inside the specified subnet.
PrivateLinkServiceConnections: This section defines the private link connection between the private endpoint and the Key Vault. PrivateLinkServiceID references the Key Vault. GroupIds: “vault” tells Azure which Key Vault Service Endpoint to connect to
return vault allows the values of the created resource to be used in other parts of the program.
// Create Private Endpoint Key Vault
_, err = network.NewPrivateEndpoint(ctx, vaultName+"-pe", &network.PrivateEndpointArgs{
ResourceGroupName: resourceGroupName,
Location: pulumi.String(location),
Subnet: &network.SubnetTypeArgs{
Id: subnetID,
},
PrivateLinkServiceConnections: network.PrivateLinkServiceConnectionArray{
&network.PrivateLinkServiceConnectionArgs{
Name: pulumi.String("kv-endpoint-connection"),
PrivateLinkServiceId: vault.ID(),
GroupIds: pulumi.StringArray{
pulumi.String("vault"),
},
},
},
})
if err != nil {
return nil, err
}
return vault, nil
}
Container Registry
The function call tells the program to create a new Azure Container Registry.
Parameters passed in:
ctx – Pulumi context for resource tracking.
Registry Name: The unique name for the container registry
rg.Name – the Resource Group where the subnet will be deployed
SKU – Specifies the SKU tier of the registry. The options are Standard, Basic, or Premium
//Container Registry
acr, err := createACR(ctx, "kubeACRTest12312399", rg.Name, "Standard")
if err != nil {
return err
}
Helper Function
containerregistry.NewRegistry is the Pulumi SDK function that instructs Azure to create a new Container Registry.
&containerregistry.RegistryArgs{} holds the configuration values needed to create the registry in Azure.
return registry – When the registry creates, the function returns a pointer to the containerregistry.Registry object. It allows the caller in main to reference the registy later in it’s configuration. for examples assigning roles or connecting it to the AKS Cluster.
func createACR(ctx *pulumi.Context, name string, rgName pulumi.StringInput, sku string) (*containerregistry.Registry, error) {
registry, err := containerregistry.NewRegistry(ctx, name, &containerregistry.RegistryArgs{
ResourceGroupName: rgName,
RegistryName: pulumi.String(name),
Sku: &containerregistry.SkuArgs{
Name: pulumi.String(sku),
},
})
if err != nil {
return nil, err
}
return registry, nil
}
AKS Cluster
This section of the Pulumi program provisions the Azure Kubernetes Service Cluster.
The function call calls createAKS (the helper function) and passes in the below parameters:
- ctx: Pulumi execution context
- rgName: Resource group name where AKS will be deployed
- aksSubnetID: Subnet for the AKS nodes
- appGwSubnetID: Subnet for the Application Gateway (AppGateway must be created in a separatly created subnet)
- clusterName: Name for the AKS cluster
//AKS Cluster
_, kubeletObjectId, err := createAKS(ctx, rg.Name, aksSubnet.ID().ToStringOutput(), appGwSubnet.ID(), "tradingAksCluster")
if err != nil {
return err
}
Helper Function – Contains a several sections that define the settings for the cluster. Addon Profiles, Agent Profiles, ApiServerAccessProfile, Networking Configuration, Identity and Security. The settings defined in the code are the same settings that would be defined in an Azure ARM Template or within the Azure Portal to create a new cluster.
Addon Profiles:
The same page as the integrations page in the Azure portal. The section integrates access to Azure Resources within the AKS Config. For example allowing Key Vault to be referenced inside of containers within the cluster or connecting the cluster to Application Gateway.
Agent Profiles:
Defining the virtual machine node pools that the cluster uses to run pods on.
ApiServerAccessProfile: Defines the cluster as a private cluster. Change the boolean to false for a public cluster.
NetworkProfile: defines the ip space configuration for cluster components.
- NetworkPlugin: “azure” enables Azure CNI for IP allocation
- ServiceCidr is the IP range for Kubernetes services.
- DnsServiceIP is the internal DNS for the cluster
- DockerBridgeCidr is the bridge network for container networking
Identity and Security:
Enables Role Based Access Control for Kubernetes resources and defines that a system assigned managed identity will allow AKS to interact with other Azure resources securly.
The section at the end kubeletObjectId := cluster.IdentityProfile.ApplyT :
Extracts the kublet object ID from the managed identity and returns it to the program so that it can be used for Azure Role Assignment in the upcoming part of this walk through.
// createAKS creates a new Azure AKS Cluster
func createAKS(ctx *pulumi.Context, rgName pulumi.StringInput, aksSubnetID pulumi.StringInput, appGwSubnetID pulumi.StringInput, clusterName string) (*containerservice.ManagedCluster, pulumi.StringOutput, error) {
cluster, err := containerservice.NewManagedCluster(ctx, "managedCluster", &containerservice.ManagedClusterArgs{
AddonProfiles: containerservice.ManagedClusterAddonProfileMap{
"azureKeyvaultSecretsProvider": &containerservice.ManagedClusterAddonProfileArgs{
Enabled: pulumi.Bool(true),
Config: pulumi.StringMap{
"enableSecretRotation": pulumi.String("true"),
"rotationPollInterval": pulumi.String("2m"),
},
},
"IngressApplicationGateway": &containerservice.ManagedClusterAddonProfileArgs{
Enabled: pulumi.Bool(true),
Config: pulumi.StringMap{"subnetId": appGwSubnetID},
},
},
AgentPoolProfiles: containerservice.ManagedClusterAgentPoolProfileArray{
&containerservice.ManagedClusterAgentPoolProfileArgs{
Count: pulumi.Int(2),
Name: pulumi.String("nodepool1"),
OsType: pulumi.String(containerservice.OSTypeLinux),
VmSize: pulumi.String("Standard_DS2_v2"),
VnetSubnetID: aksSubnetID,
Mode: pulumi.String("System"),
},
},
ApiServerAccessProfile: &containerservice.ManagedClusterAPIServerAccessProfileArgs{
EnablePrivateCluster: pulumi.Bool(true),
},
NetworkProfile: &containerservice.ContainerServiceNetworkProfileArgs{
NetworkPlugin: pulumi.String("azure"),
ServiceCidr: pulumi.String("10.10.0.0/16"),
DnsServiceIP: pulumi.String("10.10.0.10"),
DockerBridgeCidr: pulumi.String("172.17.0.1/16"),
},
EnableRBAC: pulumi.Bool(true),
Location: pulumi.String(resourceGroupLocation),
ResourceGroupName: rgName,
ResourceName: pulumi.String(clusterName),
DnsPrefix: pulumi.String(clusterName),
Identity: &containerservice.ManagedClusterIdentityArgs{
Type: containerservice.ResourceIdentityTypeSystemAssigned,
},
Sku: &containerservice.ManagedClusterSKUArgs{
Name: pulumi.String("Basic"),
Tier: pulumi.String(containerservice.ManagedClusterSKUTierFree),
},
})
if err != nil {
return nil, pulumi.StringOutput{}, err
}
kubeletObjectId := cluster.IdentityProfile.ApplyT(func(idMap map[string]containerservice.ManagedClusterPropertiesResponseIdentityProfile) string {
if val, ok := idMap["kubeletidentity"]; ok && val.ObjectId != nil {
return *val.ObjectId
}
return ""
}).(pulumi.StringOutput)
return cluster, kubeletObjectId, nil
}
Role Assignments
Role assignments explain what access is being granted and to where. The Role definitions are grouped into a constant to help with easier program readability and modification.
The two RBAC roles:
- Reader – Allows AKS to discover Azure resources, read networking configuration, observe changes within Azure
- AcrPull – Allows pulling container images from Azure Container Registry
The code below the const calls the assignRole function once to assign the reader role and again for AcrPull permissions.
Parameters passed in:
ctx: Pulumi execution context
name – The name that Pulumi uses to track the role assignment creation within the deployment process. However it is not the Azure role name itself.
kubletObjectId – referencing the AKS kublet managed identity assigning the permissions to AKS identity.
RoleReader/RoleAcrPull – the Azure Role Definition IDs specified in the const.
rg.ID | acr.ID – defining the resource where the permissions are applied
“ServicePrincipal” – tells Azure what type of identity the principal ID represents.
//Role Assignments
const (
RoleReader = "/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7"
RoleAcrPull = "/providers/Microsoft.Authorization/roleDefinitions/7f951dda-4ed3-4680-a7ca-43fe172d538d"
)
err = assignRole(ctx, "agic-rg-reader", kubeletObjectId, RoleReader, rg.ID(), "ServicePrincipal")
if err != nil {
return err
}
err = assignRole(ctx, "aks-acr-pull", kubeletObjectId, RoleAcrPull, acr.ID(), "ServicePrincipal")
if err != nil {
return err
}
Helper Function
authorization.NewRoleAssignment calls the Pulumi Azure Native SDK to create a new RoleAssignment. Pulumi translates this into an Azure ARM request.
NewRoleAssignmentArgs values:
- RoleAssignmentName: role assignments must have a unique name and RoleAssignmentName – uuid.New().String(): Adds in a unique string for the role assignment
- PrincipalId: The kubletobjectId from the call
- RoleDefinitionId: Also references the passed in value
- Scope: References the rg.ID() and acr.ID() values from the call
- PrincipalType: references the passed in value
func assignRole(
ctx *pulumi.Context,
name string,
principalId pulumi.StringInput,
roleDefinitionId string,
scope pulumi.StringInput,
principalType string,
) error {
_, err := authorization.NewRoleAssignment(ctx, name, &authorization.RoleAssignmentArgs{
RoleAssignmentName: pulumi.String(uuid.New().String()),
PrincipalId: principalId,
RoleDefinitionId: pulumi.String(roleDefinitionId),
Scope: scope,
PrincipalType: pulumi.String(principalType),
})