Mutli-region Functions and Cosmos sample
Multi-region deployment of Azure Functions and Cosmos DB with Traffic Manager
ADD
azure-ts-serverless-url-shortener-global/Pulumi.yaml
+name: azure-ts-serverless-url-shorterner-global
+runtime: nodejs
+description: Multi-region deployment of Azure Functions and Cosmos DB with Traffic Manager
+template:
+ config:
+ azure:environment:
+ description: The Azure environment to use (`public`, `usgovernment`, `german`, `china`)
+ default: public
+ locations:
+ description: Comma-separated list of location aliases
+ default: westus,westeurope

ADD
azure-ts-serverless-url-shortener-global/README.md
+[![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new)
+
+# Globally distributed serverless URL-shortener
+
+Multi-region deployment of Azure Functions and Cosmos DB with Traffic Manager
+
+## 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. Specify the Azure regions to deploy the application:
+
+ ```
+ $ pulumi config set locations westus,westeurope
+ ```
+
+1. Run `pulumi up` to preview and deploy changes:
+
+ ```
+ $ pulumi up
+ Previewing changes:
+ ...
+
+ Performing changes:
+ ...
+ info: 23 changes performed:
+ + 23 resources created
+ Update duration: 21m33.3252322s
+ ```
+
+1. Add a short URL:
+
+ ```
+ $ pulumi stack output addEndpoint
+ https://urlshort-add94ac80f8.azurewebsites.net/api/urlshort-add
+ $ curl -H "Content-Type: application/json" \
+ --request POST \
+ -D '{"id":"pulumi","url":"https://pulumi.io"}' \
+ "$(pulumi stack output addEndpoint)"
+ Short URL saved
+ ```
+
+1. Query a short URL:
+
+ ```
+ $ pulumi stack output endpoint
+ http://urlshort-tm.trafficmanager.net/api/{key}
+ $ curl http://urlshort-tm.trafficmanager.net/api/pulumi
+ <!doctype html>
+ <html lang="en-US" prefix="og: http://ogp.me/ns#">
+ <head>
+ <title>
+ Pulumi
+ </title>
+ ...
+ ```

ADD
azure-ts-serverless-url-shortener-global/cosmosclient.ts
+import * as cosmos from "@azure/cosmos";
+
+export async function getContainer(endpoint: string, masterKey: string, region: string) {
+ let connectionPolicy = new cosmos.ConnectionPolicy();
+ connectionPolicy.PreferredLocations = [region];
+
+ const client = new cosmos.CosmosClient({
+ endpoint,
+ auth: { masterKey: masterKey },
+ connectionPolicy,
+ });
+
+ const { database: db } = await client.databases.createIfNotExists({ id: "thedb" });
+ const { container } = await db.containers.createIfNotExists({ id: "urls" });
+ return container;
+}

ADD
azure-ts-serverless-url-shortener-global/index.ts
+import * as pulumi from "@pulumi/pulumi";
+import * as azure from "@pulumi/azure";
+import { Container } from "@azure/cosmos";
+import { getContainer } from "./cosmosclient";
+
+const config = new pulumi.Config();
+// Read a list of target locations from the config file:
+// Expecting a comma-separated list, e.g., "westus,eastus,westeurope"
+const locations = config.require("locations").split(',');
+// The first location is considered primary
+const primaryLocation = locations[0];
+
+let resourceGroup = new azure.core.ResourceGroup("UrlShorterner", {
+ location: primaryLocation,
+});
+
+// Cosmos DB with a single write region (primary location) and multiple read replicas
+let cosmosdb = new azure.cosmosdb.Account("UrlStore", {
+ resourceGroupName: resourceGroup.name,
+ location: primaryLocation,
+ geoLocations: locations.map((location, failoverPriority) => ({ location, failoverPriority })),
+ offerType: "Standard",
+ consistencyPolicy: {
+ consistencyLevel: "Session",
+ maxIntervalInSeconds: 5,
+ maxStalenessPrefix: 100,
+ },
+});
+
+// Traffic Manager as a global HTTP endpoint
+const profile = new azure.trafficmanager.Profile("UrlShortEndpoint", {
+ resourceGroupName: resourceGroup.name,
+ trafficRoutingMethod: 'Performance',
+ dnsConfigs: [{
+ // Subdomain must be globally unique, so we default it with the full resource group name
+ relativeName: resourceGroup.name,
+ ttl: 60,
+ }],
+ monitorConfigs: [{
+ protocol: 'HTTP',
+ port: 80,
+ path: '/api/ping',
+ }]
+});
+
+// Azure Function to accept new URL shortcodes and save to Cosmos DB
+const fn = new azure.appservice.HttpEventSubscription("AddUrl", {
+ resourceGroup,
+ location: primaryLocation,
+ methods: ["POST"],
+ callbackFactory: () => {
+ const endpoint = cosmosdb.endpoint.get();
+ const masterKey = cosmosdb.primaryMasterKey.get();
+
+ let container: Container;
+ return async (_, request: azure.appservice.HttpRequest) => {
+ container = container || await getContainer(endpoint, masterKey, primaryLocation);
+
+ await container.items.create(request.body);
+ return { status: 200, body: 'Short URL saved' };
+ };
+ }
+});
+export const addEndpoint = fn.url;
+
+for (const location of locations) {
+
+ // URL redirection function - one per region
+ const fn = new azure.appservice.HttpEventSubscription(`GetUrl-${location}`, {
+ resourceGroup,
+ location,
+ route: "{key}",
+ callbackFactory: () => {
+ const endpoint = cosmosdb.endpoint.get();
+ const masterKey = cosmosdb.primaryMasterKey.get();
+
+ let container: Container;
+ return async (_, request: azure.appservice.HttpRequest) => {
+ container = container || await getContainer(endpoint, masterKey, location);
+
+ const key = request.params['key'];
+ if (key === 'ping') {
+ // Handle traffic manager live pings
+ return { status: 200, body: 'Ping ACK' };
+ }
+
+ try {
+ const response = await container.item(key.toString()).read();
+
+ return response.body && response.body.url
+ // HTTP redirect for known URLs
+ ? { status: 301, headers: { "location": response.body.url }, body: '' }
+ // 404 for malformed documents
+ : { status: 404, body: '' };
+ } catch (e) {
+ // Cosmos SDK throws an error for non-existing documents
+ return { status: 404, body: '' }
+ }
+ };
+ }
+ });
+
+ const app = fn.functionApp;
+
+ // An endpoint per region for Traffic Manager, link to the corresponding Function App
+ new azure.trafficmanager.Endpoint(`tme${location}`, {
+ resourceGroupName: resourceGroup.name,
+ profileName: profile.name,
+ type: 'azureEndpoints',
+ targetResourceId: app.id,
+ target: app.defaultHostname,
+ endpointLocation: app.location,
+ });
+}
+
+export const endpoint = profile.fqdn.apply(h => {
+ return `http://${h}/api/{key}`;
+});

ADD
azure-ts-serverless-url-shortener-global/package.json
+{
+ "name": "azure-ts-serverless-url-shortener-global",
+ "version": "1.0.0",
+ "devDependencies": {
+ "@types/node": "^10.3.3",
+ "typescript": "^3.1.1"
+ },
+ "dependencies": {
+ "@azure/cosmos": "latest",
+ "@pulumi/pulumi": "latest",
+ "@pulumi/azure": "latest"
+ }
+}

ADD
azure-ts-serverless-url-shortener-global/tsconfig.json
+{
+ "compilerOptions": {
+ "outDir": "bin",
+ "target": "es6",
+ "lib": [
+ "es6"
+ ],
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "sourceMap": true,
+ "experimentalDecorators": true,
+ "pretty": true,
+ "noFallthroughCasesInSwitch": true,
+ "noImplicitAny": true,
+ "noImplicitReturns": true,
+ "forceConsistentCasingInFileNames": true,
+ "strictNullChecks": true
+ },
+ "files": [
+ "cosmosclient.ts",
+ "index.ts",
+ ]
+}

Mutli-region Functions and Cosmos sample (#292)

Multi-region deployment of Azure Functions and Cosmos DB with Traffic Manager