├── .gitignore ├── 00-installing-prerequisites.md ├── 01-iac ├── 01-creating-a-new-project.md ├── 02-configuring-azure.md ├── 03-provisioning-infrastructure.md ├── 04-updating-your-infrastructure.md ├── 05-making-your-stack-configurable.md ├── 06-creating-a-second-stack.md ├── 07-destroying-your-infrastructure.md └── code │ ├── 03 │ └── index.ts │ ├── 04 │ └── index.ts │ └── 05 │ ├── config.ts │ └── index.ts ├── 02-serverless ├── README.md └── code │ ├── step1.ts │ ├── step2.ts │ ├── step3.ts │ ├── step4.ts │ └── step5.ts ├── 03-telemetry ├── README.md └── code │ ├── step1 │ ├── common.ts │ └── index.ts │ ├── step2 │ ├── common.ts │ ├── cosmos.ts │ └── index.ts │ ├── step3 │ ├── common.ts │ ├── cosmos.ts │ ├── eventHub.ts │ └── index.ts │ └── step4 │ ├── common.ts │ ├── cosmos.ts │ ├── eventHub.ts │ ├── functionApp.ts │ └── index.ts ├── 04-status ├── README.md └── code │ ├── step1 │ ├── common.ts │ └── index.ts │ ├── step2 │ ├── common.ts │ ├── functionApp.ts │ └── index.ts │ ├── step3 │ ├── common.ts │ ├── functionApp.ts │ └── index.ts │ └── step4 │ ├── common.ts │ ├── functionApp.ts │ └── index.ts ├── 05-frontend ├── .DS_Store ├── README.md ├── code │ ├── .DS_Store │ ├── step2 │ │ ├── common.ts │ │ ├── functionApp.ts │ │ ├── index.ts │ │ └── website.ts │ ├── step4 │ │ └── package.json │ └── step5 │ │ ├── common.ts │ │ ├── functionApp.ts │ │ ├── index.ts │ │ ├── package.json │ │ ├── website.ts │ │ └── websiteFiles.ts └── img │ └── dronesite.png ├── 06-api ├── README.md └── code │ ├── step1 │ ├── common.ts │ ├── functionApp.ts │ ├── index.ts │ ├── website.ts │ └── websiteFiles.ts │ ├── step3 │ ├── api.ts │ ├── common.ts │ ├── functionApp.ts │ ├── index.ts │ ├── website.ts │ └── websiteFiles.ts │ ├── step4 │ ├── api.ts │ ├── common.ts │ ├── functionApp.ts │ ├── index.ts │ ├── website.ts │ └── websiteFiles.ts │ └── step5 │ ├── api.ts │ ├── common.ts │ ├── functionApp.ts │ ├── index.ts │ ├── website.ts │ └── websiteFiles.ts ├── 07-cdn ├── README.md └── code │ ├── step1 │ ├── api.ts │ ├── common.ts │ ├── functionApp.ts │ ├── index.ts │ ├── website.ts │ └── websiteFiles.ts │ └── step2 │ ├── api.ts │ ├── common.ts │ ├── functionApp.ts │ ├── index.ts │ ├── website.ts │ └── websiteFiles.ts ├── 08-aad ├── README.md ├── code │ ├── step2 │ │ ├── api.ts │ │ ├── common.ts │ │ ├── functionApp.ts │ │ ├── index.ts │ │ ├── website.ts │ │ └── websiteFiles.ts │ └── step3 │ │ ├── api.ts │ │ ├── common.ts │ │ ├── functionApp.ts │ │ ├── index.ts │ │ ├── website.ts │ │ └── websiteFiles.ts └── img │ └── auth.png ├── LICENSE ├── README.md ├── ServerlessArchitecture_Workshop_2020.pdf ├── img ├── status.png └── telemetry.png └── website ├── .gitignore ├── auth ├── .gitignore ├── gatsby-config.js ├── gatsby-ssr.js ├── package-lock.json ├── package.json └── src │ ├── components │ ├── drone-status.tsx │ └── spinner-basic.tsx │ ├── pages │ ├── 404.tsx │ └── index.tsx │ └── services │ ├── adal.js │ ├── auth.tsx │ └── config.js └── noauth ├── gatsby-config.js ├── gatsby-ssr.js ├── package-lock.json ├── package.json └── src ├── components ├── drone-status.tsx └── spinner-basic.tsx ├── pages ├── 404.tsx └── index.tsx └── services └── config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /00-installing-prerequisites.md: -------------------------------------------------------------------------------- 1 | # Installing Prerequisites 2 | 3 | The hands-on workshop will walk you through various tasks of managing Azure infrastructure with the focus on serverless compute and managed Azure services. The prerequisites listed below are required to successfully complete them. 4 | 5 | ## Node.js 6 | 7 | You will need Node.js version 10 or later to run Pulumi programs written in [TypeScript](https://www.typescriptlang.org/). 8 | Install your desired LTS version from [the Node.js download page](https://nodejs.org/en/download/) or 9 | [using a package manager](https://nodejs.org/en/download/package-manager/). 10 | 11 | After installing, verify that Node.js is working: 12 | 13 | ```bash 14 | $ node --version 15 | v12.10.0 16 | ``` 17 | 18 | Also verify that the Node Package Manager (NPM) is working: 19 | 20 | ```bash 21 | $ npm --version 22 | 6.10.3 23 | ``` 24 | 25 | ## Azure Subscription and CLI 26 | 27 | You need an active Azure subscription to deploy the components of the application. You may use your developer subscription, or create a free Azure subscription [here](https://azure.microsoft.com/free/). 28 | 29 | Please be sure to have administrative access to the subscription. 30 | 31 | Be sure to clean up the resources after you complete the workshop, as described at the last step of each lab. 32 | 33 | You will also use the command-line interface (CLI) tool to log in to an Azure subscription. You can install the CLI tool, as described [here](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest). 34 | 35 | After you complete the installation, open a command prompt and type `az`. You should see the welcome message: 36 | 37 | ``` 38 | $ az 39 | /\ 40 | / \ _____ _ _ ___ _ 41 | / /\ \ |_ / | | | \'__/ _\ 42 | / ____ \ / /| |_| | | | __/ 43 | /_/ \_\/___|\__,_|_| \___| 44 | 45 | 46 | Welcome to the cool new Azure CLI! 47 | ``` 48 | 49 | ## Pulumi 50 | 51 | You will use Pulumi to depoy infrastructure changes using code. [Install Pulumi here](https://www.pulumi.com/docs/get-started/install/). After installing the CLI, verify that it is working: 52 | 53 | ```bash 54 | $ pulumi version 55 | v2.6.1 56 | ``` 57 | 58 | The Pulumi CLI will ask you to login to your Pulumi account as needed. If you prefer to signup now, [go to the signup page](http://app.pulumi.com/signup). Multiple identity provider options are available — email, GitHub, GitLab, or Atlassian — and each of them will work equally well for these labs. 59 | -------------------------------------------------------------------------------- /01-iac/01-creating-a-new-project.md: -------------------------------------------------------------------------------- 1 | # Creating a New Project 2 | 3 | Infrastructure in Pulumi is organized into projects. Each project is a single program that, when run, declares the desired infrastructure for Pulumi to manage. 4 | 5 | ## Step 1 — Create a Directory 6 | 7 | Each Pulumi project lives in its own directory. Create one now and change into it: 8 | 9 | ```bash 10 | mkdir iac-workshop 11 | cd iac-workshop 12 | ``` 13 | 14 | > Pulumi will use the directory name as your project name by default. To create an independent project, simply name the directory differently. 15 | 16 | ## Step 2 — Initialize Your Project 17 | 18 | A Pulumi project is just a directory with some files in it. It's possible for you to create a new one by hand. The `pulumi new` command, however, automates the process: 19 | 20 | ```bash 21 | pulumi new typescript -y 22 | ``` 23 | 24 | This will print output similar to the following with a bit more information and status as it goes: 25 | 26 | ``` 27 | Created project 'iac-workshop' 28 | Created stack 'dev' 29 | Installing dependencies... 30 | 31 | Finished installing dependencies 32 | 33 | Your new project is ready to go! ✨ 34 | 35 | To perform an initial deployment, run 'pulumi up' 36 | ``` 37 | 38 | This command has created all the files we need, initialized a new stack named `dev` (an instance of our project), and installed the needed package dependencies from NPM. 39 | 40 | ## Step 3 — Inspect Your New Project 41 | 42 | Our project is comprised of multiple files: 43 | 44 | * **`index.ts`**: your program's main entrypoint file 45 | * **`package.json`** and **`package-lock.json`**: your project's NPM dependency information 46 | * **`Pulumi.yaml`**: your project's metadata, containing its name and language 47 | * **`tsconfig.json`**: your project's TypeScript settings 48 | * **`node_modules/`**: a directory containing your project's installed NPM dependencies 49 | 50 | Run `cat index.ts` to see the contents of your project's empty program: 51 | 52 | ```typescript 53 | import * as pulumi from "@pulumi/pulumi"; 54 | ``` 55 | 56 | Feel free to explore the other files, although we won't be editing any of them by hand. 57 | 58 | # Next Steps 59 | 60 | * [Configuring Azure](./02-configuring-azure.md) 61 | -------------------------------------------------------------------------------- /01-iac/02-configuring-azure.md: -------------------------------------------------------------------------------- 1 | # Configuring Azure 2 | 3 | Now that you have a basic project, let's configure Azure support for it. 4 | 5 | ## Step 1 — Install the Azure NextGen Package 6 | 7 | Run the following command to install the Azure NextGen package: 8 | 9 | ```bash 10 | npm install @pulumi/azure-nextgen 11 | ``` 12 | 13 | The package will be added to `node_modules/`, `package.json`, and `package-lock.json`. 14 | 15 | ## Step 2 — Use the Azure NextGen Package 16 | 17 | Now that the Azure NextGen package is installed, add the following lines to `index.ts` to import two modules from it. We will use one module to define a resource group and another one to define a storage account. 18 | 19 | ```ts 20 | ... 21 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 22 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 23 | ``` 24 | 25 | ## Step 3 — Login to Azure 26 | 27 | Simply login to the Azure CLI and Pulumi will automatically use your credentials: 28 | 29 | ``` 30 | az login 31 | ... 32 | You have logged in. Now let us find all the subscriptions to which you have access... 33 | ... 34 | ``` 35 | 36 | The Azure CLI, and thus Pulumi, will use the Default Subscription by default, however it is possible to override the subscription, by simply setting your subscription ID to the id output from `az account list`’s output: 37 | 38 | ``` 39 | $ az account list 40 | ``` 41 | 42 | Pick out the `` from the list and run: 43 | 44 | ``` 45 | $ az account set --subscription= 46 | ``` 47 | 48 | ## Next Steps 49 | 50 | * [Provisioning a Resource Group](./03-provisioning-infrastructure.md) 51 | -------------------------------------------------------------------------------- /01-iac/03-provisioning-infrastructure.md: -------------------------------------------------------------------------------- 1 | # Provisioning Infrastructure 2 | 3 | Now that you have a project configured to use Azure, you'll create some basic infrastructure in it. We will start with a Resource Group. 4 | 5 | ## Step 1 — Declare a New Resource Group 6 | 7 | Add the following to your `index.ts` file: 8 | 9 | ```ts 10 | ... 11 | const resourceGroup = new resources.ResourceGroup("my-group", { 12 | resourceGroupName: "my-group", 13 | location: "westus", 14 | }); 15 | ``` 16 | 17 | Feel free to choose any Azure region that supports the services used in these labs ([see this infographic](https://azure.microsoft.com/en-us/global-infrastructure/regions/) for a list of available regions). 18 | 19 | Note that we specified the resource name twice. The first name is a logical name of the Pulumi resource that you see in the previews and logs. The second name is the physical name of the resource in Azure. The names may match, as above, but may also be different, if that makes sense in your case. 20 | 21 | > :white_check_mark: After this change, your `index.ts` should [look like this](./code/03/index.ts). 22 | 23 | ## Step 2 — Preview Your Changes 24 | 25 | Now preview your changes: 26 | 27 | ``` 28 | pulumi up 29 | ``` 30 | 31 | This command evaluates your program, determines the resource updates to make, and shows you an outline of these changes: 32 | 33 | ``` 34 | Previewing update (dev): 35 | 36 | Type Name Plan 37 | + pulumi:pulumi:Stack iac-workshop-dev create 38 | + └─ azure-nextgen:resources/latest:ResourceGroup my-group create 39 | 40 | Resources: 41 | + 2 to create 42 | 43 | Do you want to perform this update? 44 | yes 45 | > no 46 | details 47 | ``` 48 | 49 | This is a summary view. Select `details` to view the full set of properties: 50 | 51 | ``` 52 | + pulumi:pulumi:Stack: (create) 53 | [urn=urn:pulumi:dev::iac-workshop::pulumi:pulumi:Stack::iac-workshop-dev] 54 | + azure-nextgen:resources/latest:ResourceGroup: (create) 55 | [urn=urn:pulumi:dev::iac-workshop::azure-nextgen:resources/latest:ResourceGroup::my-group] 56 | [provider=urn:pulumi:dev::iac-workshop::pulumi:providers:azure-nextgen::default_0_2_3::04da6b54-80e4-46f7-96ec-b56ff0331ba9] 57 | location : "westus" 58 | resourceGroupName: "my-group" 59 | 60 | Do you want to perform this update? 61 | yes 62 | > no 63 | details 64 | ``` 65 | 66 | The stack resource is a synthetic resource that all resources your program creates are parented to. 67 | 68 | ## Step 3 — Deploy Your Changes 69 | 70 | Now that we've seen the full set of changes, let's deploy them. Select `yes`: 71 | 72 | ``` 73 | Updating (dev): 74 | 75 | Type Name Status 76 | + pulumi:pulumi:Stack iac-workshop-dev created 77 | + └─ azure-nextgen:resources/latest:ResourceGroup my-group created 78 | 79 | Resources: 80 | + 2 created 81 | 82 | Duration: 8s 83 | 84 | Permalink: https://app.pulumi.com/myuser/iac-workshop/dev/updates/1 85 | ``` 86 | 87 | Now your resource group has been created in your Azure account. Feel free to click the Permalink URL and explore; this will take you to the [Pulumi Console](https://www.pulumi.com/docs/intro/console/), which records your deployment history. 88 | 89 | ## Next Steps 90 | 91 | * [Updating Your Infrastructure](./04-updating-your-infrastructure.md) 92 | -------------------------------------------------------------------------------- /01-iac/04-updating-your-infrastructure.md: -------------------------------------------------------------------------------- 1 | # Updating Your Infrastructure 2 | 3 | We just saw how to create new infrastructure from scratch. Next, let's add an Azure Storage Account to the existing resource group. 4 | 5 | This demonstrates how declarative infrastructure as code tools can be used not just for initial provisioning, but also subsequent changes to existing resources. 6 | 7 | ## Step 1 — Add a Storage Account 8 | 9 | And then add these lines to `index.ts` right after creating the resource group: 10 | 11 | ```ts 12 | ... 13 | const storageAccount = new storage.StorageAccount("mystorage", { 14 | resourceGroupName: resourceGroup.name, 15 | accountName: "myuniquename", 16 | location: resourceGroup.location, 17 | sku: { 18 | name: "Standard_LRS", 19 | }, 20 | kind: "StorageV2", 21 | }); 22 | ``` 23 | 24 | Azure requires each storage account to have a globally unique names across all tenants. Change the `accountName` parameter from "myuniquename" to a globally unique name that you can think of. This is a good example of when a logical resource name may differ from a physical name. 25 | 26 | Deploy the changes: 27 | 28 | ```bash 29 | pulumi up 30 | ``` 31 | 32 | This will give you a preview and selecting `yes` will apply the changes: 33 | 34 | ``` 35 | Updating (dev): 36 | 37 | Type Name Status 38 | pulumi:pulumi:Stack iac-workshop-dev 39 | + └─ azure-nextgen:storage/latest:StorageAccount mystorage created 40 | 41 | Resources: 42 | + 1 created 43 | 2 unchanged 44 | 45 | Duration: 4s 46 | 47 | Permalink: https://app.pulumi.com/myuser/iac-workshop/dev/updates/2 48 | ``` 49 | 50 | A single resource is added and two existing resources are left unchanged. This is a key attribute of infrastructure as code — such tools determine the minimal set of changes necessary to update your infrastructure from one version to the next. 51 | 52 | ## Step 2 — Export Your New Storage Account Name 53 | 54 | Programs can export variables which are shown in the CLI and recorded for each deployment. Export your account's name by adding this line to `index.ts`: 55 | 56 | ```ts 57 | export const accountName = storageAccount.name; 58 | ``` 59 | 60 | Now deploy the changes: 61 | 62 | ```bash 63 | pulumi up 64 | ``` 65 | 66 | Notice a new `Outputs` section is included in the output containing the account's name: 67 | 68 | ``` 69 | ... 70 | 71 | Outputs: 72 | + accountName: "myuniquename" 73 | 74 | Resources: 75 | 3 unchanged 76 | 77 | Duration: 7s 78 | 79 | Permalink: https://app.pulumi.com/myuser/iac-workshop/dev/updates/3 80 | ``` 81 | 82 | ## Step 3 — Inspect Your New Storage Account 83 | 84 | Now run the `az` CLI to list the containers in this new account: 85 | 86 | ``` 87 | az storage container list --account-name $(pulumi stack output accountName) 88 | [] 89 | ``` 90 | 91 | Note that the account is currently empty. 92 | 93 | ## Step 4 — Add a Container to Your Storage Account 94 | 95 | Add these lines to the `index.ts` file: 96 | 97 | ```ts 98 | ... 99 | const container = new storage.BlobContainer("mycontainer", { 100 | resourceGroupName: resourceGroup.name, 101 | accountName: storageAccount.name, 102 | containerName: "files", 103 | }); 104 | ... 105 | ``` 106 | 107 | > :white_check_mark: After this change, your `index.ts` should [look like this](./code/04/index.ts). 108 | 109 | Deploy the changes: 110 | 111 | ```bash 112 | pulumi up 113 | ``` 114 | 115 | This will give you a preview and selecting `yes` will apply the changes: 116 | 117 | ``` 118 | Updating (dev): 119 | 120 | Type Name Status 121 | pulumi:pulumi:Stack iac-workshop-dev 122 | + └─ azure-nextgen:storage/latest:BlobContainer mycontainer created 123 | 124 | Resources: 125 | + 1 created 126 | 3 unchanged 127 | 128 | Duration: 9s 129 | 130 | Permalink: https://app.pulumi.com/myuser/iac-workshop/dev/updates/4 131 | 132 | Finally, relist the contents of your account: 133 | 134 | ```bash 135 | az storage container list --account-name $(pulumi stack output accountName) -o table 136 | Name Lease Status Last Modified 137 | ------ -------------- ------------------------- 138 | files unlocked 2020-02-10T12:51:16+00:00 139 | ``` 140 | 141 | Notice that your `files` container has been added. 142 | 143 | ## Next Steps 144 | 145 | * [Making Your Stack Configurable](./05-making-your-stack-configurable.md) 146 | -------------------------------------------------------------------------------- /01-iac/05-making-your-stack-configurable.md: -------------------------------------------------------------------------------- 1 | # Making Your Stack Configurable 2 | 3 | Right now, the container's name is hard-coded. Next, you'll make the name configurable. 4 | 5 | ## Step 1 — Adding a Config Variable 6 | 7 | Instead of hard-coding the `"files"` container, we will use configuration to make it easy to change the name without editing the program. 8 | 9 | Create a file `config.ts` and add this to it: 10 | 11 | ```ts 12 | import { Config } from "@pulumi/pulumi"; 13 | 14 | const config = new Config(); 15 | export const containerName = config.require("container"); 16 | ``` 17 | 18 | ## Step 2 — Populating the Container Based on Config 19 | 20 | Add this line to your `index.ts` file's import statements: 21 | 22 | ```ts 23 | ... 24 | import { containerName } from "./config"; 25 | ... 26 | ``` 27 | 28 | And replace the hard-coded `"files"` parameter with this imported `containerName` variable: 29 | 30 | ```typescript 31 | const container = new storage.BlobContainer("mycontainer", { 32 | resourceGroupName: resourceGroup.name, 33 | accountName: storageAccount.name, 34 | containerName: containerName, 35 | }); 36 | ``` 37 | 38 | > :white_check_mark: After this change, your files should [look like this](./code/05/). 39 | 40 | ## Step 3 — Deploying the Changes 41 | 42 | Now, deploy your changes. To do so, first configure your stack. If you don't, you'll get an error: 43 | 44 | ```bash 45 | pulumi up 46 | ``` 47 | 48 | This results in an error like the following: 49 | 50 | ``` 51 | ... 52 | ConfigMissingException: Missing Required configuration variable 'iac-workshop:container' 53 | please set a value using the command `pulumi config set iac-workshop:container ` 54 | ... 55 | ``` 56 | 57 | Configure the `iac-workshop:container` variable: 58 | 59 | ```bash 60 | pulumi config set container html 61 | ``` 62 | 63 | To make things interesting, I set the name to `html` which is different from the previously hard-coded value `files`. 64 | 65 | Run `pulumi up` again. This detects that the container has changed and will perform a simple update: 66 | 67 | ``` 68 | Updating (dev): 69 | 70 | Type Name Status Info 71 | pulumi:pulumi:Stack iac-workshop-dev 72 | +- └─ azure-nextgen:storage/latest:BlobContainer mycontainer replaced [diff: ~containerName] 73 | 74 | Outputs: 75 | AccountName: "myuniquename" 76 | 77 | Resources: 78 | +-1 replaced 79 | 3 unchanged 80 | 81 | Duration: 10s 82 | 83 | Permalink: https://app.pulumi.com/myuser/iac-workshop/dev/updates/5 84 | ``` 85 | 86 | And you will see the contents added above. 87 | 88 | ## Next Steps 89 | 90 | * [Creating a Second Stack](./06-creating-a-second-stack.md) 91 | -------------------------------------------------------------------------------- /01-iac/06-creating-a-second-stack.md: -------------------------------------------------------------------------------- 1 | # Creating a Second Stack 2 | 3 | It is easy to create multiple instances of the same project. This is called a stack. This is handy for multiple development or test environments, staging versus production, and scaling a given infrastructure across many regions. 4 | 5 | ## Step 1 — Create and Configure a New Stack 6 | 7 | Create a new stack: 8 | 9 | ```bash 10 | pulumi stack init prod 11 | ``` 12 | 13 | Next, configure its required variable: 14 | 15 | ```bash 16 | pulumi config set container htmlprod 17 | ``` 18 | 19 | If you are ever curious to see the list of stacks for your current project, run this command: 20 | 21 | ```bash 22 | pulumi stack ls 23 | ``` 24 | 25 | It will print all stacks for this project that are available to you: 26 | 27 | ``` 28 | NAME LAST UPDATE RESOURCE COUNT URL 29 | dev 30 minutes ago 4 https://app.pulumi.com/myuser/iac-workshop/dev 30 | prod* 3 minutes ago 0 https://app.pulumi.com/myuser/iac-workshop/prod 31 | ``` 32 | 33 | Adjust the code to change the names of the resource group and the storage account to avoid duplication. Alternatively, you could make these names include stack names or make them configurable. 34 | 35 | ## Step 2 — Deploy the New Stack 36 | 37 | Now deploy all of the changes: 38 | 39 | ```bash 40 | pulumi up 41 | ``` 42 | 43 | This will create an entirely new set of resources from scratch, unrelated to the existing `dev` stack's resources. 44 | 45 | ``` 46 | Updating (prod): 47 | 48 | Type Name Status 49 | + pulumi:pulumi:Stack iac-workshop-prod created 50 | + ├─ azure-nextgen:resources/latest:ResourceGroup my-group created 51 | + ├─ azure-nextgen:storage/latest:StorageAccount mystorage created 52 | + └─ azure-nextgen:storage/latest:BlobContainer mycontainer created 53 | 54 | Outputs: 55 | AccountName: "myuniquenameprod" 56 | 57 | Resources: 58 | + 4 created 59 | 60 | Duration: 30s 61 | 62 | Permalink: https://app.pulumi.com/myuser/iac-workshop/prod/updates/1 63 | ``` 64 | 65 | A new set of resources has been created for the `prod` stack. 66 | 67 | ## Next Steps 68 | 69 | * [Destroying Your Infrastructure](./07-destroying-your-infrastructure.md) 70 | -------------------------------------------------------------------------------- /01-iac/07-destroying-your-infrastructure.md: -------------------------------------------------------------------------------- 1 | # Destroying Your Infrastructure 2 | 3 | The final step is to destroy all of the resources from the two stacks created. 4 | 5 | ## Step 1 — Destroy Resources 6 | 7 | First, destroy the resources in your current stack: 8 | 9 | ```bash 10 | pulumi destroy 11 | ``` 12 | 13 | This will show you a preview, much like the `pulumi up` command does: 14 | 15 | ``` 16 | Previewing destroy (prod): 17 | 18 | Type Name Plan 19 | - pulumi:pulumi:Stack iac-workshop-prod delete 20 | - ├─ azure-nextgen:storage/latest:BlobContainer mycontainer delete 21 | - ├─ azure-nextgen:storage/latest:StorageAccount mystorage delete 22 | - └─ azure-nextgen:resources/latest:ResourceGroup my-group delete 23 | 24 | Outputs: 25 | - AccountName: "myuniquename" 26 | 27 | Resources: 28 | - 4 to delete 29 | 30 | Do you want to perform this destroy? 31 | yes 32 | > no 33 | details 34 | ``` 35 | 36 | To proceed, select `yes`. 37 | 38 | ``` 39 | Destroying (prod): 40 | 41 | Type Name Status 42 | - pulumi:pulumi:Stack iac-workshop-prod deleted 43 | - ├─ azure-nextgen:storage/latest:BlobContainer mycontainer deleted 44 | - ├─ azure-nextgen:storage/latest:StorageAccount mystorage deleted 45 | - └─ azure-nextgen:resources/latest:ResourceGroup my-group deleted 46 | 47 | Outputs: 48 | - AccountName: "myuniquename" 49 | 50 | Resources: 51 | - 4 deleted 52 | 53 | Duration: 1m0s 54 | 55 | Permalink: https://app.pulumi.com/myuser/iac-workshop/prod/updates/2 56 | The resources in the stack have been deleted, but the history and configuration 57 | associated with the stack are still maintained. If you want to remove the stack 58 | completely, run 'pulumi stack rm prod'. 59 | ``` 60 | 61 | ## Step 2 — Remove the Stack 62 | 63 | The Azure resources for this stack have been destroyed. Per the message printed at the end, however, the stack itself is still known to Pulumi. This means all past history is still available and you can perform subsequent updates on this stack. 64 | 65 | Now, fully remove the stack and all history: 66 | 67 | ```bash 68 | pulumi stack rm 69 | ``` 70 | 71 | This is irreversible and so asks to confirm that this is your intent: 72 | 73 | ``` 74 | This will permanently remove the 'prod' stack! 75 | Please confirm that this is what you'd like to do by typing ("prod"): 76 | ``` 77 | 78 | Type the name of the stack and hit enter. The stack is now gone. 79 | 80 | ## Step 3 — Select Another Stack, Rinse and Repeat 81 | 82 | After destroying `prod`, you still have the `dev` stack. To destroy it too, first select it: 83 | 84 | ``` 85 | pulumi stack select dev 86 | ``` 87 | 88 | Now, go back and repeat steps 1 and 2. 89 | 90 | ## Step 4 — Verify That Stacks are Gone 91 | 92 | Verify that all of this projec'ts stacks are now gone 93 | 94 | ```bash 95 | pulumi stack ls 96 | ``` 97 | 98 | ## Next Steps 99 | 100 | Congratulations! :tada: You have completed the first lab. 101 | 102 | Now that you're more familiar with infrastructure as code concepts and how the tool works, you can feel free to explore the more advanced collection of labs. These labs will teach you how to provision serverless workloads. 103 | 104 | Lab 2 deploys an Azure Function App with HTTP-triggered serverless functions. 105 | 106 | [Get Started with Lab 2](../02-serverless/README.md) 107 | -------------------------------------------------------------------------------- /01-iac/code/03/index.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | 5 | const resourceGroup = new resources.ResourceGroup("my-group", { 6 | resourceGroupName: "my-group", 7 | location: "westus", 8 | }); 9 | -------------------------------------------------------------------------------- /01-iac/code/04/index.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | 5 | const resourceGroup = new resources.ResourceGroup("my-group", { 6 | resourceGroupName: "my-group", 7 | location: "westus", 8 | }); 9 | 10 | const storageAccount = new storage.StorageAccount("mystorage", { 11 | resourceGroupName: resourceGroup.name, 12 | accountName: "myuniquename", 13 | location: resourceGroup.location, 14 | sku: { 15 | name: "Standard_LRS", 16 | }, 17 | kind: "StorageV2", 18 | }); 19 | 20 | const container = new storage.BlobContainer("mycontainer", { 21 | resourceGroupName: resourceGroup.name, 22 | accountName: storageAccount.name, 23 | containerName: "files", 24 | }); 25 | 26 | export const accountName = storageAccount.name; 27 | -------------------------------------------------------------------------------- /01-iac/code/05/config.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "@pulumi/pulumi"; 2 | 3 | const config = new Config(); 4 | export const containerName = config.require("container"); 5 | -------------------------------------------------------------------------------- /01-iac/code/05/index.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | import { containerName } from "./config"; 5 | 6 | const resourceGroup = new resources.ResourceGroup("my-group", { 7 | resourceGroupName: "my-group", 8 | location: "westus", 9 | }); 10 | 11 | const storageAccount = new storage.StorageAccount("mystorage", { 12 | resourceGroupName: resourceGroup.name, 13 | accountName: "myuniquename", 14 | location: resourceGroup.location, 15 | sku: { 16 | name: "Standard_LRS", 17 | }, 18 | kind: "StorageV2", 19 | }); 20 | 21 | const container = new storage.BlobContainer("mycontainer", { 22 | resourceGroupName: resourceGroup.name, 23 | accountName: storageAccount.name, 24 | containerName: containerName, 25 | }); 26 | 27 | export const accountName = storageAccount.name; 28 | -------------------------------------------------------------------------------- /02-serverless/code/step1.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | 5 | const resourceGroup = new resources.ResourceGroup("my-group", { 6 | resourceGroupName: "my-group", 7 | location: "westus", 8 | }); 9 | 10 | const storageAccount = new storage.StorageAccount("mystorage", { 11 | resourceGroupName: resourceGroup.name, 12 | accountName: "myuniquename", 13 | location: resourceGroup.location, 14 | sku: { 15 | name: "Standard_LRS", 16 | }, 17 | kind: "StorageV2", 18 | }); 19 | -------------------------------------------------------------------------------- /02-serverless/code/step2.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | import * as web from "@pulumi/azure-nextgen/web/latest"; 5 | 6 | const resourceGroup = new resources.ResourceGroup("my-group", { 7 | resourceGroupName: "my-group", 8 | location: "westus", 9 | }); 10 | 11 | const storageAccount = new storage.StorageAccount("mystorage", { 12 | resourceGroupName: resourceGroup.name, 13 | accountName: "myuniquename", 14 | location: resourceGroup.location, 15 | sku: { 16 | name: "Standard_LRS", 17 | }, 18 | kind: "StorageV2", 19 | }); 20 | 21 | const plan = new web.AppServicePlan("asp", { 22 | resourceGroupName: resourceGroup.name, 23 | name: "consumption-plan", 24 | location: resourceGroup.location, 25 | sku: { 26 | name: "Y1", 27 | tier: "Dynamic", 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /02-serverless/code/step3.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | import * as web from "@pulumi/azure-nextgen/web/latest"; 5 | 6 | const resourceGroup = new resources.ResourceGroup("my-group", { 7 | resourceGroupName: "my-group", 8 | location: "westus", 9 | }); 10 | 11 | const storageAccount = new storage.StorageAccount("mystorage", { 12 | resourceGroupName: resourceGroup.name, 13 | accountName: "myuniquename", 14 | location: resourceGroup.location, 15 | sku: { 16 | name: "Standard_LRS", 17 | }, 18 | kind: "StorageV2", 19 | }); 20 | 21 | const plan = new web.AppServicePlan("asp", { 22 | resourceGroupName: resourceGroup.name, 23 | name: "consumption-plan", 24 | location: resourceGroup.location, 25 | sku: { 26 | name: "Y1", 27 | tier: "Dynamic", 28 | }, 29 | }); 30 | 31 | const storageAccountKeys = pulumi.all([resourceGroup.name, storageAccount.name]).apply(([resourceGroupName, accountName]) => 32 | storage.listStorageAccountKeys({ resourceGroupName, accountName })); 33 | 34 | const primaryStorageKey = storageAccountKeys.keys[0].value; 35 | const storageConnectionString = pulumi.interpolate`DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${primaryStorageKey}`; 36 | -------------------------------------------------------------------------------- /02-serverless/code/step4.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | import * as web from "@pulumi/azure-nextgen/web/latest"; 5 | 6 | const resourceGroup = new resources.ResourceGroup("my-group", { 7 | resourceGroupName: "my-group", 8 | location: "westus", 9 | }); 10 | 11 | const storageAccount = new storage.StorageAccount("mystorage", { 12 | resourceGroupName: resourceGroup.name, 13 | accountName: "myuniquename", 14 | location: resourceGroup.location, 15 | sku: { 16 | name: "Standard_LRS", 17 | }, 18 | kind: "StorageV2", 19 | }); 20 | 21 | const plan = new web.AppServicePlan("asp", { 22 | resourceGroupName: resourceGroup.name, 23 | name: "consumption-plan", 24 | location: resourceGroup.location, 25 | sku: { 26 | name: "Y1", 27 | tier: "Dynamic", 28 | }, 29 | }); 30 | 31 | const storageAccountKeys = pulumi.all([resourceGroup.name, storageAccount.name]).apply(([resourceGroupName, accountName]) => 32 | storage.listStorageAccountKeys({ resourceGroupName, accountName })); 33 | 34 | const primaryStorageKey = storageAccountKeys.keys[0].value; 35 | const storageConnectionString = pulumi.interpolate`DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${primaryStorageKey}`; 36 | 37 | const app = new web.WebApp("fa", { 38 | resourceGroupName: resourceGroup.name, 39 | name: "myuniqueapp", 40 | location: resourceGroup.location, 41 | serverFarmId: plan.id, 42 | kind: "functionapp", 43 | siteConfig: { 44 | appSettings: [ 45 | { name: "AzureWebJobsStorage", value: storageConnectionString }, 46 | { name: "FUNCTIONS_EXTENSION_VERSION", value: "~3" }, 47 | { name: "FUNCTIONS_WORKER_RUNTIME", value: "node" }, 48 | { name: "WEBSITE_NODE_DEFAULT_VERSION", value: "10.14.1" }, 49 | { name: "WEBSITE_RUN_FROM_PACKAGE", value: "https://mikhailworkshop.blob.core.windows.net/zips/app.zip" }, 50 | ] 51 | }, 52 | }); 53 | -------------------------------------------------------------------------------- /02-serverless/code/step5.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | import * as web from "@pulumi/azure-nextgen/web/latest"; 5 | 6 | const resourceGroup = new resources.ResourceGroup("my-group", { 7 | resourceGroupName: "my-group", 8 | location: "westus", 9 | }); 10 | 11 | const storageAccount = new storage.StorageAccount("mystorage", { 12 | resourceGroupName: resourceGroup.name, 13 | accountName: "myuniquename", 14 | location: resourceGroup.location, 15 | sku: { 16 | name: "Standard_LRS", 17 | }, 18 | kind: "StorageV2", 19 | }); 20 | 21 | const plan = new web.AppServicePlan("asp", { 22 | resourceGroupName: resourceGroup.name, 23 | name: "consumption-plan", 24 | location: resourceGroup.location, 25 | sku: { 26 | name: "Y1", 27 | tier: "Dynamic", 28 | }, 29 | }); 30 | 31 | const storageAccountKeys = pulumi.all([resourceGroup.name, storageAccount.name]).apply(([resourceGroupName, accountName]) => 32 | storage.listStorageAccountKeys({ resourceGroupName, accountName })); 33 | 34 | const primaryStorageKey = storageAccountKeys.keys[0].value; 35 | const storageConnectionString = pulumi.interpolate`DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${primaryStorageKey}`; 36 | 37 | const app = new web.WebApp("fa", { 38 | resourceGroupName: resourceGroup.name, 39 | name: "myuniqueapp", 40 | location: resourceGroup.location, 41 | serverFarmId: plan.id, 42 | kind: "functionapp", 43 | siteConfig: { 44 | appSettings: [ 45 | { name: "AzureWebJobsStorage", value: storageConnectionString }, 46 | { name: "FUNCTIONS_EXTENSION_VERSION", value: "~3" }, 47 | { name: "FUNCTIONS_WORKER_RUNTIME", value: "node" }, 48 | { name: "WEBSITE_NODE_DEFAULT_VERSION", value: "10.14.1" }, 49 | { name: "WEBSITE_RUN_FROM_PACKAGE", value: "https://mikhailworkshop.blob.core.windows.net/zips/app.zip" }, 50 | ] 51 | }, 52 | }); 53 | 54 | export const endpoint = pulumi.interpolate`https://${app.defaultHostName}/api/hello`; 55 | -------------------------------------------------------------------------------- /03-telemetry/code/step1/common.ts: -------------------------------------------------------------------------------- 1 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 2 | 3 | export const appName = "telemetry"; 4 | 5 | const resourceGroup = new resources.ResourceGroup(`${appName}-rg`, { 6 | resourceGroupName: `${appName}-rg`, 7 | location: "WestEurope", 8 | }); 9 | 10 | export const resourceGroupName = resourceGroup.name; 11 | export const location = resourceGroup.location; 12 | -------------------------------------------------------------------------------- /03-telemetry/code/step1/index.ts: -------------------------------------------------------------------------------- 1 | import "./common"; 2 | -------------------------------------------------------------------------------- /03-telemetry/code/step2/common.ts: -------------------------------------------------------------------------------- 1 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 2 | 3 | export const appName = "telemetry"; 4 | 5 | const resourceGroup = new resources.ResourceGroup(`${appName}-rg`, { 6 | resourceGroupName: `${appName}-rg`, 7 | location: "WestEurope", 8 | }); 9 | 10 | export const resourceGroupName = resourceGroup.name; 11 | export const location = resourceGroup.location; 12 | -------------------------------------------------------------------------------- /03-telemetry/code/step2/cosmos.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as documentdb from "@pulumi/azure-nextgen/documentdb/latest"; 3 | import { appName, location, resourceGroupName } from "./common"; 4 | 5 | const databaseAccount = new documentdb.DatabaseAccount(`${appName}-acc`, { 6 | resourceGroupName: resourceGroupName, 7 | accountName: `${appName}-acc`, 8 | location: location, 9 | databaseAccountOfferType: "Standard", 10 | capabilities: [{ 11 | name: "EnableServerless", 12 | }], 13 | locations: [{ locationName: location, failoverPriority: 0 }], 14 | consistencyPolicy: { 15 | defaultConsistencyLevel: "Session", 16 | }, 17 | }); 18 | 19 | export const databaseName = "db"; 20 | const database = new documentdb.SqlResourceSqlDatabase(databaseName, { 21 | databaseName: databaseName, 22 | resourceGroupName: resourceGroupName, 23 | accountName: databaseAccount.name, 24 | resource: { 25 | id: databaseName, 26 | }, 27 | options: {}, 28 | }, { parent: databaseAccount }); 29 | 30 | export const collectionName = "items"; 31 | const collection = new documentdb.SqlResourceSqlContainer(collectionName, { 32 | containerName: collectionName, 33 | resourceGroupName: resourceGroupName, 34 | accountName: databaseAccount.name, 35 | databaseName: database.name, 36 | resource: { 37 | id: collectionName, 38 | partitionKey: { 39 | paths: ["/id"] 40 | }, 41 | }, 42 | options: {}, 43 | }, { parent: database }); 44 | 45 | const keys = pulumi.all([resourceGroupName, databaseAccount.name]) 46 | .apply(([resourceGroupName, accountName]) => 47 | documentdb.listDatabaseAccountKeys({ resourceGroupName, accountName })); 48 | 49 | const connectionStrings = pulumi.all([resourceGroupName, databaseAccount.name]) 50 | .apply(([resourceGroupName, accountName]) => 51 | documentdb.listDatabaseAccountConnectionStrings({ resourceGroupName, accountName })); 52 | 53 | export const connectionString = connectionStrings.apply(cs => cs.connectionStrings![0].connectionString); 54 | export const endpoint = databaseAccount.documentEndpoint; 55 | export const masterKey = keys.primaryMasterKey; 56 | -------------------------------------------------------------------------------- /03-telemetry/code/step2/index.ts: -------------------------------------------------------------------------------- 1 | import "./common"; 2 | import * as cosmos from "./cosmos"; 3 | 4 | export const cosmosDatabaseName = cosmos.databaseName; 5 | export const cosmosCollectionName = cosmos.collectionName; 6 | export const cosmosConnectionString = cosmos.connectionString; 7 | export const cosmosEndpoint = cosmos.endpoint; 8 | export const cosmosMasterKey = cosmos.masterKey; 9 | -------------------------------------------------------------------------------- /03-telemetry/code/step3/common.ts: -------------------------------------------------------------------------------- 1 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 2 | 3 | export const appName = "telemetry"; 4 | 5 | const resourceGroup = new resources.ResourceGroup(`${appName}-rg`, { 6 | resourceGroupName: `${appName}-rg`, 7 | location: "WestEurope", 8 | }); 9 | 10 | export const resourceGroupName = resourceGroup.name; 11 | export const location = resourceGroup.location; 12 | -------------------------------------------------------------------------------- /03-telemetry/code/step3/cosmos.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as documentdb from "@pulumi/azure-nextgen/documentdb/latest"; 3 | import { appName, location, resourceGroupName } from "./common"; 4 | 5 | const databaseAccount = new documentdb.DatabaseAccount(`${appName}-acc`, { 6 | resourceGroupName: resourceGroupName, 7 | accountName: `${appName}-acc`, 8 | location: location, 9 | databaseAccountOfferType: "Standard", 10 | capabilities: [{ 11 | name: "EnableServerless", 12 | }], 13 | locations: [{ locationName: location, failoverPriority: 0 }], 14 | consistencyPolicy: { 15 | defaultConsistencyLevel: "Session", 16 | }, 17 | }); 18 | 19 | export const databaseName = "db"; 20 | const database = new documentdb.SqlResourceSqlDatabase(databaseName, { 21 | databaseName: databaseName, 22 | resourceGroupName: resourceGroupName, 23 | accountName: databaseAccount.name, 24 | resource: { 25 | id: databaseName, 26 | }, 27 | options: {}, 28 | }, { parent: databaseAccount }); 29 | 30 | export const collectionName = "items"; 31 | const collection = new documentdb.SqlResourceSqlContainer(collectionName, { 32 | containerName: collectionName, 33 | resourceGroupName: resourceGroupName, 34 | accountName: databaseAccount.name, 35 | databaseName: database.name, 36 | resource: { 37 | id: collectionName, 38 | partitionKey: { 39 | paths: ["/id"] 40 | }, 41 | }, 42 | options: {}, 43 | }, { parent: database }); 44 | 45 | const keys = pulumi.all([resourceGroupName, databaseAccount.name]) 46 | .apply(([resourceGroupName, accountName]) => 47 | documentdb.listDatabaseAccountKeys({ resourceGroupName, accountName })); 48 | 49 | const connectionStrings = pulumi.all([resourceGroupName, databaseAccount.name]) 50 | .apply(([resourceGroupName, accountName]) => 51 | documentdb.listDatabaseAccountConnectionStrings({ resourceGroupName, accountName })); 52 | 53 | export const connectionString = connectionStrings.apply(cs => cs.connectionStrings![0].connectionString); 54 | export const endpoint = databaseAccount.documentEndpoint; 55 | export const masterKey = keys.primaryMasterKey; 56 | -------------------------------------------------------------------------------- /03-telemetry/code/step3/eventHub.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as eventhub from "@pulumi/azure-nextgen/eventhub/latest"; 3 | import { appName, location, resourceGroupName } from "./common"; 4 | 5 | const eventHubNamespace = new eventhub.Namespace(`${appName}-ns`, { 6 | resourceGroupName: resourceGroupName, 7 | namespaceName: `${appName}-ns`, 8 | location: location, 9 | sku: { 10 | name: "Standard", 11 | }, 12 | }); 13 | 14 | const eventHub = new eventhub.EventHub(`${appName}-eh`, { 15 | resourceGroupName: resourceGroupName, 16 | namespaceName: eventHubNamespace.name, 17 | eventHubName: `${appName}-eh`, 18 | messageRetentionInDays: 1, 19 | partitionCount: 4, 20 | }, { parent: eventHubNamespace }); 21 | 22 | export const consumerGroupName = "dronetelemetry"; 23 | const consumerGroup = new eventhub.ConsumerGroup(consumerGroupName, { 24 | resourceGroupName: resourceGroupName, 25 | namespaceName: eventHubNamespace.name, 26 | eventHubName: eventHub.name, 27 | consumerGroupName: consumerGroupName, 28 | }, { parent: eventHub }); 29 | 30 | export const namespace = eventHubNamespace.name; 31 | export const name = eventHub.name; 32 | 33 | const sendEventSourceKey = new eventhub.EventHubAuthorizationRule("send", { 34 | resourceGroupName: resourceGroupName, 35 | namespaceName: eventHubNamespace.name, 36 | eventHubName: eventHub.name, 37 | authorizationRuleName: "send", 38 | rights: ["send"], 39 | }, { parent: eventHub }); 40 | 41 | const listenEventSourceKey = new eventhub.EventHubAuthorizationRule("listen", { 42 | resourceGroupName: resourceGroupName, 43 | namespaceName: eventHubNamespace.name, 44 | eventHubName: eventHub.name, 45 | authorizationRuleName: "listen", 46 | rights: ["listen"], 47 | }, { parent: eventHub }); 48 | 49 | const sendKeys = pulumi.all([resourceGroupName, eventHubNamespace.name, eventHub.name, sendEventSourceKey.name]) 50 | .apply(([resourceGroupName, namespaceName, eventHubName, authorizationRuleName]) => 51 | eventhub.listEventHubKeys({ 52 | resourceGroupName, 53 | namespaceName, 54 | eventHubName, 55 | authorizationRuleName, 56 | })); 57 | export const sendConnectionString = sendKeys.primaryConnectionString; 58 | 59 | const listenKeys = pulumi.all([resourceGroupName, eventHubNamespace.name, eventHub.name, listenEventSourceKey.name]) 60 | .apply(([resourceGroupName, namespaceName, eventHubName, authorizationRuleName]) => 61 | eventhub.listEventHubKeys({ 62 | resourceGroupName, 63 | namespaceName, 64 | eventHubName, 65 | authorizationRuleName, 66 | })); 67 | export const listenConnectionString = listenKeys.primaryConnectionString; 68 | -------------------------------------------------------------------------------- /03-telemetry/code/step3/index.ts: -------------------------------------------------------------------------------- 1 | import "./common"; 2 | import * as cosmos from "./cosmos"; 3 | import { namespace, sendConnectionString } from "./eventHub"; 4 | 5 | export const cosmosDatabaseName = cosmos.databaseName; 6 | export const cosmosCollectionName = cosmos.collectionName; 7 | export const cosmosConnectionString = cosmos.connectionString; 8 | export const cosmosEndpoint = cosmos.endpoint; 9 | export const cosmosMasterKey = cosmos.masterKey; 10 | 11 | export const eventHubNamespace = namespace; 12 | export const eventHubSendConnectionString = sendConnectionString; 13 | -------------------------------------------------------------------------------- /03-telemetry/code/step4/common.ts: -------------------------------------------------------------------------------- 1 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 2 | 3 | export const appName = "telemetry"; 4 | 5 | const resourceGroup = new resources.ResourceGroup(`${appName}-rg`, { 6 | resourceGroupName: `${appName}-rg`, 7 | location: "WestEurope", 8 | }); 9 | 10 | export const resourceGroupName = resourceGroup.name; 11 | export const location = resourceGroup.location; 12 | -------------------------------------------------------------------------------- /03-telemetry/code/step4/cosmos.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as documentdb from "@pulumi/azure-nextgen/documentdb/latest"; 3 | import { appName, location, resourceGroupName } from "./common"; 4 | 5 | const databaseAccount = new documentdb.DatabaseAccount(`${appName}-acc`, { 6 | resourceGroupName: resourceGroupName, 7 | accountName: `${appName}-acc`, 8 | location: location, 9 | databaseAccountOfferType: "Standard", 10 | capabilities: [{ 11 | name: "EnableServerless", 12 | }], 13 | locations: [{ locationName: location, failoverPriority: 0 }], 14 | consistencyPolicy: { 15 | defaultConsistencyLevel: "Session", 16 | }, 17 | }); 18 | 19 | export const databaseName = "db"; 20 | const database = new documentdb.SqlResourceSqlDatabase(databaseName, { 21 | databaseName: databaseName, 22 | resourceGroupName: resourceGroupName, 23 | accountName: databaseAccount.name, 24 | resource: { 25 | id: databaseName, 26 | }, 27 | options: {}, 28 | }, { parent: databaseAccount }); 29 | 30 | export const collectionName = "items"; 31 | const collection = new documentdb.SqlResourceSqlContainer(collectionName, { 32 | containerName: collectionName, 33 | resourceGroupName: resourceGroupName, 34 | accountName: databaseAccount.name, 35 | databaseName: database.name, 36 | resource: { 37 | id: collectionName, 38 | partitionKey: { 39 | paths: ["/id"] 40 | }, 41 | }, 42 | options: {}, 43 | }, { parent: database }); 44 | 45 | const keys = pulumi.all([resourceGroupName, databaseAccount.name]) 46 | .apply(([resourceGroupName, accountName]) => 47 | documentdb.listDatabaseAccountKeys({ resourceGroupName, accountName })); 48 | 49 | const connectionStrings = pulumi.all([resourceGroupName, databaseAccount.name]) 50 | .apply(([resourceGroupName, accountName]) => 51 | documentdb.listDatabaseAccountConnectionStrings({ resourceGroupName, accountName })); 52 | 53 | export const connectionString = connectionStrings.apply(cs => cs.connectionStrings![0].connectionString); 54 | export const endpoint = databaseAccount.documentEndpoint; 55 | export const masterKey = keys.primaryMasterKey; 56 | -------------------------------------------------------------------------------- /03-telemetry/code/step4/eventHub.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as eventhub from "@pulumi/azure-nextgen/eventhub/latest"; 3 | import { appName, location, resourceGroupName } from "./common"; 4 | 5 | const eventHubNamespace = new eventhub.Namespace(`${appName}-ns`, { 6 | resourceGroupName: resourceGroupName, 7 | namespaceName: `${appName}-ns`, 8 | location: location, 9 | sku: { 10 | name: "Standard", 11 | }, 12 | }); 13 | 14 | const eventHub = new eventhub.EventHub(`${appName}-eh`, { 15 | resourceGroupName: resourceGroupName, 16 | namespaceName: eventHubNamespace.name, 17 | eventHubName: `${appName}-eh`, 18 | messageRetentionInDays: 1, 19 | partitionCount: 4, 20 | }, { parent: eventHubNamespace }); 21 | 22 | export const consumerGroupName = "dronetelemetry"; 23 | const consumerGroup = new eventhub.ConsumerGroup(consumerGroupName, { 24 | resourceGroupName: resourceGroupName, 25 | namespaceName: eventHubNamespace.name, 26 | eventHubName: eventHub.name, 27 | consumerGroupName: consumerGroupName, 28 | }, { parent: eventHub }); 29 | 30 | const sendEventSourceKey = new eventhub.EventHubAuthorizationRule("send", { 31 | resourceGroupName: resourceGroupName, 32 | namespaceName: eventHubNamespace.name, 33 | eventHubName: eventHub.name, 34 | authorizationRuleName: "send", 35 | rights: ["send"], 36 | }, { parent: eventHub }); 37 | 38 | const listenEventSourceKey = new eventhub.EventHubAuthorizationRule("listen", { 39 | resourceGroupName: resourceGroupName, 40 | namespaceName: eventHubNamespace.name, 41 | eventHubName: eventHub.name, 42 | authorizationRuleName: "listen", 43 | rights: ["listen"], 44 | }, { parent: eventHub }); 45 | 46 | const sendKeys = pulumi.all([resourceGroupName, eventHubNamespace.name, eventHub.name, sendEventSourceKey.name]) 47 | .apply(([resourceGroupName, namespaceName, eventHubName, authorizationRuleName]) => 48 | eventhub.listEventHubKeys({ 49 | resourceGroupName, 50 | namespaceName, 51 | eventHubName, 52 | authorizationRuleName, 53 | })); 54 | 55 | const listenKeys = pulumi.all([resourceGroupName, eventHubNamespace.name, eventHub.name, listenEventSourceKey.name]) 56 | .apply(([resourceGroupName, namespaceName, eventHubName, authorizationRuleName]) => 57 | eventhub.listEventHubKeys({ 58 | resourceGroupName, 59 | namespaceName, 60 | eventHubName, 61 | authorizationRuleName, 62 | })); 63 | 64 | export const namespace = eventHubNamespace.name; 65 | export const name = eventHub.name; 66 | export const listenConnectionString = listenKeys.primaryConnectionString; 67 | export const sendConnectionString = sendKeys.primaryConnectionString; 68 | -------------------------------------------------------------------------------- /03-telemetry/code/step4/functionApp.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as insights from "@pulumi/azure-nextgen/insights/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | import * as web from "@pulumi/azure-nextgen/web/latest"; 5 | import { appName, location, resourceGroupName } from "./common"; 6 | import * as cosmos from "./cosmos"; 7 | import * as eventHub from "./eventHub"; 8 | 9 | const storageAccountType = { 10 | resourceGroupName: resourceGroupName, 11 | location: location, 12 | sku: { 13 | name: "Standard_LRS", 14 | }, 15 | kind: "StorageV2", 16 | }; 17 | 18 | function getStorageConnectionString(account: storage.StorageAccount): pulumi.Output { 19 | const keys = pulumi.all([resourceGroupName, account.name]).apply(([resourceGroupName, accountName]) => 20 | storage.listStorageAccountKeys({ resourceGroupName, accountName })); 21 | const key = keys.keys[0].value; 22 | return pulumi.interpolate`DefaultEndpointsProtocol=https;AccountName=${account.name};AccountKey=${key}`; 23 | } 24 | 25 | // Drone Telemetry storage account 26 | const droneTelemetryStorageAccount = new storage.StorageAccount(`${appName}sa`, { 27 | accountName: `${appName}funcappsa`, 28 | tags: { 29 | displayName: "Drone Telemetry Function App Storage", 30 | }, 31 | ...storageAccountType, 32 | }); 33 | 34 | // Drone Telemetry DLQ storage account 35 | const droneTelemetryDeadLetterStorageQueueAccount = new storage.StorageAccount(`${appName}dlq`, { 36 | accountName: `${appName}dlqsa`, 37 | tags: { 38 | displayName: "Drone Telemetry DLQ Storage", 39 | }, 40 | ...storageAccountType, 41 | }); 42 | 43 | const droneTelemetryAppInsights = new insights.Component(`${appName}-ai`, { 44 | resourceGroupName: resourceGroupName, 45 | resourceName: `${appName}-ai`, 46 | location: location, 47 | applicationType: "web", 48 | kind: "web", 49 | }); 50 | 51 | const hostingPlan = new web.AppServicePlan(`${appName}-asp`, { 52 | resourceGroupName: resourceGroupName, 53 | name: `${appName}-asp`, 54 | location: location, 55 | sku: { 56 | name: "Y1", 57 | tier: "Dynamic", 58 | }, 59 | }); 60 | 61 | const droneTelemetryFunctionApp = new web.WebApp(`${appName}-app`, { 62 | resourceGroupName: resourceGroupName, 63 | name: "myappdf78s", 64 | location: location, 65 | serverFarmId: hostingPlan.id, 66 | kind: "functionapp", 67 | siteConfig: { 68 | appSettings: [ 69 | { name: "APPINSIGHTS_INSTRUMENTATIONKEY", value: droneTelemetryAppInsights.instrumentationKey }, 70 | { name: "APPLICATIONINSIGHTS_CONNECTION_STRING", value: pulumi.interpolate`InstrumentationKey=${droneTelemetryAppInsights.instrumentationKey}` }, 71 | { name: "ApplicationInsightsAgent_EXTENSION_VERSION", value: "~2" }, 72 | { name: "AzureWebJobsStorage", value: getStorageConnectionString(droneTelemetryStorageAccount) }, 73 | { name: "COSMOSDB_CONNECTION_STRING", value: cosmos.connectionString }, 74 | { name: "CosmosDBEndpoint", value: cosmos.endpoint }, 75 | { name: "CosmosDBKey", value: cosmos.masterKey }, 76 | { name: "COSMOSDB_DATABASE_NAME", value: cosmos.databaseName }, 77 | { name: "COSMOSDB_DATABASE_COL", value: cosmos.collectionName }, 78 | { name: "DeadLetterStorage", value: getStorageConnectionString(droneTelemetryDeadLetterStorageQueueAccount) }, 79 | { name: "EventHubConnection", value: eventHub.listenConnectionString }, 80 | { name: "EventHubConsumerGroup", value: eventHub.consumerGroupName }, 81 | { name: "EventHubName", value: eventHub.name }, 82 | { name: "FUNCTIONS_EXTENSION_VERSION", value: "~3" }, 83 | { name: "FUNCTIONS_WORKER_RUNTIME", value: "dotnet" }, 84 | { name: "WEBSITE_NODE_DEFAULT_VERSION", value: "10.14.1" }, 85 | { name: "WEBSITE_RUN_FROM_PACKAGE", value: "https://mikhailworkshop.blob.core.windows.net/zips/telemetryapp.zip" }, 86 | ] 87 | }, 88 | tags: { 89 | displayName: "Drone Telemetry Function App", 90 | }, 91 | }); 92 | -------------------------------------------------------------------------------- /03-telemetry/code/step4/index.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as eventhub from "@pulumi/azure-nextgen/eventhub/latest"; 3 | import { appName, location, resourceGroupName } from "./common"; 4 | 5 | const eventHubNamespace = new eventhub.Namespace(`${appName}-ns`, { 6 | resourceGroupName: resourceGroupName, 7 | namespaceName: `${appName}-ns`, 8 | location: location, 9 | sku: { 10 | name: "Standard", 11 | }, 12 | }); 13 | 14 | const eventHub = new eventhub.EventHub(`${appName}-eh`, { 15 | resourceGroupName: resourceGroupName, 16 | namespaceName: eventHubNamespace.name, 17 | eventHubName: `${appName}-eh`, 18 | messageRetentionInDays: 1, 19 | partitionCount: 4, 20 | }, { parent: eventHubNamespace }); 21 | 22 | export const consumerGroupName = "dronetelemetry"; 23 | const consumerGroup = new eventhub.ConsumerGroup(consumerGroupName, { 24 | resourceGroupName: resourceGroupName, 25 | namespaceName: eventHubNamespace.name, 26 | eventHubName: eventHub.name, 27 | consumerGroupName: consumerGroupName, 28 | }, { parent: eventHub }); 29 | 30 | const sendEventSourceKey = new eventhub.EventHubAuthorizationRule("send", { 31 | resourceGroupName: resourceGroupName, 32 | namespaceName: eventHubNamespace.name, 33 | eventHubName: eventHub.name, 34 | authorizationRuleName: "send", 35 | rights: ["send"], 36 | }, { parent: eventHub }); 37 | 38 | const listenEventSourceKey = new eventhub.EventHubAuthorizationRule("listen", { 39 | resourceGroupName: resourceGroupName, 40 | namespaceName: eventHubNamespace.name, 41 | eventHubName: eventHub.name, 42 | authorizationRuleName: "listen", 43 | rights: ["listen"], 44 | }, { parent: eventHub }); 45 | 46 | const sendKeys = pulumi.all([resourceGroupName, eventHubNamespace.name, eventHub.name, sendEventSourceKey.name]) 47 | .apply(([resourceGroupName, namespaceName, eventHubName, authorizationRuleName]) => 48 | eventhub.listEventHubKeys({ 49 | resourceGroupName, 50 | namespaceName, 51 | eventHubName, 52 | authorizationRuleName, 53 | })); 54 | 55 | const listenKeys = pulumi.all([resourceGroupName, eventHubNamespace.name, eventHub.name, listenEventSourceKey.name]) 56 | .apply(([resourceGroupName, namespaceName, eventHubName, authorizationRuleName]) => 57 | eventhub.listEventHubKeys({ 58 | resourceGroupName, 59 | namespaceName, 60 | eventHubName, 61 | authorizationRuleName, 62 | })); 63 | 64 | export const namespace = eventHubNamespace.name; 65 | export const name = eventHub.name; 66 | export const listenConnectionString = listenKeys.primaryConnectionString; 67 | export const sendConnectionString = sendKeys.primaryConnectionString; 68 | -------------------------------------------------------------------------------- /04-status/code/step1/common.ts: -------------------------------------------------------------------------------- 1 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 2 | 3 | export const appName = "status"; 4 | 5 | const resourceGroup = new resources.ResourceGroup(`${appName}-rg`, { 6 | resourceGroupName: `${appName}-rg`, 7 | location: "WestEurope", 8 | }); 9 | 10 | export const resourceGroupName = resourceGroup.name; 11 | export const resourceGroupId = resourceGroup.id; 12 | export const location = resourceGroup.location; 13 | -------------------------------------------------------------------------------- /04-status/code/step1/index.ts: -------------------------------------------------------------------------------- 1 | import "./common"; 2 | -------------------------------------------------------------------------------- /04-status/code/step2/common.ts: -------------------------------------------------------------------------------- 1 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 2 | 3 | export const appName = "status"; 4 | 5 | const resourceGroup = new resources.ResourceGroup(`${appName}-rg`, { 6 | resourceGroupName: `${appName}-rg`, 7 | location: "WestEurope", 8 | }); 9 | 10 | export const resourceGroupName = resourceGroup.name; 11 | export const resourceGroupId = resourceGroup.id; 12 | export const location = resourceGroup.location; 13 | -------------------------------------------------------------------------------- /04-status/code/step2/functionApp.ts: -------------------------------------------------------------------------------- 1 | import * as insights from "@pulumi/azure-nextgen/insights/latest"; 2 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 3 | import * as web from "@pulumi/azure-nextgen/web/latest"; 4 | import { appName, location, resourceGroupName } from "./common"; 5 | 6 | const droneStatusStorageAccount = new storage.StorageAccount(`${appName}sa`, { 7 | resourceGroupName: resourceGroupName, 8 | location: location, 9 | accountName: `${appName}sa`, 10 | sku: { 11 | name: "Standard_LRS", 12 | }, 13 | kind: "StorageV2", 14 | tags: { 15 | displayName: "Drone Status Function App", 16 | }, 17 | }); 18 | 19 | const droneStatusAppInsights = new insights.Component(`${appName}-ai`, { 20 | resourceGroupName: resourceGroupName, 21 | resourceName: `${appName}-ai`, 22 | location: location, 23 | applicationType: "web", 24 | kind: "web", 25 | }); 26 | 27 | const hostingPlan = new web.AppServicePlan(`${appName}-asp`, { 28 | resourceGroupName: resourceGroupName, 29 | name: `${appName}-asp`, 30 | location: location, 31 | sku: { 32 | name: "Y1", 33 | tier: "Dynamic", 34 | }, 35 | }); -------------------------------------------------------------------------------- /04-status/code/step2/index.ts: -------------------------------------------------------------------------------- 1 | import "./common"; -------------------------------------------------------------------------------- /04-status/code/step3/common.ts: -------------------------------------------------------------------------------- 1 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 2 | 3 | export const appName = "status"; 4 | 5 | const resourceGroup = new resources.ResourceGroup(`${appName}-rg`, { 6 | resourceGroupName: `${appName}-rg`, 7 | location: "WestEurope", 8 | }); 9 | 10 | export const resourceGroupName = resourceGroup.name; 11 | export const resourceGroupId = resourceGroup.id; 12 | export const location = resourceGroup.location; 13 | -------------------------------------------------------------------------------- /04-status/code/step3/functionApp.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as insights from "@pulumi/azure-nextgen/insights/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | import * as web from "@pulumi/azure-nextgen/web/latest"; 5 | import { appName, location, resourceGroupName } from "./common"; 6 | 7 | const droneStatusStorageAccount = new storage.StorageAccount(`${appName}sa`, { 8 | resourceGroupName: resourceGroupName, 9 | location: location, 10 | accountName: `${appName}sa`, 11 | sku: { 12 | name: "Standard_LRS", 13 | }, 14 | kind: "StorageV2", 15 | tags: { 16 | displayName: "Drone Status Function App", 17 | }, 18 | }); 19 | 20 | const droneStatusAppInsights = new insights.Component(`${appName}-ai`, { 21 | resourceGroupName: resourceGroupName, 22 | resourceName: `${appName}-ai`, 23 | location: location, 24 | applicationType: "web", 25 | kind: "web", 26 | }); 27 | 28 | const hostingPlan = new web.AppServicePlan(`${appName}-asp`, { 29 | resourceGroupName: resourceGroupName, 30 | name: `${appName}-asp`, 31 | location: location, 32 | sku: { 33 | name: "Y1", 34 | tier: "Dynamic", 35 | }, 36 | }); 37 | 38 | const telemetry = new pulumi.StackReference("mikhailshilkov/telemetry/dev"); 39 | const cosmosDatabaseName = telemetry.requireOutput("cosmosDatabaseName"); 40 | const cosmosCollectionName = telemetry.requireOutput("cosmosCollectionName"); 41 | const cosmosConnectionString = telemetry.requireOutput("cosmosConnectionString"); 42 | const cosmosEndpoint = telemetry.requireOutput("cosmosEndpoint"); 43 | const cosmosMasterKey = telemetry.requireOutput("cosmosMasterKey"); 44 | -------------------------------------------------------------------------------- /04-status/code/step3/index.ts: -------------------------------------------------------------------------------- 1 | import "./common"; -------------------------------------------------------------------------------- /04-status/code/step4/common.ts: -------------------------------------------------------------------------------- 1 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 2 | 3 | export const appName = "status"; 4 | 5 | const resourceGroup = new resources.ResourceGroup(`${appName}-rg`, { 6 | resourceGroupName: `${appName}-rg`, 7 | location: "WestEurope", 8 | }); 9 | 10 | export const resourceGroupName = resourceGroup.name; 11 | export const resourceGroupId = resourceGroup.id; 12 | export const location = resourceGroup.location; 13 | -------------------------------------------------------------------------------- /04-status/code/step4/functionApp.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as insights from "@pulumi/azure-nextgen/insights/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | import * as web from "@pulumi/azure-nextgen/web/latest"; 5 | import { appName, location, resourceGroupName } from "./common"; 6 | 7 | const droneStatusStorageAccount = new storage.StorageAccount(`${appName}sa`, { 8 | resourceGroupName: resourceGroupName, 9 | location: location, 10 | accountName: `${appName}sa`, 11 | sku: { 12 | name: "Standard_LRS", 13 | }, 14 | kind: "StorageV2", 15 | tags: { 16 | displayName: "Drone Status Function App", 17 | }, 18 | }); 19 | 20 | function getStorageConnectionString(account: storage.StorageAccount): pulumi.Output { 21 | const keys = pulumi.all([resourceGroupName, account.name]).apply(([resourceGroupName, accountName]) => 22 | storage.listStorageAccountKeys({ resourceGroupName, accountName })); 23 | const key = keys.keys[0].value; 24 | return pulumi.interpolate`DefaultEndpointsProtocol=https;AccountName=${account.name};AccountKey=${key}`; 25 | } 26 | 27 | const droneStatusAppInsights = new insights.Component(`${appName}-ai`, { 28 | resourceGroupName: resourceGroupName, 29 | resourceName: `${appName}-ai`, 30 | location: location, 31 | applicationType: "web", 32 | kind: "web", 33 | }); 34 | 35 | const hostingPlan = new web.AppServicePlan(`${appName}-asp`, { 36 | resourceGroupName: resourceGroupName, 37 | name: `${appName}-asp`, 38 | location: location, 39 | sku: { 40 | name: "Y1", 41 | tier: "Dynamic", 42 | }, 43 | }); 44 | 45 | const telemetry = new pulumi.StackReference("yourusername/telemetry/dev"); 46 | const cosmosDatabaseName = telemetry.requireOutput("cosmosDatabaseName"); 47 | const cosmosCollectionName = telemetry.requireOutput("cosmosCollectionName"); 48 | const cosmosConnectionString = telemetry.requireOutput("cosmosConnectionString"); 49 | const cosmosEndpoint = telemetry.requireOutput("cosmosEndpoint"); 50 | const cosmosMasterKey = telemetry.requireOutput("cosmosMasterKey"); 51 | 52 | const droneStatusFunctionApp = new web.WebApp(`${appName}-app`, { 53 | resourceGroupName: resourceGroupName, 54 | name: `${appName}-app123`, 55 | location: location, 56 | serverFarmId: hostingPlan.id, 57 | kind: "functionapp", 58 | siteConfig: { 59 | appSettings: [ 60 | { name: "APPINSIGHTS_INSTRUMENTATIONKEY", value: droneStatusAppInsights.instrumentationKey }, 61 | { name: "APPLICATIONINSIGHTS_CONNECTION_STRING", value: pulumi.interpolate`InstrumentationKey=${droneStatusAppInsights.instrumentationKey}` }, 62 | { name: "ApplicationInsightsAgent_EXTENSION_VERSION", value: "~2" }, 63 | { name: "AzureWebJobsStorage", value: getStorageConnectionString(droneStatusStorageAccount) }, 64 | { name: "COSMOSDB_CONNECTION_STRING", value: cosmosConnectionString }, 65 | { name: "CosmosDBEndpoint", value: cosmosEndpoint }, 66 | { name: "CosmosDBKey", value: cosmosMasterKey }, 67 | { name: "COSMOSDB_DATABASE_NAME", value: cosmosDatabaseName }, 68 | { name: "COSMOSDB_DATABASE_COL", value: cosmosCollectionName }, 69 | { name: "FUNCTIONS_EXTENSION_VERSION", value: "~3" }, 70 | { name: "FUNCTIONS_WORKER_RUNTIME", value: "dotnet" }, 71 | { name: "WEBSITE_NODE_DEFAULT_VERSION", value: "10.14.1" }, 72 | { name: "WEBSITE_RUN_FROM_PACKAGE", value: "https://mikhailworkshop.blob.core.windows.net/zips/statusapp.zip" }, 73 | ], 74 | cors: { 75 | allowedOrigins: ["*"], 76 | }, 77 | }, 78 | tags: { 79 | displayName: "Drone Telemetry Function App", 80 | }, 81 | }); 82 | 83 | export const functionUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api/GetStatusFunction?deviceId=`; 84 | -------------------------------------------------------------------------------- /04-status/code/step4/index.ts: -------------------------------------------------------------------------------- 1 | import "./common"; 2 | 3 | import * as functions from "./functionApp"; 4 | 5 | export const functionUrl = functions.functionUrl; 6 | -------------------------------------------------------------------------------- /05-frontend/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikhailshilkov/azure-serverless-workshop/031b7807a1d0835f683336239469be0d14800cd2/05-frontend/.DS_Store -------------------------------------------------------------------------------- /05-frontend/code/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikhailshilkov/azure-serverless-workshop/031b7807a1d0835f683336239469be0d14800cd2/05-frontend/code/.DS_Store -------------------------------------------------------------------------------- /05-frontend/code/step2/common.ts: -------------------------------------------------------------------------------- 1 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 2 | 3 | export const appName = "status"; 4 | 5 | const resourceGroup = new resources.ResourceGroup(`${appName}-rg`, { 6 | resourceGroupName: `${appName}-rg`, 7 | location: "WestEurope", 8 | }); 9 | 10 | export const resourceGroupName = resourceGroup.name; 11 | export const resourceGroupId = resourceGroup.id; 12 | export const location = resourceGroup.location; 13 | -------------------------------------------------------------------------------- /05-frontend/code/step2/functionApp.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as insights from "@pulumi/azure-nextgen/insights/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | import * as web from "@pulumi/azure-nextgen/web/latest"; 5 | import { appName, location, resourceGroupName } from "./common"; 6 | 7 | const droneStatusStorageAccount = new storage.StorageAccount(`${appName}sa`, { 8 | resourceGroupName: resourceGroupName, 9 | location: location, 10 | accountName: `${appName}sa`, 11 | sku: { 12 | name: "Standard_LRS", 13 | }, 14 | kind: "StorageV2", 15 | tags: { 16 | displayName: "Drone Status Function App", 17 | }, 18 | }); 19 | 20 | function getStorageConnectionString(account: storage.StorageAccount): pulumi.Output { 21 | const keys = pulumi.all([resourceGroupName, account.name]).apply(([resourceGroupName, accountName]) => 22 | storage.listStorageAccountKeys({ resourceGroupName, accountName })); 23 | const key = keys.keys[0].value; 24 | return pulumi.interpolate`DefaultEndpointsProtocol=https;AccountName=${account.name};AccountKey=${key}`; 25 | } 26 | 27 | const droneStatusAppInsights = new insights.Component(`${appName}-ai`, { 28 | resourceGroupName: resourceGroupName, 29 | resourceName: `${appName}-ai`, 30 | location: location, 31 | applicationType: "web", 32 | kind: "web", 33 | }); 34 | 35 | const hostingPlan = new web.AppServicePlan(`${appName}-asp`, { 36 | resourceGroupName: resourceGroupName, 37 | name: `${appName}-asp`, 38 | location: location, 39 | sku: { 40 | name: "Y1", 41 | tier: "Dynamic", 42 | }, 43 | }); 44 | 45 | const telemetry = new pulumi.StackReference("mikhailshilkov/telemetry-nextgen/dev"); 46 | const cosmosDatabaseName = telemetry.requireOutput("cosmosDatabaseName"); 47 | const cosmosCollectionName = telemetry.requireOutput("cosmosCollectionName"); 48 | const cosmosConnectionString = telemetry.requireOutput("cosmosConnectionString"); 49 | const cosmosEndpoint = telemetry.requireOutput("cosmosEndpoint"); 50 | const cosmosMasterKey = telemetry.requireOutput("cosmosMasterKey"); 51 | 52 | const droneStatusFunctionApp = new web.WebApp(`${appName}-app`, { 53 | resourceGroupName: resourceGroupName, 54 | name: `${appName}-app123`, 55 | location: location, 56 | serverFarmId: hostingPlan.id, 57 | kind: "functionapp", 58 | siteConfig: { 59 | appSettings: [ 60 | { name: "APPINSIGHTS_INSTRUMENTATIONKEY", value: droneStatusAppInsights.instrumentationKey }, 61 | { name: "APPLICATIONINSIGHTS_CONNECTION_STRING", value: pulumi.interpolate`InstrumentationKey=${droneStatusAppInsights.instrumentationKey}` }, 62 | { name: "ApplicationInsightsAgent_EXTENSION_VERSION", value: "~2" }, 63 | { name: "AzureWebJobsStorage", value: getStorageConnectionString(droneStatusStorageAccount) }, 64 | { name: "COSMOSDB_CONNECTION_STRING", value: cosmosConnectionString }, 65 | { name: "CosmosDBEndpoint", value: cosmosEndpoint }, 66 | { name: "CosmosDBKey", value: cosmosMasterKey }, 67 | { name: "COSMOSDB_DATABASE_NAME", value: cosmosDatabaseName }, 68 | { name: "COSMOSDB_DATABASE_COL", value: cosmosCollectionName }, 69 | { name: "FUNCTIONS_EXTENSION_VERSION", value: "~3" }, 70 | { name: "FUNCTIONS_WORKER_RUNTIME", value: "dotnet" }, 71 | { name: "WEBSITE_NODE_DEFAULT_VERSION", value: "10.14.1" }, 72 | { name: "WEBSITE_RUN_FROM_PACKAGE", value: "https://mikhailworkshop.blob.core.windows.net/zips/statusapp.zip" }, 73 | ], 74 | cors: { 75 | allowedOrigins: ["*"], 76 | }, 77 | }, 78 | tags: { 79 | displayName: "Drone Telemetry Function App", 80 | }, 81 | }); 82 | 83 | export const functionUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api/GetStatusFunction?deviceId=`; 84 | -------------------------------------------------------------------------------- /05-frontend/code/step2/index.ts: -------------------------------------------------------------------------------- 1 | import "./common"; 2 | 3 | import * as functions from "./functionApp"; 4 | import * as website from "./website"; 5 | 6 | export const functionUrl = functions.functionUrl; 7 | export const storageAccountUrl = website.storageAccountUrl; 8 | -------------------------------------------------------------------------------- /05-frontend/code/step2/website.ts: -------------------------------------------------------------------------------- 1 | import * as azure from "@pulumi/azure"; 2 | import { appName, resourceGroupName } from "./common"; 3 | 4 | export const storageAccount = new azure.storage.Account(`${appName}fe`, { 5 | resourceGroupName: resourceGroupName, 6 | tags: { 7 | displayName: "Drone Front End Storage Account", 8 | }, 9 | accountTier: "Standard", 10 | accountReplicationType: "LRS", 11 | staticWebsite: { 12 | indexDocument: "index.html", 13 | error404Document: "404.html", 14 | }, 15 | }); 16 | 17 | export const storageAccountUrl = storageAccount.primaryWebEndpoint; 18 | -------------------------------------------------------------------------------- /05-frontend/code/step4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "statusapp", 3 | "devDependencies": { 4 | "@types/node": "^10.0.0" 5 | }, 6 | "dependencies": { 7 | "@pulumi/azure": "^3.0.0", 8 | "@pulumi/azure-nextgen": "^0.2.2", 9 | "@pulumi/pulumi": "^2.0.0", 10 | "@types/mime": "^2.0.3", 11 | "@types/node-dir": "0.0.33", 12 | "mime": "^2.4.6", 13 | "node-dir": "^0.1.17" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /05-frontend/code/step5/common.ts: -------------------------------------------------------------------------------- 1 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 2 | 3 | export const appName = "status"; 4 | 5 | const resourceGroup = new resources.ResourceGroup(`${appName}-rg`, { 6 | resourceGroupName: `${appName}-rg`, 7 | location: "WestEurope", 8 | }); 9 | 10 | export const resourceGroupName = resourceGroup.name; 11 | export const resourceGroupId = resourceGroup.id; 12 | export const location = resourceGroup.location; 13 | -------------------------------------------------------------------------------- /05-frontend/code/step5/functionApp.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as insights from "@pulumi/azure-nextgen/insights/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | import * as web from "@pulumi/azure-nextgen/web/latest"; 5 | import { appName, location, resourceGroupName } from "./common"; 6 | 7 | const droneStatusStorageAccount = new storage.StorageAccount(`${appName}sa`, { 8 | resourceGroupName: resourceGroupName, 9 | location: location, 10 | accountName: `${appName}sa`, 11 | sku: { 12 | name: "Standard_LRS", 13 | }, 14 | kind: "StorageV2", 15 | tags: { 16 | displayName: "Drone Status Function App", 17 | }, 18 | }); 19 | 20 | function getStorageConnectionString(account: storage.StorageAccount): pulumi.Output { 21 | const keys = pulumi.all([resourceGroupName, account.name]).apply(([resourceGroupName, accountName]) => 22 | storage.listStorageAccountKeys({ resourceGroupName, accountName })); 23 | const key = keys.keys[0].value; 24 | return pulumi.interpolate`DefaultEndpointsProtocol=https;AccountName=${account.name};AccountKey=${key}`; 25 | } 26 | 27 | const droneStatusAppInsights = new insights.Component(`${appName}-ai`, { 28 | resourceGroupName: resourceGroupName, 29 | resourceName: `${appName}-ai`, 30 | location: location, 31 | applicationType: "web", 32 | kind: "web", 33 | }); 34 | 35 | const hostingPlan = new web.AppServicePlan(`${appName}-asp`, { 36 | resourceGroupName: resourceGroupName, 37 | name: `${appName}-asp`, 38 | location: location, 39 | sku: { 40 | name: "Y1", 41 | tier: "Dynamic", 42 | }, 43 | }); 44 | 45 | const telemetry = new pulumi.StackReference("mikhailshilkov/telemetry-nextgen/dev"); 46 | const cosmosDatabaseName = telemetry.requireOutput("cosmosDatabaseName"); 47 | const cosmosCollectionName = telemetry.requireOutput("cosmosCollectionName"); 48 | const cosmosConnectionString = telemetry.requireOutput("cosmosConnectionString"); 49 | const cosmosEndpoint = telemetry.requireOutput("cosmosEndpoint"); 50 | const cosmosMasterKey = telemetry.requireOutput("cosmosMasterKey"); 51 | 52 | const droneStatusFunctionApp = new web.WebApp(`${appName}-app`, { 53 | resourceGroupName: resourceGroupName, 54 | name: `${appName}-app123`, 55 | location: location, 56 | serverFarmId: hostingPlan.id, 57 | kind: "functionapp", 58 | siteConfig: { 59 | appSettings: [ 60 | { name: "APPINSIGHTS_INSTRUMENTATIONKEY", value: droneStatusAppInsights.instrumentationKey }, 61 | { name: "APPLICATIONINSIGHTS_CONNECTION_STRING", value: pulumi.interpolate`InstrumentationKey=${droneStatusAppInsights.instrumentationKey}` }, 62 | { name: "ApplicationInsightsAgent_EXTENSION_VERSION", value: "~2" }, 63 | { name: "AzureWebJobsStorage", value: getStorageConnectionString(droneStatusStorageAccount) }, 64 | { name: "COSMOSDB_CONNECTION_STRING", value: cosmosConnectionString }, 65 | { name: "CosmosDBEndpoint", value: cosmosEndpoint }, 66 | { name: "CosmosDBKey", value: cosmosMasterKey }, 67 | { name: "COSMOSDB_DATABASE_NAME", value: cosmosDatabaseName }, 68 | { name: "COSMOSDB_DATABASE_COL", value: cosmosCollectionName }, 69 | { name: "FUNCTIONS_EXTENSION_VERSION", value: "~3" }, 70 | { name: "FUNCTIONS_WORKER_RUNTIME", value: "dotnet" }, 71 | { name: "WEBSITE_NODE_DEFAULT_VERSION", value: "10.14.1" }, 72 | { name: "WEBSITE_RUN_FROM_PACKAGE", value: "https://mikhailworkshop.blob.core.windows.net/zips/statusapp.zip" }, 73 | ], 74 | cors: { 75 | allowedOrigins: ["*"], 76 | }, 77 | }, 78 | tags: { 79 | displayName: "Drone Telemetry Function App", 80 | }, 81 | }); 82 | 83 | export const functionUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api/GetStatusFunction?deviceId=`; 84 | -------------------------------------------------------------------------------- /05-frontend/code/step5/index.ts: -------------------------------------------------------------------------------- 1 | import "./common"; 2 | import "./websiteFiles"; 3 | 4 | import * as functions from "./functionApp"; 5 | import * as website from "./website"; 6 | 7 | export const functionUrl = functions.functionUrl; 8 | export const storageAccountUrl = website.storageAccountUrl; 9 | -------------------------------------------------------------------------------- /05-frontend/code/step5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "statusapp", 3 | "devDependencies": { 4 | "@types/node": "^10.0.0" 5 | }, 6 | "dependencies": { 7 | "@pulumi/azure": "^3.0.0", 8 | "@pulumi/azure-nextgen": "^0.2.2", 9 | "@pulumi/pulumi": "^2.0.0", 10 | "@types/mime": "^2.0.3", 11 | "@types/node-dir": "0.0.33", 12 | "mime": "^2.4.6", 13 | "node-dir": "^0.1.17" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /05-frontend/code/step5/website.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as azure from "@pulumi/azure"; 3 | import * as website from "./website"; 4 | import * as functionApp from "./functionApp"; 5 | import * as mime from "mime"; 6 | import * as nodedir from "node-dir"; 7 | import * as fs from "fs"; 8 | 9 | const folderName = "droneapp-noauth"; 10 | const files = nodedir.files(folderName, { sync: true }); 11 | for (const file of files) { 12 | const name = file.substring(folderName.length+1); 13 | const contentType = mime.getType(file) || undefined; 14 | 15 | const rawText = fs.readFileSync(file, "utf8").toString(); 16 | const asset = functionApp.functionUrl 17 | .apply(url => rawText.replace("[API_URL]", url)) 18 | .apply(text => new pulumi.asset.StringAsset(text)); 19 | 20 | const myObject = new azure.storage.Blob(name, { 21 | name, 22 | storageAccountName: website.storageAccount.name, 23 | storageContainerName: "$web", 24 | type: "Block", 25 | source: asset, 26 | contentType, 27 | }, { parent: website.storageAccount }); 28 | } 29 | -------------------------------------------------------------------------------- /05-frontend/code/step5/websiteFiles.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as azure from "@pulumi/azure"; 3 | import * as website from "./website"; 4 | import * as functionApp from "./functionApp"; 5 | import * as mime from "mime"; 6 | import * as nodedir from "node-dir"; 7 | import * as fs from "fs"; 8 | 9 | const folderName = "droneapp-noauth"; 10 | const files = nodedir.files(folderName, { sync: true }); 11 | for (const file of files) { 12 | const name = file.substring(folderName.length+1); 13 | const contentType = mime.getType(file) || undefined; 14 | 15 | const rawText = fs.readFileSync(file, "utf8").toString(); 16 | const asset = functionApp.functionUrl 17 | .apply(url => rawText.replace("[API_URL]", url)) 18 | .apply(text => new pulumi.asset.StringAsset(text)); 19 | 20 | const myObject = new azure.storage.Blob(name, { 21 | name, 22 | storageAccountName: website.storageAccount.name, 23 | storageContainerName: "$web", 24 | type: "Block", 25 | source: asset, 26 | contentType, 27 | }, { parent: website.storageAccount }); 28 | } 29 | -------------------------------------------------------------------------------- /05-frontend/img/dronesite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikhailshilkov/azure-serverless-workshop/031b7807a1d0835f683336239469be0d14800cd2/05-frontend/img/dronesite.png -------------------------------------------------------------------------------- /06-api/code/step1/common.ts: -------------------------------------------------------------------------------- 1 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 2 | 3 | export const appName = "status"; 4 | 5 | const resourceGroup = new resources.ResourceGroup(`${appName}-rg`, { 6 | resourceGroupName: `${appName}-rg`, 7 | location: "WestEurope", 8 | }); 9 | 10 | export const resourceGroupName = resourceGroup.name; 11 | export const resourceGroupId = resourceGroup.id; 12 | export const location = resourceGroup.location; 13 | -------------------------------------------------------------------------------- /06-api/code/step1/functionApp.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as insights from "@pulumi/azure-nextgen/insights/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | import * as web from "@pulumi/azure-nextgen/web/latest"; 5 | import { appName, location, resourceGroupName } from "./common"; 6 | 7 | const droneStatusStorageAccount = new storage.StorageAccount(`${appName}sa`, { 8 | resourceGroupName: resourceGroupName, 9 | location: location, 10 | accountName: `${appName}sa`, 11 | sku: { 12 | name: "Standard_LRS", 13 | }, 14 | kind: "StorageV2", 15 | tags: { 16 | displayName: "Drone Status Function App", 17 | }, 18 | }); 19 | 20 | function getStorageConnectionString(account: storage.StorageAccount): pulumi.Output { 21 | const keys = pulumi.all([resourceGroupName, account.name]).apply(([resourceGroupName, accountName]) => 22 | storage.listStorageAccountKeys({ resourceGroupName, accountName })); 23 | const key = keys.keys[0].value; 24 | return pulumi.interpolate`DefaultEndpointsProtocol=https;AccountName=${account.name};AccountKey=${key}`; 25 | } 26 | 27 | const droneStatusAppInsights = new insights.Component(`${appName}-ai`, { 28 | resourceGroupName: resourceGroupName, 29 | resourceName: `${appName}-ai`, 30 | location: location, 31 | applicationType: "web", 32 | kind: "web", 33 | }); 34 | 35 | const hostingPlan = new web.AppServicePlan(`${appName}-asp`, { 36 | resourceGroupName: resourceGroupName, 37 | name: `${appName}-asp`, 38 | location: location, 39 | sku: { 40 | name: "Y1", 41 | tier: "Dynamic", 42 | }, 43 | }); 44 | 45 | const telemetry = new pulumi.StackReference("mikhailshilkov/telemetry-nextgen/dev"); 46 | const cosmosDatabaseName = telemetry.requireOutput("cosmosDatabaseName"); 47 | const cosmosCollectionName = telemetry.requireOutput("cosmosCollectionName"); 48 | const cosmosConnectionString = telemetry.requireOutput("cosmosConnectionString"); 49 | const cosmosEndpoint = telemetry.requireOutput("cosmosEndpoint"); 50 | const cosmosMasterKey = telemetry.requireOutput("cosmosMasterKey"); 51 | 52 | const droneStatusFunctionApp = new web.WebApp(`${appName}-app`, { 53 | resourceGroupName: resourceGroupName, 54 | name: `${appName}-app`, 55 | location: location, 56 | serverFarmId: hostingPlan.id, 57 | kind: "functionapp", 58 | siteConfig: { 59 | appSettings: [ 60 | { name: "APPINSIGHTS_INSTRUMENTATIONKEY", value: droneStatusAppInsights.instrumentationKey }, 61 | { name: "APPLICATIONINSIGHTS_CONNECTION_STRING", value: pulumi.interpolate`InstrumentationKey=${droneStatusAppInsights.instrumentationKey}` }, 62 | { name: "ApplicationInsightsAgent_EXTENSION_VERSION", value: "~2" }, 63 | { name: "AzureWebJobsStorage", value: getStorageConnectionString(droneStatusStorageAccount) }, 64 | { name: "COSMOSDB_CONNECTION_STRING", value: cosmosConnectionString }, 65 | { name: "CosmosDBEndpoint", value: cosmosEndpoint }, 66 | { name: "CosmosDBKey", value: cosmosMasterKey }, 67 | { name: "COSMOSDB_DATABASE_NAME", value: cosmosDatabaseName }, 68 | { name: "COSMOSDB_DATABASE_COL", value: cosmosCollectionName }, 69 | { name: "FUNCTIONS_EXTENSION_VERSION", value: "~3" }, 70 | { name: "FUNCTIONS_WORKER_RUNTIME", value: "dotnet" }, 71 | { name: "WEBSITE_NODE_DEFAULT_VERSION", value: "10.14.1" }, 72 | { name: "WEBSITE_RUN_FROM_PACKAGE", value: "https://mikhailworkshop.blob.core.windows.net/zips/statusapp.zip" }, 73 | ], 74 | cors: { 75 | allowedOrigins: ["*"], 76 | }, 77 | }, 78 | tags: { 79 | displayName: "Drone Telemetry Function App", 80 | }, 81 | }); 82 | 83 | export const functionUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api/GetStatusFunction?deviceId=`; 84 | export const id = droneStatusFunctionApp.id; 85 | export const appUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api`; 86 | -------------------------------------------------------------------------------- /06-api/code/step1/index.ts: -------------------------------------------------------------------------------- 1 | import "./common"; 2 | import "./websiteFiles"; 3 | 4 | import * as functions from "./functionApp"; 5 | import * as website from "./website"; 6 | 7 | export const functionUrl = functions.functionUrl; 8 | export const storageAccountUrl = website.storageAccountUrl; 9 | -------------------------------------------------------------------------------- /06-api/code/step1/website.ts: -------------------------------------------------------------------------------- 1 | import * as azure from "@pulumi/azure"; 2 | import { appName, resourceGroupName } from "./common"; 3 | 4 | export const storageAccount = new azure.storage.Account(`${appName}fe`, { 5 | resourceGroupName: resourceGroupName, 6 | tags: { 7 | displayName: "Drone Front End Storage Account", 8 | }, 9 | accountTier: "Standard", 10 | accountReplicationType: "LRS", 11 | staticWebsite: { 12 | indexDocument: "index.html", 13 | error404Document: "404.html", 14 | }, 15 | }); 16 | 17 | export const storageAccountUrl = storageAccount.primaryWebEndpoint; 18 | -------------------------------------------------------------------------------- /06-api/code/step1/websiteFiles.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as azure from "@pulumi/azure"; 3 | import * as website from "./website"; 4 | import * as functionApp from "./functionApp"; 5 | import * as mime from "mime"; 6 | import * as nodedir from "node-dir"; 7 | import * as fs from "fs"; 8 | 9 | const folderName = "droneapp-noauth"; 10 | const files = nodedir.files(folderName, { sync: true }); 11 | for (const file of files) { 12 | const name = file.substring(folderName.length+1); 13 | const contentType = mime.getType(file) || undefined; 14 | 15 | const rawText = fs.readFileSync(file, "utf8").toString(); 16 | const asset = functionApp.functionUrl 17 | .apply(url => rawText.replace("[API_URL]", url)) 18 | .apply(text => new pulumi.asset.StringAsset(text)); 19 | 20 | const myObject = new azure.storage.Blob(name, { 21 | name, 22 | storageAccountName: website.storageAccount.name, 23 | storageContainerName: "$web", 24 | type: "Block", 25 | source: asset, 26 | contentType, 27 | }, { parent: website.storageAccount }); 28 | } 29 | -------------------------------------------------------------------------------- /06-api/code/step3/api.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as apimanagement from "@pulumi/azure-nextgen/apimanagement/latest"; 3 | import * as azure from "@pulumi/azure"; 4 | import { appName, location, resourceGroupName } from "./common"; 5 | import * as functionApp from "./functionApp"; 6 | import * as website from "./website"; 7 | 8 | const apiManagementName = `${appName}-apim`; 9 | const apiManagement = new apimanagement.ApiManagementService(apiManagementName, { 10 | resourceGroupName: resourceGroupName, 11 | serviceName: apiManagementName, 12 | location: location, 13 | sku: { 14 | name: "Consumption", 15 | capacity: 0, 16 | }, 17 | publisherEmail: "drones@contoso.com", 18 | publisherName: "contoso", 19 | }); 20 | const apiManagementId = apiManagement.id; 21 | -------------------------------------------------------------------------------- /06-api/code/step3/common.ts: -------------------------------------------------------------------------------- 1 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 2 | 3 | export const appName = "status"; 4 | 5 | const resourceGroup = new resources.ResourceGroup(`${appName}-rg`, { 6 | resourceGroupName: `${appName}-rg`, 7 | location: "WestEurope", 8 | }); 9 | 10 | export const resourceGroupName = resourceGroup.name; 11 | export const resourceGroupId = resourceGroup.id; 12 | export const location = resourceGroup.location; 13 | -------------------------------------------------------------------------------- /06-api/code/step3/functionApp.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as insights from "@pulumi/azure-nextgen/insights/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | import * as web from "@pulumi/azure-nextgen/web/latest"; 5 | import { appName, location, resourceGroupName } from "./common"; 6 | 7 | const droneStatusStorageAccount = new storage.StorageAccount(`${appName}sa`, { 8 | resourceGroupName: resourceGroupName, 9 | location: location, 10 | accountName: `${appName}sa`, 11 | sku: { 12 | name: "Standard_LRS", 13 | }, 14 | kind: "StorageV2", 15 | tags: { 16 | displayName: "Drone Status Function App", 17 | }, 18 | }); 19 | 20 | function getStorageConnectionString(account: storage.StorageAccount): pulumi.Output { 21 | const keys = pulumi.all([resourceGroupName, account.name]).apply(([resourceGroupName, accountName]) => 22 | storage.listStorageAccountKeys({ resourceGroupName, accountName })); 23 | const key = keys.keys[0].value; 24 | return pulumi.interpolate`DefaultEndpointsProtocol=https;AccountName=${account.name};AccountKey=${key}`; 25 | } 26 | 27 | const droneStatusAppInsights = new insights.Component(`${appName}-ai`, { 28 | resourceGroupName: resourceGroupName, 29 | resourceName: `${appName}-ai`, 30 | location: location, 31 | applicationType: "web", 32 | kind: "web", 33 | }); 34 | 35 | const hostingPlan = new web.AppServicePlan(`${appName}-asp`, { 36 | resourceGroupName: resourceGroupName, 37 | name: `${appName}-asp`, 38 | location: location, 39 | sku: { 40 | name: "Y1", 41 | tier: "Dynamic", 42 | }, 43 | }); 44 | 45 | const telemetry = new pulumi.StackReference("mikhailshilkov/telemetry-nextgen/dev"); 46 | const cosmosDatabaseName = telemetry.requireOutput("cosmosDatabaseName"); 47 | const cosmosCollectionName = telemetry.requireOutput("cosmosCollectionName"); 48 | const cosmosConnectionString = telemetry.requireOutput("cosmosConnectionString"); 49 | const cosmosEndpoint = telemetry.requireOutput("cosmosEndpoint"); 50 | const cosmosMasterKey = telemetry.requireOutput("cosmosMasterKey"); 51 | 52 | const droneStatusFunctionApp = new web.WebApp(`${appName}-app`, { 53 | resourceGroupName: resourceGroupName, 54 | name: `${appName}-app`, 55 | location: location, 56 | serverFarmId: hostingPlan.id, 57 | kind: "functionapp", 58 | siteConfig: { 59 | appSettings: [ 60 | { name: "APPINSIGHTS_INSTRUMENTATIONKEY", value: droneStatusAppInsights.instrumentationKey }, 61 | { name: "APPLICATIONINSIGHTS_CONNECTION_STRING", value: pulumi.interpolate`InstrumentationKey=${droneStatusAppInsights.instrumentationKey}` }, 62 | { name: "ApplicationInsightsAgent_EXTENSION_VERSION", value: "~2" }, 63 | { name: "AzureWebJobsStorage", value: getStorageConnectionString(droneStatusStorageAccount) }, 64 | { name: "COSMOSDB_CONNECTION_STRING", value: cosmosConnectionString }, 65 | { name: "CosmosDBEndpoint", value: cosmosEndpoint }, 66 | { name: "CosmosDBKey", value: cosmosMasterKey }, 67 | { name: "COSMOSDB_DATABASE_NAME", value: cosmosDatabaseName }, 68 | { name: "COSMOSDB_DATABASE_COL", value: cosmosCollectionName }, 69 | { name: "FUNCTIONS_EXTENSION_VERSION", value: "~3" }, 70 | { name: "FUNCTIONS_WORKER_RUNTIME", value: "dotnet" }, 71 | { name: "WEBSITE_NODE_DEFAULT_VERSION", value: "10.14.1" }, 72 | { name: "WEBSITE_RUN_FROM_PACKAGE", value: "https://mikhailworkshop.blob.core.windows.net/zips/statusapp.zip" }, 73 | ], 74 | cors: { 75 | allowedOrigins: ["*"], 76 | }, 77 | }, 78 | tags: { 79 | displayName: "Drone Telemetry Function App", 80 | }, 81 | }); 82 | 83 | export const functionUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api/GetStatusFunction?deviceId=`; 84 | export const id = droneStatusFunctionApp.id; 85 | export const appUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api`; 86 | -------------------------------------------------------------------------------- /06-api/code/step3/index.ts: -------------------------------------------------------------------------------- 1 | import "./common"; 2 | import "./websiteFiles"; 3 | 4 | import * as functions from "./functionApp"; 5 | import * as website from "./website"; 6 | 7 | export const functionUrl = functions.functionUrl; 8 | export const storageAccountUrl = website.storageAccountUrl; 9 | -------------------------------------------------------------------------------- /06-api/code/step3/website.ts: -------------------------------------------------------------------------------- 1 | import * as azure from "@pulumi/azure"; 2 | import { appName, resourceGroupName } from "./common"; 3 | 4 | export const storageAccount = new azure.storage.Account(`${appName}fe`, { 5 | resourceGroupName: resourceGroupName, 6 | tags: { 7 | displayName: "Drone Front End Storage Account", 8 | }, 9 | accountTier: "Standard", 10 | accountReplicationType: "LRS", 11 | staticWebsite: { 12 | indexDocument: "index.html", 13 | error404Document: "404.html", 14 | }, 15 | }); 16 | 17 | export const storageAccountUrl = storageAccount.primaryWebEndpoint; 18 | -------------------------------------------------------------------------------- /06-api/code/step3/websiteFiles.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as azure from "@pulumi/azure"; 3 | import * as website from "./website"; 4 | import * as functionApp from "./functionApp"; 5 | import * as mime from "mime"; 6 | import * as nodedir from "node-dir"; 7 | import * as fs from "fs"; 8 | 9 | const folderName = "droneapp-noauth"; 10 | const files = nodedir.files(folderName, { sync: true }); 11 | for (const file of files) { 12 | const name = file.substring(folderName.length+1); 13 | const contentType = mime.getType(file) || undefined; 14 | 15 | const rawText = fs.readFileSync(file, "utf8").toString(); 16 | const asset = functionApp.functionUrl 17 | .apply(url => rawText.replace("[API_URL]", url)) 18 | .apply(text => new pulumi.asset.StringAsset(text)); 19 | 20 | const myObject = new azure.storage.Blob(name, { 21 | name, 22 | storageAccountName: website.storageAccount.name, 23 | storageContainerName: "$web", 24 | type: "Block", 25 | source: asset, 26 | contentType, 27 | }, { parent: website.storageAccount }); 28 | } 29 | -------------------------------------------------------------------------------- /06-api/code/step4/api.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as apimanagement from "@pulumi/azure-nextgen/apimanagement/latest"; 3 | import * as azure from "@pulumi/azure"; 4 | import { appName, location, resourceGroupName } from "./common"; 5 | import * as functionApp from "./functionApp"; 6 | import * as website from "./website"; 7 | 8 | const apiManagementName = `${appName}-apim`; 9 | const apiManagement = new apimanagement.ApiManagementService(apiManagementName, { 10 | resourceGroupName: resourceGroupName, 11 | serviceName: apiManagementName, 12 | location: location, 13 | sku: { 14 | name: "Consumption", 15 | capacity: 0, 16 | }, 17 | publisherEmail: "drones@contoso.com", 18 | publisherName: "contoso", 19 | }); 20 | const apiManagementId = apiManagement.id; 21 | 22 | const versionSet = new apimanagement.ApiVersionSet("dronestatusversionset", { 23 | resourceGroupName: resourceGroupName, 24 | serviceName: apiManagement.name, 25 | versionSetId: "dronestatusversionset", 26 | displayName: "Drone Delivery API", 27 | versioningScheme: "Segment", 28 | }); 29 | 30 | const api = new apimanagement.Api("dronedeliveryapiv1", { 31 | resourceGroupName: resourceGroupName, 32 | serviceName: apiManagementName, 33 | apiId: "dronedeliveryapiv1", 34 | displayName: "Drone Delivery API", 35 | description: "Drone Delivery API", 36 | path: "api", 37 | apiVersion: "v1", 38 | apiRevision: "1", 39 | apiVersionSetId: versionSet.id, 40 | protocols: ["https"], 41 | }); 42 | 43 | const apiOperation = new apimanagement.ApiOperation("dronestatusGET", { 44 | resourceGroupName: resourceGroupName, 45 | serviceName: apiManagementName, 46 | apiId: api.name, 47 | operationId: "dronestatusGET", 48 | displayName: "Retrieve drone status", 49 | description: "Retrieve drone status", 50 | method: "GET", 51 | urlTemplate: "/dronestatus/{deviceid}", 52 | templateParameters: [ 53 | { 54 | name: "deviceid", 55 | description: "device id", 56 | type: "string", 57 | required: true, 58 | }, 59 | ], 60 | }); 61 | 62 | const backend = new apimanagement.Backend("dronestatusdotnet", { 63 | resourceGroupName: resourceGroupName, 64 | serviceName: apiManagementName, 65 | backendId: "dronestatusdotnet", 66 | resourceId: pulumi.interpolate`https://management.azure.com/${functionApp.id}`, 67 | // credentials: { 68 | // query: { 69 | // code: pulumi.interpolate`{{${apiValueFunctionCode.name}}}`, 70 | // }, 71 | // }, 72 | url: functionApp.appUrl, 73 | protocol: "http", 74 | }); 75 | 76 | const apiPolicy = new apimanagement.ApiPolicy("policy", { 77 | resourceGroupName: resourceGroupName, 78 | serviceName: apiManagementName, 79 | apiId: api.name, 80 | policyId: "policy", 81 | value: pulumi.interpolate` 82 | 83 | 84 | 85 | 86 | 87 | ${website.storageAccountUrl} 88 | 89 | GET 90 |
*
91 |
92 | 93 | 94 |
95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |
`, 105 | }); 106 | 107 | const product = new apimanagement.Product("dronedeliveryprodapi", { 108 | resourceGroupName: resourceGroupName, 109 | serviceName: apiManagementName, 110 | productId: "dronedeliveryprodapi", 111 | displayName: "drone delivery product api", 112 | description: "drone delivery product api", 113 | terms: "terms for example product", 114 | subscriptionRequired: false, 115 | state: "published", 116 | }); 117 | 118 | const productApi = new azure.apimanagement.ProductApi("dronedeliveryapiv1", { 119 | resourceGroupName: resourceGroupName, 120 | apiManagementName: apiManagementName, 121 | apiName: api.name, 122 | productId: product.name, 123 | }); 124 | 125 | export const apiUrl = pulumi.interpolate`https://${apiManagementName}.azure-api.net/${api.path}/v1/dronestatus/`; 126 | -------------------------------------------------------------------------------- /06-api/code/step4/common.ts: -------------------------------------------------------------------------------- 1 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 2 | 3 | export const appName = "status"; 4 | 5 | const resourceGroup = new resources.ResourceGroup(`${appName}-rg`, { 6 | resourceGroupName: `${appName}-rg`, 7 | location: "WestEurope", 8 | }); 9 | 10 | export const resourceGroupName = resourceGroup.name; 11 | export const resourceGroupId = resourceGroup.id; 12 | export const location = resourceGroup.location; 13 | -------------------------------------------------------------------------------- /06-api/code/step4/functionApp.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as insights from "@pulumi/azure-nextgen/insights/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | import * as web from "@pulumi/azure-nextgen/web/latest"; 5 | import { appName, location, resourceGroupName } from "./common"; 6 | 7 | const droneStatusStorageAccount = new storage.StorageAccount(`${appName}sa`, { 8 | resourceGroupName: resourceGroupName, 9 | location: location, 10 | accountName: `${appName}sa`, 11 | sku: { 12 | name: "Standard_LRS", 13 | }, 14 | kind: "StorageV2", 15 | tags: { 16 | displayName: "Drone Status Function App", 17 | }, 18 | }); 19 | 20 | function getStorageConnectionString(account: storage.StorageAccount): pulumi.Output { 21 | const keys = pulumi.all([resourceGroupName, account.name]).apply(([resourceGroupName, accountName]) => 22 | storage.listStorageAccountKeys({ resourceGroupName, accountName })); 23 | const key = keys.keys[0].value; 24 | return pulumi.interpolate`DefaultEndpointsProtocol=https;AccountName=${account.name};AccountKey=${key}`; 25 | } 26 | 27 | const droneStatusAppInsights = new insights.Component(`${appName}-ai`, { 28 | resourceGroupName: resourceGroupName, 29 | resourceName: `${appName}-ai`, 30 | location: location, 31 | applicationType: "web", 32 | kind: "web", 33 | }); 34 | 35 | const hostingPlan = new web.AppServicePlan(`${appName}-asp`, { 36 | resourceGroupName: resourceGroupName, 37 | name: `${appName}-asp`, 38 | location: location, 39 | sku: { 40 | name: "Y1", 41 | tier: "Dynamic", 42 | }, 43 | }); 44 | 45 | const telemetry = new pulumi.StackReference("mikhailshilkov/telemetry-nextgen/dev"); 46 | const cosmosDatabaseName = telemetry.requireOutput("cosmosDatabaseName"); 47 | const cosmosCollectionName = telemetry.requireOutput("cosmosCollectionName"); 48 | const cosmosConnectionString = telemetry.requireOutput("cosmosConnectionString"); 49 | const cosmosEndpoint = telemetry.requireOutput("cosmosEndpoint"); 50 | const cosmosMasterKey = telemetry.requireOutput("cosmosMasterKey"); 51 | 52 | const droneStatusFunctionApp = new web.WebApp(`${appName}-app`, { 53 | resourceGroupName: resourceGroupName, 54 | name: `${appName}-app`, 55 | location: location, 56 | serverFarmId: hostingPlan.id, 57 | kind: "functionapp", 58 | siteConfig: { 59 | appSettings: [ 60 | { name: "APPINSIGHTS_INSTRUMENTATIONKEY", value: droneStatusAppInsights.instrumentationKey }, 61 | { name: "APPLICATIONINSIGHTS_CONNECTION_STRING", value: pulumi.interpolate`InstrumentationKey=${droneStatusAppInsights.instrumentationKey}` }, 62 | { name: "ApplicationInsightsAgent_EXTENSION_VERSION", value: "~2" }, 63 | { name: "AzureWebJobsStorage", value: getStorageConnectionString(droneStatusStorageAccount) }, 64 | { name: "COSMOSDB_CONNECTION_STRING", value: cosmosConnectionString }, 65 | { name: "CosmosDBEndpoint", value: cosmosEndpoint }, 66 | { name: "CosmosDBKey", value: cosmosMasterKey }, 67 | { name: "COSMOSDB_DATABASE_NAME", value: cosmosDatabaseName }, 68 | { name: "COSMOSDB_DATABASE_COL", value: cosmosCollectionName }, 69 | { name: "FUNCTIONS_EXTENSION_VERSION", value: "~3" }, 70 | { name: "FUNCTIONS_WORKER_RUNTIME", value: "dotnet" }, 71 | { name: "WEBSITE_NODE_DEFAULT_VERSION", value: "10.14.1" }, 72 | { name: "WEBSITE_RUN_FROM_PACKAGE", value: "https://mikhailworkshop.blob.core.windows.net/zips/statusapp.zip" }, 73 | ], 74 | cors: { 75 | allowedOrigins: ["*"], 76 | }, 77 | }, 78 | tags: { 79 | displayName: "Drone Telemetry Function App", 80 | }, 81 | }); 82 | 83 | export const functionUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api/GetStatusFunction?deviceId=`; 84 | export const id = droneStatusFunctionApp.id; 85 | export const appUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api`; 86 | -------------------------------------------------------------------------------- /06-api/code/step4/index.ts: -------------------------------------------------------------------------------- 1 | import "./common"; 2 | import "./websiteFiles"; 3 | 4 | import * as functions from "./functionApp"; 5 | import * as website from "./website"; 6 | 7 | export const functionUrl = functions.functionUrl; 8 | export const storageAccountUrl = website.storageAccountUrl; 9 | -------------------------------------------------------------------------------- /06-api/code/step4/website.ts: -------------------------------------------------------------------------------- 1 | import * as azure from "@pulumi/azure"; 2 | import { appName, resourceGroupName } from "./common"; 3 | 4 | export const storageAccount = new azure.storage.Account(`${appName}fe`, { 5 | resourceGroupName: resourceGroupName, 6 | tags: { 7 | displayName: "Drone Front End Storage Account", 8 | }, 9 | accountTier: "Standard", 10 | accountReplicationType: "LRS", 11 | staticWebsite: { 12 | indexDocument: "index.html", 13 | error404Document: "404.html", 14 | }, 15 | }); 16 | 17 | export const storageAccountUrl = storageAccount.primaryWebEndpoint; 18 | -------------------------------------------------------------------------------- /06-api/code/step4/websiteFiles.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as azure from "@pulumi/azure"; 3 | import * as website from "./website"; 4 | import * as functionApp from "./functionApp"; 5 | import * as mime from "mime"; 6 | import * as nodedir from "node-dir"; 7 | import * as fs from "fs"; 8 | 9 | const folderName = "droneapp-noauth"; 10 | const files = nodedir.files(folderName, { sync: true }); 11 | for (const file of files) { 12 | const name = file.substring(folderName.length+1); 13 | const contentType = mime.getType(file) || undefined; 14 | 15 | const rawText = fs.readFileSync(file, "utf8").toString(); 16 | const asset = functionApp.functionUrl 17 | .apply(url => rawText.replace("[API_URL]", url)) 18 | .apply(text => new pulumi.asset.StringAsset(text)); 19 | 20 | const myObject = new azure.storage.Blob(name, { 21 | name, 22 | storageAccountName: website.storageAccount.name, 23 | storageContainerName: "$web", 24 | type: "Block", 25 | source: asset, 26 | contentType, 27 | }, { parent: website.storageAccount }); 28 | } 29 | -------------------------------------------------------------------------------- /06-api/code/step5/api.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as apimanagement from "@pulumi/azure-nextgen/apimanagement/latest"; 3 | import * as azure from "@pulumi/azure"; 4 | import { appName, location, resourceGroupName } from "./common"; 5 | import * as functionApp from "./functionApp"; 6 | import * as website from "./website"; 7 | 8 | const apiManagementName = `${appName}-apim`; 9 | const apiManagement = new apimanagement.ApiManagementService(apiManagementName, { 10 | resourceGroupName: resourceGroupName, 11 | serviceName: apiManagementName, 12 | location: location, 13 | sku: { 14 | name: "Consumption", 15 | capacity: 0, 16 | }, 17 | publisherEmail: "drones@contoso.com", 18 | publisherName: "contoso", 19 | }); 20 | const apiManagementId = apiManagement.id; 21 | 22 | const versionSet = new apimanagement.ApiVersionSet("dronestatusversionset", { 23 | resourceGroupName: resourceGroupName, 24 | serviceName: apiManagement.name, 25 | versionSetId: "dronestatusversionset", 26 | displayName: "Drone Delivery API", 27 | versioningScheme: "Segment", 28 | }); 29 | 30 | const api = new apimanagement.Api("dronedeliveryapiv1", { 31 | resourceGroupName: resourceGroupName, 32 | serviceName: apiManagementName, 33 | apiId: "dronedeliveryapiv1", 34 | displayName: "Drone Delivery API", 35 | description: "Drone Delivery API", 36 | path: "api", 37 | apiVersion: "v1", 38 | apiRevision: "1", 39 | apiVersionSetId: versionSet.id, 40 | protocols: ["https"], 41 | }); 42 | 43 | const apiOperation = new apimanagement.ApiOperation("dronestatusGET", { 44 | resourceGroupName: resourceGroupName, 45 | serviceName: apiManagementName, 46 | apiId: api.name, 47 | operationId: "dronestatusGET", 48 | displayName: "Retrieve drone status", 49 | description: "Retrieve drone status", 50 | method: "GET", 51 | urlTemplate: "/dronestatus/{deviceid}", 52 | templateParameters: [ 53 | { 54 | name: "deviceid", 55 | description: "device id", 56 | type: "string", 57 | required: true, 58 | }, 59 | ], 60 | }); 61 | 62 | const backend = new apimanagement.Backend("dronestatusdotnet", { 63 | resourceGroupName: resourceGroupName, 64 | serviceName: apiManagementName, 65 | backendId: "dronestatusdotnet", 66 | resourceId: pulumi.interpolate`https://management.azure.com/${functionApp.id}`, 67 | // credentials: { 68 | // query: { 69 | // code: pulumi.interpolate`{{${apiValueFunctionCode.name}}}`, 70 | // }, 71 | // }, 72 | url: functionApp.appUrl, 73 | protocol: "http", 74 | }); 75 | 76 | const apiPolicy = new apimanagement.ApiPolicy("policy", { 77 | resourceGroupName: resourceGroupName, 78 | serviceName: apiManagementName, 79 | apiId: api.name, 80 | policyId: "policy", 81 | value: pulumi.interpolate` 82 | 83 | 84 | 85 | 86 | 87 | ${website.storageAccountUrl} 88 | 89 | GET 90 |
*
91 |
92 | 93 | 94 |
95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |
`, 105 | }); 106 | 107 | const product = new apimanagement.Product("dronedeliveryprodapi", { 108 | resourceGroupName: resourceGroupName, 109 | serviceName: apiManagementName, 110 | productId: "dronedeliveryprodapi", 111 | displayName: "drone delivery product api", 112 | description: "drone delivery product api", 113 | terms: "terms for example product", 114 | subscriptionRequired: false, 115 | state: "published", 116 | }); 117 | 118 | const productApi = new azure.apimanagement.ProductApi("dronedeliveryapiv1", { 119 | resourceGroupName: resourceGroupName, 120 | apiManagementName: apiManagementName, 121 | apiName: api.name, 122 | productId: product.name, 123 | }); 124 | 125 | export const apiUrl = pulumi.interpolate`https://${apiManagementName}.azure-api.net/${api.path}/v1/dronestatus/`; 126 | -------------------------------------------------------------------------------- /06-api/code/step5/common.ts: -------------------------------------------------------------------------------- 1 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 2 | 3 | export const appName = "status"; 4 | 5 | const resourceGroup = new resources.ResourceGroup(`${appName}-rg`, { 6 | resourceGroupName: `${appName}-rg`, 7 | location: "WestEurope", 8 | }); 9 | 10 | export const resourceGroupName = resourceGroup.name; 11 | export const resourceGroupId = resourceGroup.id; 12 | export const location = resourceGroup.location; 13 | -------------------------------------------------------------------------------- /06-api/code/step5/functionApp.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as insights from "@pulumi/azure-nextgen/insights/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | import * as web from "@pulumi/azure-nextgen/web/latest"; 5 | import { appName, location, resourceGroupName } from "./common"; 6 | 7 | const droneStatusStorageAccount = new storage.StorageAccount(`${appName}sa`, { 8 | resourceGroupName: resourceGroupName, 9 | location: location, 10 | accountName: `${appName}sa`, 11 | sku: { 12 | name: "Standard_LRS", 13 | }, 14 | kind: "StorageV2", 15 | tags: { 16 | displayName: "Drone Status Function App", 17 | }, 18 | }); 19 | 20 | function getStorageConnectionString(account: storage.StorageAccount): pulumi.Output { 21 | const keys = pulumi.all([resourceGroupName, account.name]).apply(([resourceGroupName, accountName]) => 22 | storage.listStorageAccountKeys({ resourceGroupName, accountName })); 23 | const key = keys.keys[0].value; 24 | return pulumi.interpolate`DefaultEndpointsProtocol=https;AccountName=${account.name};AccountKey=${key}`; 25 | } 26 | 27 | const droneStatusAppInsights = new insights.Component(`${appName}-ai`, { 28 | resourceGroupName: resourceGroupName, 29 | resourceName: `${appName}-ai`, 30 | location: location, 31 | applicationType: "web", 32 | kind: "web", 33 | }); 34 | 35 | const hostingPlan = new web.AppServicePlan(`${appName}-asp`, { 36 | resourceGroupName: resourceGroupName, 37 | name: `${appName}-asp`, 38 | location: location, 39 | sku: { 40 | name: "Y1", 41 | tier: "Dynamic", 42 | }, 43 | }); 44 | 45 | const telemetry = new pulumi.StackReference("mikhailshilkov/telemetry-nextgen/dev"); 46 | const cosmosDatabaseName = telemetry.requireOutput("cosmosDatabaseName"); 47 | const cosmosCollectionName = telemetry.requireOutput("cosmosCollectionName"); 48 | const cosmosConnectionString = telemetry.requireOutput("cosmosConnectionString"); 49 | const cosmosEndpoint = telemetry.requireOutput("cosmosEndpoint"); 50 | const cosmosMasterKey = telemetry.requireOutput("cosmosMasterKey"); 51 | 52 | const droneStatusFunctionApp = new web.WebApp(`${appName}-app`, { 53 | resourceGroupName: resourceGroupName, 54 | name: `${appName}-app`, 55 | location: location, 56 | serverFarmId: hostingPlan.id, 57 | kind: "functionapp", 58 | siteConfig: { 59 | appSettings: [ 60 | { name: "APPINSIGHTS_INSTRUMENTATIONKEY", value: droneStatusAppInsights.instrumentationKey }, 61 | { name: "APPLICATIONINSIGHTS_CONNECTION_STRING", value: pulumi.interpolate`InstrumentationKey=${droneStatusAppInsights.instrumentationKey}` }, 62 | { name: "ApplicationInsightsAgent_EXTENSION_VERSION", value: "~2" }, 63 | { name: "AzureWebJobsStorage", value: getStorageConnectionString(droneStatusStorageAccount) }, 64 | { name: "COSMOSDB_CONNECTION_STRING", value: cosmosConnectionString }, 65 | { name: "CosmosDBEndpoint", value: cosmosEndpoint }, 66 | { name: "CosmosDBKey", value: cosmosMasterKey }, 67 | { name: "COSMOSDB_DATABASE_NAME", value: cosmosDatabaseName }, 68 | { name: "COSMOSDB_DATABASE_COL", value: cosmosCollectionName }, 69 | { name: "FUNCTIONS_EXTENSION_VERSION", value: "~3" }, 70 | { name: "FUNCTIONS_WORKER_RUNTIME", value: "dotnet" }, 71 | { name: "WEBSITE_NODE_DEFAULT_VERSION", value: "10.14.1" }, 72 | { name: "WEBSITE_RUN_FROM_PACKAGE", value: "https://mikhailworkshop.blob.core.windows.net/zips/statusapp.zip" }, 73 | ], 74 | cors: { 75 | allowedOrigins: ["*"], 76 | }, 77 | }, 78 | tags: { 79 | displayName: "Drone Telemetry Function App", 80 | }, 81 | }); 82 | 83 | export const functionUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api/GetStatusFunction?deviceId=`; 84 | export const id = droneStatusFunctionApp.id; 85 | export const appUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api`; 86 | -------------------------------------------------------------------------------- /06-api/code/step5/index.ts: -------------------------------------------------------------------------------- 1 | import "./common"; 2 | import "./websiteFiles"; 3 | 4 | import * as functions from "./functionApp"; 5 | import * as website from "./website"; 6 | 7 | export const functionUrl = functions.functionUrl; 8 | export const storageAccountUrl = website.storageAccountUrl; 9 | -------------------------------------------------------------------------------- /06-api/code/step5/website.ts: -------------------------------------------------------------------------------- 1 | import * as azure from "@pulumi/azure"; 2 | import { appName, resourceGroupName } from "./common"; 3 | 4 | export const storageAccount = new azure.storage.Account(`${appName}fe`, { 5 | resourceGroupName: resourceGroupName, 6 | tags: { 7 | displayName: "Drone Front End Storage Account", 8 | }, 9 | accountTier: "Standard", 10 | accountReplicationType: "LRS", 11 | staticWebsite: { 12 | indexDocument: "index.html", 13 | error404Document: "404.html", 14 | }, 15 | }); 16 | 17 | export const storageAccountUrl = storageAccount.primaryWebEndpoint; 18 | -------------------------------------------------------------------------------- /06-api/code/step5/websiteFiles.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as azure from "@pulumi/azure"; 3 | import * as website from "./website"; 4 | import * as mime from "mime"; 5 | import * as nodedir from "node-dir"; 6 | import * as fs from "fs"; 7 | import * as api from "./api"; 8 | 9 | const folderName = "droneapp-noauth"; 10 | const files = nodedir.files(folderName, { sync: true }); 11 | for (const file of files) { 12 | const name = file.substring(folderName.length+1); 13 | const contentType = mime.getType(file) || undefined; 14 | 15 | const rawText = fs.readFileSync(file, "utf8").toString(); 16 | const asset = api.apiUrl 17 | .apply(url => rawText.replace("[API_URL]", url)) 18 | .apply(text => new pulumi.asset.StringAsset(text)); 19 | 20 | const myObject = new azure.storage.Blob(name, { 21 | name, 22 | storageAccountName: website.storageAccount.name, 23 | storageContainerName: "$web", 24 | type: "Block", 25 | source: asset, 26 | contentType, 27 | }, { parent: website.storageAccount }); 28 | } 29 | -------------------------------------------------------------------------------- /07-cdn/README.md: -------------------------------------------------------------------------------- 1 | # Lab 7: Deploying Website Behind Azure CDN 2 | 3 | Currently, your frontend website is served directly from the Storage Account. You can improve the performance of serving static web files for users around the world using a Content Delivery Network (CDN). 4 | 5 | In this lab, you will extend the `statusapp` project to add an Azure CDN service in front of the Storage Account. 6 | 7 | Make sure you are still in the `statusapp` folder with the same files that you created in Labs 4-6. 8 | 9 | ## Step 1 — Add Azure CDN Profile and Endpoint 10 | 11 | Extend the existing `website.ts` file with these resources: 12 | 13 | ```ts 14 | import * as pulumi from "@pulumi/pulumi"; 15 | import * as cdn from "@pulumi/azure-nextgen/cdn/latest"; 16 | import { appName, location, resourceGroupName } from "./common"; 17 | 18 | const cdnProfile = new cdn.Profile("profile", { 19 | resourceGroupName: resourceGroupName, 20 | profileName: `${appName}-cdn`, 21 | location: location, 22 | sku: { name: "Standard_Microsoft" }, 23 | }); 24 | 25 | const cdnEndpoint = new cdn.Endpoint("endpoint", { 26 | resourceGroupName: resourceGroupName, 27 | profileName: cdnProfile.name, 28 | endpointName: `${appName}-endpoint`, 29 | location: location, 30 | isHttpAllowed: false, 31 | origins: [{ name: "origin", hostName: storageAccount.primaryWebHost }], 32 | originHostHeader: storageAccount.primaryWebHost, 33 | }); 34 | 35 | export const cdnUrl = pulumi.interpolate`https://${cdnEndpoint.hostName}`; 36 | ``` 37 | 38 | Adjust the `endpointName` property value to a globally unique string. 39 | 40 | Also, export the CDN URL from the `index.ts` file: 41 | 42 | ```ts 43 | export const cdnUrl = website.cdnUrl; 44 | ``` 45 | 46 | > :white_check_mark: After these changes, your files should [look like this](./code/step1). 47 | 48 | ## Step 2 — Add the CDN Endpoint to API CORS 49 | 50 | Open the file `api.ts`, find the `ApiPolicy` resource and add a new line to its CORS definitions node: 51 | 52 | ```ts 53 | const apiPolicy = new azure.apimanagement.ApiPolicy("policy", { 54 | ... 55 | 56 | ${website.storageAccountUrl} 57 | ${website.cdnUrl} 58 | 59 | ... 60 | ``` 61 | 62 | > :white_check_mark: After these changes, your files should [look like this](./code/step2). 63 | 64 | ## Step 3 — Deploy and Test the Stack 65 | 66 | Deploy the stack 67 | 68 | ```bash 69 | $ pulumi up 70 | ... 71 | Updating (dev): 72 | Type Name Status Info 73 | pulumi:pulumi:Stack statusapp-dev 74 | + ├─ azure-nextgen:cdn/latest:Profile profile created 75 | + ├─ azure-nextgen:cdn/latest:Endpoint endpoint created 76 | ~ └─ azure-nextgen:apimanagement/latest:ApiPolicy policy updated [diff: ~value] 77 | 78 | Outputs: 79 | + cdnUrl : "https://endpoint0962acd7.azureedge.net" 80 | functionUrl : "https://status-app47000f49.azurewebsites.net/api/GetStatusFunction?deviceId=" 81 | storageAccountUrl: "https://statusfe1867bccd.z6.web.core.windows.net/" 82 | 83 | Resources: 84 | + 2 created 85 | ~ 1 updated 86 | 3 changes. 35 unchanged 87 | ``` 88 | 89 | Navigate to the `cdnUrl` in a browser and make sure that the app still works. Note that CDN propagation may take up to several minutes, so if you get 404s, wait a bit and retry. 90 | 91 | ## Next Steps 92 | 93 | Congratulations! :tada: You have successfully provisioned Azure CDN resources that stand in front of Azure Storage to provide users with faster static websites. 94 | 95 | Next, you will enable Azure Active Directory OAuth2 authentication in the status website. 96 | 97 | [Get Started with Lab 8](../08-aad/README.md) 98 | -------------------------------------------------------------------------------- /07-cdn/code/step1/api.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as apimanagement from "@pulumi/azure-nextgen/apimanagement/latest"; 3 | import * as azure from "@pulumi/azure"; 4 | import { appName, location, resourceGroupName } from "./common"; 5 | import * as functionApp from "./functionApp"; 6 | import * as website from "./website"; 7 | 8 | const apiManagementName = `${appName}-apim`; 9 | const apiManagement = new apimanagement.ApiManagementService(apiManagementName, { 10 | resourceGroupName: resourceGroupName, 11 | serviceName: apiManagementName, 12 | location: location, 13 | sku: { 14 | name: "Consumption", 15 | capacity: 0, 16 | }, 17 | publisherEmail: "drones@contoso.com", 18 | publisherName: "contoso", 19 | }); 20 | const apiManagementId = apiManagement.id; 21 | 22 | const versionSet = new apimanagement.ApiVersionSet("dronestatusversionset", { 23 | resourceGroupName: resourceGroupName, 24 | serviceName: apiManagement.name, 25 | versionSetId: "dronestatusversionset", 26 | displayName: "Drone Delivery API", 27 | versioningScheme: "Segment", 28 | }); 29 | 30 | const api = new apimanagement.Api("dronedeliveryapiv1", { 31 | resourceGroupName: resourceGroupName, 32 | serviceName: apiManagementName, 33 | apiId: "dronedeliveryapiv1", 34 | displayName: "Drone Delivery API", 35 | description: "Drone Delivery API", 36 | path: "api", 37 | apiVersion: "v1", 38 | apiRevision: "1", 39 | apiVersionSetId: versionSet.id, 40 | protocols: ["https"], 41 | }); 42 | 43 | const apiOperation = new apimanagement.ApiOperation("dronestatusGET", { 44 | resourceGroupName: resourceGroupName, 45 | serviceName: apiManagementName, 46 | apiId: api.name, 47 | operationId: "dronestatusGET", 48 | displayName: "Retrieve drone status", 49 | description: "Retrieve drone status", 50 | method: "GET", 51 | urlTemplate: "/dronestatus/{deviceid}", 52 | templateParameters: [ 53 | { 54 | name: "deviceid", 55 | description: "device id", 56 | type: "string", 57 | required: true, 58 | }, 59 | ], 60 | }); 61 | 62 | const backend = new apimanagement.Backend("dronestatusdotnet", { 63 | resourceGroupName: resourceGroupName, 64 | serviceName: apiManagementName, 65 | backendId: "dronestatusdotnet", 66 | resourceId: pulumi.interpolate`https://management.azure.com/${functionApp.id}`, 67 | // credentials: { 68 | // query: { 69 | // code: pulumi.interpolate`{{${apiValueFunctionCode.name}}}`, 70 | // }, 71 | // }, 72 | url: functionApp.appUrl, 73 | protocol: "http", 74 | }); 75 | 76 | const apiPolicy = new apimanagement.ApiPolicy("policy", { 77 | resourceGroupName: resourceGroupName, 78 | serviceName: apiManagementName, 79 | apiId: api.name, 80 | policyId: "policy", 81 | value: pulumi.interpolate` 82 | 83 | 84 | 85 | 86 | 87 | ${website.storageAccountUrl} 88 | 89 | GET 90 |
*
91 |
92 | 93 | 94 |
95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |
`, 105 | }); 106 | 107 | const product = new apimanagement.Product("dronedeliveryprodapi", { 108 | resourceGroupName: resourceGroupName, 109 | serviceName: apiManagementName, 110 | productId: "dronedeliveryprodapi", 111 | displayName: "drone delivery product api", 112 | description: "drone delivery product api", 113 | terms: "terms for example product", 114 | subscriptionRequired: false, 115 | state: "published", 116 | }); 117 | 118 | const productApi = new azure.apimanagement.ProductApi("dronedeliveryapiv1", { 119 | resourceGroupName: resourceGroupName, 120 | apiManagementName: apiManagementName, 121 | apiName: api.name, 122 | productId: product.name, 123 | }); 124 | 125 | export const apiUrl = pulumi.interpolate`https://${apiManagementName}.azure-api.net/${api.path}/v1/dronestatus/`; 126 | -------------------------------------------------------------------------------- /07-cdn/code/step1/common.ts: -------------------------------------------------------------------------------- 1 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 2 | 3 | export const appName = "status"; 4 | 5 | const resourceGroup = new resources.ResourceGroup(`${appName}-rg`, { 6 | resourceGroupName: `${appName}-rg`, 7 | location: "WestEurope", 8 | }); 9 | 10 | export const resourceGroupName = resourceGroup.name; 11 | export const resourceGroupId = resourceGroup.id; 12 | export const location = resourceGroup.location; 13 | -------------------------------------------------------------------------------- /07-cdn/code/step1/functionApp.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as insights from "@pulumi/azure-nextgen/insights/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | import * as web from "@pulumi/azure-nextgen/web/latest"; 5 | import { appName, location, resourceGroupName } from "./common"; 6 | 7 | const droneStatusStorageAccount = new storage.StorageAccount(`${appName}sa`, { 8 | resourceGroupName: resourceGroupName, 9 | location: location, 10 | accountName: `${appName}sa`, 11 | sku: { 12 | name: "Standard_LRS", 13 | }, 14 | kind: "StorageV2", 15 | tags: { 16 | displayName: "Drone Status Function App", 17 | }, 18 | }); 19 | 20 | function getStorageConnectionString(account: storage.StorageAccount): pulumi.Output { 21 | const keys = pulumi.all([resourceGroupName, account.name]).apply(([resourceGroupName, accountName]) => 22 | storage.listStorageAccountKeys({ resourceGroupName, accountName })); 23 | const key = keys.keys[0].value; 24 | return pulumi.interpolate`DefaultEndpointsProtocol=https;AccountName=${account.name};AccountKey=${key}`; 25 | } 26 | 27 | const droneStatusAppInsights = new insights.Component(`${appName}-ai`, { 28 | resourceGroupName: resourceGroupName, 29 | resourceName: `${appName}-ai`, 30 | location: location, 31 | applicationType: "web", 32 | kind: "web", 33 | }); 34 | 35 | const hostingPlan = new web.AppServicePlan(`${appName}-asp`, { 36 | resourceGroupName: resourceGroupName, 37 | name: `${appName}-asp`, 38 | location: location, 39 | sku: { 40 | name: "Y1", 41 | tier: "Dynamic", 42 | }, 43 | }); 44 | 45 | const telemetry = new pulumi.StackReference("mikhailshilkov/telemetry-nextgen/dev"); 46 | const cosmosDatabaseName = telemetry.requireOutput("cosmosDatabaseName"); 47 | const cosmosCollectionName = telemetry.requireOutput("cosmosCollectionName"); 48 | const cosmosConnectionString = telemetry.requireOutput("cosmosConnectionString"); 49 | const cosmosEndpoint = telemetry.requireOutput("cosmosEndpoint"); 50 | const cosmosMasterKey = telemetry.requireOutput("cosmosMasterKey"); 51 | 52 | const droneStatusFunctionApp = new web.WebApp(`${appName}-app`, { 53 | resourceGroupName: resourceGroupName, 54 | name: `${appName}-app`, 55 | location: location, 56 | serverFarmId: hostingPlan.id, 57 | kind: "functionapp", 58 | siteConfig: { 59 | appSettings: [ 60 | { name: "APPINSIGHTS_INSTRUMENTATIONKEY", value: droneStatusAppInsights.instrumentationKey }, 61 | { name: "APPLICATIONINSIGHTS_CONNECTION_STRING", value: pulumi.interpolate`InstrumentationKey=${droneStatusAppInsights.instrumentationKey}` }, 62 | { name: "ApplicationInsightsAgent_EXTENSION_VERSION", value: "~2" }, 63 | { name: "AzureWebJobsStorage", value: getStorageConnectionString(droneStatusStorageAccount) }, 64 | { name: "COSMOSDB_CONNECTION_STRING", value: cosmosConnectionString }, 65 | { name: "CosmosDBEndpoint", value: cosmosEndpoint }, 66 | { name: "CosmosDBKey", value: cosmosMasterKey }, 67 | { name: "COSMOSDB_DATABASE_NAME", value: cosmosDatabaseName }, 68 | { name: "COSMOSDB_DATABASE_COL", value: cosmosCollectionName }, 69 | { name: "FUNCTIONS_EXTENSION_VERSION", value: "~3" }, 70 | { name: "FUNCTIONS_WORKER_RUNTIME", value: "dotnet" }, 71 | { name: "WEBSITE_NODE_DEFAULT_VERSION", value: "10.14.1" }, 72 | { name: "WEBSITE_RUN_FROM_PACKAGE", value: "https://mikhailworkshop.blob.core.windows.net/zips/statusapp.zip" }, 73 | ], 74 | cors: { 75 | allowedOrigins: ["*"], 76 | }, 77 | }, 78 | tags: { 79 | displayName: "Drone Telemetry Function App", 80 | }, 81 | }); 82 | 83 | export const functionUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api/GetStatusFunction?deviceId=`; 84 | export const id = droneStatusFunctionApp.id; 85 | export const appUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api`; 86 | -------------------------------------------------------------------------------- /07-cdn/code/step1/index.ts: -------------------------------------------------------------------------------- 1 | import "./common"; 2 | import "./websiteFiles"; 3 | 4 | import * as functions from "./functionApp"; 5 | import * as website from "./website"; 6 | 7 | export const functionUrl = functions.functionUrl; 8 | export const storageAccountUrl = website.storageAccountUrl; 9 | export const cdnUrl = website.cdnUrl; 10 | -------------------------------------------------------------------------------- /07-cdn/code/step1/website.ts: -------------------------------------------------------------------------------- 1 | import * as azure from "@pulumi/azure"; 2 | import * as pulumi from "@pulumi/pulumi"; 3 | import * as cdn from "@pulumi/azure-nextgen/cdn/latest"; 4 | import { appName, location, resourceGroupName } from "./common"; 5 | 6 | export const storageAccount = new azure.storage.Account(`${appName}fe`, { 7 | resourceGroupName: resourceGroupName, 8 | tags: { 9 | displayName: "Drone Front End Storage Account", 10 | }, 11 | accountTier: "Standard", 12 | accountReplicationType: "LRS", 13 | staticWebsite: { 14 | indexDocument: "index.html", 15 | error404Document: "404.html", 16 | }, 17 | }); 18 | 19 | export const storageAccountUrl = storageAccount.primaryWebEndpoint; 20 | 21 | const cdnProfile = new cdn.Profile("profile", { 22 | resourceGroupName: resourceGroupName, 23 | profileName: `${appName}-cdn`, 24 | location: location, 25 | sku: { name: "Standard_Microsoft" }, 26 | }); 27 | 28 | const cdnEndpoint = new cdn.Endpoint("endpoint", { 29 | resourceGroupName: resourceGroupName, 30 | profileName: cdnProfile.name, 31 | endpointName: `${appName}-endpoint`, 32 | location: location, 33 | isHttpAllowed: false, 34 | origins: [{ name: "origin", hostName: storageAccount.primaryWebHost }], 35 | originHostHeader: storageAccount.primaryWebHost, 36 | }); 37 | 38 | export const cdnUrl = pulumi.interpolate`https://${cdnEndpoint.hostName}`; 39 | -------------------------------------------------------------------------------- /07-cdn/code/step1/websiteFiles.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as azure from "@pulumi/azure"; 3 | import * as website from "./website"; 4 | import * as mime from "mime"; 5 | import * as nodedir from "node-dir"; 6 | import * as fs from "fs"; 7 | import * as api from "./api"; 8 | 9 | const folderName = "droneapp-noauth"; 10 | const files = nodedir.files(folderName, { sync: true }); 11 | for (const file of files) { 12 | const name = file.substring(folderName.length+1); 13 | const contentType = mime.getType(file) || undefined; 14 | 15 | const rawText = fs.readFileSync(file, "utf8").toString(); 16 | const asset = api.apiUrl 17 | .apply(url => rawText.replace("[API_URL]", url)) 18 | .apply(text => new pulumi.asset.StringAsset(text)); 19 | 20 | const myObject = new azure.storage.Blob(name, { 21 | name, 22 | storageAccountName: website.storageAccount.name, 23 | storageContainerName: "$web", 24 | type: "Block", 25 | source: asset, 26 | contentType, 27 | }, { parent: website.storageAccount }); 28 | } 29 | -------------------------------------------------------------------------------- /07-cdn/code/step2/api.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as apimanagement from "@pulumi/azure-nextgen/apimanagement/latest"; 3 | import * as azure from "@pulumi/azure"; 4 | import { appName, location, resourceGroupName } from "./common"; 5 | import * as functionApp from "./functionApp"; 6 | import * as website from "./website"; 7 | 8 | const apiManagementName = `${appName}-apim`; 9 | const apiManagement = new apimanagement.ApiManagementService(apiManagementName, { 10 | resourceGroupName: resourceGroupName, 11 | serviceName: apiManagementName, 12 | location: location, 13 | sku: { 14 | name: "Consumption", 15 | capacity: 0, 16 | }, 17 | publisherEmail: "drones@contoso.com", 18 | publisherName: "contoso", 19 | }); 20 | const apiManagementId = apiManagement.id; 21 | 22 | const versionSet = new apimanagement.ApiVersionSet("dronestatusversionset", { 23 | resourceGroupName: resourceGroupName, 24 | serviceName: apiManagement.name, 25 | versionSetId: "dronestatusversionset", 26 | displayName: "Drone Delivery API", 27 | versioningScheme: "Segment", 28 | }); 29 | 30 | const api = new apimanagement.Api("dronedeliveryapiv1", { 31 | resourceGroupName: resourceGroupName, 32 | serviceName: apiManagementName, 33 | apiId: "dronedeliveryapiv1", 34 | displayName: "Drone Delivery API", 35 | description: "Drone Delivery API", 36 | path: "api", 37 | apiVersion: "v1", 38 | apiRevision: "1", 39 | apiVersionSetId: versionSet.id, 40 | protocols: ["https"], 41 | }); 42 | 43 | const apiOperation = new apimanagement.ApiOperation("dronestatusGET", { 44 | resourceGroupName: resourceGroupName, 45 | serviceName: apiManagementName, 46 | apiId: api.name, 47 | operationId: "dronestatusGET", 48 | displayName: "Retrieve drone status", 49 | description: "Retrieve drone status", 50 | method: "GET", 51 | urlTemplate: "/dronestatus/{deviceid}", 52 | templateParameters: [ 53 | { 54 | name: "deviceid", 55 | description: "device id", 56 | type: "string", 57 | required: true, 58 | }, 59 | ], 60 | }); 61 | 62 | const backend = new apimanagement.Backend("dronestatusdotnet", { 63 | resourceGroupName: resourceGroupName, 64 | serviceName: apiManagementName, 65 | backendId: "dronestatusdotnet", 66 | resourceId: pulumi.interpolate`https://management.azure.com/${functionApp.id}`, 67 | // credentials: { 68 | // query: { 69 | // code: pulumi.interpolate`{{${apiValueFunctionCode.name}}}`, 70 | // }, 71 | // }, 72 | url: functionApp.appUrl, 73 | protocol: "http", 74 | }); 75 | 76 | const apiPolicy = new apimanagement.ApiPolicy("policy", { 77 | resourceGroupName: resourceGroupName, 78 | serviceName: apiManagementName, 79 | apiId: api.name, 80 | policyId: "policy", 81 | value: pulumi.interpolate` 82 | 83 | 84 | 85 | 86 | 87 | ${website.storageAccountUrl} 88 | ${website.cdnUrl} 89 | 90 | GET 91 |
*
92 |
93 | 94 | 95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |
`, 106 | }); 107 | 108 | const product = new apimanagement.Product("dronedeliveryprodapi", { 109 | resourceGroupName: resourceGroupName, 110 | serviceName: apiManagementName, 111 | productId: "dronedeliveryprodapi", 112 | displayName: "drone delivery product api", 113 | description: "drone delivery product api", 114 | terms: "terms for example product", 115 | subscriptionRequired: false, 116 | state: "published", 117 | }); 118 | 119 | const productApi = new azure.apimanagement.ProductApi("dronedeliveryapiv1", { 120 | resourceGroupName: resourceGroupName, 121 | apiManagementName: apiManagementName, 122 | apiName: api.name, 123 | productId: product.name, 124 | }); 125 | 126 | export const apiUrl = pulumi.interpolate`https://${apiManagementName}.azure-api.net/${api.path}/v1/dronestatus/`; 127 | -------------------------------------------------------------------------------- /07-cdn/code/step2/common.ts: -------------------------------------------------------------------------------- 1 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 2 | 3 | export const appName = "status"; 4 | 5 | const resourceGroup = new resources.ResourceGroup(`${appName}-rg`, { 6 | resourceGroupName: `${appName}-rg`, 7 | location: "WestEurope", 8 | }); 9 | 10 | export const resourceGroupName = resourceGroup.name; 11 | export const resourceGroupId = resourceGroup.id; 12 | export const location = resourceGroup.location; 13 | -------------------------------------------------------------------------------- /07-cdn/code/step2/functionApp.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as insights from "@pulumi/azure-nextgen/insights/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | import * as web from "@pulumi/azure-nextgen/web/latest"; 5 | import { appName, location, resourceGroupName } from "./common"; 6 | 7 | const droneStatusStorageAccount = new storage.StorageAccount(`${appName}sa`, { 8 | resourceGroupName: resourceGroupName, 9 | location: location, 10 | accountName: `${appName}sa`, 11 | sku: { 12 | name: "Standard_LRS", 13 | }, 14 | kind: "StorageV2", 15 | tags: { 16 | displayName: "Drone Status Function App", 17 | }, 18 | }); 19 | 20 | function getStorageConnectionString(account: storage.StorageAccount): pulumi.Output { 21 | const keys = pulumi.all([resourceGroupName, account.name]).apply(([resourceGroupName, accountName]) => 22 | storage.listStorageAccountKeys({ resourceGroupName, accountName })); 23 | const key = keys.keys[0].value; 24 | return pulumi.interpolate`DefaultEndpointsProtocol=https;AccountName=${account.name};AccountKey=${key}`; 25 | } 26 | 27 | const droneStatusAppInsights = new insights.Component(`${appName}-ai`, { 28 | resourceGroupName: resourceGroupName, 29 | resourceName: `${appName}-ai`, 30 | location: location, 31 | applicationType: "web", 32 | kind: "web", 33 | }); 34 | 35 | const hostingPlan = new web.AppServicePlan(`${appName}-asp`, { 36 | resourceGroupName: resourceGroupName, 37 | name: `${appName}-asp`, 38 | location: location, 39 | sku: { 40 | name: "Y1", 41 | tier: "Dynamic", 42 | }, 43 | }); 44 | 45 | const telemetry = new pulumi.StackReference("mikhailshilkov/telemetry-nextgen/dev"); 46 | const cosmosDatabaseName = telemetry.requireOutput("cosmosDatabaseName"); 47 | const cosmosCollectionName = telemetry.requireOutput("cosmosCollectionName"); 48 | const cosmosConnectionString = telemetry.requireOutput("cosmosConnectionString"); 49 | const cosmosEndpoint = telemetry.requireOutput("cosmosEndpoint"); 50 | const cosmosMasterKey = telemetry.requireOutput("cosmosMasterKey"); 51 | 52 | const droneStatusFunctionApp = new web.WebApp(`${appName}-app`, { 53 | resourceGroupName: resourceGroupName, 54 | name: `${appName}-app`, 55 | location: location, 56 | serverFarmId: hostingPlan.id, 57 | kind: "functionapp", 58 | siteConfig: { 59 | appSettings: [ 60 | { name: "APPINSIGHTS_INSTRUMENTATIONKEY", value: droneStatusAppInsights.instrumentationKey }, 61 | { name: "APPLICATIONINSIGHTS_CONNECTION_STRING", value: pulumi.interpolate`InstrumentationKey=${droneStatusAppInsights.instrumentationKey}` }, 62 | { name: "ApplicationInsightsAgent_EXTENSION_VERSION", value: "~2" }, 63 | { name: "AzureWebJobsStorage", value: getStorageConnectionString(droneStatusStorageAccount) }, 64 | { name: "COSMOSDB_CONNECTION_STRING", value: cosmosConnectionString }, 65 | { name: "CosmosDBEndpoint", value: cosmosEndpoint }, 66 | { name: "CosmosDBKey", value: cosmosMasterKey }, 67 | { name: "COSMOSDB_DATABASE_NAME", value: cosmosDatabaseName }, 68 | { name: "COSMOSDB_DATABASE_COL", value: cosmosCollectionName }, 69 | { name: "FUNCTIONS_EXTENSION_VERSION", value: "~3" }, 70 | { name: "FUNCTIONS_WORKER_RUNTIME", value: "dotnet" }, 71 | { name: "WEBSITE_NODE_DEFAULT_VERSION", value: "10.14.1" }, 72 | { name: "WEBSITE_RUN_FROM_PACKAGE", value: "https://mikhailworkshop.blob.core.windows.net/zips/statusapp.zip" }, 73 | ], 74 | cors: { 75 | allowedOrigins: ["*"], 76 | }, 77 | }, 78 | tags: { 79 | displayName: "Drone Telemetry Function App", 80 | }, 81 | }); 82 | 83 | export const functionUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api/GetStatusFunction?deviceId=`; 84 | export const id = droneStatusFunctionApp.id; 85 | export const appUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api`; 86 | -------------------------------------------------------------------------------- /07-cdn/code/step2/index.ts: -------------------------------------------------------------------------------- 1 | import "./common"; 2 | import "./websiteFiles"; 3 | 4 | import * as functions from "./functionApp"; 5 | import * as website from "./website"; 6 | 7 | export const functionUrl = functions.functionUrl; 8 | export const storageAccountUrl = website.storageAccountUrl; 9 | export const cdnUrl = website.cdnUrl; 10 | -------------------------------------------------------------------------------- /07-cdn/code/step2/website.ts: -------------------------------------------------------------------------------- 1 | import * as azure from "@pulumi/azure"; 2 | import * as pulumi from "@pulumi/pulumi"; 3 | import * as cdn from "@pulumi/azure-nextgen/cdn/latest"; 4 | import { appName, location, resourceGroupName } from "./common"; 5 | 6 | export const storageAccount = new azure.storage.Account(`${appName}fe`, { 7 | resourceGroupName: resourceGroupName, 8 | tags: { 9 | displayName: "Drone Front End Storage Account", 10 | }, 11 | accountTier: "Standard", 12 | accountReplicationType: "LRS", 13 | staticWebsite: { 14 | indexDocument: "index.html", 15 | error404Document: "404.html", 16 | }, 17 | }); 18 | 19 | export const storageAccountUrl = storageAccount.primaryWebEndpoint; 20 | 21 | const cdnProfile = new cdn.Profile("profile", { 22 | resourceGroupName: resourceGroupName, 23 | profileName: `${appName}-cdn`, 24 | location: location, 25 | sku: { name: "Standard_Microsoft" }, 26 | }); 27 | 28 | const cdnEndpoint = new cdn.Endpoint("endpoint", { 29 | resourceGroupName: resourceGroupName, 30 | profileName: cdnProfile.name, 31 | endpointName: `${appName}-endpoint`, 32 | location: location, 33 | isHttpAllowed: false, 34 | origins: [{ name: "origin", hostName: storageAccount.primaryWebHost }], 35 | originHostHeader: storageAccount.primaryWebHost, 36 | }); 37 | 38 | export const cdnUrl = pulumi.interpolate`https://${cdnEndpoint.hostName}`; 39 | -------------------------------------------------------------------------------- /07-cdn/code/step2/websiteFiles.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as azure from "@pulumi/azure"; 3 | import * as website from "./website"; 4 | import * as mime from "mime"; 5 | import * as nodedir from "node-dir"; 6 | import * as fs from "fs"; 7 | import * as api from "./api"; 8 | 9 | const folderName = "droneapp-noauth"; 10 | const files = nodedir.files(folderName, { sync: true }); 11 | for (const file of files) { 12 | const name = file.substring(folderName.length+1); 13 | const contentType = mime.getType(file) || undefined; 14 | 15 | const rawText = fs.readFileSync(file, "utf8").toString(); 16 | const asset = api.apiUrl 17 | .apply(url => rawText.replace("[API_URL]", url)) 18 | .apply(text => new pulumi.asset.StringAsset(text)); 19 | 20 | const myObject = new azure.storage.Blob(name, { 21 | name, 22 | storageAccountName: website.storageAccount.name, 23 | storageContainerName: "$web", 24 | type: "Block", 25 | source: asset, 26 | contentType, 27 | }, { parent: website.storageAccount }); 28 | } 29 | -------------------------------------------------------------------------------- /08-aad/README.md: -------------------------------------------------------------------------------- 1 | # Lab 8: Enabling User Authentication 2 | 3 | Currently, your frontend website is open to anonymous users. In this lab, you will enable Azure Active Directory OAuth2 authentication in the `statusapp` project. 4 | 5 | Make sure you are still in the `statusapp` folder with the same files that you created in Labs 4-7. 6 | 7 | ## Step 1 — Install AzureAD Provider 8 | 9 | Azure Active Directory objects are managed by a separate Pulumi provider called `AzureAD`. Install the provider by running 10 | 11 | ```bash 12 | npm install @pulumi/azuread 13 | ``` 14 | 15 | ## Step 2 — Add an Azure AD Application 16 | 17 | Extend the file `website.ts` with the definition of an Azure AD Application: 18 | 19 | ```ts 20 | import * as azuread from "@pulumi/azuread"; 21 | 22 | export const tenantId = pulumi.output(azure.core.getClientConfig()).tenantId; 23 | const apiAppName=`${appName}-api`; 24 | 25 | const apiApp = new azuread.Application(apiAppName, { 26 | name: apiAppName, 27 | oauth2AllowImplicitFlow: true, 28 | replyUrls: [storageAccountUrl, cdnUrl], 29 | identifierUris: [`http://${apiAppName}`], 30 | appRoles: [{ 31 | allowedMemberTypes: [ "User" ], 32 | description:"Access to device status", 33 | displayName:"Get Device Status", 34 | isEnabled:true, 35 | value: "GetStatus", 36 | }], 37 | requiredResourceAccesses: [{ 38 | resourceAppId: "00000003-0000-0000-c000-000000000000", 39 | resourceAccesses: [ { id: "e1fe6dd8-ba31-4d61-89e7-88639da4683d", type: "Scope" } ], 40 | }], 41 | }); 42 | export const applicationId = apiApp.applicationId; 43 | ``` 44 | 45 | Note that it lists URLs of the Storage Account and CDN as well-known reply URLs. 46 | 47 | > :white_check_mark: After these changes, your files should [look like this](./code/step2). 48 | 49 | ## Step 3 — Adjust the Static Website 50 | 51 | Download the zip archive from https://mikhailworkshop.blob.core.windows.net/zips/droneapp-auth.zip. It contains the files for the same static website but with authentication enabled. 52 | 53 | Extract the contents into the folder `droneapp-auth` under the folder `statusapp`. Make sure that the HTML and JavaScript files are located directly inside `statusapp/droneapp-auth` (not in a subfolder below). 54 | 55 | The source for this application is available [here](https://github.com/mikhailshilkov/azure-serverveless-workshop/tree/master/website/auth). 56 | 57 | Open the file `websiteFiles.ts`. Change the name of the folder there: 58 | 59 | ```ts 60 | const folderName = "droneapp-auth"; 61 | ``` 62 | 63 | Also, change the `asset` calculation block to populate the tenant ID, client ID, and application ID inside the static files: 64 | 65 | ```ts 66 | const asset = pulumi.all([api.apiUrl, website.tenantId, website.applicationId]) 67 | .apply(([url, tenant, app]) => 68 | rawText.replace("[API_URL]", url) 69 | .replace("[TENANT_ID]", tenant) 70 | .replace("[APP_ID]", app) 71 | .replace("[CLIENT_ID]", app)) 72 | .apply(text => new pulumi.asset.StringAsset(text)); 73 | ``` 74 | 75 | Now, you should also enforce authentication in the API Management layer. Navigate to `api.ts`, find the `ApiPolicy` resource and insert `validate-jwt` block as below: 76 | 77 | ```ts 78 | const apiPolicy = new azure.apimanagement.ApiPolicy("policy", { 79 | ... 80 | 81 | 82 | 83 | ${website.applicationId} 84 | 85 | 86 | ... 87 | ``` 88 | 89 | > :white_check_mark: After these changes, your files should [look like this](./code/step3). 90 | 91 | ## Step 4 — Deploy and Test the Stack 92 | 93 | Deploy the stack 94 | 95 | ```bash 96 | $ pulumi up 97 | ... 98 | Updating (dev): 99 | ... 100 | Resources: 101 | + 13 created 102 | - 12 deleted 103 | +-8 replaced 104 | ~ 1 updated 105 | 34 changes. 17 unchanged 106 | ``` 107 | 108 | Navigate to the website in a browser and make sure that you get a login screen like this one: 109 | 110 | ![Sign In](./img/auth.png) 111 | 112 | Note: if you are using the CDN URL, mind that the old files may be served for a while due to caching. 113 | 114 | Click the "Sign In" button and login with your Azure username and password, then accept the permission request. 115 | 116 | Now, the search screen should appear, and your user account is shown in the top-right corner. 117 | 118 | ## Next Steps 119 | 120 | Congratulations! :tada: You have successfully setup Azure AD authentication for your website. 121 | 122 | You have completed the labs! You are awesome! :tada: 123 | 124 | After you are done with playing with the application, don't forget to clean up the infrastructure by running `pulumi destroy` in both `statusapp` and `telemetry` projects (in this order). 125 | 126 | You will always be able to re-create them, because the infrastructure is defined as code! 127 | -------------------------------------------------------------------------------- /08-aad/code/step2/api.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as apimanagement from "@pulumi/azure-nextgen/apimanagement/latest"; 3 | import * as azure from "@pulumi/azure"; 4 | import { appName, location, resourceGroupName } from "./common"; 5 | import * as functionApp from "./functionApp"; 6 | import * as website from "./website"; 7 | 8 | const apiManagementName = `${appName}-apim`; 9 | const apiManagement = new apimanagement.ApiManagementService(apiManagementName, { 10 | resourceGroupName: resourceGroupName, 11 | serviceName: apiManagementName, 12 | location: location, 13 | sku: { 14 | name: "Consumption", 15 | capacity: 0, 16 | }, 17 | publisherEmail: "drones@contoso.com", 18 | publisherName: "contoso", 19 | }); 20 | const apiManagementId = apiManagement.id; 21 | 22 | const versionSet = new apimanagement.ApiVersionSet("dronestatusversionset", { 23 | resourceGroupName: resourceGroupName, 24 | serviceName: apiManagement.name, 25 | versionSetId: "dronestatusversionset", 26 | displayName: "Drone Delivery API", 27 | versioningScheme: "Segment", 28 | }); 29 | 30 | const api = new apimanagement.Api("dronedeliveryapiv1", { 31 | resourceGroupName: resourceGroupName, 32 | serviceName: apiManagementName, 33 | apiId: "dronedeliveryapiv1", 34 | displayName: "Drone Delivery API", 35 | description: "Drone Delivery API", 36 | path: "api", 37 | apiVersion: "v1", 38 | apiRevision: "1", 39 | apiVersionSetId: versionSet.id, 40 | protocols: ["https"], 41 | }); 42 | 43 | const apiOperation = new apimanagement.ApiOperation("dronestatusGET", { 44 | resourceGroupName: resourceGroupName, 45 | serviceName: apiManagementName, 46 | apiId: api.name, 47 | operationId: "dronestatusGET", 48 | displayName: "Retrieve drone status", 49 | description: "Retrieve drone status", 50 | method: "GET", 51 | urlTemplate: "/dronestatus/{deviceid}", 52 | templateParameters: [ 53 | { 54 | name: "deviceid", 55 | description: "device id", 56 | type: "string", 57 | required: true, 58 | }, 59 | ], 60 | }); 61 | 62 | const backend = new apimanagement.Backend("dronestatusdotnet", { 63 | resourceGroupName: resourceGroupName, 64 | serviceName: apiManagementName, 65 | backendId: "dronestatusdotnet", 66 | resourceId: pulumi.interpolate`https://management.azure.com/${functionApp.id}`, 67 | // credentials: { 68 | // query: { 69 | // code: pulumi.interpolate`{{${apiValueFunctionCode.name}}}`, 70 | // }, 71 | // }, 72 | url: functionApp.appUrl, 73 | protocol: "http", 74 | }); 75 | 76 | const apiPolicy = new apimanagement.ApiPolicy("policy", { 77 | resourceGroupName: resourceGroupName, 78 | serviceName: apiManagementName, 79 | apiId: api.name, 80 | policyId: "policy", 81 | value: pulumi.interpolate` 82 | 83 | 84 | 85 | 86 | 87 | ${website.storageAccountUrl} 88 | ${website.cdnUrl} 89 | 90 | GET 91 |
*
92 |
93 | 94 | 95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |
`, 106 | }); 107 | 108 | const product = new apimanagement.Product("dronedeliveryprodapi", { 109 | resourceGroupName: resourceGroupName, 110 | serviceName: apiManagementName, 111 | productId: "dronedeliveryprodapi", 112 | displayName: "drone delivery product api", 113 | description: "drone delivery product api", 114 | terms: "terms for example product", 115 | subscriptionRequired: false, 116 | state: "published", 117 | }); 118 | 119 | const productApi = new azure.apimanagement.ProductApi("dronedeliveryapiv1", { 120 | resourceGroupName: resourceGroupName, 121 | apiManagementName: apiManagementName, 122 | apiName: api.name, 123 | productId: product.name, 124 | }); 125 | 126 | export const apiUrl = pulumi.interpolate`https://${apiManagementName}.azure-api.net/${api.path}/v1/dronestatus/`; 127 | -------------------------------------------------------------------------------- /08-aad/code/step2/common.ts: -------------------------------------------------------------------------------- 1 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 2 | 3 | export const appName = "status"; 4 | 5 | const resourceGroup = new resources.ResourceGroup(`${appName}-rg`, { 6 | resourceGroupName: `${appName}-rg`, 7 | location: "WestEurope", 8 | }); 9 | 10 | export const resourceGroupName = resourceGroup.name; 11 | export const resourceGroupId = resourceGroup.id; 12 | export const location = resourceGroup.location; 13 | -------------------------------------------------------------------------------- /08-aad/code/step2/functionApp.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as insights from "@pulumi/azure-nextgen/insights/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | import * as web from "@pulumi/azure-nextgen/web/latest"; 5 | import { appName, location, resourceGroupName } from "./common"; 6 | 7 | const droneStatusStorageAccount = new storage.StorageAccount(`${appName}sa`, { 8 | resourceGroupName: resourceGroupName, 9 | location: location, 10 | accountName: `${appName}sa`, 11 | sku: { 12 | name: "Standard_LRS", 13 | }, 14 | kind: "StorageV2", 15 | tags: { 16 | displayName: "Drone Status Function App", 17 | }, 18 | }); 19 | 20 | function getStorageConnectionString(account: storage.StorageAccount): pulumi.Output { 21 | const keys = pulumi.all([resourceGroupName, account.name]).apply(([resourceGroupName, accountName]) => 22 | storage.listStorageAccountKeys({ resourceGroupName, accountName })); 23 | const key = keys.keys[0].value; 24 | return pulumi.interpolate`DefaultEndpointsProtocol=https;AccountName=${account.name};AccountKey=${key}`; 25 | } 26 | 27 | const droneStatusAppInsights = new insights.Component(`${appName}-ai`, { 28 | resourceGroupName: resourceGroupName, 29 | resourceName: `${appName}-ai`, 30 | location: location, 31 | applicationType: "web", 32 | kind: "web", 33 | }); 34 | 35 | const hostingPlan = new web.AppServicePlan(`${appName}-asp`, { 36 | resourceGroupName: resourceGroupName, 37 | name: `${appName}-asp`, 38 | location: location, 39 | sku: { 40 | name: "Y1", 41 | tier: "Dynamic", 42 | }, 43 | }); 44 | 45 | const telemetry = new pulumi.StackReference("mikhailshilkov/telemetry-nextgen/dev"); 46 | const cosmosDatabaseName = telemetry.requireOutput("cosmosDatabaseName"); 47 | const cosmosCollectionName = telemetry.requireOutput("cosmosCollectionName"); 48 | const cosmosConnectionString = telemetry.requireOutput("cosmosConnectionString"); 49 | const cosmosEndpoint = telemetry.requireOutput("cosmosEndpoint"); 50 | const cosmosMasterKey = telemetry.requireOutput("cosmosMasterKey"); 51 | 52 | const droneStatusFunctionApp = new web.WebApp(`${appName}-app`, { 53 | resourceGroupName: resourceGroupName, 54 | name: `${appName}-app`, 55 | location: location, 56 | serverFarmId: hostingPlan.id, 57 | kind: "functionapp", 58 | siteConfig: { 59 | appSettings: [ 60 | { name: "APPINSIGHTS_INSTRUMENTATIONKEY", value: droneStatusAppInsights.instrumentationKey }, 61 | { name: "APPLICATIONINSIGHTS_CONNECTION_STRING", value: pulumi.interpolate`InstrumentationKey=${droneStatusAppInsights.instrumentationKey}` }, 62 | { name: "ApplicationInsightsAgent_EXTENSION_VERSION", value: "~2" }, 63 | { name: "AzureWebJobsStorage", value: getStorageConnectionString(droneStatusStorageAccount) }, 64 | { name: "COSMOSDB_CONNECTION_STRING", value: cosmosConnectionString }, 65 | { name: "CosmosDBEndpoint", value: cosmosEndpoint }, 66 | { name: "CosmosDBKey", value: cosmosMasterKey }, 67 | { name: "COSMOSDB_DATABASE_NAME", value: cosmosDatabaseName }, 68 | { name: "COSMOSDB_DATABASE_COL", value: cosmosCollectionName }, 69 | { name: "FUNCTIONS_EXTENSION_VERSION", value: "~3" }, 70 | { name: "FUNCTIONS_WORKER_RUNTIME", value: "dotnet" }, 71 | { name: "WEBSITE_NODE_DEFAULT_VERSION", value: "10.14.1" }, 72 | { name: "WEBSITE_RUN_FROM_PACKAGE", value: "https://mikhailworkshop.blob.core.windows.net/zips/statusapp.zip" }, 73 | ], 74 | cors: { 75 | allowedOrigins: ["*"], 76 | }, 77 | }, 78 | tags: { 79 | displayName: "Drone Telemetry Function App", 80 | }, 81 | }); 82 | 83 | export const functionUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api/GetStatusFunction?deviceId=`; 84 | export const id = droneStatusFunctionApp.id; 85 | export const appUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api`; 86 | -------------------------------------------------------------------------------- /08-aad/code/step2/index.ts: -------------------------------------------------------------------------------- 1 | import "./common"; 2 | import "./websiteFiles"; 3 | 4 | import * as functions from "./functionApp"; 5 | import * as website from "./website"; 6 | 7 | export const functionUrl = functions.functionUrl; 8 | export const storageAccountUrl = website.storageAccountUrl; 9 | export const cdnUrl = website.cdnUrl; 10 | -------------------------------------------------------------------------------- /08-aad/code/step2/website.ts: -------------------------------------------------------------------------------- 1 | import * as azure from "@pulumi/azure"; 2 | import * as azuread from "@pulumi/azuread"; 3 | import * as pulumi from "@pulumi/pulumi"; 4 | import * as cdn from "@pulumi/azure-nextgen/cdn/latest"; 5 | import { appName, location, resourceGroupName } from "./common"; 6 | 7 | export const storageAccount = new azure.storage.Account(`${appName}fe`, { 8 | resourceGroupName: resourceGroupName, 9 | tags: { 10 | displayName: "Drone Front End Storage Account", 11 | }, 12 | accountTier: "Standard", 13 | accountReplicationType: "LRS", 14 | staticWebsite: { 15 | indexDocument: "index.html", 16 | error404Document: "404.html", 17 | }, 18 | }); 19 | 20 | export const storageAccountUrl = storageAccount.primaryWebEndpoint; 21 | 22 | const cdnProfile = new cdn.Profile("profile", { 23 | resourceGroupName: resourceGroupName, 24 | profileName: `${appName}-cdn`, 25 | location: location, 26 | sku: { name: "Standard_Microsoft" }, 27 | }); 28 | 29 | const cdnEndpoint = new cdn.Endpoint("endpoint", { 30 | resourceGroupName: resourceGroupName, 31 | profileName: cdnProfile.name, 32 | endpointName: `${appName}-endpoint`, 33 | location: location, 34 | isHttpAllowed: false, 35 | origins: [{ name: "origin", hostName: storageAccount.primaryWebHost }], 36 | originHostHeader: storageAccount.primaryWebHost, 37 | }); 38 | 39 | export const cdnUrl = pulumi.interpolate`https://${cdnEndpoint.hostName}`; 40 | 41 | export const tenantId = pulumi.output(azure.core.getClientConfig()).tenantId; 42 | const apiAppName=`${appName}-api`; 43 | 44 | const apiApp = new azuread.Application(apiAppName, { 45 | name: apiAppName, 46 | oauth2AllowImplicitFlow: true, 47 | replyUrls: [storageAccountUrl, cdnUrl], 48 | identifierUris: [`http://${apiAppName}`], 49 | appRoles: [{ 50 | allowedMemberTypes: [ "User" ], 51 | description:"Access to device status", 52 | displayName:"Get Device Status", 53 | isEnabled:true, 54 | value: "GetStatus", 55 | }], 56 | requiredResourceAccesses: [{ 57 | resourceAppId: "00000003-0000-0000-c000-000000000000", 58 | resourceAccesses: [ { id: "e1fe6dd8-ba31-4d61-89e7-88639da4683d", type: "Scope" } ], 59 | }], 60 | }); 61 | export const applicationId = apiApp.applicationId; 62 | -------------------------------------------------------------------------------- /08-aad/code/step2/websiteFiles.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as azure from "@pulumi/azure"; 3 | import * as website from "./website"; 4 | import * as mime from "mime"; 5 | import * as nodedir from "node-dir"; 6 | import * as fs from "fs"; 7 | import * as api from "./api"; 8 | 9 | const folderName = "droneapp-noauth"; 10 | const files = nodedir.files(folderName, { sync: true }); 11 | for (const file of files) { 12 | const name = file.substring(folderName.length+1); 13 | const contentType = mime.getType(file) || undefined; 14 | 15 | const rawText = fs.readFileSync(file, "utf8").toString(); 16 | const asset = api.apiUrl 17 | .apply(url => rawText.replace("[API_URL]", url)) 18 | .apply(text => new pulumi.asset.StringAsset(text)); 19 | 20 | const myObject = new azure.storage.Blob(name, { 21 | name, 22 | storageAccountName: website.storageAccount.name, 23 | storageContainerName: "$web", 24 | type: "Block", 25 | source: asset, 26 | contentType, 27 | }, { parent: website.storageAccount }); 28 | } 29 | -------------------------------------------------------------------------------- /08-aad/code/step3/api.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as apimanagement from "@pulumi/azure-nextgen/apimanagement/latest"; 3 | import * as azure from "@pulumi/azure"; 4 | import { appName, location, resourceGroupName } from "./common"; 5 | import * as functionApp from "./functionApp"; 6 | import * as website from "./website"; 7 | 8 | const apiManagementName = `${appName}-apim`; 9 | const apiManagement = new apimanagement.ApiManagementService(apiManagementName, { 10 | resourceGroupName: resourceGroupName, 11 | serviceName: apiManagementName, 12 | location: location, 13 | sku: { 14 | name: "Consumption", 15 | capacity: 0, 16 | }, 17 | publisherEmail: "drones@contoso.com", 18 | publisherName: "contoso", 19 | }); 20 | const apiManagementId = apiManagement.id; 21 | 22 | const versionSet = new apimanagement.ApiVersionSet("dronestatusversionset", { 23 | resourceGroupName: resourceGroupName, 24 | serviceName: apiManagement.name, 25 | versionSetId: "dronestatusversionset", 26 | displayName: "Drone Delivery API", 27 | versioningScheme: "Segment", 28 | }); 29 | 30 | const api = new apimanagement.Api("dronedeliveryapiv1", { 31 | resourceGroupName: resourceGroupName, 32 | serviceName: apiManagementName, 33 | apiId: "dronedeliveryapiv1", 34 | displayName: "Drone Delivery API", 35 | description: "Drone Delivery API", 36 | path: "api", 37 | apiVersion: "v1", 38 | apiRevision: "1", 39 | apiVersionSetId: versionSet.id, 40 | protocols: ["https"], 41 | }); 42 | 43 | const apiOperation = new apimanagement.ApiOperation("dronestatusGET", { 44 | resourceGroupName: resourceGroupName, 45 | serviceName: apiManagementName, 46 | apiId: api.name, 47 | operationId: "dronestatusGET", 48 | displayName: "Retrieve drone status", 49 | description: "Retrieve drone status", 50 | method: "GET", 51 | urlTemplate: "/dronestatus/{deviceid}", 52 | templateParameters: [ 53 | { 54 | name: "deviceid", 55 | description: "device id", 56 | type: "string", 57 | required: true, 58 | }, 59 | ], 60 | }); 61 | 62 | const backend = new apimanagement.Backend("dronestatusdotnet", { 63 | resourceGroupName: resourceGroupName, 64 | serviceName: apiManagementName, 65 | backendId: "dronestatusdotnet", 66 | resourceId: pulumi.interpolate`https://management.azure.com/${functionApp.id}`, 67 | // credentials: { 68 | // query: { 69 | // code: pulumi.interpolate`{{${apiValueFunctionCode.name}}}`, 70 | // }, 71 | // }, 72 | url: functionApp.appUrl, 73 | protocol: "http", 74 | }); 75 | 76 | const apiPolicy = new apimanagement.ApiPolicy("policy", { 77 | resourceGroupName: resourceGroupName, 78 | serviceName: apiManagementName, 79 | apiId: api.name, 80 | policyId: "policy", 81 | value: pulumi.interpolate` 82 | 83 | 84 | 85 | 86 | 87 | ${website.storageAccountUrl} 88 | ${website.cdnUrl} 89 | 90 | GET 91 |
*
92 |
93 | 94 | 95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |
`, 106 | }); 107 | 108 | const product = new apimanagement.Product("dronedeliveryprodapi", { 109 | resourceGroupName: resourceGroupName, 110 | serviceName: apiManagementName, 111 | productId: "dronedeliveryprodapi", 112 | displayName: "drone delivery product api", 113 | description: "drone delivery product api", 114 | terms: "terms for example product", 115 | subscriptionRequired: false, 116 | state: "published", 117 | }); 118 | 119 | const productApi = new azure.apimanagement.ProductApi("dronedeliveryapiv1", { 120 | resourceGroupName: resourceGroupName, 121 | apiManagementName: apiManagementName, 122 | apiName: api.name, 123 | productId: product.name, 124 | }); 125 | 126 | export const apiUrl = pulumi.interpolate`https://${apiManagementName}.azure-api.net/${api.path}/v1/dronestatus/`; 127 | -------------------------------------------------------------------------------- /08-aad/code/step3/common.ts: -------------------------------------------------------------------------------- 1 | import * as resources from "@pulumi/azure-nextgen/resources/latest"; 2 | 3 | export const appName = "status"; 4 | 5 | const resourceGroup = new resources.ResourceGroup(`${appName}-rg`, { 6 | resourceGroupName: `${appName}-rg`, 7 | location: "WestEurope", 8 | }); 9 | 10 | export const resourceGroupName = resourceGroup.name; 11 | export const resourceGroupId = resourceGroup.id; 12 | export const location = resourceGroup.location; 13 | -------------------------------------------------------------------------------- /08-aad/code/step3/functionApp.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as insights from "@pulumi/azure-nextgen/insights/latest"; 3 | import * as storage from "@pulumi/azure-nextgen/storage/latest"; 4 | import * as web from "@pulumi/azure-nextgen/web/latest"; 5 | import { appName, location, resourceGroupName } from "./common"; 6 | 7 | const droneStatusStorageAccount = new storage.StorageAccount(`${appName}sa`, { 8 | resourceGroupName: resourceGroupName, 9 | location: location, 10 | accountName: `${appName}sa`, 11 | sku: { 12 | name: "Standard_LRS", 13 | }, 14 | kind: "StorageV2", 15 | tags: { 16 | displayName: "Drone Status Function App", 17 | }, 18 | }); 19 | 20 | function getStorageConnectionString(account: storage.StorageAccount): pulumi.Output { 21 | const keys = pulumi.all([resourceGroupName, account.name]).apply(([resourceGroupName, accountName]) => 22 | storage.listStorageAccountKeys({ resourceGroupName, accountName })); 23 | const key = keys.keys[0].value; 24 | return pulumi.interpolate`DefaultEndpointsProtocol=https;AccountName=${account.name};AccountKey=${key}`; 25 | } 26 | 27 | const droneStatusAppInsights = new insights.Component(`${appName}-ai`, { 28 | resourceGroupName: resourceGroupName, 29 | resourceName: `${appName}-ai`, 30 | location: location, 31 | applicationType: "web", 32 | kind: "web", 33 | }); 34 | 35 | const hostingPlan = new web.AppServicePlan(`${appName}-asp`, { 36 | resourceGroupName: resourceGroupName, 37 | name: `${appName}-asp`, 38 | location: location, 39 | sku: { 40 | name: "Y1", 41 | tier: "Dynamic", 42 | }, 43 | }); 44 | 45 | const telemetry = new pulumi.StackReference("mikhailshilkov/telemetry-nextgen/dev"); 46 | const cosmosDatabaseName = telemetry.requireOutput("cosmosDatabaseName"); 47 | const cosmosCollectionName = telemetry.requireOutput("cosmosCollectionName"); 48 | const cosmosConnectionString = telemetry.requireOutput("cosmosConnectionString"); 49 | const cosmosEndpoint = telemetry.requireOutput("cosmosEndpoint"); 50 | const cosmosMasterKey = telemetry.requireOutput("cosmosMasterKey"); 51 | 52 | const droneStatusFunctionApp = new web.WebApp(`${appName}-app`, { 53 | resourceGroupName: resourceGroupName, 54 | name: `${appName}-app`, 55 | location: location, 56 | serverFarmId: hostingPlan.id, 57 | kind: "functionapp", 58 | siteConfig: { 59 | appSettings: [ 60 | { name: "APPINSIGHTS_INSTRUMENTATIONKEY", value: droneStatusAppInsights.instrumentationKey }, 61 | { name: "APPLICATIONINSIGHTS_CONNECTION_STRING", value: pulumi.interpolate`InstrumentationKey=${droneStatusAppInsights.instrumentationKey}` }, 62 | { name: "ApplicationInsightsAgent_EXTENSION_VERSION", value: "~2" }, 63 | { name: "AzureWebJobsStorage", value: getStorageConnectionString(droneStatusStorageAccount) }, 64 | { name: "COSMOSDB_CONNECTION_STRING", value: cosmosConnectionString }, 65 | { name: "CosmosDBEndpoint", value: cosmosEndpoint }, 66 | { name: "CosmosDBKey", value: cosmosMasterKey }, 67 | { name: "COSMOSDB_DATABASE_NAME", value: cosmosDatabaseName }, 68 | { name: "COSMOSDB_DATABASE_COL", value: cosmosCollectionName }, 69 | { name: "FUNCTIONS_EXTENSION_VERSION", value: "~3" }, 70 | { name: "FUNCTIONS_WORKER_RUNTIME", value: "dotnet" }, 71 | { name: "WEBSITE_NODE_DEFAULT_VERSION", value: "10.14.1" }, 72 | { name: "WEBSITE_RUN_FROM_PACKAGE", value: "https://mikhailworkshop.blob.core.windows.net/zips/statusapp.zip" }, 73 | ], 74 | cors: { 75 | allowedOrigins: ["*"], 76 | }, 77 | }, 78 | tags: { 79 | displayName: "Drone Telemetry Function App", 80 | }, 81 | }); 82 | 83 | export const functionUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api/GetStatusFunction?deviceId=`; 84 | export const id = droneStatusFunctionApp.id; 85 | export const appUrl = pulumi.interpolate`https://${droneStatusFunctionApp.defaultHostName}/api`; 86 | -------------------------------------------------------------------------------- /08-aad/code/step3/index.ts: -------------------------------------------------------------------------------- 1 | import "./common"; 2 | import "./websiteFiles"; 3 | 4 | import * as functions from "./functionApp"; 5 | import * as website from "./website"; 6 | 7 | export const functionUrl = functions.functionUrl; 8 | export const storageAccountUrl = website.storageAccountUrl; 9 | export const cdnUrl = website.cdnUrl; 10 | -------------------------------------------------------------------------------- /08-aad/code/step3/website.ts: -------------------------------------------------------------------------------- 1 | import * as azure from "@pulumi/azure"; 2 | import * as azuread from "@pulumi/azuread"; 3 | import * as pulumi from "@pulumi/pulumi"; 4 | import * as cdn from "@pulumi/azure-nextgen/cdn/latest"; 5 | import { appName, location, resourceGroupName } from "./common"; 6 | 7 | export const storageAccount = new azure.storage.Account(`${appName}fe`, { 8 | resourceGroupName: resourceGroupName, 9 | tags: { 10 | displayName: "Drone Front End Storage Account", 11 | }, 12 | accountTier: "Standard", 13 | accountReplicationType: "LRS", 14 | staticWebsite: { 15 | indexDocument: "index.html", 16 | error404Document: "404.html", 17 | }, 18 | }); 19 | 20 | export const storageAccountUrl = storageAccount.primaryWebEndpoint; 21 | 22 | const cdnProfile = new cdn.Profile("profile", { 23 | resourceGroupName: resourceGroupName, 24 | profileName: `${appName}-cdn`, 25 | location: location, 26 | sku: { name: "Standard_Microsoft" }, 27 | }); 28 | 29 | const cdnEndpoint = new cdn.Endpoint("endpoint", { 30 | resourceGroupName: resourceGroupName, 31 | profileName: cdnProfile.name, 32 | endpointName: `${appName}-endpoint`, 33 | location: location, 34 | isHttpAllowed: false, 35 | origins: [{ name: "origin", hostName: storageAccount.primaryWebHost }], 36 | originHostHeader: storageAccount.primaryWebHost, 37 | }); 38 | 39 | export const cdnUrl = pulumi.interpolate`https://${cdnEndpoint.hostName}`; 40 | 41 | export const tenantId = pulumi.output(azure.core.getClientConfig()).tenantId; 42 | const apiAppName=`${appName}-api`; 43 | 44 | const apiApp = new azuread.Application(apiAppName, { 45 | name: apiAppName, 46 | oauth2AllowImplicitFlow: true, 47 | replyUrls: [storageAccountUrl, cdnUrl], 48 | identifierUris: [`http://${apiAppName}`], 49 | appRoles: [{ 50 | allowedMemberTypes: [ "User" ], 51 | description:"Access to device status", 52 | displayName:"Get Device Status", 53 | isEnabled:true, 54 | value: "GetStatus", 55 | }], 56 | requiredResourceAccesses: [{ 57 | resourceAppId: "00000003-0000-0000-c000-000000000000", 58 | resourceAccesses: [ { id: "e1fe6dd8-ba31-4d61-89e7-88639da4683d", type: "Scope" } ], 59 | }], 60 | }); 61 | export const applicationId = apiApp.applicationId; 62 | -------------------------------------------------------------------------------- /08-aad/code/step3/websiteFiles.ts: -------------------------------------------------------------------------------- 1 | import * as pulumi from "@pulumi/pulumi"; 2 | import * as azure from "@pulumi/azure"; 3 | import * as website from "./website"; 4 | import * as mime from "mime"; 5 | import * as nodedir from "node-dir"; 6 | import * as fs from "fs"; 7 | import * as api from "./api"; 8 | 9 | const folderName = "droneapp-auth"; 10 | const files = nodedir.files(folderName, { sync: true }); 11 | for (const file of files) { 12 | const name = file.substring(folderName.length+1); 13 | const contentType = mime.getType(file) || undefined; 14 | 15 | const rawText = fs.readFileSync(file, "utf8").toString(); 16 | const asset = pulumi.all([api.apiUrl, website.tenantId, website.applicationId]) 17 | .apply(([url, tenant, app]) => 18 | rawText.replace("[API_URL]", url) 19 | .replace("[TENANT_ID]", tenant) 20 | .replace("[APP_ID]", app) 21 | .replace("[CLIENT_ID]", app)) 22 | .apply(text => new pulumi.asset.StringAsset(text)); 23 | 24 | const myObject = new azure.storage.Blob(name, { 25 | name, 26 | storageAccountName: website.storageAccount.name, 27 | storageContainerName: "$web", 28 | type: "Block", 29 | source: asset, 30 | contentType, 31 | }, { parent: website.storageAccount }); 32 | } 33 | -------------------------------------------------------------------------------- /08-aad/img/auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikhailshilkov/azure-serverless-workshop/031b7807a1d0835f683336239469be0d14800cd2/08-aad/img/auth.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mikhail Shilkov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Azure Serverless Workshop with Pulumi 2 | 3 | This hands-on workshop will walk you through various tasks of managing Azure infrastructure with the focus on serverless compute and managed Azure services. All the resources are provisioned with [Pulumi](https://pulumi.com) in the infrastructure-as-code fashion. 4 | 5 | You will be building an end-to-end Drone Delivery application that is based on two [reference architectures](https://github.com/mspnp/serverless-reference-implementation) defined by Microsoft. 6 | 7 | ### Drone Delivery Serverless 8 | 9 | The project consists of two parts: a web front-end and a data processing pipeline. 10 | 11 | #### Serverless web application 12 | 13 | ![Serverless web application](./img/status.png) 14 | 15 | The application serves static content from Azure Blob Storage and implements an API using Azure Functions. The API reads data from Cosmos DB and returns the results to the web app. 16 | 17 | #### Serverless event processing 18 | 19 | ![Serverless event processing](./img/telemetry.png) 20 | 21 | The application ingests a stream of data, processes the data, and writes the results to a back-end database (Cosmos DB). 22 | 23 | ### Prerequisities 24 | 25 | Before proceeding, ensure your machine is ready to go: 26 | 27 | - [Installing Prerequisites](00-installing-prerequisites.md) 28 | 29 | ### Lab 1 — Modern Infrastructure as Code 30 | 31 | The first lab takes you on a tour of infrastructure as code concepts: 32 | 33 | 1. [Creating a New Project](./01-iac/01-creating-a-new-project.md) 34 | 2. [Configuring Azure](./01-iac/02-configuring-azure.md) 35 | 3. [Provisioning Infrastructure](./01-iac/03-provisioning-infrastructure.md) 36 | 4. [Updating your Infrastructure](./01-iac/04-updating-your-infrastructure.md) 37 | 5. [Making Your Stack Configurable](./01-iac/05-making-your-stack-configurable.md) 38 | 6. [Creating a Second Stack](./01-iac/06-creating-a-second-stack.md) 39 | 7. [Destroying Your Infrastructure](./01-iac/07-destroying-your-infrastructure.md) 40 | 41 | [Get Started with Lab 1](./01-iac/01-creating-a-new-project.md) 42 | 43 | ### Lab 2 - Deploy Serverless Applications with Azure Functions 44 | 45 | In this lab, you deploy an Azure Function App with HTTP-triggered serverless functions. 46 | 47 | [Get Started with Lab 2](./02-serverless/README.md) 48 | 49 | ### Lab 3 - Deploy a Data Processing Pipeline 50 | 51 | In this lab, you deploy a Azure Function Apps that is triggered by messages in an Event Hub. Device data from the messages are saved to Azure Cosmos DB. You setup a dead-letter queue for messages that failed to be processed and Azure Application Insights for monitoring. 52 | 53 | [Get Started with Lab 3](./03-telemetry/README.md) 54 | 55 | ### Lab 4 - Deploy a Status Function App 56 | 57 | In this lab, you deploy an Azure Function App that retrieves data from the Cosmos DB collection. That's the first step to building a web application that shows drone data to end users. 58 | 59 | [Get Started with Lab 4](./04-status/README.md) 60 | 61 | ### Lab 5 - Deploy a Static Website 62 | 63 | In this lab, you add an HTML frontend application that displays drone status data. You deploy a static website to a new Storage Account, including the HTML and JavaScipt files. 64 | 65 | [Get Started with Lab 5](./05-frontend/README.md) 66 | 67 | ### Lab 6 - Deploy Azure API Management 68 | 69 | In this lab, you add an API Management service in front of Azure Functions. You change the website to talk to that API Management service. 70 | 71 | [Get Started with Lab 6](./06-api/README.md) 72 | 73 | ### Lab 7 - Deploy the Website Behind Azure CDN 74 | 75 | In this lab, you add an Azure CDN service in front of the Storage Account. 76 | 77 | [Get Started with Lab 7](./07-cdn/README.md) 78 | 79 | ### Lab 8 - Enable User Authentication 80 | 81 | In this lab, you enable Azure Active Directory OAuth2 authentication in the status website. 82 | 83 | [Get Started with Lab 8](./08-aad/README.md) -------------------------------------------------------------------------------- /ServerlessArchitecture_Workshop_2020.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikhailshilkov/azure-serverless-workshop/031b7807a1d0835f683336239469be0d14800cd2/ServerlessArchitecture_Workshop_2020.pdf -------------------------------------------------------------------------------- /img/status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikhailshilkov/azure-serverless-workshop/031b7807a1d0835f683336239469be0d14800cd2/img/status.png -------------------------------------------------------------------------------- /img/telemetry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikhailshilkov/azure-serverless-workshop/031b7807a1d0835f683336239469be0d14800cd2/img/telemetry.png -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variables file 55 | .env 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | -------------------------------------------------------------------------------- /website/auth/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variables file 55 | .env 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | -------------------------------------------------------------------------------- /website/auth/gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteMetadata: { 3 | title: `Fabrikam Drone Status`, 4 | description: `Fabrikam Drone status is a SPA static website deployed to Azure Blob storage with Azure CDN`, 5 | author: `@mspnp`, 6 | }, 7 | plugins: [`gatsby-plugin-typescript`], 8 | } -------------------------------------------------------------------------------- /website/auth/gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | import { Stylesheet, InjectionMode } from "@uifabric/merge-styles" 2 | import { renderStatic } from "@uifabric/merge-styles/lib/server" 3 | import { renderToString } from "react-dom/server" 4 | import React from "react" 5 | 6 | export const replaceRenderer = ({ 7 | bodyComponent, 8 | replaceBodyHTMLString, 9 | setHeadComponents, 10 | }) => { 11 | const { html, css } = renderStatic(() => { 12 | return renderToString(bodyComponent) 13 | }) 14 | 15 | replaceBodyHTMLString(html) 16 | 17 | setHeadComponents([