Pulumi Code Examples

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",
                    },
                },
            },
       
Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: