Infrastructure as Code - An intro - Part 6 - Using Pulumi
The 6th entry in my blog series about IaC is dedicated to Pulumi.
Pulumi is a very different beast, compared to the previously covered technologies (ARM, Bicep and Terraform), in that it is not based on a Domain Specific Language. Instead, Pulumi allows you to write your IaC in your language of choice. As long as your language of choice is JavaScript/TypeScript, Python, Go or .NET Core (C#, F# and VB). This makes the Pulumi experience a lot different from using a technology that uses a DSL (or JSON).
DSL:s are often heavily tailored towards the task they are meant to solve. However, they are often lacking quite a bit of the flexibility you get from using a “full” programming language. Because of this, Pulumi can often offer us a lot more flexibility when defining our infrastructure.
On top of that, Pulumi is also provider based like Terraform, allowing us to set up resources outside of Azure as well. And even if there aren’t as many providers as there is for Terraform at the moment, more are being added continuously. And if you can’t find a provider for your scenario, you can quite easily extend Pulumi to support unsupported platforms or features.
Caveat: The support for extending Pulumi is partly dependent on language of choice. Different languages support a slightly different feature set. Generally based on possibilities/limitations within the languages. For example, JavaScript/TypeScript can offer some features that .NET Core can’t, due to the static nature of the .NET Core languages.
What is Pulumi
Pulumi is built by Pulumi Corp, aiming to allow for a more developer focused approach to IaC than the existing tools allow for. It does this, as mentioned before, by allowing us to write the IaC using a variety of languages instead of using a DSL or JSON.
In this post, I will focus on using Pulumi together with TypeScript. I’m not quite sure why I lean towards TypeScript when using Pulumi. As a C# dev, I assume I should be leaning towards using C#. However, I feel very comfortable with TypeScript, and it offers a couple of features that the C# version doesn’t. It also happened be the language I was using when I started working with Pulumi, and I never really felt a need to switch to C#.
The Pulumi architecture is a bit “interesting”. It is layered in a way that allows us to write our code in different languages, while at the same time being able to share a lot of the core functionality. The way it does this, is by using a flow that looks like this.
You tell the Pulumi CLI that you want to set up some infrastructure. The CLI then loads the correct language runtime based on the language used by the project. The language runtime then executes your code, which generates a “desired state”. This desired state is then “diff:ed” against the current state, which is stored in a state store of your choice. The result of this “diff”, basically a definition of what resources need to be created, deleted or updated, is then passed to a provider plugin. This plugin is then responsible for talking to whatever system it needs to talk to, to get your infrastructure set up.
As you can see, “our” code is a pretty small part of this. And it is only used to generate the desired state. So, as long as the language specific SDK can generate the desired state, the rest of the tool can be generic.
Note: This is very much a simplification, and it has technical details that are left out for simplicity.
The code, you as developers write, is written using a Pulumi SDK that knows how to interact with Pulumi. So, for TypeScript for example, it consists of a bunch of TypeScript/JavaScript classes that you can use to define the resources you want. These classes in turn generate the desired state that is handed to Pulumi.
Note: Even if it looks like you are creating resources in our code, you are really just defining a desired state. The actual resource creation is handled by the provider, based off of the desired state that your code generates.
The SDK:s (and provider plugins) are distributed using the package management solution used by the selected language/platform. So, for example, for C# it is distributed using NuGet, and for JavaScript/TypeScript it uses npm. And since each SDK targets a specific language and provider, they can be written in way that feels natural in your chosen language. In this post, the SDK of choice is the Azure SDK for TypeScript.
It might be worth noting that there are actually 2 sets of SDK:s for Azure, a “classic” one and a “native” one. The classic one is hand crafted, and because of this, suffers from a bit of feature lag. Just like Terraform does. The “native” one, which is definitely the recommended one, is automatically generated from the OpenAPI definition for the Azure API. This removes most of the feature lag, as new versions can be released pretty much as soon a new feature is published to the Azure API. I personally find the API:s in the “classic” SDK nicer to work with, but I still gravitate towards the “native” one as they generally have newer features. They also have naming parity with the API, allowing your to find information about the types in other places than the Pulumi docs.
State
Pulumi, just as Terraform, keeps track of the current state of the infrastructure using a state storage. This is basically just JSON-files that keeps track of what resources were available at the end of the last run, and their defined properties. This is state is then used to figure out what changes need to be performed to get the infrastructure to correspond to a new desired state.
And just as with Terraform, the state can be stored in a variety of different places, such as Azure blob storage, Amazon S3 or Pulumi’s own cloud service. However, with Pulumi, the CLI is responsible for keeping track of the store information. This is a big difference from Terraform where that information is stored in the Terraform files. And to be honest, as a consultant with a lot of clients, I’m not sure that having the CLI keep track of the back-end seems like the best solution. I can easily see this cause problems…
There is actually another big difference between Terraform and Pulumi when it comes to the state. Terraform will compare the desired state against a combination of the Terraform state and the “real” infrastructure, which allows it to reset all properties based on the desired state, including setting properties that haven’t been explicitly defined, back to their default values. Pulumi on the other hand, only uses its own state. This means that only properties specifically set during configuration is updated/reset, allowing for non-defined properties to drift in the infrastructure.
Note: The state management might not be a big deal if you lock down your environment, making sure that individuals can’t make changes to it. However, it is definitely worth mentioning, and keeping in the back of your mind when looking at IaC tools.
Pulumi pre-requisites
The tools used for working with Pulumi is a bit different depending on what language you are using. However, for TypeScript, it looks like this
- Azure CLI
- The Pulumi CLI
- Node.js - any currently supported version
- An editor of some sort. I recommend VS Code, or at least one that supports TypeScript definitions
The fact that TypeScript is a strongly typed programming language, as opposed to a DSL, means that we don’t need any extra extensions to get tooling support. Instead, as long as you editor of choice supports TypeScript, you will automatically get tooling support based on the types!
I’m going to assume that you have the Azure CLI installed already as it has been used in the previous posts. If you haven’t, I suggest going back to the post about the Azure CLI and having a look at how to install it.
I’m also going to skip out on explaining how to install Node. If you don’t have it installed, have a look at https://nodejs.org/en/.
Installing Pulumi
As I am skipping out on how to install Azure CLI and Node, the only thing you need to install is Pulumi. And in this brave, new, cross platform world, you can obviously install it on Windows, Linux and Mac. Each having its own way of doing the install.
On Windows, the recommended approach is to use Chocolatey, which is a fantastic package manager for Windows. And even if package managers might feel a bit odd for Windows people, I would definitely recommend trying it out.
Note: Package managers for Windows is going to be a thing. Just get used to it. And right now, Chocolatey is the best one available. But maybe, just maybe, WinGet might become a “thing” in the future.
To install Pulumi using Chocolatey, you just need start a Terminal with Administrator privileges and run
> choco install pulumi
During the installation you will probably have to accept that it runs a PowerShell script, which is fine.
Comment: No, in general it is not a good idea to run PowerShell scripts you found on the interwebs without making sure that they aren’t doing anything bad. But in this case, I’m pretty sure you can trust it!
Once the install has completed, you can verify that everything has worked as it should, by running
> pulumi version
v3.17.1
If it says that it can’t find the Pulumi executable, try restarting your terminal to reload the PATH variable.
Note: If you are on Linux or Mac, have a look at https://www.pulumi.com/docs/get-started/install/
I would also recommend verifying that the Azure CLI is set up to use the correct subscription if you have multiple ones. This is easily done by running
> az account show
If the selected Azure subscription isn’t the one you want to use, you can use az account list
to locate the ID of the one you want to use, and then use az account set -s <SUBSCRIPTION ID>
to set it as the selected one.
That’s it! You are ready to go!
Creating up a Pulumi project
Just as in the previous posts, the target infrastructure looks like this
- A Resource Group to hold all of the resources
- An App Service Plan using the Free tier
- A Web App, inside the App Service Plan
- Application Insights (connected to the Web App)
- A Log Analytics Workspace to store the Application Insights telemetry
- A SQL Server Database
The Web App also needs to be connected to the SQL Server, using a connection string in the Connect Strings part of the app configuration, and to the Application Insights resource using the required settings in the App Settings part.
All of this should be fairly familiar if you have read the previous posts. However, if you haven’t, you will probably be able to pick it up along the way anyway!
So…let’s go ahead and create a Pulumi project.
The Pulumi CLI has some really nice features. One of them is that it can easily create starter projects for you. However, before we can do this, we need to “log in”. This basically means telling Pulumi where you want to store the state.
In this case, I suggest simply storing the state locally, together with the application you are about to create. So, let’s go ahead and create a directory for the project, and tell Pulumi to store the state there
> mkdir dulumi-demo
> cd pulumi-demo
> pulumi login file://.
This will create a directory called PulumiDemo, and then tell Pulumi to store any generated state inside it by “logging in to” file://.
.
The next step is to tell Pulumi to create a starter project for you to work with. Luckily, this is as simple as running
> pulumi new azure-typescript
This will start an interactive walk through that will ask you for a project name, a project description, a stack name (more about that later), a passphrase and an Azure location to use as the default location when creating Azure resources.
You can set most of them to their default values by simply pressing Enter
. However, you will have to select a passphrase and a location.
Note: The passphrase is used to protect your configuration, allowing you to store encrypted secrets in your config. This can be replaced by another secret store such as Azure KeyVault. However, that is outside of the scope for this post.
Comment: Choose whatever Azure location makes most sense to you. It might not be the default, which is WestUS. In my case for example, WestEurope makes more sense.
Once you have entered all the values, Pulumi will go ahead and set up a new TypeScript starter project, and run npm install
to get all the dependencies installed.
Note: Running npm install
can take a while. But that is nothing special for Pulumi, that’s just the way npm works…
Once the project set up has completed, I suggest opening the folder in VS Code, or whatever editor you want to use. For VS Code, you just run
> code .
The starter project that the Pulumi CLI creates is a fairly standard TypeScript project that looks like this
As this is a fairly standard TypeScript project, I’m not going to say too much about it. However, it is worth noting the Pulumi.yaml
and Pulumi.dev.yaml
. These YAML-files make this a Pulumi project. In the Pulumi.yaml
file, you will find some very basic information about the project
name: pulumi-demo
runtime: nodejs
description: A minimal Azure Native TypeScript Pulumi program
and in the Pulumi.dev.yaml
file, you will find the configuration for the dev stack
encryptionsalt: v1:IykIQhHawfU=:v1:+I7tf853kbGI2RmW:iLKunFMrscgDDwEAV93fn3KwqRw0CA==
config:
azure-native:location: WestEurope
As you can see, there is a property called encryptionsalt
that was generated using your “passphrase” during set up. This allows you to have encrypted configuration values inside this file, even if you store it in source control, which is really nice for things like credentials or connection strings.
Note: I will talk more about stacks later on. For now, all you need to understand is that your configuration is stored per stack. This means that you can have several different stacks, one for each environment you want to be able to set up, storing a specific set of configuration for that specific environment.
Cleaning up the starter project
Now that you have a project to work with, you can start defining your infrastructure. However, the starter project comes with a couple of pre-defined resources. Some of which you don’t need. So, let’s start by removing the unnecessary resources.
If you open up index.ts, you will see that it creates a resource group and a storage account. And that it then uses some “weird” code to export a primaryStorageKey
.
Note: Even if you haven’t worked with Pulumi before, you are probably still able to understand most of the stuff going on, which is really cool! This is one of the benefits from using a standard programming language instead of a DSL.
You aren’t going to use the storage account, or the export, so let’s go ahead and remove that.
Looking at the imports, you can probably see that the azure-native
SDK does a great job at modularizing its content. However, this tends to lead to a lot of imports, as we tend to use several modules. To make this a little less annoying, you can replace the current @pulumi/azure-native/XXX
imports with a single import that imports all of @pulumi/azure-native
as azure
.
Once the imports have been updated, you also need to update the resource group definition from resources.ResourceGroup
to azure.resources.ResourceGroup
In the end, it should look like this
import * as pulumi from "@pulumi/pulumi";
import * as azure from "@pulumi/azure-native";
const resourceGroup = new azure.resources.ResourceGroup("resourceGroup");
As you can see, defining a resource using Pulumi is just a matter of creating an instance of a class. However, it is very important to understand that this instantiation does not actually go and create a resource in Azure. Instead, it adds a resource definition in the desired state, which is then used by Pulumi, and the provider plugin, to create the resource group.
However, the name resourceGroup
doesn’t seem like the best name for a resource group. So, go ahead and change that to PulumiDemo.
const resourceGroup = new azure.resources.ResourceGroup("PulumiDemo");
You can now go ahead and ask Pulumi what resources it thinks it needs to create based on this new desired state. All you have to do, to get this information is to run
> pulumi preview
This command will ask you for the passphrase you added to the dev stack during project set up. This is so that Pulumi can decrypt any encrypted values in the configuration.
Once you have provided the correct passphrase, you should see something like this
Previewing update (dev):
Type Name Plan
+ pulumi:pulumi:Stack pulumi-demo-dev create
+ └─ azure-native:resources:ResourceGroup PulumiDemo create
Resources:
+ 2 to create
As you can see, Pulumi has decided that it needs to create a stack and a resource group, which is correct.
Note: Yes, the stack is considered a resource of its own, under which, all other resources in this particular stack is placed.
Once you have verified that Pulumi has correctly figured our what to create, you can go ahead and “deploy” the new infrastructure by running
> pulumi up
Once again, you are asked to input your passphrase, and then presented with preview just like the one you just looked at. However, this time you also get an interactive menu that allows you to approve or cancel the update, or view “details”.
Note: Since Pulumi always runs a pulumi preview
during pulumi up
, there is generally no reason to run pulumi preview
separately.
If you go ahead and select details, you will be faced by a more detailed description of what is about to be deployed
+ pulumi:pulumi:Stack: (create)
[urn=urn:pulumi:dev::PulumiDemo2::pulumi:pulumi:Stack::PulumiDemo2-dev]
+ azure-native:resources:ResourceGroup: (create)
[urn=urn:pulumi:dev::PulumiDemo2::azure-native:resources:ResourceGroup::PulumiDemo]
[provider=urn:pulumi:dev::PulumiDemo2::pulumi:providers:azure-native::default_1_45_0::04da6b54-80e4-46f7-96ec-b56ff0331ba9]
location : "WestEurope"
resourceGroupName : "PulumiDemo0a17be63"
This is a really helpful view if you are interested in the details of what is going to happen. In this case, it actually highlights a potential problem. It says resourceGroupName : "PulumiDemo0a17be63"
. Wait, what!? That isn’t the resource group name that’s in the code.
Pulumi is very opinionated when it comes to naming resources. It is a firm believer of using “cattle-based” naming. Because of this, all resource names are suffixed with 8 random characters by default. This is a nice feature, as it makes it a lot less likely to cause naming conflicts. However, for resource groups that don’t need to be globally unique, I generally like to use “proper” names. Mainly because they are often used by humans to find things.
Luckily, Pulumi has no problems allowing you to set a “proper” name. You just need to take responsibility for the naming by setting manually.
So, go ahead and select no to cancel the update, and go back to the index.ts file.
During the creation of a resource, you can supply a second constructor parameter, containing all the properties you want to set on that resource. Using VS Code’s TypeScript tooling, we can see that the second parameter is of type ResourceGroupArgs
The ResourceGroupArgs
type is an interface that defines, among other things, a resourceGroupName
property
Note: To get the drop down to open in VS Code, just add the curly braces ({}
), put the caret in between them, and press Ctrl + Space
Setting this property will override Pulumi’s default, allowing us to set a name manually like this
const resourceGroup = new azure.resources.ResourceGroup("PulumiDemo", {
resourceGroupName: "PulumiDemo"
});
With that update in place, you can go back to the terminal and run
pulumi up
However, this time you can go ahead and select yes to perform the update.
After a short time, it should output something like this
...
Updating (dev):
Type Name Status
+ pulumi:pulumi:Stack pulumi-demo-dev created
+ └─ azure-native:resources:ResourceGroup PulumiDemo created
Resources:
+ 2 created
Duration: 12s
Now, you will have a resource group called PulumiDemo in the Azure subscription selected in the Azure CLI.
While deploying the defined infrastructure (the single resource group), Pulumi created some state information to keep track of the fact that this resource group has been created. And since you “logged in” to file://.
, this state is stored in a file located at /.pulumi/stacks/dev.json, where the filename dev.json corresponds to the name of the stack that it stores state for.
Now what we have a resource group to put the rest of the resources in, you can move on to the SQL Server database.
Adding the SQL Database
However, before you can set up the actual database, you need to set up the SQL Server that it should be hosted on.
In Pulumi, that means instantiating another class. In this case a ´new azure.sql.Server´ that looks like this
const sql = new azure.sql.Server("mydemosqlserver123", {
resourceGroupName: resourceGroup.name,
administratorLogin: "server_admin",
administratorLoginPassword: "P@ssw0rd1!"
});
First, this declaration sets the resourceGroupName
property to tell it what resource group to put the server in. And instead of manually setting the name to a string, it uses the resourceGroup.name
property.
Next, it sets the username and password for the admin user.
For now, this is hard coded into the application, which I know is a really bad idea. However, it is only a temporary solution until you get a bit further into this post. I promise!
But what about the name? Well, for a SQL Server instance, it needs to be globally unique. And as mentioned before, Pulumi solves this by adding a random suffix to the name. So, all in all mydemosqlserver123, with a suffix added, should be good enough. However, I would definitely recommend implementing some form of naming standard for your resources. Luckily, with all the features of TypeScript available for use, that is easily solved by declaring a function like this
function getName(type: string) {
return `${ pulumi.getProject().toLowerCase() }-${ type }`;
}
This function concatenates the project name, which was defined when setting up the project, with the type of resource, which is passed in as a parameter.
Comment: With JavaScript “hoisting” it doesn’t matter where in the TypeScript file you declare the function. It will be “hoisted”, and available throughout the entire file just because it is a function. So, if you prefer putting it out of the way at the bottom of the file, that’s fine. You can find more information about it at https://developer.mozilla.org/en-US/docs/Glossary/Hoisting
Note: This is probably a too simple naming convention for most projects, as it assumes a single instance of each resource type. It also does not include a location or environment, which is very useful in projects that are used to deploy multiple environments across multiple regions. But for this demo, it will just have to do!
Once the getName()
method has been added, you can update the SQL Server name like this
const sql = new azure.sql.Server(getName("sql"), {
...
});
This should give the SQL Server a name that looks something like pulumidemo-sqlXXXXXXXX
, which should be good enough for the current project.
Now that the server has been defined, you can go ahead and define the database.
const db = new azure.sql.Database(getName("db"), {
databaseName: "MyDemoDb",
resourceGroupName: resourceGroup.name,
serverName: sql.name,
sku: {
name: config.require("sqlSize")
},
});
Once again, the resourceGroupName
is set, as well as the resource specific properties. In this case that means the name of the server to host the database and the SKU to use. However, since this resource does not need to have a unique name, and the fact that the name of the database should probably be somewhat consistent across deployments, you can also set the databaseName
to a “proper” name like MyDemoDb.
The last part of setting up the database, is to set up the SQL Server firewall to allow access from your application. In this case, for the sake of simplicity, I suggest opening it for all Azure services. This is “easily” done by adding a firewall rule that opens an IP range from 0.0.0.0 to 0.0.0.0.
In Pulumi, that means instantiating an azure.sql.FirewallRule
that looks like this
new azure.sql.FirewallRule("allowAllAzureIps", {
firewallRuleName: "AllowAllWindowsAzureIps",
resourceGroupName: resourceGroup.name,
serverName: sql.name,
startIpAddress: "0.0.0.0",
endIpAddress: "0.0.0.0"
});
That’s it! Let’s see what Pulumi thinks it needs to do to get to this new desired state by running
> pulumi up
Unfortunately, running this command fails with an error that looks like this
error: constructing secrets manager of type “passphrase”: unable to find either
PULUMI_CONFIG_PASSPHRASE
orPULUMI_CONFIG_PASSPHRASE_FILE
when trying to access the Passphrase Secrets Provider; please ensure one of these environment variables is set to allow the operation to continue
What? Why is that? Did you do something wrong?
No, not at all! The first time you run pulumi up
, you are allowed to manually input the passphrase in the terminal. However, as soon as it has run once, it requires the passphrase to be provided either as an environment variable called PULUMI_CONFIG_PASSPHRASE
, or through a file, whose path is added to an environment variable called PULUMI_CONFIG_PASSPHRASE_FILE
.
In this case, it is just a simple matter or defining an environment variable called PULUMI_CONFIG_PASSPHRASE
, containing the passphrase for the stack.
In my case, I used the passphrase test123!
, and I’m using PowerShell, so that means that I need to run a command like this
> $env.PULUMI_CONFIG_PASSPHRASE = "test123!"
However, it does depend on what terminal you are using. For example, if you use bash, you would instead have to run
> export PULUMI_CONFIG_PASSPHRASE=test123!
Once this environment variable has been defined, you can re-run pulumi up
> pulumi up
Previewing update (dev):
Type Name Plan
pulumi:pulumi:Stack pulumi-demo-dev
+ ├─ azure-native:sql:Server pulumi-demo-sql-demo create
+ ├─ azure-native:sql:Database pulumi-demo-db-demo create
+ └─ azure-native:sql:FirewallRule allowAllAzureIps create
Resources:
+ 3 to create
2 unchanged
Ok, so Pulumi thinks it needs to add 3 resources, which looks correct.
However, if you look at the output, it is presented in a tree structure. This indicates that Pulumi has a parent/child relationship between all its resources. And by default, resources will be created as children to the stack. Luckily, it is quite easy to update the relationships to get a better semantic representation. All you have to do, is to use a 3rd constructor parameter for the resources.
The 3rd parameter allows you to set a ton of options for the resource. Things like parent/child relationships and resource dependencies that Pulumi can’t figure out on its own, as well as a ton of more advanced things.
In this case, it’s enough to update the parent
property to set up the correct parent/child relationships.
const sql = new azure.sql.Server(getName("sql"), {
...
}, { parent: resourceGroup });
const db = new azure.sql.Database(getName("db"), {
...
}, { parent: sql });
new azure.sql.FirewallRule("allowAllAzureIps", {
...
}, { parent: sql });
This adds the resource group as the parent for the SQL Server, and the SQL Server as the parent for the database and firewall rule. Just as it should be.
Not only does this mean that child resources can never be orphaned, as the TypeScript code will fail to compile if the parent is removed. But it will also enable Pulumi to give us a better view of the relationships between the resources when running pulumi up
> pulumi up
Previewing update (dev):
Type Name Plan
pulumi:pulumi:Stack pulumi-demo-dev
└─ azure-native:resources:ResourceGroup PulumiDemo
+ └─ azure-native:sql:Server pulumi-demo-sql-demo create
+ ├─ azure-native:sql:FirewallRule allowAllAzureIps create
+ └─ azure-native:sql:Database pulumi-demo-db-demo create
Resources:
+ 3 to create
2 unchanged
This time the resource relationships look much better, so go ahead and select yes to deploy the update.
Once the update has been deployed, you should have an environment that looks something like this
Now that the database has been created, you can start focusing on the web app!
Creating the Azure Web App
The first thing you need to host a web app, is an app service plan. And just as with all other resources, it is just a matter of creating an instance of one of the classes in the SDK. In this case the azure.web.AppServicePlan
class.
const svcPlan = new azure.web.AppServicePlan(getName("plan"), {
resourceGroupName: resourceGroup.name,
sku: {
name: "F1"
tier: "Free",
},
});
All you need to define is the resource group to place it in, and the SKU.
Although…you also need to remember to set the correct parent, which in this case is the resource group
const svcPlan = new azure.web.AppServicePlan(getName("plan"), {
...
}, { parent: resourceGroup });
Cool, now you have an App Service Plan! The next step is the actual web app.
To add a Web App, you need to instantiate yet another resource. In this case an azure.web.WebApp
, which needs to be configured with a resource group and an App Service Plan. However, instead of using a property called appServicePlanId
, as you expect, it uses one called serverFarmId
. The reason for this is that this is what it is called in ARM for some reason. And since Pulumi is auto generated from the Azure API’s OpenApi spec, it picks up that name.
It should look something like this
const web = new azure.web.WebApp(getName("web"), {
resourceGroupName: resourceGroup.name,
serverFarmId: svcPlan.id
}, { parent: svcPlan })
Oh, yeah, don’t forget to add the parent
in there as well.
However, if you want to make it run ASP.NET Core applications, you also need to update the web app’s site configuration, and set the correct .NET Framework version.
Note: Yes, weirdly enough you need to set the .NET Framework version, even if it is .NET Core you want to use. However, you need to set it to “v5.0”, which doesn’t even exist in .NET Framework… Slightly confusing, but there is nothing we can do about it. On the other hand, if you want to configure a Linux-based web app to use .NET Core, you need to set the “Linux Fx version”, which makes even less sense to be honest…
Anyhow, to set the app’s site configuration, you use the azure.web.WebApp
’s siteConfig
property. This is set using an object, inside which you can set the netFrameworkVersion
property to v5.0
to configure it to use .NET Core. It looks like this
const web = new azure.web.WebApp(getName("web"), {
...
siteConfig: {
netFrameworkVersion: "v5.0"
}
}, ...)
That should take care of the .NET Core stuff. However, you also need to add a connection string to it, to allow the application to talk to the database. This is quite easily done by setting the siteConfig
’s connectionStrings
property.
The connectionStrings
property is defined as an array, containing all the connection strings that the app needs. In this case, you only need a single connection string called connectionstring. However, you can’t just the connection string to the array. Instead, you need to add an object that also contains a type
and name
property, like this
const web = new azure.web.WebApp(getName("web"), {
...,
siteConfig: {
...,
connectionStrings: [
{
name: "connectionstring",
type: "SQLAzure",
connectionString: `Data Source=tcp:${sql.fullyQualifiedDomainName},1433;Initial Catalog=${db.name};User Id=server_admin;Password='P@ssw0rd1!';`
}
]
},
}, ...)
As you can see, the actual connection string is created by using string interpolation.
Note: Yes, that’s another set of hard coded credentials… But I promise that that will be fixed later in this post!
The problem with this string interpolation is that the sql.fullyQualifiedDomainName
and db.name
properties are not actually strings. Instead, they are defined as pulumi.Output<string>
. This is Pulumi’s way of handling asynchronous properties.
So, what do I mean by asynchronous properties? Well, some property values aren’t actually known at “compile time”. Instead, these values are generated by Azure, and returned as part of the resource creation. So, to be able to use these values, Pulumi needs to wait for the resource to be created, before it can get hold of the values, and carry on creating and configuring the resource that depend on them. To handle this asynchronous situation, Pulumi uses pulumi.Output<T>
, which is a lot like TypeScript/JavaScript’s Promise<T>
. Pulumi can then give all pulumi.Output<T>
instances special handling inside Pulumi.
Now that you know that the sql.fullyQualifiedDomainName
and db.name
are defined as pulumi.Output<string>
, it probably makes sense that the string interpolation won’t work, as TypeScript has no idea of how to handle pulumi.Output<T>
. Becasue of this, you need to tell Pulumi to handle the interpolation for you. This can be achieved in several ways, but when you need to use more than one output, you need to use pulumi.all()
to turn multiple outputs into a single combined output. And once you are down to a single output, you can use the apply()
method from the pulumi.Output<T>
class to asynchronously get hold of the resolved values, by passing in a callback that will be called when all the values have been resolved.
It comes out looking like this
const web = new azure.web.WebApp(getName("web"), {
...,
siteConfig: {
...,
connectionStrings: [
{
...,
connectionString: pulumi.all([sql.fullyQualifiedDomainName, db.name])
.apply(([fqdn, dbName]) => `Data Source=tcp:${fqdn},1433;Initial Catalog=${dbName};User Id=server_admin;Password='P@ssw0rd1!';`)
}
]
},
}, ...)
This might look a bit complicated, and I completely agree! But I do suggest studying the above code a bit, to make sure that you at least get the general gist of what it does. It is a very important part of Pulumi, so understanding it is a bit important.
On the other hand, I think it might be easier to understand, if I also provide a more basic example with just a single pulumi.Output<T>
. The pulumi.all()
method unfortunately makes it extra complicated…
If you imagine that you only cared about the sql.fullyQualifiedDomainName
property, the syntax becomes a bit simpler
sql.fullyQualifiedDomainName.apply(fqdn => `Data Source=tcp:${fqdn},1433;`)
If you are familiar with JavaScript/TypeScript’s Promise<T>
, it is definitely very similar, and should be somewhat easy to understand. However, if you are coming from a different background, it might take a little while to get used to. But trust me, it will make sense quite quickly, even it looks weird now!
Luckily, when using pulumi.Output<T>
, Pulumi can automatically figure out dependencies between different resources. In this case, it automatically figures out that the web app depends on both the SQL Server and database resources. However, in some cases, it isn’t that simple. In those cases, Pulumi requires you to explicitly configure the dependencies by setting a dependsOn
property on the third constructor parameter. Like this
const web = new azure.web.WebApp(getName("web"), {
...
}, {
parent: svcPlan,
dependsOn: [ sql, db ]
})
Yes, in this case it isn’t necessary, but I thought it was worth showing. And also, it doesn’t cause any problems if you do set it explicitly even if Pulumi can figure it out…
Now that you have defined all the resources needed for the application, you can move your focus to the “supporting” resources. And by that, I mean the Application Insights resource, and the Log Analytics Workspace that it will use to store its data.
Setting up Application Insights using a Log Analytics Workspace
Let’s start with the Log Analytics Workspace, which is really simple to define.
const workspace = new azure.operationalinsights.Workspace(getName("laws"), {
resourceGroupName: resourceGroup.name
}, { parent: resourceGroup });
It really only needs a name, a resource group and a parent in this case. Sure, it can be made a lot more complex, but for this demo, we can simply use the default values for pretty much all of the settings. However, if you need a more complex set up, this is obviously supported as well!
Setting up the Application Insights resource is a different story though… Unfortunately, the @pulumi/azure_native
SDK doesn’t support setting up Application Insights with a Log Analytics Workspace back-end. There are 2 ways to fix this. One is to manually create a custom resource that sets it up for you. But this is a bit complicated to be honest… The other is to use the “old” @pulumi/azure
SDK, as this contains a resource that allows you to set up this configuration.
Luckily, the Pulumi state management allows us to combine different SDK:s in the same project. So, all you need to do, to use resources from the “old”, hand-crafted SDK, is to add the package, and start using it.
To add the SDK, you just run
> npm i @pulumi/azure
Once that has been added, you can go ahead and add an import for that package in the index.ts file like this
import * as az from "@pulumi/azure";
That’s it! You now have full access to all the resources in that SDK as well. And the resource you need in this case is the az.appinsights.Insights
. This resource needs a resource group, an application type, a workspace ID and a parent, like this
const ai = new az.appinsights.Insights(getName("ai"), {
resourceGroupName: resourceGroup.name,
applicationType: "web",
workspaceId: workspace.id,
}, { parent: resourceGroup });
That’s all there is to it!
The ability to combine different SDK:s allows for some really cool things! In this case it was a combination of 2 different Azure SDK:s, but it could just as well have been a combination of the Azure SDK and the AWS SDK for example.
At this point, the index.ts file is getting quite large… However, since we are using TypeScript, we can remedy this quite easily. You could for example go ahead and just move some of the resources into a separate TypeScript file, and reference that file from index.ts. But we can do even better with Pulumi!
In this demo, the Log Analytics Workspace and the Application Insights resources are quite tightly coupled. Because of this, you might as well combine them into a single resource.
Note: In this case, this might make sense, but in a real application I would probably not recommend this specific combination of resources. Why? Well, you might want to have several Application Insights resources writing to the same workspace. But the important part right now is to show you how to combine resources, not to do it “the right way”.
So, go ahead and create a new TypeScript file called app-insights.ts, and add the required imports
import * as pulumi from "@pulumi/pulumi";
import * as azure from "@pulumi/azure-native";
import * as az from "@pulumi/azure";
Next, you need to “export” a new class called WorskpaceBasedApplicationInsights, inheriting from pulumi.ComponentResource
.
Note: pulumi.ComponentResource
is a base class specifically made for combining several resources into a reusable unit
The pulumi.ComponentResource
class has a constructor that accepts a “type name”, a name, an “arguments” object and a pulumi.ResourceOptions
. Basically a “type name” plus the 3 standard resource parameters So, the WorskpaceBasedApplicationInsights class declaration ends up looking like this
export class WorskpaceBasedApplicationInsights extends pulumi.ComponentResource {
constructor(private name: string, private args: any, opts?: pulumi.ResourceOptions) {
super("pulumidemo:components:WorskpaceBasedApplicationInsights", name, args, opts);
}
}
The constructor arguments are identical to the ones you would use for a built-in resource. So, using this “custom” resource will feel just like using any other resource.
The first parameter to the base constructor (super()
) is a “type name”. This is a string identifier for this specific type, which is used internally by Pulumi to keep track of the type.
Now that you have this new class defined, you can move the workspace and AI resource declarations inside the constructor.
export class WorskpaceBasedApplicationInsights extends pulumi.ComponentResource {
constructor(private name: string, private args: any, opts?: pulumi.ResourceOptions) {
super("pulumidemo:components:WorskpaceBasedApplicationInsights", name, args, opts);
const workspace = new azure.operationalinsights.Workspace(getName("laws"), {
resourceGroupName: resourceGroup.name
}, { parent: resourceGroup });
const ai = new az.appinsights.Insights(getName("ai"), {
resourceGroupName: resourceGroup.name,
workspaceId: workspace.id,
applicationType: "web",
}, { parent: resourceGroup });
}
}
Unfortunately, this move has some problems. For example that the resourceGroup
variable and getName()
function isn’t available. To sort this out, you need to use the args
argument, and get the required values passed into the constructor. However, the args
parameter is currently typed as any
, which isn’t that great, as it won’t tell the consumers of this class what properties they can pass in. This is easy to fix using an interface though
interface WorskpaceBasedApplicationInsightsArgs {
resourceGroupName: pulumi.Input<string>
workspaceName: string
insightsName: string
}
export class WorskpaceBasedApplicationInsights extends pulumi.ComponentResource {
constructor(..., private args: WorskpaceBasedApplicationInsightsArgs, ...) {
...
}
}
Note: Take note of the use of pulumi.Input<string>
for the resourceGroupName
property. This type allows you to either set the property using a string, or a pulumi.Output<string>
!
With the args
“typed”, you can update the resource definitions to use the passed in values as follows
export class WorskpaceBasedApplicationInsights extends pulumi.ComponentResource {
constructor(private name: string, private args: any, opts?: pulumi.ResourceOptions) {
super("pulumidemo:components:WorskpaceBasedApplicationInsights", name, args, opts);
const workspace = new azure.operationalinsights.Workspace(args.workspaceName, {
resourceGroupName: args.resourceGroupName
}, { parent: this });
const ai = new az.appinsights.Insights(args.insightsName, {
resourceGroupName: args.resourceGroupName,
workspaceId: workspace.id,
applicationType: "web",
}, { parent: this });
}
}
As you might see, the resource names and the resourceGroupName
property have all been updated to use the passed in arguments. On top of that, the parent
property has been set to this
, as the resources are now children of the WorskpaceBasedApplicationInsights
class and not the resource group.
There is only one thing left to do in this class, and that is to expose any properties that you might want to have access to when using this class. In this case, it is very likely that you will want to use the instrumentationKey
and connectionString
properties on the az.appinsights.Insights
instance.
There are 2 ways to expose these properties. One is to simply turn the ai
instance into a public field, which would allow anyone using this class to access these value, as well as any other value on the az.appinsights.Insights
instance. The other is to use a bit more encapsulation, and expose individual properties for the values. Like this
export class WorskpaceBasedApplicationInsights extends pulumi.ComponentResource {
private ai: az.appinsights.Insights;
constructor(private name: string, private args: WorskpaceBasedApplicationInsightsArgs, opts?: pulumi.ResourceOptions) {
...
this.ai = new az.appinsights.Insights(…);
}
get instrumentationKey() {
return this.ai.instrumentationKey;
}
get connectionString() {
return this.ai.connectionString;
}
}
Both options are definitely viable. To me, it depends on how many properties you want to expose. If it is only a few, then I think this approach is a bit cleaner. But, if you want to expose a lot of properties, simply exposing the instance might be easier.
Now that we have this new type defined, you can go back to the index.ts file, add an import it, and then create an instance of WorskpaceBasedApplicationInsights
using the following code
...
import { WorskpaceBasedApplicationInsights } from "./app-insights";
...
const ai = new WorskpaceBasedApplicationInsights(getName("ai"), {
resourceGroupName: resourceGroup.name,
workspaceName: getName("laws"),
insightsName: getName("ai")
}, { parent: resourceGroup });
That’s pretty nice in my opinion! Imagine all the nice semantics you can show by using this type of code! And how easily you can distribute standardized resources inside your company by adding them to your own, custom npm packages!
Cool, now you have the Application Insights stuff set up! But you still need to add some configuration to the web app to “connect” them. This is done by setting a couple of app settings.
However, before you go and do that, you need to make sure that the WorskpaceBasedApplicationInsights
instance is moved up above the creation of the web app. Otherwise you won’t be able to use it to set the values…
To set the web app’s app settings, you use the appSettings
property on the siteConfig
object. This property is declared as an array of objects containing name
and value
properties. So, to set an app setting, you need to use a syntax that looks like this
const web = new azure.web.WebApp(getName("web"), {
...,
siteConfig: {
...,
appSettings: [
{
name: "APP_SETTING_NAME",
value: "APP_SETTING_VALUE"
}
]
},
}, ...)
And once again, since you are using TypeScript, the editor should be able to help you to figure it out…
The settings needed for Application Insights are
- APPINSIGHTS_INSTRUMENTATIONKEY – The unique key generated by the Application Insights resource
- APPLICATIONINSIGHTS_CONNECTION_STRING – The connection string to the Application Insights resource
- ApplicationInsightsAgent_EXTENSION_VERSION – Set to
~3
for Linux and~2
for Windows - XDT_MicrosoftApplicationInsights_Mode – Set to
recommended
for…well…recommended data gathering
So, you need to set these settings using the following code
const web = new azure.web.WebApp(getName("web"), {
...,
siteConfig: {
...,
appSettings: [
{
name: "APPINSIGHTS_INSTRUMENTATIONKEY",
value: ai.instrumentationKey
},
{
name: "APPLICATIONINSIGHTS_CONNECTION_STRING",
value: ai.connectionString
},
{
name: "ApplicationInsightsAgent_EXTENSION_VERSION",
value: "~2"
},
{
name: "XDT_MicrosoftApplicationInsights_Mode",
value: "recommended"
}
]
},
}, ...)
That’s “all” there is to it!
At this point, the infrastructure is defined as needed. However, there are quite a few hard coded values that would make sense to have as configuration instead. This would allow us to re-use this code to create multiple environments with slightly different settings. It might for example be useful to have a smaller app service plan for dev/test, than for production. And with all the power of TypeScript at your fingertips, just imagine how much customization you could easily achieve by adding just some quite basic configuration.
Configuration using “stacks”
Pulumi comes with a built-in configuration system. It is based around the idea of a stack. A stack is basically a named set of configuration settings that you can use when deploying your project. Currently, you have a single stack called dev. This is represented by the Pulumi.dev.yaml file in the root of the application.
Right now, that stack contains a single property called azure-native:location
. This was set during project creation when you answered what default Azure location you wanted to use. You can see it by opening the Pulumi.dev.yaml file
encryptionsalt: v1:IykIQhHawfU=:v1:+I7tf853kbGI2RmW:iLKunFMrscgDDwEAV93fn3KwqRw0CA==
config:
azure-native:location: WestEurope
There are a couple of things to note in this file. First of all, the encryptionsalt
property. This contains the information needed when adding encrypted values to the config. Generally, you can ignore this, but now you at least know what it is.
Note: Secrets can be stored in other locations as well. Your Pulumi project can for example be set up to store them in Azure KeyVault instead. But the ability to just store them in the YAML-file, encrypted with a passphrase, is quite nice.
Next, you should note that all config keys are prefixed in some way. This allows 3rd party packages to define config keys in a way that doesn’t interfere with other packages, or your local keys. In the Pulumi.dev.yaml file, the location
key is prefixed with azure-native
, indicating that it is a configuration used by the @pulumi/azure_native
package. Any local config key is prefixed with the current project name.
To add a configuration setting, you can either add it to the YAML-file manually, or by using the Pulumi CLI. The upside to using the CLI, is that it will make sure to correctly prefix the key for you. And secondly, it allows you to easily add encrypted secrets by simply adding a --secret
parameter.
For this project, you need to add the following configuration settings
- appSvcPlanSkuSize - set to “F1”
- appSvcPlanSkuTier - set to “Free”
- sqlSize - set to “Basic”
- sqlUser - set to “server_admin”
- sqlPassword - set to your password of choice, and encrypted
To add these using the Pulumi CLI, you just need to run the following commands
> pulumi config set appSvcPlanSkuSize F1
> pulumi config set appSvcPlanSkuTier Free
> pulumi config set sqlSize Basic
> pulumi config set sqlUser server_admin
> pulumi config set sqlPassword P@ssw0rd1! --secret
If you open the Pulumi.dev.yaml file after running these commands, you will see something that looks similar to this
encryptionsalt: v1:IykIQhHawfU=:v1:+I7tf853kbGI2RmW:iLKunFMrscgDDwEAV93fn3KwqRw0CA==
config:
azure-native:location: WestEurope
pulumi-demo:appSvcPlanSkuSize: F1
pulumi-demo:appSvcPlanSkuTier: Free
pulumi-demo:sqlPassword:
secure: v1:Oql4A3yQJ1PJ7W+5:BBSCfpFLr+x8CkQEQqv5r7uASjECSK5f
pulumi-demo:sqlSize: Basic
pulumi-demo:sqlUser: server_admin
As you can see, all the config keys have been prefixed with the project name pulumi-demo
. And the sqlPassword
setting has been encrypted for you, since you used the --secret
parameter.
Now that the configuration has been set up, you just need to update the code to make use of it.
The first step is to create an instance of pulumi.Config()
, which is a class that allows you to easily get hold of the configuration values you need, by calling methods like require()
and get()
.
The require()
and get()
methods will assume the type to be string. For other types, there are corresponding methods, for example getNumer()
and requireNumber()
for numbers.
Note: Methods prefixed with require, for example requireNumber()
, will throw an exception if the value is missing, while the corresponding “get method”, getNumber()
, will return undefined if it is missing.
const config = new pulumi.Config();
The second step is to update all the places where the configuration values should be used
const sql = new azure.sql.Server(getName("sql"), {
...,
administratorLogin: config.require("sqlUser"),
administratorLoginPassword: config.requireSecret("sqlPassword")
}, ...);
const db = new azure.sql.Database(getName("db"), {
...,
sku: {
name: config.require("sqlSize")
},
}, ...);
const svcPlan = new azure.web.AppServicePlan(getName("plan"), {
...,
sku: {
name: config.require("appSvcPlanSkuName"),
tier: config.require("appSvcPlanSkuTier")
},
}, ...);
const web = new azure.web.WebApp(getName("web"), {
...,
siteConfig: {
...,
connectionStrings: [
{
...,
connectionString: `Data Source=tcp:${sql.fullyQualifiedDomainName},1433;Initial Catalog=${db.name};User Id=${config.require(sqlUser)};Password='${config.require(sqlPassword)}';`
}
]
},
}, ...)
Now that the configuration is in place, you can try to deploy the infrastructure by running
> pulumi up
Previewing update (dev):
Type Name Plan
pulumi:pulumi:Stack pulumi-demo-dev
└─ azure-native:resources:ResourceGroup PulumiDemo
+ ├─ pulumidemo:components:WorskpaceBasedApplicationInsights pulumi-demo-ai create
+ │ ├─ azure-native:operationalinsights:Workspace pulumi-demo-laws create
+ │ └─ azure:appinsights:Insights pulumi-demo-ai create
+ └─ azure-native:web:AppServicePlan pulumi-demo-plan create
+ └─ azure-native:web:WebApp pulumi-demo-web create
Resources:
+ 5 to create
5 unchanged
It’s worth noting the pulumidemo:components:WorskpaceBasedApplicationInsights
entry. This is your custom type, underneath which you can see the workspace and Application Insights resources.
I suggest selecting no to update the environment in this case, as there is a little piece missing…
It’s very common that we need to get hold of values generated by the IaC code from the outside, like for example the name of a SQL Server database, or the address to a web application. In Pulumi, this is solved by using output properties.
Output properties
When you need to expose values from the IaC code, you have to rely on JavaScript/TypeScript’s ability to “export” values from a module. You simple declare the value you want to export as an exported value. For example, if you want to expose the address of the created web app, you can simply add the following code at the end of the index.ts file
export const websiteAddress = web.defaultHostName
Once that is in place, you can run pulumi up
. And since we know that the code looks good, you can go ahead and add the -y
parameter, which will automatically approve the update without an interactive prompt.
pulumi up -y
Previewing update (dev):
...
Outputs:
+ websiteAddress: output<string>
Resources:
+ 5 to create
5 unchanged
Updating (dev):
Type Name Status
pulumi:pulumi:Stack pulumi-demo-dev
└─ azure-native:resources:ResourceGroup PulumiDemo
+ ├─ pulumidemo:components:WorskpaceBasedApplicationInsights pulumi-demo-ai created
+ │ ├─ azure-native:operationalinsights:Workspace pulumi-demo-laws created
+ │ └─ azure:appinsights:Insights pulumi-demo-ai created
+ └─ azure-native:web:AppServicePlan pulumi-demo-plan created
+ └─ azure-native:web:WebApp pulumi-demo-web created
Outputs:
+ websiteAddress: "pulumi-demo-dev-webd442b66a.azurewebsites.net"
Resources:
+ 5 created
5 unchanged
Ok, that looks like it worked. It seems like it created the new resources, and it output the new websiteAddress
output to the console. And if you have a look at the Azure portal, you should now have something that looks like this
Note: When running in a non-interactive environment, like for example in a deployment pipeline, the -y
parameter is very important to add. Without it, the pulumi up
command will just lock up waiting for input. Or simply just fail, because it is running in a non-interactive session.
However nice it is to have the website address written to the output, the goal is obviously to be able to get hold of the value programmatically. This is done using the pulumi stack output
command, which clearly indicates that the output is stored per stack. There are two ways to use this command. You either run it without any extra parameters, which will get you all the outputs like this
> pulumi stack output
Current stack outputs (1):
OUTPUT VALUE
websiteAddress pulumi-demo-dev-webd442b66a.azurewebsites.net
Sure, there is only a single output here. But if you had more than one, they would all show up when using this command.
Or, you can add the name of the output you want to get hold of as the last parameter, and get that specific value. Like this
> pulumi stack output websiteAddress
pulumi-demo-dev-webd442b66a.azurewebsites.net
However, the websiteAddress
output looks a little naked. It would be really nice if that included the “https://” prefix and “/” suffix…
To add the prefix and suffix to the address, you could use the apply()
method from pulumi.Output<string>
, as you did before. Or, you can use this other interpolation syntax that relies on a pretty cool JavaScript feature that allows you to extend JavaScript’s own string interpolation syntax.
The syntax for this way of performing interpolation looks like this
export const websiteAddress = pulumi.interpolate `https://${web.defaultHostName}/`
You simply prefix the native JavaScript “backtick interpolation” with pulumi.interpolate
, and then you use the pulumi.Output<string>
as if it was a regular string. Pulumi will then figure out the rest.
With that in place, you can re-deploy the environment, which is basically a no-op
> pulumi up -y
...
Outputs:
~ websiteAddress: "pulumi-demo-dev-web442b66a.azurewebsites.net" => "https://pulumi-demo-dev-webd442b66a.azurewebsites.net/"
...
If you browse to the output address, you should now end up at a website with the default “empty web app screen”.
Using multiple stacks
If you have multiple environments, you use multiple stacks to store the configuration for the different environments.
To create a new stack, you just need to run the following command
> pulumi stack init prod
where prod is the name of the stack.
This will create a new empty stack, which is represented by a new Pulumi.prod.yml file in the root of your project. It will also set this new stack as the “active” stack.
Note: By “active” stack, I mean that it is the stack that is used when running Pulumi commands.
However, since this stack is completely empty, you will need to set up the required configuration for the prod stack. There are 2 ways of doing this. Either you open the Plumi.dev.yml and copy the config values across, or you run the pulumi config set
commands again with this stack selected.
In this case, I suggest copying across the non-encrypted values like this
encryptionsalt: ...
config:
azure-native:location: WestEurope
pulumi-demo:appSvcPlanSkuName: F1
pulumi-demo:appSvcPlanSkuTier: Free
pulumi-demo:sqlSize: Basic
pulumi-demo:sqlUser: server_admin
Note: Do not copy across the encryptionsalt
, this should be left alone!
Once the non-encrypted values have been set up, you can set up the encrypted values by using the Pulumi CLI
> pulumi config set sqlPassword P@ssw0rd1! --secret
Sure, you probably want different configuration between the stacks, otherwise it doesn’t make sense to have multiple stacks. But once again…it is a demo…
It might also be a good idea to include the stack name in the resource names. Luckily, this is easily fixed by modifying the getName()
method, and updating the resource group name like this
function getName(type: string) {
return `${ pulumi.getProject().toLowerCase() }-${ pulumi.getStack() }-${ type }`;
}
const resourceGroup = new azure.resources.ResourceGroup("PulumiDemo", {
resourceGroupName: `PulumiDemo-${ pulumi.getStack() }`
});
As you can see, the names now include the stack name, which is retrieved using the pulumi.getStack()
method.
If you try running pulumi up
, with the prod stack selected, and select details, you should get the following output
+ pulumi:pulumi:Stack: (create)
[urn=urn:pulumi:prod::pulumi-demo::pulumi:pulumi:Stack::pulumi-demo-prod]
+ azure-native:resources:ResourceGroup: (create)
[urn=urn:pulumi:prod::pulumi-demo::azure-native:resources:ResourceGroup::PulumiDemo]
[provider=urn:pulumi:prod::pulumi-demo::pulumi:providers:azure-native::default_1_45_0::04da6b54-80e4-46f7-96ec-b56ff0331ba9]
location : "WestEurope"
resourceGroupName: "PulumiDemo-prod"
+ azure-native:sql:Server: (create)
[urn=urn:pulumi:prod::pulumi-demo::azure-native:resources:ResourceGroup$azure-native:sql:Server::pulumi-demo-prod-sql]
[provider=urn:pulumi:prod::pulumi-demo::pulumi:providers:azure-native::default_1_45_0::04da6b54-80e4-46f7-96ec-b56ff0331ba9]
administratorLogin : "server_admin"
administratorLoginPassword: "[secret]"
location : "WestEurope"
resourceGroupName : output<string>
serverName : "pulumi-demo-prod-sql765429b5"
Here you can see that the stack name is being added to the names. For example, the serverName is now set to pulumi-demo-prod-sql765429b5
, just as we wanted. But you can also see that the configuration values are being picked up properly, with the administratorLoginPassword set to [secret]
, as it is a secret, encrypted value that should not be output in any logs.
There is no reason to deploy this stack, so I suggest selecting no, when asked if you want to update the deployment.
You can try switching back to the dev stack using the following command
> pulumi stack select dev
Once you have selected the dev stack, you can perform a preview to see what changes Pulumi think it needs to perform
pulumi preview
Previewing update (dev):
Type Name Plan Info
pulumi:pulumi:Stack pulumi-demo-dev
└─ azure-native:resources:ResourceGroup PulumiDemo
+ ├─ pulumidemo:components:WorskpaceBasedApplicationInsights pulumi-demo-dev-ai create
+ │ ├─ azure-native:operationalinsights:Workspace pulumi-demo-dev-laws create
+ │ └─ azure:appinsights:Insights pulumi-demo-dev-ai create
+ ├─ azure-native:web:AppServicePlan pulumi-demo-dev-plan create
+ │ └─ azure-native:web:WebApp pulumi-demo-dev-web create
+ ├─ azure-native:sql:Server pulumi-demo-dev-sql create
+- │ ├─ azure-native:sql:FirewallRule allowAllAzureIps replace [diff: ~serverName]
+ │ └─ azure-native:sql:Database pulumi-demo-dev-db create
- ├─ azure-native:web:AppServicePlan pulumi-demo-plan delete
- │ └─ azure-native:web:WebApp pulumi-demo-web delete
- ├─ azure-native:sql:Server pulumi-demo-sql delete
- │ └─ azure-native:sql:Database pulumi-demo-db delete
- └─ pulumidemo:components:WorskpaceBasedApplicationInsights pulumi-demo-ai delete
- ├─ azure:appinsights:Insights pulumi-demo-ai delete
- └─ azure-native:operationalinsights:Workspace pulumi-demo-laws delete
Outputs:
~ websiteAddress: "https://pulumi-demo-webd442b66a.azurewebsites.net/" => output<string>
Resources:
+ 7 to create
- 7 to delete
+-1 to replace
15 changes. 2 unchanged
As you can see, Pulumi thinks that all the resources have to be recreated. This is because you have changed the names, which causes Pulumi to find a bunch of resources that aren’t available anymore, and a bunch of new ones, that it hasn’t seen before…
There is no need to run this update, so select no to the update question.
The last step is to remove any resource that you have created. Luckily, this is as simple as running
> pulumi destroy
and selecting yes.
Your Azure subscription should now be back to the state it was before you started!
Conclusion
Pulumi is definitely a different way of doing IaC, than what you have seen in the previous posts. Instead of using a DSL, you get a full programming language to work with. This gives us a ton more flexibility when defining our environment. Couple that with a built-in configuration system that allows you to configure the code using different stacks, and you have a very powerful tool to put in your toolbelt!
I personally find Pulumi to be very interesting way of doing IaC. And as a person with a development background, it feels very natural to me. However, I do completely understand that people with a more operations inspired background might find it a bit awkward and weird. And in those cases, a DSL might feel a bit more natural. Having that said, a lot of operations people are quite comfortable using for example PowerShell and Bash scripts. So, I don’t think it is impossible for people with a background like that to like Pulumi.
One thing that I’m not too thrilled about, when it comes to Pulumi, is the state management. Pulumi only uses with its own state when evaluating the desired state, which means that only properties explicitly defined in your code will be updated during a deployment. This unfortunately means that there could be quite a bit of configuration drift in your environment without Pulumi reversing it. ARM/Bicep solves this by not storing any “local” state at all, and instead looks at the actual environment. Terraform on the other hand, has the ability to combine its own state with the actual environment state before trying to figure out what has changed. This allows it to reset, not only the explicitly defined properties, but also the ones left as defaults, making the risk for configuration drift less.
Having that said, I still find the Pulumi paradigm really cool, and comfortable to work with. So even if I find the state handling slightly flawed, I still find it a very good option when looking for an IaC solution.
The next part, “Conclusion”, contains my final thoughts on these technologies. It takes a look at them side-by-side, in an effort to highlight the pros and cons of them.
Feel free to reach out and give feedback or ask questions! I’m available on Twitter @ZeroKoll.