Skip to content

TechWatching/pulumi-azure-workshop

Repository files navigation

Getting Started Provisioning Infrastructure on Azure with Pulumi

Prerequisites

Installations & configurations

  • Azure Subscription
  • Azure CLI
  • Pulumi CLI
  • Your preferred language runtime
  • Your favorite IDE

This page in the documentation covers all you need to do to set up your environment.

Note

You can use the OS, language, and IDE you want for this workshop. Yet for the sake of simplicity, the samples in the tutorial won't cover every possible configuration. That should not prevent you from choosing the technologies and tools you are already familiar with to complete this workshop.

On Windows for instance, you can set up you environment using PowerShell and Windows Package Manager like this:

# Install Azure CLI using winget
winget install -e --id Microsoft.AzureCLI

# Install Pulumi CLI using winget
winget install -e --id=Pulumi.Pulumi

# Log in to Azure
# You can specify the -t option with your tenant identifier if you have multiple tenants
az login

# (Optional) List your available subscriptions and grab the identifier of the subscription you want to use
az account list --query "[].{id:id, name:name}"

# (Optional) Set the correct subscription identifier, "79400867-f366-4ec9-84ba-d1dca756beb5 in the example below
az account set -s 79400867-f366-4ec9-84ba-d1dca756beb5
az account show

# (Optional) Install the .NET SDK
winget install Microsoft.DotNet.SDK.8

Choose a backend

As Pulumi is a declarative IaC solution that uses a state to manage the cloud resources, a place to store this state is needed: the "backend". An encryption provider is also needed to encrypt that will be used. You can check this article in the documentation to see the different backends and encryption providers available.

The most convenient way of doing this workshop without worrying about configuring a backend or an encryption provider is to use Pulumi Cloud which is free for individuals. You can just create an account here (or sign in using your GitHub/GitLab account) and that's it.

If you don't want to use Pulumi Cloud, that's totally fine too, check the documentation or this article that demonstrates how to use Pulumi with Azure Blob Storage as the backend and Azure Key Vault as the encryption provider (script to configure these resources is available at the end of the article).

Log in to your backend using the pulumi login CLI command

Log in to Pulumi Cloud
pulumi login

Pulumi fundamentals

Create a basic Pulumi project

  1. Create a new directory for the workshop with a new infra directory in it.
mkdir pulumi-workshop; cd pulumi-workshop; mkdir infra; cd infra
  1. List the available templates
pulumi new -l

There are several azure templates (prefixed by azure) that are already configured to provision resources to Azure, but for the purpose of this workshop you will start a project from scratch to better understand how everything works.

  1. Create a new Pulumi project using an empty template (corresponding to the language of your choice)
pulumi new typescript -n PulumiAzureWorkshop -s dev -d "Workshop to learn Pulumi with Azure fundamentals"

The -s dev option is used to initialize the project with a stack named dev. A stack is an independently configurable instance of a Pulumi program. Stacks are mainly use to have a different instance for each environment (dev, staging, preprod, prod ...). or for each developer making changes to the infrastructure.

Note

If you forget to log in before, you will be prompted to log in to Pulumi Cloud when running this command. Just use your GitHub/GitLab account or the credentials of the account you previously created. If you use a self-hosted backend, log in with the appropriate backend url before running the pulumi new command.

Open the project in your favorite IDE to browse the files.

Deploy a stack

Use pulumi up to deploy the stack

The command will first display a preview of the changes and then ask you whether you want to apply the changes. Select yes.

As there are currently no resources in the Pulumi program, only the stack itself will be created in the state, no cloud resources will be provisioned.

Depending on your template, the Pulumi program may contain an output that is displayed once the command is executed. Outputs can be used to retrieve information from a Pulumi stack like URL from provisioned cloud resources.

  • If there is not existing output, add an output outputKey with a value outputValue.
Code in C#
return new Dictionary<string, object?>
{
   ["outputKey"] = "outputValue"
};
Code in TypeScript
export const outputKey = "outputValue"
Code in Python
pulumi.export("outputKey", "outputValue")

Handle stack configuration, stack outputs, and secrets

Configuration allows you to configure resources with different settings depending on the stack you are using. A basic use case is to have the pricing tier of a resource in the configuration to have less expensive/powerful machines in the development environment than in production.

  1. Add a setting named AppServiceSku with the value F1 to the stack configuration using the command pulumi config set
Command
pulumi config set AppServiceSku F1

The new setting is displayed in the dev stack configuration file: Pulumi.dev.yaml.

  1. Modify the code to retrieve the AppServiceSku setting and put it in the outputs (cf. doc).
Code to retrieve the configuration in C#
var config = new Config();
var appServiceSku = config.Get("AppServiceSku");

return new Dictionary<string, object?>
{
   ["outputKey"] = "outputValue",
   ["appServiceSku"] = appServiceSku
};
Code to retrieve the configuration in TypeScript
import {Config} from "@pulumi/pulumi";

const config = new Config()
const appServiceSkuSetting = config.get("AppServiceSku")

export const outputKey = "outputValue"
export const appServiceSku = appServiceSkuSetting
Code to retrieve the configuration in Python
import pulumi 
from pulumi import Config


config = Config()
app_service_sku = config.get("AppServiceSku")

pulumi.export("outputKey", "outputValue")
pulumi.export("appServiceSku", app_service_sku)

Note

Run pulumi up -y (the -y option is to automatically approve the preview) to update the stack and verify your code is working as expected. This will not always be specified in the rest of the workshop.

Pulumi has built-in supports for secrets that are encrypted in the state.

  1. Add a new secret setting ExternalApiKey with the value SecretToBeKeptSecure to the configuration and to the outputs.
Command
pulumi config set --secret ExternalApiKey SecretToBeKeptSecure
Code in C#
var config = new Config();
var appServiceSku = config.Get("AppServiceSku");
var externalApiKey = config.RequireSecret("ExternalApiKey");

return new Dictionary<string, object?>
{
   ["outputKey"] = "outputValue",
   ["appServiceSku"] = appServiceSku,
   ["apiKey"] = externalApiKey
};
Code in TypeScript
const config = new Config()
const appServiceSkuSetting = config.get("AppServiceSku")
const externalApiKey = config.requireSecret("ExternalApiKey")

export const outputKey = "outputValue"
export const appServiceSku = appServiceSkuSetting
export const apiKey = externalApiKey
Code in Python
config = Config()
app_service_sku = config.get("AppServiceSku")
external_api_key = config.require_secret("ExternalApiKey")

pulumi.export("outputKey", "outputValue")
pulumi.export("appServiceSku", app_service_sku)
pulumi.export("apiKey", external_api_key)

You can see that the secret is masked in the logs and that you have to use the command pulumi stack output --show-secrets to display it.

Provision Azure resources

Configure the program to use the Azure provider

Providers are the packages that allow you to provision resources in cloud providers or SaaS. Each resource provider is specific to a cloud provider/SaaS.

  1. Add the Azure Native Provider package to the project.
Command for C#
dotnet add package Pulumi.AzureNative
Command for TypeScript
pnpm add @pulumi/azure-native
Command for Python
pip install pulumi-azure-native
## Or if you use poetry :
## poetry add pulumi-azure-native

Note

The package is big so it can take some time to download and install especially if you are using Node.js

Azure providers allows to configure a default location for Azure resources so that you don't need to specify it each time you create a new resource.

  1. Configure the default location for your Azure resources.
Command
pulumi config set azure-native:location westeurope

Note

All azure locations can be listed using the following command: az account list-locations -o table

  1. Ensure you are correctly logged in the azure CLI using the az account show command. Otherwise, use the az login command.

Work with Azure resources

You can explore all Azure resources in the documentation of the Azure API Native Provider to find the resources you want to create.

  1. Create a resource group named rg-workshop that will contain the resources you will create next.
Code in C#
var resourceGroup = new ResourceGroup("workshop");   
Code in TypeScript
import {ResourceGroup} from "@pulumi/azure-native/resources";

const resourceGroup = new ResourceGroup("workshop");
Code in Python
import pulumi_azure_native as azure_native

resource_group = azure_native.resources.ResourceGroup("workshop")

When executing the pulumi up command, you will see that pulumi detects there is a new resource to create. Apply the update and verify the resource group is created.

Note

You don't have to specify a location for the resource group, by default it will use the location you previously specified in the configuration.

  1. Configure the resource group to have the tag Type with the value Demo and the tag ProvisionedBy with the value Pulumi.
Code in C#
var resourceGroup = new ResourceGroup("workshop", new()
{
    Tags =
    {
        { "Type", "Demo" },
        { "ProvisionedBy", "Pulumi" }
    }
});
Code in TypeScript
const resourceGroup = new ResourceGroup("workshop", {
  tags: {
    Type: "demo",
    ProvisionedBy: "Pulumi"
  }
});
Code in Python
resource_group = azure_native.resources.ResourceGroup(
    "workshop",
    tags={
        "Type": "Demo",
        "ProvisionedBy": "Pulumi",
    }
)

When updating the stack, you will see that pulumi detects the resource group needs to be updated.

