Skip to content

Commit

Permalink
Add Azure functions example in Typescript
Browse files Browse the repository at this point in the history
  • Loading branch information
mmdriley committed Jun 13, 2018
1 parent 40808c3 commit b503c64
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 0 deletions.
2 changes: 2 additions & 0 deletions azure-ts-functions/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
name: azure-functions
runtime: nodejs
53 changes: 53 additions & 0 deletions azure-ts-functions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Azure Functions

An example Pulumi component that deploys a Typescript function to Azure Functions.

## Running the App

1. Create a new stack:

```
$ pulumi stack init azure-fn
```

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. Build the Typescript project:

```
$ npm run build
```

1. Run `pulumi update` to preview and deploy changes:

```
$ pulumi update
Previewing changes:
...
Performing changes:
...
info: 9 changes performed:
+ 9 resources created
Update duration: 1m20.493392283s
```

1. Check the deployed function endpoint:

```
$ pulumi stack output endpoint
https://fn-app051a4f8b.azurewebsites.net/api/fn
$ curl "$(pulumi stack output endpoint)"
Greetings from Azure Functions!
...
```
124 changes: 124 additions & 0 deletions azure-ts-functions/azureFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import * as pulumi from "@pulumi/pulumi";
import * as azure from "@pulumi/azure";

import * as azurefunctions from "azure-functions-ts-essentials";

import { signedBlobReadUrl } from "./sas";

export type Context = azurefunctions.Context;
export type HttpRequest = azurefunctions.HttpRequest;
export * from "azure-functions-ts-essentials";

export type AzureFunctionHandler = (context: Context, request: HttpRequest) => void;


async function blobContent(name: string, handler: AzureFunctionHandler): Promise<pulumi.asset.AssetMap> {
let serializedHandler = await pulumi.runtime.serializeFunction(handler);
let map: pulumi.asset.AssetMap = {};

map["host.json"] = new pulumi.asset.StringAsset(JSON.stringify({}));
map[`${name}/function.json`] = new pulumi.asset.StringAsset(JSON.stringify({
"disabled": false,
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req"
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}));
map[`${name}/index.js`] = new pulumi.asset.StringAsset(`module.exports = require("./handler").handler`),
map[`${name}/handler.js`] = new pulumi.asset.StringAsset(serializedHandler.text);

return map;
}


// An Azure function exposed via an HTTP endpoint.

export class HttpFunction extends pulumi.ComponentResource {
readonly resourceGroup: azure.core.ResourceGroup;

readonly storageAccount: azure.storage.Account;
readonly storageContainer: azure.storage.Container;
readonly blob: azure.storage.ZipBlob;

readonly appServicePlan: azure.appservice.Plan;
readonly functionApp: azure.appservice.FunctionApp;

readonly codeBlobUrl: pulumi.Output<string>;
readonly endpoint: pulumi.Output<string>;

constructor(name: string, handler: AzureFunctionHandler, options?: pulumi.ResourceOptions) {
super("azure:HttpFunction", name, {}, options);

let parentArgs = { parent: this };

this.resourceGroup = new azure.core.ResourceGroup(`${name}-rg`, {
location: "West US 2",
}, parentArgs);

let resourceGroupArgs = {
resourceGroupName: this.resourceGroup.name,
location: this.resourceGroup.location,
};

this.storageAccount = new azure.storage.Account(`${name}sa`, {
...resourceGroupArgs,

accountKind: "StorageV2",
accountTier: "Standard",
accountReplicationType: "LRS",
}, parentArgs);

this.storageContainer = new azure.storage.Container(`${name}-c`, {
resourceGroupName: this.resourceGroup.name,
storageAccountName: this.storageAccount.name,
containerAccessType: "private",
}, parentArgs);

this.blob = new azure.storage.ZipBlob(`${name}-b`, {
resourceGroupName: this.resourceGroup.name,
storageAccountName: this.storageAccount.name,
storageContainerName: this.storageContainer.name,
type: "block",

content: new pulumi.asset.AssetArchive(blobContent(name, handler)),
}, parentArgs);

this.codeBlobUrl = signedBlobReadUrl(this.blob, this.storageAccount, this.storageContainer);

this.appServicePlan = new azure.appservice.Plan(`${name}-p`, {
...resourceGroupArgs,

kind: "FunctionApp",

// https://social.msdn.microsoft.com/Forums/azure/en-US/665c365d-2b86-4a77-8cea-72ccffef216c
sku: {
tier: "Dynamic",
size: "Y1",
},
}, parentArgs);

this.functionApp = new azure.appservice.FunctionApp(`${name}-app`, {
...resourceGroupArgs,

appServicePlanId: this.appServicePlan.id,
storageConnectionString: this.storageAccount.primaryConnectionString,

appSettings: {
"WEBSITE_RUN_FROM_ZIP": this.codeBlobUrl,
},
}, parentArgs);

this.endpoint = this.functionApp.defaultHostname.apply(h => {
return `https://${h}/api/${name}`;
});
}
};
24 changes: 24 additions & 0 deletions azure-ts-functions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as azureFunction from "./azureFunction";

// Create an Azure function that prints a message and the request headers.

function handler(context: azureFunction.Context, request: azureFunction.HttpRequest) {
let body = "";
let headers = context.req!.headers!;
for (let h in headers) {
body = body + `${h} = ${headers[h]}\n`;
}

let res: azureFunction.HttpResponse = {
status: azureFunction.HttpStatusCode.OK,
headers: {
"content-type": "text/plain",
},
body: `Greetings from Azure Functions!\n\n===\n\n${body}`,
};

context.done(undefined, res);
}

let fn = new azureFunction.HttpFunction("fn", handler);
export let endpoint = fn.endpoint;
19 changes: 19 additions & 0 deletions azure-ts-functions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "azure-ts-functions",
"version": "1.0.0",
"main": "bin/index.js",
"typings": "bin/index.d.ts",
"scripts": {
"build": "tsc"
},
"devDependencies": {
"@types/node": "^10.3.3",
"azure-functions-ts-essentials": "^1.3.2",
"typescript": "^2.9.2"
},
"dependencies": {
"@pulumi/azure": "^0.14.0",
"@pulumi/pulumi": "^0.14.0",
"azure-storage": "^2.9.0-preview"
}
}
37 changes: 37 additions & 0 deletions azure-ts-functions/sas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as pulumi from "@pulumi/pulumi";
import * as azure from "@pulumi/azure";

import * as azurestorage from "azure-storage";

// Given an Azure blob, create a SAS URL that can read it.
export function signedBlobReadUrl(
blob: azure.storage.Blob | azure.storage.ZipBlob,
account: azure.storage.Account,
container: azure.storage.Container,
): pulumi.Output<string> {
// 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 = new Date(2100, 1);

return pulumi.all([
account.primaryConnectionString,
container.name,
blob.name,
]).apply(([connectionString, containerName, blobName]) => {
let blobService = new azurestorage.BlobService(connectionString);
let signature = blobService.generateSharedAccessSignature(
containerName,
blobName,
{
AccessPolicy: {
Expiry: signatureExpiration,
Permissions: azurestorage.BlobUtilities.SharedAccessPermissions.READ,
},
}
);

return blobService.getUrl(containerName, blobName, signature);
});
}
26 changes: 26 additions & 0 deletions azure-ts-functions/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"outDir": "bin",
"target": "es6",
"lib": [
"es6"
],
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"sourceMap": true,
"stripInternal": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true
},
"files": [
"azureFunction.ts",
"index.ts",
"sas.ts",
]
}

0 comments on commit b503c64

Please sign in to comment.