- 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
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
- Create a new directory for the workshop with a new
infra
directory in it.
mkdir pulumi-workshop; cd pulumi-workshop; mkdir infra; cd infra
- 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.
- 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.
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 valueoutputValue
.
Code in C#
return new Dictionary<string, object?>
{
["outputKey"] = "outputValue"
};
Code in TypeScript
export const outputKey = "outputValue"
Code in Python
pulumi.export("outputKey", "outputValue")
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.
- Add a setting named
AppServiceSku
with the valueF1
to the stack configuration using the commandpulumi config set
Command
pulumi config set AppServiceSku F1
The new setting is displayed in the dev stack configuration file: Pulumi.dev.yaml
.
- 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.
- Add a new secret setting
ExternalApiKey
with the valueSecretToBeKeptSecure
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.
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.
- 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.
- 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
- Ensure you are correctly logged in the azure CLI using the
az account show
command. Otherwise, use theaz login
command.
You can explore all Azure resources in the documentation of the Azure API Native Provider to find the resources you want to create.
- 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.
- Configure the resource group to have the tag
Type
with the valueDemo
and the tagProvisionedBy
with the valuePulumi
.
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/workloaddev
is the name of the environment/stack
- 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.
- 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.
- Update the infrastructure to use the
AppServiceSku
setting from the configuration instead of hard coding the SKUF1
.
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).
- 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
- Add 2 outputs to the stack
PublishingUsername
andPublishingUserPassword
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.
- Use the
pulumi about
command to get some information about the current Pulumi environment. It also displays information about the current stack.
- Use the
pulumi stack init
command to create a newprod
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.
- 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...
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
To continue this lab and see more advanced features, you can check the next parts: