Cosmos App example
ADD
azure-ts-cosmosapp-component/Pulumi.yaml
+name: azure-ts-cosmosapp-component
+runtime: nodejs
+description: Reusable Component to Create Globally-distributed Applications with Azure Cosmos DB
+template:
+ config:
+ azure:environment:
+ description: The Azure environment to use (`public`, `usgovernment`, `german`, `china`)
+ default: public
+ locations:
+ description: Comma-separated list of Azure regions
+ default: westus,westeurope

ADD
azure-ts-cosmosapp-component/README.md
+# Reusable Component to Create Globally-distributed Applications with Azure Cosmos DB
+
+This example demonstrates the usage of Pulumi to create globally-distributed applications with Azure Cosmos DB as the backend and pluggable infrastrustructure as the web tier.
+
+The application shows several notable features:
+
+1. Easy global deployments - a config setting provides a list of all the regions to deploy and a single execution deploys across them all.
+2. Abstraction - the `CosmosApp` component - abstracts away all the common logic for a global app with Cosmos DB multi-region data distribution and Traffic Manager for routing the traffic.
+3. Multi-model - examples are provided for serverless functions, containers, and virtual machines, all fitting to the above abstraction.
+
+## `CosmosApp` component
+
+The [`CosmosApp`](cosmosApp.ts) defines a skeleton for the application. While not limiting the type of compute resources, it creates the multi-regional pieces of the infrastructure:
+
+![Cosmos App](https://github.com/mikhailshilkov/pulumi-cosmos/raw/master/pictures/globalapp.png)
+
+The application has three example of using this component with the following compute services:
+
+- Azure Functions
+- Azure Container Intances
+- Azure VM Scale Sets + Azure Load Balancer
+
+## Prerequisites
+
+1. [Install Pulumi](https://www.pulumi.com/docs/get-started/install/)
+1. [Configure Azure credentials](https://www.pulumi.com/docs/intro/cloud-providers/azure/setup/)
+
+## Steps
+
+### Step 1: Create a new stack
+
+```
+$ pulumi stack init dev
+```
+
+### Step 2: Log in to the Azure CLI
+
+You will be prompted to do this during deployment if you forget this step.
+
+```
+$ az login
+```
+
+### Step 3: Install NPM dependencies
+
+```
+$ npm install
+```
+
+### Step 4: Deploy your changes
+
+Run `pulumi up` to preview and deploy changes:
+
+```
+$ pulumi up
+Previewing changes:
++ azure-ts-cosmosapp-component-dev create
++ examples:azure:CosmosApp vms create
++ azure:network:VirtualNetwork vnet-westeurope create
++ azure:network:PublicIp pip-westeurope create
++ azure:trafficmanager:Profile tmvms create
++ azure:trafficmanager:Endpoint tmvmswesteurope create
++ azure:cosmosdb:Account cosmos-vms
+...
+```
+
+### Step 5: Check the deployed website endpoints
+
+Three endpoints are now available. For example,
+
+```
+$ pulumi stack output functionsEndpoint
+http://functionscosmosfunc-rgcc15ea50.trafficmanager.net/api/cosmos
+
+$ curl "$(pulumi stack output functionsEndpoint)"
+Document 'cosmos' not found
+```
+
+Go to the Azure portal and add a document with the ID "cosmos" to receive a non-empty response.
\ No newline at end of file

ADD
azure-ts-cosmosapp-component/aci.ts
+// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
+
+import * as azure from "@pulumi/azure";
+import * as docker from "@pulumi/docker";
+import * as pulumi from "@pulumi/pulumi";
+import { CosmosApp, GlobalContext, RegionalContext } from "./cosmosApp";
+
+// Read a list of target locations from the config file:
+// Expecting a comma-separated list, e.g., "westus,eastus,westeurope"
+const locations = new pulumi.Config().require("locations").split(",");
+
+const resourceGroup = new azure.core.ResourceGroup("cosmosaci-rg", {
+ location: locations[0],
+});
+
+function buildContainerApp({ cosmosAccount, database, container, opts }: GlobalContext) {
+
+ const registry = new azure.containerservice.Registry("global", {
+ resourceGroupName: resourceGroup.name,
+ adminEnabled: true,
+ sku: "Premium",
+ }, opts);
+
+ const dockerImage = new docker.Image("node-app", {
+ imageName: pulumi.interpolate`${registry.loginServer}/mynodeapp:v1.0.0`,
+ build: {
+ context: "./container",
+ },
+ registry: {
+ server: registry.loginServer,
+ username: registry.adminUsername,
+ password: registry.adminPassword,
+ },
+ }, opts);
+
+ return ({ location }: RegionalContext) => {
+ const group = new azure.containerservice.Group(`aci-${location}`, {
+ resourceGroupName: resourceGroup.name,
+ location,
+ imageRegistryCredentials: [{
+ server: registry.loginServer,
+ username: registry.adminUsername,
+ password: registry.adminPassword,
+ }],
+ osType: "Linux",
+ containers: [
+ {
+ cpu: 0.5,
+ image: dockerImage.imageName,
+ memory: 1.5,
+ name: "hello-world",
+ ports: [{
+ port: 80,
+ protocol: "TCP",
+ }],
+ environmentVariables: {
+ ENDPOINT: cosmosAccount.endpoint,
+ MASTER_KEY: cosmosAccount.primaryMasterKey,
+ DATABASE: database.name,
+ COLLECTION: container.name,
+ LOCATION: location,
+ },
+ },
+ ],
+ ipAddressType: "public",
+ dnsNameLabel: `acishop-${location}`,
+ }, { deleteBeforeReplace: true, ...opts });
+
+ return {
+ url: group.fqdn,
+ };
+ };
+}
+
+export const aci = new CosmosApp("aci", {
+ resourceGroup,
+ locations,
+ databaseName: "cartdb",
+ containerName: "items",
+ factory: buildContainerApp,
+ enableMultiMaster: true,
+});

ADD
azure-ts-cosmosapp-component/container/.dockerignore
+node_modules
+npm-debug.log
\ No newline at end of file

ADD
azure-ts-cosmosapp-component/container/Dockerfile
+FROM node:8.9.3-alpine
+RUN mkdir -p /usr/src/app
+COPY ./app/* /usr/src/app/
+WORKDIR /usr/src/app
+RUN npm install
+CMD node /usr/src/app/index.js
\ No newline at end of file

ADD
azure-ts-cosmosapp-component/container/app/index.html
+<html>
+<head>
+ <title>YEAH Welcome to Azure Container Instances!</title>
+</head>
+<style>
+h1 {
+ color: darkblue;
+ font-family:arial, sans-serif;
+ font-weight: lighter;
+}
+</style>
+
+<body>
+
+<div align="center">
+<h1>Welcome to Azure Container Instances!</h1>
+
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 49.8 49.9" width="250px" height="250px">
+ <title>ContainerInstances_rgb_UI</title>
+ <path d="M41.9,11.368A11.929,11.929,0,0,0,20.3,5.061a9.444,9.444,0,0,0-14.932,9.8A8.969,8.969,0,0,0,9.064,32H39.442A10.463,10.463,0,0,0,41.9,11.368Z" transform="translate(-0.1 -0.1)" fill="#fff"/>
+ <path d="M41.9,11.368A11.929,11.929,0,0,0,20.3,5.061a9.444,9.444,0,0,0-14.932,9.8A8.969,8.969,0,0,0,9.064,32H39.442A10.463,10.463,0,0,0,41.9,11.368Z" transform="translate(-0.1 -0.1)" fill="#27a9e1" opacity="0.6" style="isolation:isolate"/>
+ <path d="M13,22a1,1,0,0,0-1,1V49a1,1,0,0,0,1,1H37a1,1,0,0,0,1-1V23a1,1,0,0,0-1-1Z" transform="translate(-0.1 -0.1)" fill="#672a7a"/>
+ <path d="M26.95,16" transform="translate(-0.1 -0.1)" fill="none"/>
+ <path d="M34.95,20" transform="translate(-0.1 -0.1)" fill="none"/>
+ <polygon points="22.9 21.9 22.9 14.9 19.9 14.9 24.9 7.9 29.9 14.9 26.9 14.9 26.9 21.9 22.9 21.9" fill="#fff"/>
+ <path d="M26.95,16" transform="translate(-0.1 -0.1)" fill="#814a98"/>
+ <path d="M33,25H15V47H35V25ZM21,45H17V27h4Zm6,0H23V27h4Zm6,0H29V27h4Z" transform="translate(-0.1 -0.1)" fill="#b92025" opacity="0.3" style="isolation:isolate"/>
+ <path d="M33,25H15V47H35V25ZM21,45H17V27h4Zm6,0H23V27h4Zm6,0H29V27h4Z" transform="translate(-0.1 -0.1)" fill="#fff" style="isolation:isolate"/>
+</svg>
+</div>
+
+</body>
+</html>
\ No newline at end of file

ADD
azure-ts-cosmosapp-component/container/app/index.js
+const express = require('express');
+const morgan = require('morgan');
+const cosmos = require('@azure/cosmos');
+
+const app = express();
+app.use(morgan('combined'));
+
+
+app.get('/', (req, res) => {
+ res.sendFile(__dirname + '/index.html')
+});
+
+app.get('/cosmos', async (req, res) => {
+ const endpoint = process.env.ENDPOINT;
+ const key = process.env.MASTER_KEY;
+ const database = process.env.DATABASE;
+ const collection = process.env.COLLECTION;
+ const location = process.env.LOCATION;
+
+ const client = new cosmos.CosmosClient({ endpoint, key, connectionPolicy: { preferredLocations: [location] } });
+ const container = client.database(database).container(collection);
+ const response = await container.item("test", undefined).read();
+
+ if (response.resource && response.resource.url) {
+ res.send(response.resource.url);
+ } else {
+ res.status(404).end();
+ }
+});
+
+app.get('/api/ping', (req, res) => {
+ res.send('Ack')
+});
+
+var listener = app.listen(process.env.PORT || 80, function() {
+ console.log('listening on port ' + listener.address().port);
+});
\ No newline at end of file

ADD
azure-ts-cosmosapp-component/container/app/package.json
+{
+ "name": "aci-helloworld",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "dependencies": {
+ "@azure/cosmos": "^3.1.1",
+ "express": "^4.14.0",
+ "morgan": "^1.8.2"
+ },
+ "devDependencies": {},
+ "author": ""
+}

ADD
azure-ts-cosmosapp-component/cosmosApp.ts
+// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
+
+import * as azure from "@pulumi/azure";
+import * as pulumi from "@pulumi/pulumi";
+
+export interface GlobalContext {
+ resourceGroup: azure.core.ResourceGroup;
+ cosmosAccount: azure.cosmosdb.Account;
+ database: azure.cosmosdb.SqlDatabase;
+ container: azure.cosmosdb.SqlContainer;
+ opts: pulumi.ResourceOptions;
+}
+
+export interface RegionalContext {
+ location: string;
+}
+
+export interface RegionalEndpoint {
+ // Azure resource ID (App Service and Public IP are supported)
+ id?: pulumi.Input<string>;
+ // An arbitrary URL for other resource types
+ url?: pulumi.Input<string>;
+}
+
+type BuildLocation = (context: RegionalContext) => RegionalEndpoint;
+type BuildLocationFactory = (context: GlobalContext) => BuildLocation;
+
+export interface CosmosAppArgs {
+ resourceGroup: azure.core.ResourceGroup;
+ locations: pulumi.Input<pulumi.Input<string>[]>;
+ databaseName: pulumi.Input<string>;
+ containerName: pulumi.Input<string>;
+ factory: BuildLocationFactory;
+ enableMultiMaster?: boolean;
+}
+
+export class CosmosApp extends pulumi.ComponentResource {
+ public endpoint: pulumi.Output<string>;
+
+ constructor(name: string,
+ args: CosmosAppArgs,
+ opts: pulumi.ComponentResourceOptions = {}) {
+ super("examples:azure:CosmosApp", name, args, opts);
+
+ const resourceGroup = args.resourceGroup;
+ const locations = pulumi.output(args.locations);
+ const primaryLocation = locations[0];
+ const parentOpts = { parent: this, ...opts };
+
+ // Cosmos DB Account with multiple replicas
+ const cosmosAccount = new azure.cosmosdb.Account(`cosmos-${name}`, {
+ resourceGroupName: resourceGroup.name,
+ location: primaryLocation,
+ geoLocations: locations.apply(ls => ls.map((location, failoverPriority) => ({ location, failoverPriority }))),
+ offerType: "Standard",
+ consistencyPolicy: {
+ consistencyLevel: "Session",
+ maxIntervalInSeconds: 5,
+ maxStalenessPrefix: 100,
+ },
+ enableMultipleWriteLocations: args.enableMultiMaster,
+ }, parentOpts);
+
+ const database = new azure.cosmosdb.SqlDatabase(`db-${name}`, {
+ resourceGroupName: resourceGroup.name,
+ accountName: cosmosAccount.name,
+ name: args.databaseName,
+ }, opts);
+
+ const container = new azure.cosmosdb.SqlContainer(`sql-${name}`, {
+ resourceGroupName: resourceGroup.name,
+ accountName: cosmosAccount.name,
+ databaseName: database.name,
+ }, opts);
+
+ // Traffic Manager as a global HTTP endpoint
+ const profile = new azure.trafficmanager.Profile(`tm${name}`, {
+ resourceGroupName: resourceGroup.name,
+ trafficRoutingMethod: "Performance",
+ dnsConfigs: [{
+ // Subdomain must be globally unique, so we default it with the full resource group name
+ relativeName: pulumi.interpolate`${name}${resourceGroup.name}`,
+ ttl: 60,
+ }],
+ monitorConfigs: [{
+ protocol: "HTTP",
+ port: 80,
+ path: "/api/ping",
+ }],
+ }, parentOpts);
+
+ const buildLocation = args.factory({ resourceGroup, cosmosAccount, database, container, opts: parentOpts });
+
+ const endpoints = locations.apply(ls => ls.map(location => {
+ const app = buildLocation({ location });
+
+ // An endpoint per region for Traffic Manager, link to the corresponding instance
+ return new azure.trafficmanager.Endpoint(`tm${name}${location}`.substring(0, 16), {
+ resourceGroupName: resourceGroup.name,
+ profileName: profile.name,
+ type: app.id ? "azureEndpoints" : "externalEndpoints",
+ targetResourceId: app.id,
+ target: app.url,
+ endpointLocation: location,
+ }, { parent: profile, deleteBeforeReplace: true, ...opts });
+ }));
+
+ this.endpoint = pulumi.interpolate`http://${profile.fqdn}`;
+
+ this.registerOutputs();
+ }
+}

ADD
azure-ts-cosmosapp-component/functionApp.ts
+// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
+
+import { CosmosClient } from "@azure/cosmos";
+import * as azure from "@pulumi/azure";
+import * as pulumi from "@pulumi/pulumi";
+import { CosmosApp, GlobalContext, RegionalContext } from "./cosmosApp";
+
+// Read a list of target locations from the config file:
+// Expecting a comma-separated list, e.g., "westus,eastus,westeurope"
+const locations = new pulumi.Config().require("locations").split(",");
+
+const resourceGroup = new azure.core.ResourceGroup("cosmosfunc-rg", {
+ location: locations[0],
+});
+
+function buildFunctionApp({ cosmosAccount, database, container, opts }: GlobalContext) {
+ return ({ location }: RegionalContext) => {
+ const fn = new azure.appservice.HttpEventSubscription(`GetUrl-${location}`, {
+ resourceGroup,
+ location,
+ route: "{key}",
+ callbackFactory: () => {
+ const client = new CosmosClient({
+ endpoint: cosmosAccount.endpoint.get(),
+ key: cosmosAccount.primaryMasterKey.get(),
+ connectionPolicy: { preferredLocations: [location] },
+ });
+ const collection = client.database(database.name.get()).container(container.name.get());
+
+ return async (_, request: azure.appservice.HttpRequest) => {
+ const key = request.params.key;
+ if (key === "ping") {
+ // Handle traffic manager live pings
+ return { status: 200, body: "Ping ACK" };
+ }
+
+ try {
+ const response = await collection.item(key, undefined).read();
+
+ return { status: 200, body: response.resource || `Document '${key}' not found` };
+ } catch (e) {
+ // Cosmos SDK throws an error for non-existing documents
+ return { status: 200, body: `Document '${key}' not found` };
+ }
+ };
+ },
+ }, opts);
+
+ return {
+ id: fn.functionApp.id,
+ };
+ };
+}
+
+export const functions = new CosmosApp("functions", {
+ resourceGroup,
+ locations,
+ databaseName: "productsdb",
+ containerName: "products",
+ factory: buildFunctionApp,
+});
+
+export function buildProductApp({ resourceGroup, cosmosAccount, opts }: GlobalContext) {
+ return ({ location }: RegionalContext) => {
+ const app = new azure.appservice.ArchiveFunctionApp("function-app", {
+ resourceGroupName: resourceGroup.name,
+ archive: new pulumi.asset.FileArchive("./app"),
+ appSettings: {
+ COSMOSDB_ENDPOINT: cosmosAccount.endpoint,
+ COSMOSDB_KEY: cosmosAccount.primaryMasterKey,
+ COSMOSDB_LOCATION: location,
+ },
+ }, opts);
+
+ return {
+ id: app.functionApp.id,
+ };
+ };
+}

ADD
azure-ts-cosmosapp-component/images/globalapp.png
azure-ts-cosmosapp-component/images/globalapp.png
Binary files /dev/null and b/azure-ts-cosmosapp-component/images/globalapp.png differ

ADD
azure-ts-cosmosapp-component/index.ts
+// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
+
+import * as pulumi from "@pulumi/pulumi";
+
+// Cosmos App with Azure Container Instances
+import { aci } from "./aci";
+export const aciEndpoint = pulumi.interpolate`${aci.endpoint}/cosmos`;
+
+// Cosmos App with Azure Functions
+import { functions } from "./functionApp";
+export const functionsEndpoint = pulumi.interpolate`${functions.endpoint}/api/cosmos`;
+
+// Cosmos App with Azure VM Scale Sets
+import { vmss } from "./vms";
+export const vmssEndpoint = pulumi.interpolate`${vmss.endpoint}/cosmos`;

ADD
azure-ts-cosmosapp-component/package.json
+{
+ "name": "azure-ts-cosmosapp-component",
+ "version": "1.0.0",
+ "devDependencies": {
+ "@types/node": "^10.3.3",
+ "typescript": "^3.1.1"
+ },
+ "dependencies": {
+ "@azure/cosmos": "latest",
+ "@pulumi/azure": "^1.0.0",
+ "@pulumi/docker": "latest",
+ "@pulumi/pulumi": "^1.0.0",
+ "@pulumi/random": "latest"
+ }
+}
\ No newline at end of file

ADD
azure-ts-cosmosapp-component/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": [
+ "aci.ts",
+ "cosmosApp.ts",
+ "functionApp.ts",
+ "index.ts",
+ "vms.ts"
+ ]
+}
\ No newline at end of file

ADD
azure-ts-cosmosapp-component/vm/vmCustomData.yaml
+#cloud-config
+package_upgrade: true
+packages:
+ - nginx
+ - nodejs
+ - npm
+write_files:
+ - owner: www-data:www-data
+ - path: /etc/nginx/sites-available/default
+ content: |
+ server {
+ listen 80;
+ location / {
+ proxy_pass http://localhost:3000;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection keep-alive;
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ }
+ }
+ - owner: azureuser:azureuser
+ - path: /home/azureuser/myapp/index.js
+ content: |
+ var express = require('express')
+ var app = express()
+ var os = require('os');
+ const cosmos = require('@azure/cosmos');
+ const endpoint = process.env.ENDPOINT;
+ const key = process.env.MASTER_KEY;
+ const database = process.env.DATABASE;
+ const collection = process.env.COLLECTION;
+ const preferredLocations = [process.env.LOCATION];
+ const connectionPolicy = { preferredLocations };
+ const client = new cosmos.CosmosClient({ endpoint, key, connectionPolicy });
+ app.get('/', function (req, res) {
+ res.send('Hello World1 from host ' + os.hostname() + '!')
+ })
+ app.get('/node', function (req, res) {
+ const version = require('child_process').execSync('nodejs --version');
+ res.send(version.toString());
+ })
+ app.get('/cosmos', (req, res) => {
+ client
+ .database(database)
+ .container(collection)
+ .item('test', undefined)
+ .read()
+ .then(r => res.send(r.resource))
+ .catch(err => res.status(500).send(err));
+ })
+ app.get('/api/ping', (req, res) => {
+ res.send('Ack')
+ })
+ app.listen(3000, function () {
+ console.log('Hello world app listening on port 3000!')
+ })
+runcmd:
+ - export ENDPOINT="${ENDPOINT}"
+ - export MASTER_KEY="${MASTER_KEY}"
+ - export DATABASE="${DATABASE}"
+ - export COLLECTION="${COLLECTION}"
+ - export LOCATION="${LOCATION}"
+ - service nginx restart
+ - cd "/home/azureuser/myapp"
+ - npm init
+ - npm install express -y
+ - npm install @azure/cosmos -y
+ - nodejs index.js
\ No newline at end of file

ADD
azure-ts-cosmosapp-component/vms.ts
+// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
+
+import * as azure from "@pulumi/azure";
+import * as pulumi from "@pulumi/pulumi";
+import * as random from "@pulumi/random";
+
+import { readFileSync } from "fs";
+import { CosmosApp, GlobalContext, RegionalContext } from "./cosmosApp";
+
+// Read a list of target locations from the config file:
+// Expecting a comma-separated list, e.g., "westus,eastus,westeurope"
+const locations = new pulumi.Config().require("locations").split(",");
+
+const resourceGroup = new azure.core.ResourceGroup("cosmosvms-rg", {
+ location: locations[0],
+});
+
+function buildVMScaleSetApp({ cosmosAccount, database, container, opts }: GlobalContext) {
+ const file = readFileSync("./vm/vmCustomData.yaml").toString();
+
+ return ({ location }: RegionalContext) => {
+ const domainName = new random.RandomString(`pipdns${location}`, { length: 10, special: false, upper: false }).result;
+
+ const publicIp = new azure.network.PublicIp(`pip-${location}`, {
+ resourceGroupName: resourceGroup.name,
+ allocationMethod: "Static",
+ domainNameLabel: domainName,
+ }, opts);
+
+ const loadBalancer = new azure.lb.LoadBalancer(`lb-${location}`, {
+ resourceGroupName: resourceGroup.name,
+ frontendIpConfigurations: [{
+ name: "PublicIPAddress",
+ publicIpAddressId: publicIp.id,
+ }],
+ }, opts);
+
+ const bpepool = new azure.lb.BackendAddressPool(`bap-${location}`, {
+ resourceGroupName: resourceGroup.name,
+ loadbalancerId: loadBalancer.id,
+ }, opts);
+
+ const probe = new azure.lb.Probe(`ssh-probe-${location}`.substring(0, 16), {
+ resourceGroupName: resourceGroup.name,
+ loadbalancerId: loadBalancer.id,
+ port: 80,
+ }, opts);
+
+ const rule = new azure.lb.Rule(`rule-${location}`, {
+ resourceGroupName: resourceGroup.name,
+ backendAddressPoolId: bpepool.id,
+ backendPort: 80,
+ frontendIpConfigurationName: "PublicIPAddress",
+ frontendPort: 80,
+ loadbalancerId: loadBalancer.id,
+ probeId: probe.id,
+ protocol: "Tcp",
+ }, opts);
+
+ const vnet = new azure.network.VirtualNetwork(`vnet-${location}`, {
+ resourceGroupName: resourceGroup.name,
+ addressSpaces: ["10.0.0.0/16"],
+ }, opts);
+
+ const subnet = new azure.network.Subnet(`subnet-${location}`, {
+ resourceGroupName: resourceGroup.name,
+ addressPrefix: "10.0.2.0/24",
+ virtualNetworkName: vnet.name,
+ }, opts);
+
+ const customData = pulumi.all([cosmosAccount.endpoint, cosmosAccount.primaryMasterKey, database.name, container.name])
+ .apply(([endpoint, key, databaseName, collectionName]) => {
+ const s = file.replace("${ENDPOINT}", endpoint)
+ .replace("${MASTER_KEY}", key)
+ .replace("${DATABASE}", databaseName)
+ .replace("${COLLECTION}", collectionName)
+ .replace("${LOCATION}", location);
+ return s;
+ });
+
+ const scaleSet = new azure.compute.ScaleSet(`vmss-${location}`, {
+ resourceGroupName: resourceGroup.name,
+ networkProfiles: [{
+ ipConfigurations: [{
+ loadBalancerBackendAddressPoolIds: [bpepool.id],
+ name: "IPConfiguration",
+ primary: true,
+ subnetId: subnet.id,
+ }],
+ name: "networkprofile",
+ primary: true,
+ }],
+ osProfile: {
+ adminUsername: "neo",
+ adminPassword: "SEcurePwd$3",
+ computerNamePrefix: "lab",
+ customData,
+ },
+ osProfileLinuxConfig: {
+ disablePasswordAuthentication: false,
+ },
+ sku: {
+ capacity: 1,
+ name: "Standard_DS1_v2",
+ tier: "Standard",
+ },
+ storageProfileDataDisks: [{
+ caching: "ReadWrite",
+ createOption: "Empty",
+ diskSizeGb: 10,
+ lun: 0,
+ }],
+ storageProfileImageReference: {
+ offer: "UbuntuServer",
+ publisher: "Canonical",
+ sku: "18.04-LTS",
+ version: "latest",
+ },
+ storageProfileOsDisk: {
+ caching: "ReadWrite",
+ createOption: "FromImage",
+ managedDiskType: "Standard_LRS",
+ name: "",
+ },
+ upgradePolicyMode: "Automatic",
+ }, { dependsOn: [bpepool], ...opts });
+
+ const autoscale = new azure.monitoring.AutoscaleSetting(`as-${location}`, {
+ resourceGroupName: resourceGroup.name,
+ notification: {
+ email: {
+ customEmails: ["admin@contoso.com"],
+ sendToSubscriptionAdministrator: true,
+ sendToSubscriptionCoAdministrator: true,
+ },
+ },
+ profiles: [{
+ capacity: {
+ default: 1,
+ maximum: 10,
+ minimum: 1,
+ },
+ name: "defaultProfile",
+ rules: [
+ {
+ metricTrigger: {
+ metricName: "Percentage CPU",
+ metricResourceId: scaleSet.id,
+ operator: "GreaterThan",
+ statistic: "Average",
+ threshold: 75,
+ timeAggregation: "Average",
+ timeGrain: "PT1M",
+ timeWindow: "PT5M",
+ },
+ scaleAction: {
+ cooldown: "PT1M",
+ direction: "Increase",
+ type: "ChangeCount",
+ value: 1,
+ },
+ },
+ {
+ metricTrigger: {
+ metricName: "Percentage CPU",
+ metricResourceId: scaleSet.id,
+ operator: "LessThan",
+ statistic: "Average",
+ threshold: 25,
+ timeAggregation: "Average",
+ timeGrain: "PT1M",
+ timeWindow: "PT5M",
+ },
+ scaleAction: {
+ cooldown: "PT1M",
+ direction: "Decrease",
+ type: "ChangeCount",
+ value: 1,
+ },
+ },
+ ],
+ }],
+ targetResourceId: scaleSet.id,
+ }, opts);
+
+ return {
+ id: publicIp.id,
+ };
+ };
+}
+
+export const vmss = new CosmosApp("vms", {
+ resourceGroup,
+ locations,
+ databaseName: "pricedb",
+ containerName: "prices",
+ factory: buildVMScaleSetApp,
+});

Cosmos App example (#399)