My hobby project consists of building a mobile delivery platform. Thus, I needed some Infrastructure as Code (aka: IaC) to provision deployments.
My goal was to have code structured as follows:

Fortunately, I was able to learn the basics and write the proof of concept in a short amount of time.
Here’s some clients to the code above:


The remainder of this document will provide some code examples for how I wired up the code.
Setup
Here’s a link that provides guidance on how to install Pulumi on your machine.
Here’s another link for Pulumi’s Automation API.
Here’s another link that details Azure support using Pulumi’s Automation API.
Test Setup
The following test setup deploys Pulumi resources:
[<OneTimeSetUp>]
let setup() =
async {
try
let program = PulumiFn.Create(fun _ -> Build.EnvironmentFor(DevResourceGroup, configuration()))
let args = new InlineProgramArgs("IaC-BeachDelivery", "DEV", program)
let! stack = LocalWorkspace.CreateOrSelectStackAsync(args) |> Async.AwaitTask
_stack <- stack
do! stack.Workspace.InstallPluginAsync("azure-native", "v1.88.1") |> Async.AwaitTask
do! stack.SetConfigAsync("azure-native:location", new ConfigValue(DataCenter.EastUS2)) |> Async.AwaitTask
let! result = stack.UpAsync() |> Async.AwaitTask
result.Outputs |> Seq.iter (fun v -> Debug.WriteLine $"{v.Key}: {v.Value}")
with ex -> ex.GetBaseException().Message |> Debug.WriteLine
}
Teardown
The teardown method is as follows:
[<OneTimeTearDown>]
let teardown() =
async {
try
let! result = _stack.DestroyAsync() |> Async.AwaitTask
result.Summary.Message |> Debug.WriteLine
with ex -> ex.GetBaseException().Message |> Debug.WriteLine
}
Supporting infrastructure for the setup and teardown are the following:
module Test_ResourcesExists
open System.Linq
open System.Diagnostics
open System.Configuration
open System.Collections.Generic
open System.Collections.Specialized
open NUnit.Framework
open Pulumi.Automation
open IaC.BeachDelivery.Client
open type IaC.BeachDelivery.Client.ResourceNames;
let mutable _stack : WorkspaceStack = null
let settingsFrom(sectionName:string) : AppSettings =
let section = ConfigurationManager.GetSection(sectionName) :?> NameValueCollection;
let functionAppItems = section.AllKeys.Select(fun k -> new KeyValuePair<string, string>(k, section[k]));
let appName = functionAppItems.Single(fun kv -> kv.Key = "Name");
AppSettings(appName.Value, functionAppItems);
let entriesFrom(sectionName:string) =
let section = ConfigurationManager.GetSection(sectionName) :?> NameValueCollection;
let kvPairs = section.AllKeys.Select(fun k -> new KeyValuePair<string, string>(k, section[k]));
kvPairs
let configuration() =
let secretItems, identityItems = entriesFrom "SecretsSection", entriesFrom "IdentitySection"
let appSettingList = seq [ "FunctionApp1Section"
"FunctionApp2Section"
] |> Seq.map(fun v -> v |> settingsFrom);
Configurations(secretItems, appSettingList, identityItems);
Infrastructure
To create an environment with Azure resources, both configuration and Service Bus scaffolding need to be provided:
Configuration
I provided an app config file that looks something like this:
<configSections>
<section name="IdentitySection" type="System.Configuration.AppSettingsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<section name="SecretsSection" type="System.Configuration.AppSettingsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<section name="FunctionApp1Section" type="System.Configuration.AppSettingsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<section name="FunctionApp2Section" type="System.Configuration.AppSettingsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
</configSections>
<IdentitySection>
<add key="ClientId" value="some client id" />
<add key="SubscriptionId" value="some subscription id />
<add key="TenantId" value="some tenant id" />
<add key="ObjectId" value="some object id" />
</IdentitySection>
<FunctionApp1Section>
<add key="Name" value="some-app-name" />
<add key="Item1" value="some-value" />
</FunctionApp1Section>
<FunctionApp2Section>
<add key="Name" value="some-other-app-name" />
<add key="Item1" value="some-other-value" />
</FunctionApp2Section>
<SecretsSection>
<add key="SomeAzureFunction" value="some api key" />
<add key="SomeOtherAzureFunction" value="some api key" />
</SecretsSection>
The following code exposes the scaffolding for configuration data:
using LabelValues = System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string, string>>;
using SettingItems = System.Collections.Generic.IEnumerable<IaC.BeachDelivery.Client.AppSettings>;
namespace IaC.BeachDelivery.Client;
public record Configurations(LabelValues Secrets, SettingItems AppSettingItems, LabelValues Identity);
public record AppSettings(string FunctionAppName, LabelValues Settings);
Service Bus
Service Bus infrastructure includes supporting the creation of topics and their corresponding subscriptions.
Subscription
The following code exposes support for a subscription.
namespace IaC.BeachDelivery.Client;
public class Subscription<T>
{
public Subscription(string value, Topic<T> topic) => (Value, Topic) = (value, topic);
public string Value { get; }
public Topic<T> Topic { get; }
}
Topic
The following code exposes support for a topic.
namespace IaC.BeachDelivery.Client;
public class Topic<T>
{
public Topic(string value, T @namespace) => (Value, Namespace) = (value, @namespace);
public string Value { get; }
public T Namespace { get; }
}
Resource Generation
The code that drives resource generation are as follows:
using System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using static IaC.BeachDelivery.Client.ResourceNames;
using LabelValues = System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<string, string>>;
namespace IaC.BeachDelivery.Client;
public static class Build
{
public static IDictionary<string, object?> EnvironmentFor(string resourceGroupName, Configurations configuration)
{
try
{
var resourceGroup = resourceGroupName.ToResourceGroup(DataCenter.EastUS2);
var objectId = configuration.Identity.Single(v => v.Key == "ObjectId").Value;
var tenantId = configuration.Identity.Single(v => v.Key == "TenantId").Value;
var servicebus = Bus .Build(resourceGroup, ServiceBus);
var topics = Topics .Build(resourceGroup, Topics(servicebus));
var subscriptions = Subscriptions .Build(resourceGroup, Subscriptions(servicebus));
var vault = Vault .Build(resourceGroup, KeyVault, tenantId, objectId);
var secrets = VaultSecrets .Build(resourceGroup, vault, new LabelValues(configuration.Secrets));
var storageAccount = StorageAccounts.Build(resourceGroup, StorageAccount);
var plan = Plans .Build(resourceGroup, Plan, "FunctionApp");
var functionApps = FunctionsApps .Build(resourceGroup, storageAccount, plan, configuration.AppSettingItems);
var signalR = SignalR .Build(resourceGroup, Signal_R, UpstreamTemplateUrl);
return new Dictionary<string, object?>(new List<KeyValuePair<string, object?>>() { /*TODO*/ });
}
catch(Exception ex) {
Debug.WriteLine(ex.GetBaseException().Message);
return new Dictionary<string, object?>(new List<KeyValuePair<string, object?>>() { /*TODO*/ });
}
}
}
Resource Group
using Pulumi.AzureNative.Resources;
namespace IaC.BeachDelivery.Client;
public static class ResourceGroups
{
public static ResourceGroup ToResourceGroup(this string resourceGroup, string location) =>
new ResourceGroup(resourceGroup, new ()
{
Location = location,
ResourceGroupName = resourceGroup,
});
}
Service Bus
using Pulumi.AzureNative.Resources;
using AzureNative = Pulumi.AzureNative;
using ServiceBus = Pulumi.AzureNative.ServiceBus;
namespace IaC.BeachDelivery.Client;
public static class Bus
{
public static ServiceBus.Namespace Build(this ResourceGroup resourceGroup, string namespaceName) =>
new ServiceBus.Namespace(namespaceName, new() {
NamespaceName = namespaceName,
Location = resourceGroup.Location,
ResourceGroupName = resourceGroup.Name,
Sku = new ServiceBus.Inputs.SBSkuArgs {
Name = AzureNative.ServiceBus.SkuName.Standard,
Tier = AzureNative.ServiceBus.SkuTier.Standard,
}
});
}
Service Bus Topics
using System.Linq;
using System.Collections.Generic;
using Pulumi.AzureNative.Resources;
using Pulumi;
using ServiceBus = Pulumi.AzureNative.ServiceBus;
namespace IaC.BeachDelivery.Client;
public static class Topics
{
public static ServiceBus.Topic Topic(ResourceGroup resourceGroup, Topic<Output<string>> topic) =>
new ServiceBus.Topic(topic.Value, new() {
TopicName = topic.Value,
EnableExpress = true,
NamespaceName = topic.Namespace,
ResourceGroupName = resourceGroup.Name,
});
public static IEnumerable<ServiceBus.Topic> Build(this ResourceGroup resourceGroup, IEnumerable<Topic<Output<string>>> topics) =>
topics.Select(topic => Topic(resourceGroup, topic))
.ToList();
}
Service Bus Subscription
using System;
using System.Linq;
using System.Collections.Generic;
using Pulumi.AzureNative.Resources;
using Pulumi;
using ServiceBus = Pulumi.AzureNative.ServiceBus;
namespace IaC.BeachDelivery.Client;
using SubscriptionItems = IEnumerable<Subscription<Output<string>>>;
public static partial class Subscriptions
{
public static ServiceBus.Subscription Subscription(ResourceGroup resourceGroup, Subscription<Output<string>> subscription)
{
static string UniqueSuffix(string guid) => $"{guid.Substring(guid.Length - 4)}";
string name = $"{subscription.Value}_{UniqueSuffix(Guid.NewGuid().ToString())}";
return
new ServiceBus.Subscription(name, new ServiceBus.SubscriptionArgs()
{
SubscriptionName = subscription.Value,
TopicName = subscription.Topic.Value,
NamespaceName = subscription.Topic.Namespace,
ResourceGroupName = resourceGroup.Name,
EnableBatchedOperations = true,
});
}
public static IEnumerable<ServiceBus.Subscription> Build(this ResourceGroup resourceGroup, SubscriptionItems subscriptions) =>
subscriptions.Select(v => Subscription(resourceGroup, v))
.ToList();
}
KeyVault
using Pulumi;
using Pulumi.AzureNative.Resources;
using AzureNative = Pulumi.AzureNative;
namespace IaC.BeachDelivery.Client;
public static partial class Vault
{
public static AzureNative.KeyVault.Vault Build(this ResourceGroup resourceGroup, string vaultName, string tenantId, string objectId)
{
var vault = new AzureNative.KeyVault.Vault(vaultName, new()
{
Location = resourceGroup.Location,
Properties = new AzureNative.KeyVault.Inputs.VaultPropertiesArgs
{
AccessPolicies = new[]
{
new AzureNative.KeyVault.Inputs.AccessPolicyEntryArgs
{
ObjectId = objectId,
Permissions = new AzureNative.KeyVault.Inputs.PermissionsArgs
{
Secrets = new InputList<Union<string, AzureNative.KeyVault.SecretPermissions>>
{
"get",
"list",
"set",
"delete",
"backup",
"restore",
"recover",
"purge",
},
},
TenantId = tenantId,
},
},
EnabledForDeployment = true,
EnabledForDiskEncryption = true,
EnabledForTemplateDeployment = true,
Sku = new AzureNative.KeyVault.Inputs.SkuArgs
{
Family = "A",
Name = AzureNative.KeyVault.SkuName.Standard,
},
TenantId = tenantId,
},
ResourceGroupName = resourceGroup.Name,
VaultName = vaultName,
});
return vault;
}
}
Secrets
using System.Collections.Generic;
using System.Linq;
using Pulumi.AzureNative.Resources;
using Inputs = Pulumi.AzureNative.KeyVault.Inputs;
using Secret = Pulumi.AzureNative.KeyVault.Secret;
using KeyVault = Pulumi.AzureNative.KeyVault.Vault;
using LabelValue = System.Collections.Generic.KeyValuePair<string, string>;
using LabelValues = System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string, string>>;
namespace IaC.BeachDelivery.Client;
public static class VaultSecrets
{
public static Secret Build(this ResourceGroup resourceGroup, KeyVault vault, LabelValue item) =>
new Secret(item.Key, new() {
ResourceGroupName = resourceGroup.Name,
Properties = new Inputs.SecretPropertiesArgs { Value = item.Value },
SecretName = item.Key,
VaultName = vault.Name,
});
public static IEnumerable<Secret> Build(this ResourceGroup resourceGroup, KeyVault vault, LabelValues items) =>
items.Select(item => Build(resourceGroup, vault, item))
.ToList();
}
Storage Account
using Pulumi.AzureNative.Resources;
using StorageAccount = Pulumi.Azure.Storage.Account;
namespace IaC.BeachDelivery.Client;
public static class StorageAccounts
{
public static StorageAccount Build(ResourceGroup resourceGroup, string storageAccountName)
{
var storageAccount = new StorageAccount(storageAccountName, new()
{
ResourceGroupName = resourceGroup.Name,
Location = resourceGroup.Location,
Name = storageAccountName,
AccountTier = "Standard",
AccountReplicationType = "LRS",
});
return storageAccount;
}
}
Plan
using Pulumi.Azure.AppService;
using Pulumi.Azure.AppService.Inputs;
using Pulumi.AzureNative.Resources;
namespace IaC.BeachDelivery.Client;
public static class Plans
{
public static Plan Build(ResourceGroup resourceGroup, string planName, string planKind) =>
new Plan(planName, new() {
ResourceGroupName = resourceGroup.Name,
Kind = planKind,
Location = resourceGroup.Location,
Name = planName,
Sku = new PlanSkuArgs
{
Tier = "Dynamic",
Size = "Y1",
},
});
}
Function App
using System.Linq;
using System.Collections.Generic;
using Pulumi.Azure.AppService;
using Pulumi.AzureNative.Resources;
using StorageAccount = Pulumi.Azure.Storage.Account;
using SettingItems = System.Collections.Generic.IEnumerable<IaC.BeachDelivery.Client.AppSettings>;
namespace IaC.BeachDelivery.Client;
public static class FunctionsApps
{
public static FunctionApp Build(ResourceGroup resourceGroup, StorageAccount storageAccount, Plan plan, AppSettings appSettings)
{
return
new FunctionApp(appSettings.FunctionAppName, new()
{
Name = appSettings.FunctionAppName,
Location = resourceGroup.Location,
ResourceGroupName = resourceGroup.Name,
AppServicePlanId = plan.Id,
StorageAccountName = storageAccount.Name,
AppSettings = new Pulumi.InputMap<string>() { new Dictionary<string, string>(appSettings.Settings.ToList()) },
StorageAccountAccessKey = storageAccount.PrimaryAccessKey,
});
}
public static IEnumerable<FunctionApp> Build(this ResourceGroup resourceGroup, StorageAccount storageAccount, Plan plan, SettingItems appSettingItems) =>
appSettingItems.Select(settings => Build(resourceGroup, storageAccount, plan, settings))
.ToList();
}
SignalR
using Pulumi;
using Pulumi.AzureNative.Resources;
using Pulumi.AzureNative.SignalRService;
using AzureNative = Pulumi.AzureNative;
namespace IaC.BeachDelivery.Client;
public static class SignalR
{
public static AzureNative.SignalRService.SignalR Build(ResourceGroup resourceGroup, string signslRName, string upstreamTemplateUrl) =>
new AzureNative.SignalRService.SignalR(signslRName, new SignalRArgs()
{
Cors = new AzureNative.SignalRService.Inputs.SignalRCorsSettingsArgs()
{
AllowedOrigins = new[]
{
"https://foo.com",
"https://bar.com",
},
},
Features = new[]
{
new AzureNative.SignalRService.Inputs.SignalRFeatureArgs()
{
Flag = "ServiceMode",
Properties = null,
Value = "Serverless",
},
new AzureNative.SignalRService.Inputs.SignalRFeatureArgs()
{
Flag = "EnableConnectivityLogs",
Properties = null,
Value = "True",
},
new AzureNative.SignalRService.Inputs.SignalRFeatureArgs()
{
Flag = "EnableMessagingLogs",
Properties = null,
Value = "False",
},
},
Kind = "SignalR",
Location = resourceGroup.Location,
NetworkACLs = new AzureNative.SignalRService.Inputs.SignalRNetworkACLsArgs()
{
DefaultAction = "Deny",
PrivateEndpoints = new InputList<AzureNative.SignalRService.Inputs.PrivateEndpointACLArgs>()
{
new AzureNative.SignalRService.Inputs.PrivateEndpointACLArgs
{
Allow = new InputList<Union<string, SignalRRequestType>>
{
"ServerConnection",
},
Name = "mySignalRService.123",
},
},
PublicNetwork = new AzureNative.SignalRService.Inputs.NetworkACLArgs
{
Allow = new InputList<Union<string, SignalRRequestType>>()
{
"ClientConnection",
},
},
},
ResourceGroupName = resourceGroup.Name,
ResourceName = signslRName,
Sku = new AzureNative.SignalRService.Inputs.ResourceSkuArgs
{
Capacity = 1,
Name = "Standard_S1",
Tier = "Standard",
},
Upstream = new AzureNative.SignalRService.Inputs.ServerlessUpstreamSettingsArgs
{
Templates = new[]
{
new AzureNative.SignalRService.Inputs.UpstreamTemplateArgs
{
CategoryPattern = "*",
EventPattern = "connect,disconnect",
HubPattern = "*",
UrlTemplate = upstreamTemplateUrl //"https://example.com/chat/api/connect",
},
},
},