" width="1000" style="--opacity:1">
Sam Magura
Pulumi is an Infrastructure as Code (IaC) platform that enables you to deploy to AWS, Azure, and Google Cloud Platform. The Pulumi toolchain is vendor agnostic, so you won't have to relearn any core Pulumi concepts if you decide to switch to a different cloud provider.
Pulumi is similar to Terraform in many ways, but with one key difference. While Terraform is typically written in a domain-specific language called HCL , Pulumi allows you to define your infrastructure using a programming language you are already familiar with. Pulumi currently supports JavaScript, TypeScript, Python, Go, C#, Java, and YAML. Click here to see an example of how Pulumi works in each of these languages.
Not having to learn yet another language is a huge convenience factor. Having the full power of a general purpose programming language at your disposal also makes it much easier to add logic into your infrastructure templates. For example, you might want a specific resource to exist only in the production system, not in the staging system. With Pulumi, implementing this is as simple as writing an if
statement.
Zero is a modern secrets manager built with usability at its core. Reliable and secure, it saves time and effort.
This article will walk you through using Pulumi to deploy an Azure Functions project to Microsoft Azure. We'll store both the Pulumi and Azure credentials in the Zero secrets manager. The credentials will be retrieved from Zero in our deployment script, and then Pulumi will handle the rest.
Both the Pulumi code and the Azure Functions project will be written in TypeScript. For more background on Azure Functions, check out my previous blog post where I showed how to call the Twilio SMS API from an Azure Function App.
🔗 The full code for this example is available in the zerosecrets/examples GitHub repository.
Here's what you'll need to get started:
az login
to authenticate.To bootstrap a new Pulumi project, create a new directory called pulumi-azure-functions
and cd
into it. Then run
You'll be prompted to log in to Pulumi and then asked a few questions about your project. While we eventually want to authenticate with Pulumi via an access token that is stored in Zero, for now, it's easiest to just log in via the web.
Pulumi will populate your directory with a package.json
, a Pulumi.yaml
, and an index.ts
, among other files. index.ts
is where you'll define the Azure resources needed to deploy your project. The azure-typescript
template defines a resource group and a storage account:
If you like, you can run pulumi up
to test that everything is working. The resource group and storage account won't do anything interesting on their own, but we will need them later for our Azure Function App.
Now let's create an Azure Functions Project as a subdirectory inside pulumi-azure-functions
. In the pulumi-azure-functions
directory, run
and select Node and TypeScript at the prompts. Then cd
into MyFunctionProject
and run npm install
.
The Functions project is initially empty, so run
to scaffold a new function, and select "HTTP trigger" for the type of function. Azure Functions supports a wide variety of triggers — we're using an HTTP-triggered function simply because it is the easiest to test with. For my function, I used the default name of HttpTrigger
.
Now you can run
to run the Functions project locally. Go to http://localhost:7071/api/HttpTrigger in your browser and you'll see a "hello world" message from your function.
Now let's get the Functions project deployed to Azure. To do this, we'll edit the index.ts
file to tell Pulumi about the Azure resources that make up our application. The following code is based on this example provided by Pulumi.
The Functions project will be deployed as a zip file, so we'll need a storage container to house the zip:
Then tell Pulumi to upload the MyFunctionProject
archive to the storage container:
Next, define the App Service Plan that will host our Function App. We'll use a Consumption Plan, which is the true serverless option for Azure Functions where you only pay for what you use.
The next part is the most complicated. We need to provide the Function App with the storage account connection string and the URL for our code zip file as app settings. These values can't be hardcoded since they depend on things like the unique name of our storage account, which is generated by Pulumi when you deploy. We'll write the code to get the connection string and blob URL in a new file called helpers.ts
— click that link to see the full source of this file.
With the helpers in place, we can define the Azure Function App:
As a convenience, we'll have Pulumi output the URL of our HTTP-triggered function:
Now that our infrastructure is defined, let's test it out. First, compile the Functions project from TypeScript to plain JavaScript:
Then return to the pulumi-azure-functions
directory and run
to deploy to Azure. It really is as simple as that.
Our function is configured with function-level authorization by default, so you'll get an HTTP 401 error if you attempt to visit https://<HOST_NAME>/api/HttpTrigger
in your browser. The easiest way to make an authorized request to the function is to copy the function key from Azure Portal and include that in the URL.
In Azure Portal, navigate to your Function App and select Functions > HttpTrigger. Then click "Function keys" in the menu on the left and copy the function key.
Now go to
in your web browser and you'll see a successful response from the function!
Our end goal is to deploy the Functions project from a continuous integration system, using Zero to fetch both the Pulumi and Azure credentials. To make this happen, we'll need a deployment script that calls Zero and then passes the secrets returned by Zero to Pulumi. The deployment script should also build the Functions project, so that you never accidentally deploy an old version of the code if you forget to run npm run build
before the deployment.
I'll be writing my deployment script as a Node.js script using zx , though you could implement the same functionality as a series of steps in the YAML file that defines your CI workflow. I like implementing the deployment process as a script since you can run the script locally while you're actively working on it. This saves a ton of time because you don't have to wait 10 minutes for your CI workflow to run each time you make a change.
Once the deployment script is complete, you can integrate it into your CI workflow. Since the script only depends on Node.js and zx, you can easily run it from any CI platform, whether that be GitHub Actions, Azure Pipelines, or CircleCI.
Let's write a simple initial version of the deployment script that just builds the Functions project and runs pulumi up
. To get started with zx , simply install the zx
npm package. It can either be installed globally or as a dev dependency of your project. I'll add zx
as a dev dependency of my project to keep everything neatly encapsulated:
💡 The package should be installed in the
pulumi-azure-functions
directory, not theMyFunctionProject
directory.
Then add a package.json
script:
pulumi-azure-functions/deploy.mjs
is where our code will go. For this initial version, we simply build the the Functions project and then run pulumi up
:
The $
syntax shown here is a feature of zx which makes it super convenient to run shell commands from your Node.js script.
To test the deployment script, simply run
To get a Pulumi access token, log in to your account at https://app.pulumi.com/ . Click your profile picture in the upper right corner, and select "Personal access tokens". Create a new token and copy it into the Zero secrets manager as shown here:
To grant Pulumi access to our Azure subscription, we need to create a Service Principal in Azure. You can create one by following the steps below. Refer to this page in the official Azure docs for more details.
Now, create a new Azure secret in your Zero project. The Azure secret should have the following fields:
CLIENT_ID
— From the App Registration. Also known as the Application ID.CLIENT_SECRET
— From the App Registration.TENANT_ID
— Your Azure Active Directory tenant ID. Can be copied from the Overview page of the App Registration.SUBSCRIPTION_ID
— To find your subscription's ID, search for "Subscriptions" in the main search bar of Azure Portal.After creating the secret in Zero, the next step is to grant the Service Principal access to create and modify resources within the Resource Group that contains your Function App.
With our secrets stored securely in Zero, it's time to update the deployment script to retrieve the secrets from Zero and then pass them to the Pulumi CLI. First, install the Zero JavaScript SDK:
Then, in deploy.mjs
, import the Zero SDK and pull down the Azure and Pulumi secrets:
Then use pulumi config set
to tell Pulumi to use our Service Principal to authenticate with Azure:
See this page in the Pulumi docs for more information about this step.
Finally, we set the PULUMI_ACCESS_TOKEN
environment variable so that Pulumi has our personal access token:
ℹ️ Before you test the updated deployment script, make sure to log out of both the Pulumi and Azure CLIs:
12shellYour CI system won't be logged into either CLI, so we need to log out locally to test the script properly.
The deployment script can be run the same way as before, but now you'll need to pass your Zero token in as an environment variable:
If everything is configured properly, Pulumi will update your Azure resources and your Function App code to match what you have locally!
While all of the Azure resources used in this walkthrough are virtually free, it's still a best practice to delete the resources when you are done so that there are not any unexpected charges. To delete all of the Azure resources, simply run
You should log in to the Pulumi CLI again if you ran pulumi logout
earlier.
This guide showed you how to deploy a serverless Azure Functions application to the cloud, using Pulumi for Infrastructure as Code and Zero for secure secrets storage.
If you are building a production application using these tools, the next step would be to run the deployment script as part of your continuous integration workflow. This should be straightforward regardless of which CI platform you are using. That said, make sure that you give the CI workflow access to your Zero token in a secure way. Instead of including the Zero token in the workflow's YAML file, add it as a secret environment variable. If using GitHub Actions, for example, you would add the Zero token in the "Secrets and variables" section of your repository's settings page. The exact terminology used will vary between CI platforms.
Happy coding!
cdk8s is a command-line tool that enables you to create a Kubernetes manifest using a general purpose programming language. In this post, we'll use cdk8s to deploy the nginx web server to DigitalOcean Kubernetes.
Does your app need to send text alerts to users? In this article, we'll show you how to send SMS text messages progammatically using Twilio's API.
Zero is a modern secrets manager built with usability at its core. Reliable and secure, it saves time and effort.