It's a good practice to follow a naming convention. Like the name rg-workshop-dev where:

  • rg is the abbreviation for the resource type "resource group"
  • workshop is the name of the application/workload
  • dev is the name of the environment/stack
  1. Update the resource group name to rg-workshop-dev for your resource group.
Code in C#
var stackName = Deployment.Instance.StackName;
var resourceGroup = new ResourceGroup($"rg-workshop-{stackName}", new()
{
    Tags =
    {
        { "Type", "Demo" },
        { "ProvisionedBy", "Pulumi" }
    }
});

The stack name is directly retrieved from Pulumi to avoid hardcoding it.

Code in TypeScript
const stackName = pulumi.getStack()
const resourceGroup = new ResourceGroup(`rg-workshop-${stackName}`, {
  tags: {
    Type: "demo",
    ProvisionedBy: "Pulumi"
  }
});

The stack name is directly retrieved from Pulumi to avoid hardcoding it.

Code in Python
stack_name = pulumi.get_stack()

resource_group = azure_native.resources.ResourceGroup(
    f"rg-workshop-{stack_name}",
    tags={
        "Type": "Demo",
        "ProvisionedBy": "Pulumi",
    }
)

The stack name is directly retrieved from Pulumi to avoid hardcoding it.

When updating the stack, you will see that pulumi detects the resource group needs to be recreated (delete the one with the old name and create a new one with the new name). Indeed, when some input properties of a resource change, it triggers a replacement of the resource. The input properties concerned are always specified in the documentation of each resource.

Note

You have seen that depending on what you do, updating the stack will result in creating, updating, or deleting resources. Instead of executing the pulumi up command each time you want to see the result of your changes, you can use the pulumi watch command that will act as hot reload for your infrastructure code (each time you make a change and save your code file, pulumi will detect it, build the code, and deploy the changes ). You can use that for the rest of the workshop or continue using pulumi up -y if you prefer.

Sometimes it's not easy to find the correct type for the resource we want to create. You can use the pulumi ai web command to use natural-language prompts to generate Pulumi infrastructure-as-code.

  1. Use pulumi ai to provision a free Web App/App Service.
Command for C#
pulumi ai web -l C# "Using Azure Native Provider, create a free App Service."
Command for TypeScript
pulumi ai web -l typescript "Using Azure Native Provider, create a free App Service."
Command for Python
pulumi ai web -l python "Using Azure Native Provider, create a free App Service."
Code in C#
var appServicePlan = new AppServicePlan($"sp-workshop-{stackName}", new()
{
    ResourceGroupName = resourceGroup.Name,
    Sku = new SkuDescriptionArgs()
    {
        Name = "F1",
    },
});

var appService = new WebApp($"app-workshop-{stackName}", new()
{
    ResourceGroupName = resourceGroup.Name,
    ServerFarmId = appServicePlan.Id,
});

An App Service Plan is needed to create an App Service.

Code in TypeScript
const appServicePlan = new AppServicePlan(`sp-workshop-${stackName}`, {
  resourceGroupName: resourceGroup.name,
  sku: {
    name: "F1",
  },
});

const appService = new WebApp(`app-workshop-${stackName}`, {
  resourceGroupName: resourceGroup.name,
  serverFarmId: appServicePlan.id,
});

An App Service Plan is needed to create an App Service.

Code in Python
app_service_plan = azure_native.web.AppServicePlan(
    f"sp-workshop-{stack_name}",
    resource_group_name=resource_group.name
    sku=azure_native.web.SkuDescriptionArgs(
        name="F1"
    )
)

app_service = azure_native.web.WebApp(
    f"app-workshop-{stack_name}",
    resource_group_name=resource_group.name,
    server_farm_id=app_service_plan.id
)

Note

To access properties from other resources, you can just use variables.

  1. Update the infrastructure to use the AppServiceSku setting from the configuration instead of hard coding the SKU F1.
Code in C#
var appServiceSku = config.Require("AppServiceSku");
var appServicePlan = new AppServicePlan($"sp-workshop-{stackName}", new()
{
    ResourceGroupName = resourceGroup.Name,
    Sku = new SkuDescriptionArgs()
    {
        Name = appServiceSku,
    },
});
Code in TypeScript
const appServiceSku = config.require("AppServiceSku")
const appServicePlan = new AppServicePlan("appServicePlan", {
  resourceGroupName: resourceGroup.name,
  sku: {
    name: appServiceSku,
  },
});
Code in Python
app_service_sku = config.require("AppServiceSku")
app_service_plan = azure_native.web.AppServicePlan(
    f"sp-workshop-{stack_name}",
    resource_group_name=resource_group.name,
    sku=azure_native.web.SkuDescriptionArgs(
        name=app_service_sku
    )
)

Not only does the stack have outputs, but the resources themselves also have outputs, which are properties returned from the cloud provider. Since these values are only known once the resources have been provisioned, there are certain considerations to keep in mind when using them in your program (particularly when performing computations based on an output).

  1. Modify the program to make the stack only return one output, that is the URL of the app service.
Code in C#
var appService = new WebApp($"app-workshop-{stackName}", new WebAppArgs
{
    ResourceGroupName = resourceGroup.Name,
    ServerFarmId = appServicePlan.Id,
});

return new Dictionary<string, object?>
{
    ["AppServiceUrl"] = Output.Format($"https://{appService.DefaultHostName}")
};
Code in TypeScript
const appService = new WebApp("appService", {
  resourceGroupName: resourceGroup.name,
  serverFarmId: appServicePlan.id,
});

export const appServiceUrl = pulumi.interpolate`https://${appService.defaultHostName}`;
Code in Python
app_service = azure_native.web.WebApp(
    f"app-workshop-{stack_name}",
    resource_group_name=resource_group.name,
    server_farm_id=app_service_plan.id
)

pulumi.export("app_service_url", app_service.default_host_name.apply(lambda hostname: f"http://{hostname}"))

Sometimes, you need some data that are not available as properties of a resource. That's exactly what provider functions are for. For instance, the ListWebAppPublishingCredentialsOutput function can be used to retrieve the publishing credentials of an App Service

  1. Add 2 outputs to the stack PublishingUsername and PublishingUserPassword that are secrets that can be used to deploy a zip package to the App Service.
Code in C#
var publishingCredentials = ListWebAppPublishingCredentials.Invoke(new()  
{  
    ResourceGroupName = resourceGroup.Name,  
    Name = appService.Name  
});

return new Dictionary<string, object?>
{
    ["AppServiceUrl"] = Output.Format($"https://{appService.DefaultHostName}"),
    ["PublishingUsername"] = Output.CreateSecret(publishingCredentials.Apply(c => c.PublishingUserName)), 
    ["PublishingUserPassword"] = Output.CreateSecret(publishingCredentials.Apply(c => c.PublishingPassword)),
};

As the function outputs are not marked as secrets, you have to manually do it.

Code in TypeScript
const publishingCredentials = listWebAppPublishingCredentialsOutput({
  name: appService.name,
  resourceGroupName: resourceGroup.name
})

export const appServiceUrl = pulumi.interpolate`https://${appService.defaultHostName}`;
export const publishingUsername = pulumi.secret(publishingCredentials.publishingUserName)
export const publishingPassword = pulumi.secret(publishingCredentials.publishingPassword)

As the function outputs are not marked as secrets, you have to manually do it.

Code in Python
publishing_credentials = azure_native.web.list_web_app_publishing_credentials(
    resource_group_name=resource_group.name,
    name=app_service.name
)

pulumi.export("app_service_url", app_service.default_host_name.apply(lambda hostname: f"http://{hostname}"))
pulumi.export("publishing_username", Output.secret(publishing_credentials.publishing_user_name))
pulumi.export("publishing_userpassword", Output.secret(publishing_credentials.publishing_password))

As the function outputs are not marked as secrets, you have to manually do it.

Manage stacks

  • Use the pulumi about command to get some information about the current Pulumi environment. It also displays information about the current stack.

Create a new stack

Command
pulumi stack init prod

You will be automatically switched to his new stack. You can switch back to the previous stack using the pulumi stack select command.

  • Switch back to the dev stack
Command
pulumi stack select dev
  • List the different stacks with the pulumi stack ls command.

Provision the infrastructure for a new environment

  • Select the prod stack and try to provision the infrastructure for this stack. It should fail because some configuration is missing.
Command
pulumi stack select prod
pulumi up
  • Add the missing configuration and provision the infrastructure
Command
pulumi config set azure-native:location westeurope
pulumi config set --secret ExternalApiKey SecretToBeKeptVerySecure
pulumi config set AppServiceSku F1
pulumi up

Note

You are on another environment so you don't have to set the same values. You can use another sku, another default azure location, another secret value...

Delete resources and stack

To delete all the resources in the stack you can run the command pulumi destroy.

  • Delete the resources on the prod environment.

If you want to delete the stack itself with its configuration and deployment history you can run the command pulumi stack rm command.

  • Delete the prod stack
Command
pulumi stack rm prod

Next

To continue this lab and see more advanced features, you can check the next parts:

About

Workshop to get started with Pulumi on Azure

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published