Skip to content

Commit

Permalink
Azure Functions with NextGen (pulumi#871)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikhailshilkov committed Jan 7, 2021
1 parent b0f3fbb commit 5e34fff
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 0 deletions.
8 changes: 8 additions & 0 deletions azure-nextgen-ts-functions/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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
55 changes: 55 additions & 0 deletions azure-nextgen-ts-functions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
[![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
```
45 changes: 45 additions & 0 deletions azure-nextgen-ts-functions/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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}`;
});
}
94 changes: 94 additions & 0 deletions azure-nextgen-ts-functions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// 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`;
19 changes: 19 additions & 0 deletions azure-nextgen-ts-functions/javascript/HelloNode/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}
16 changes: 16 additions & 0 deletions azure-nextgen-ts-functions/javascript/HelloNode/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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"
};
}
};
3 changes: 3 additions & 0 deletions azure-nextgen-ts-functions/javascript/host.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"version": "2.0"
}
4 changes: 4 additions & 0 deletions azure-nextgen-ts-functions/javascript/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "hello-node",
"version": "0.1.0"
}
13 changes: 13 additions & 0 deletions azure-nextgen-ts-functions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"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"
}
}
20 changes: 20 additions & 0 deletions azure-nextgen-ts-functions/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"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"
]
}

0 comments on commit 5e34fff

Please sign in to comment.