Azure Functions with NextGen
ADD
azure-nextgen-ts-functions/Pulumi.yaml
+name: azure-nextgen-ts-functions
+runtime: nodejs
+description: Creates and deploys an Azure Function App
+template:
+ config:
+ location:
+ description: Azure region to deploy resources to
+ default: WestUS

ADD
azure-nextgen-ts-functions/README.md
+[![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new)
+
+# Deploying Azure Functions
+
+Starting point for building serverless applications hosted in Azure Functions.
+
+## Running the App
+
+1. Create a new stack:
+
+ ```
+ $ pulumi stack init dev
+ ```
+
+1. Login to Azure CLI (you will be prompted to do this during deployment if you forget this step):
+
+ ```
+ $ az login
+ ```
+
+1. Restore NPM dependencies:
+
+ ```
+ $ npm install
+ ```
+
+1. Set the Azure region location to use:
+
+ ```
+ $ pulumi config set location westus2
+ ```
+
+1. Run `pulumi up` to preview and deploy changes:
+
+ ```
+ $ pulumi up
+ Previewing changes:
+ ...
+
+ Performing changes:
+ ...
+ Resources:
+ + 8 created
+
+ Duration: 1m18s
+ ```
+
+1. Check the deployed endpoint:
+
+ ```
+ $ pulumi stack output endpoint
+ https://appg-fsprfojnnlr.azurewebsites.net/api/HelloNode?name=Pulumi
+ $ curl "$(pulumi stack output endpoint)"
+ Hello from Node.js, Pulumi
+ ```

ADD
azure-nextgen-ts-functions/helpers.ts
+// Copyright 2016-2021, Pulumi Corporation. All rights reserved.
+
+import * as azure from "@pulumi/azure";
+import * as storage from "@pulumi/azure-nextgen/storage/latest";
+import * as pulumi from "@pulumi/pulumi";
+
+export function getConnectionString(resourceGroupName: pulumi.Input<string>, accountName: pulumi.Input<string>): pulumi.Output<string> {
+ // Retrieve the primary storage account key.
+ const storageAccountKeys = pulumi.all([resourceGroupName, accountName]).apply(([resourceGroupName, accountName]) =>
+ storage.listStorageAccountKeys({ resourceGroupName, accountName }));
+ const primaryStorageKey = storageAccountKeys.keys[0].value;
+
+ // Build the connection string to the storage account.
+ return pulumi.interpolate`DefaultEndpointsProtocol=https;AccountName=${accountName};AccountKey=${primaryStorageKey}`;
+}
+
+export function signedBlobReadUrl(resourceGroupName: pulumi.Input<string>, accountName: pulumi.Input<string>, containerName: pulumi.Input<string>, blobName: pulumi.Input<string>): pulumi.Output<string> {
+ const primaryConnectionString = getConnectionString(resourceGroupName, accountName);
+
+ // Choose a fixed, far-future expiration date for signed blob URLs. The shared access signature
+ // (SAS) we generate for the Azure storage blob must remain valid for as long as the Function
+ // App is deployed, since new instances will download the code on startup. By using a fixed
+ // date, rather than (e.g.) "today plus ten years", the signing operation is idempotent.
+ const signatureExpiration = "2100-01-01";
+
+ return pulumi.all([accountName, primaryConnectionString, containerName, blobName]).apply(
+ async ([accountName, connectionString, containerName, blobName]) => {
+ const sas = await azure.storage.getAccountBlobContainerSAS({
+ connectionString,
+ containerName,
+ start: "2019-01-01",
+ expiry: signatureExpiration,
+ permissions: {
+ read: true,
+ write: false,
+ delete: false,
+ list: false,
+ add: false,
+ create: false,
+ },
+ }, { async: true });
+
+ return `https://${accountName}.blob.core.windows.net/${containerName}/${blobName}${sas.sas}`;
+ });
+}

ADD
azure-nextgen-ts-functions/index.ts
+// Copyright 2016-2021, Pulumi Corporation. All rights reserved.
+
+import * as azure from "@pulumi/azure";
+import * as pulumi from "@pulumi/pulumi";
+import * as random from "@pulumi/random";
+
+import * as resources from "@pulumi/azure-nextgen/resources/latest";
+import * as storage from "@pulumi/azure-nextgen/storage/latest";
+import * as web from "@pulumi/azure-nextgen/web/v20200601";
+
+import { getConnectionString, signedBlobReadUrl } from "./helpers";
+
+const config = new pulumi.Config();
+const location = config.get("location") || "WestUS";
+
+// Create a separate resource group for this example.
+const resourceGroup = new resources.ResourceGroup("rg", {
+ resourceGroupName: "functions-rg",
+ location: location,
+});
+
+// Storage Account and App Service names have to be globally unique.
+// Generate a random suffix for those names.
+const suffix = new random.RandomString("suffix", {
+ length: 12,
+ number: false,
+ special: false,
+ upper: false,
+}).result;
+
+// Storage account is required by Function App.
+// Also, we will upload the function code to the same storage account.
+const storageAccount = new storage.StorageAccount("sa", {
+ resourceGroupName: resourceGroup.name,
+ accountName: pulumi.interpolate`sa${suffix}`,
+ location: resourceGroup.location,
+ sku: {
+ name: storage.SkuName.Standard_LRS,
+ },
+ kind: storage.Kind.StorageV2,
+});
+
+// Function code archives will be stored in this container.
+const codeContainer = new storage.BlobContainer("container", {
+ resourceGroupName: resourceGroup.name,
+ accountName: storageAccount.name,
+ containerName: "zips",
+});
+
+// Upload Azure Function's code as a zip archive to the storage account.
+// Note that we use the "old" provider for this: see https://github.com/pulumi/pulumi-azure-nextgen/issues/13
+const codeBlob = new azure.storage.Blob("zip", {
+ storageAccountName: storageAccount.name,
+ storageContainerName: codeContainer.name,
+ type: "Block",
+ source: new pulumi.asset.FileArchive("./javascript"),
+});
+
+// Define a Consumption Plan for the Function App.
+// You can change the SKU to Premium or App Service Plan if needed.
+const plan = new web.AppServicePlan("plan", {
+ resourceGroupName: resourceGroup.name,
+ name: "plan",
+ location: resourceGroup.location,
+ sku: {
+ name: "Y1",
+ tier: "Dynamic",
+ },
+});
+
+// Build the connection string and zip archive's SAS URL. They will go to Function App's settings.
+const storageConnectionString = getConnectionString(resourceGroup.name, storageAccount.name);
+const codeBlobUrl = signedBlobReadUrl(resourceGroup.name, storageAccount.name, codeContainer.name, codeBlob.name);
+
+const app = new web.WebApp("fa", {
+ resourceGroupName: resourceGroup.name,
+ name: pulumi.interpolate`app-${suffix}`,
+ location: resourceGroup.location,
+ serverFarmId: plan.id,
+ kind: "functionapp",
+ siteConfig: {
+ appSettings: [
+ { name: "AzureWebJobsStorage", value: storageConnectionString },
+ { name: "FUNCTIONS_EXTENSION_VERSION", value: "~3" },
+ { name: "FUNCTIONS_WORKER_RUNTIME", value: "node" },
+ { name: "WEBSITE_NODE_DEFAULT_VERSION", value: "~14" },
+ { name: "WEBSITE_RUN_FROM_PACKAGE", value: codeBlobUrl },
+ ],
+ http20Enabled: true,
+ nodeVersion: "~14",
+ },
+});
+
+export const endpoint = pulumi.interpolate`https://${app.defaultHostName}/api/HelloNode?name=Pulumi`;

ADD
azure-nextgen-ts-functions/javascript/HelloNode/function.json
+{
+ "bindings": [
+ {
+ "authLevel": "anonymous",
+ "type": "httpTrigger",
+ "direction": "in",
+ "name": "req",
+ "methods": [
+ "get",
+ "post"
+ ]
+ },
+ {
+ "type": "http",
+ "direction": "out",
+ "name": "res"
+ }
+ ]
+}

ADD
azure-nextgen-ts-functions/javascript/HelloNode/index.js
+module.exports = async function (context, req) {
+ context.log('JavaScript HTTP trigger function processed a request.');
+
+ if (req.query.name || (req.body && req.body.name)) {
+ context.res = {
+ // status: 200, /* Defaults to 200 */
+ body: "Hello from Node.js, " + (req.query.name || req.body.name)
+ };
+ }
+ else {
+ context.res = {
+ status: 400,
+ body: "Please pass a name on the query string or in the request body"
+ };
+ }
+};

ADD
azure-nextgen-ts-functions/javascript/host.json
+{
+ "version": "2.0"
+}

ADD
azure-nextgen-ts-functions/javascript/package.json
+{
+ "name": "hello-node",
+ "version": "0.1.0"
+}

ADD
azure-nextgen-ts-functions/package.json
+{
+ "name": "azure-nextgen-ts-functions",
+ "version": "1.0.0",
+ "devDependencies": {
+ "@types/node": "^8.0.0"
+ },
+ "dependencies": {
+ "@pulumi/azure": "^3.0.0",
+ "@pulumi/azure-nextgen": "^0.4.0",
+ "@pulumi/pulumi": "^2.0.0",
+ "@pulumi/random": "^2.0.0"
+ }
+}

ADD
azure-nextgen-ts-functions/tsconfig.json
+{
+ "compilerOptions": {
+ "outDir": "bin",
+ "target": "es2016",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "sourceMap": true,
+ "experimentalDecorators": true,
+ "pretty": true,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitAny": true,
+ "noImplicitReturns": true,
+ "forceConsistentCasingInFileNames": true,
+ "strictNullChecks": true
+ },
+ "files": [
+ "helpers.ts",
+ "index.ts"
+ ]
+}

Azure Functions with NextGen (#871)