Add an example of a dynamic resource provider that adds a custom domain to an Azure CDN endpoint.
MODIFY
.gitignore
.gitignore
@@ -12,3 +12,4 @@ Pulumi.*.yaml
.idea/
*.iml
key.rsa*
+obj/

ADD
azure-ts-dynamicresource/Pulumi.yaml
+name: azure-cdn-custom-domain
+runtime: nodejs
+description: Creates Azure CDN with a custom domain.
+template:
+ config:
+ azure:environment:
+ description: The Azure environment to use (`public`, `usgovernment`, `german`, `china`)
+ default: public

ADD
azure-ts-dynamicresource/README.md
+[![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new)
+
+# Azure CDN Custom Domain Dynamic Provider
+
+## Prerequisites
+
+Prior to running this example, ensure that the custom domain you will be adding to your CDN endpoint has a CNAME record in your domain registrar's DNS management portal. The dynamic provider will validate that the mapping exists and will fail the deployment if the CDN endpoint is not reachable through your custom domain.
+
+## Usage
+
+In order to create a custom domain, you must have first created a CDN profile and an endpoint in that profile. Once those are created, you can use the `CDNCustomDomainResource` just like any other resource.
+
+Here's an example:
+
+```ts
+const cdnCustomDomainResource = new CDNCustomDomainResource("cdnCustomDomain", {
+ // Ensure that there is a CNAME record for mycompany.com
+ // pointing to my-cdn-endpoint.azureedge.net.
+ // You would do that in your domain registrar's portal.
+ customDomainHostName: "mycompany.com",
+ customDomainName: "custom-domain",
+ profileName: cdnProfileName,
+ endpointName: cdnEndpointName,
+ // This will enable HTTPS through Azure's one-click
+ // automated certificate deployment.
+ // The certificate is fully managed by Azure from provisioning
+ // to automatic renewal at no additional cost to you.
+ httpsEnabled: true,
+ resourceGroupName: resourceGroupName
+}, { parent: cdnEndpoint });
+```
+
+That's it! The dynamic provider will automatically use the underlying Azure provider's configuration to determine the credentials to your subscription. If it cannot find them through the Azure provider, it will also look at the following environment variables:
+
+- `ARM_CLIENT_ID`
+- `ARM_CLIENT_SECRET` // Make sure to store the value for this var as secret in your CI/CD system.
+- `ARM_TENANT_ID`
+- `ARM_SUBSCRIPTION_ID`
+
+## Running the App
+
+1. Create a new stack:
+
+ ```
+ $ pulumi stack init azure-cdn-custom-domain
+ ```
+
+1. Login to Azure CLI (you will be prompted to do this during deployment if you forget this step):
+
+ ```
+ $ az login
+ ```
+
+1. Restore NPM dependencies:
+
+ ```
+ $ npm install
+ ```
+
+ ...or if you prefer using `yarn`, then `yarn install`.
+
+1. Run `pulumi up` to preview and deploy changes:
+
+ ```
+ $ pulumi up
+ Previewing changes:
+ ...
+
+ Performing changes:
+ ...
+ ...
+ Update duration: ...
+ ```
+
+## Dynamic Providers
+
+Learn more about dynamic providers [here](https://pulumi.io/reference/programming-model/#dynamicproviders).
+
+## Known Issues
+
+If you get a 404 error when deleting a custom domain or a deserialization error `SyntaxError: Unexpected token o in JSON at position 1`, these were due to bugs in the older versions of the Azure nodeJS SDK. See https://github.com/Azure/azure-sdk-for-js/issues/2842 for more info. Ensure that you have the latest versions by running `npm list @azure/ms-rest-js @azure/ms-rest-azure-js`. The versions you should have are:
+- `@azure/ms-rest-js` - 1.8.10
+- `@azure/ms-rest-azure-js` - 1.3.7
+
+If you do not have the latest versions, delete your `node_modules` folder and `package-lock.json` (or `yarn.lock` if you are using yarn) and run `npm install` (or `yarn install`) again.

ADD
azure-ts-dynamicresource/cdnCustomDomain.ts
+// Copyright 2016-2019, Pulumi Corporation. All rights reserved.
+
+import * as pulumi from "@pulumi/pulumi";
+import * as azure from "@pulumi/azure";
+
+import * as msRestAzure from "@azure/ms-rest-nodeauth";
+import * as cdnManagement from "@azure/arm-cdn";
+import { ServiceClientCredentials } from "@azure/ms-rest-js";
+
+/**
+ * CustomDomainOptions represents the inputs to the dynamic resource.
+ * Any property of type `Input<T>` will automatically be resolved to their type `T`
+ * by the custom dynamic resource before passing them to the functions in the
+ * dynamic resource provider.
+ */
+export interface CustomDomainOptions {
+ resourceGroupName: pulumi.Input<string>;
+ profileName: pulumi.Input<string>;
+ endpointName: pulumi.Input<string>;
+ customDomainHostName: pulumi.Input<string>;
+ httpsEnabled: pulumi.Input<boolean>;
+}
+
+/**
+ * DynamicProviderInputs represents the inputs that are passed as inputs
+ * to each function in the implementation of a `pulumi.dynamic.ResourceProvider`.
+ * The property names in this must match `CustomDomainOptions`.
+ */
+interface DynamicProviderInputs {
+ resourceGroupName: string;
+ profileName: string;
+ endpointName: string;
+ customDomainHostName: string;
+ httpsEnabled: boolean;
+}
+
+/**
+ * DynamicProviderOutputs represents the output type of `create` function in the
+ * dynamic resource provider.
+ */
+interface DynamicProviderOutputs extends DynamicProviderInputs, cdnManagement.CdnManagementModels.CustomDomainsCreateResponse {
+ name: string;
+}
+
+class CDNCustomDomainResourceProvider implements pulumi.dynamic.ResourceProvider {
+ private name: string;
+
+ constructor(name: string) {
+ this.name = name;
+ }
+
+ private async getCDNManagementClient(): Promise<cdnManagement.CdnManagementClient> {
+ let clientID = azure.config.clientId;
+ let clientSecret = azure.config.clientSecret;
+ let tenantID = azure.config.tenantId;
+ let subscriptionID = azure.config.subscriptionId;
+ let credentials: ServiceClientCredentials;
+
+ // If at least one of them is empty, try looking at the env vars.
+ if (!clientID || !clientSecret || !tenantID || !subscriptionID) {
+ await pulumi.log.info("Checking env vars for ARM credentials.", undefined, undefined, true);
+ clientID = process.env["ARM_CLIENT_ID"];
+ clientSecret = process.env["ARM_CLIENT_SECRET"];
+ tenantID = process.env["ARM_TENANT_ID"];
+ subscriptionID = process.env["ARM_SUBSCRIPTION_ID"];
+ }
+
+ // If they are still empty, try to get the creds from Az CLI.
+ if (!clientID || !clientSecret || !tenantID || !subscriptionID) {
+ await pulumi.log.info("Env vars did not contain ARM credentials. Trying Az CLI.", undefined, undefined, true);
+ // `create()` will throw an error if the Az CLI is not installed or `az login` has never been run.
+ const cliCredentials = await msRestAzure.AzureCliCredentials.create();
+ subscriptionID = cliCredentials.subscriptionInfo.id;
+ credentials = cliCredentials;
+ await pulumi.log.info("Using credentials from Az CLI.", undefined, undefined, true);
+ } else {
+ credentials = await msRestAzure.loginWithServicePrincipalSecret(clientID, clientSecret, tenantID);
+ }
+
+ return new cdnManagement.CdnManagementClient(credentials, subscriptionID);
+ }
+
+ async check(olds: DynamicProviderInputs, news: DynamicProviderInputs): Promise<pulumi.dynamic.CheckResult> {
+ // If none of the CDN properties changed, then there is nothing to be validated.
+ if (olds.profileName === news.profileName &&
+ olds.endpointName === news.endpointName &&
+ olds.customDomainHostName === news.customDomainHostName) {
+ return { inputs: news };
+ }
+
+ const failures: pulumi.dynamic.CheckFailure[] = [];
+ if (!news.endpointName) {
+ failures.push({
+ property: "endpointName",
+ reason: "endpointName is required",
+ });
+ }
+ if (!news.profileName) {
+ failures.push({
+ property: "profileName",
+ reason: "profileName is required",
+ });
+ }
+ if (!news.customDomainHostName) {
+ failures.push({
+ property: "customDomainHostName",
+ reason: "customDomainHostName is required",
+ });
+ }
+
+ if (failures.length > 0) {
+ return { failures: failures };
+ }
+
+ return { inputs: news };
+ }
+
+ async diff(id: string, previousOutput: DynamicProviderOutputs, news: DynamicProviderInputs): Promise<pulumi.dynamic.DiffResult> {
+ const replaces: string[] = [];
+ let changes = false;
+ let deleteBeforeReplace = false;
+
+ if (previousOutput.customDomainHostName !== news.customDomainHostName || previousOutput.name !== this.name) {
+ await pulumi.log.warn("Changing the domain name properties will cause downtime.", undefined, undefined, true);
+
+ changes = true;
+ deleteBeforeReplace = true;
+ if (previousOutput.customDomainHostName !== news.customDomainHostName) {
+ replaces.push("customDomainHostName");
+ }
+ if (previousOutput.name !== this.name) {
+ replaces.push("customDomainName");
+ }
+ }
+
+ if (previousOutput.endpointName !== news.endpointName) {
+ changes = true;
+ deleteBeforeReplace = true;
+ replaces.push("endpointName");
+ }
+
+ // HTTPS can be enabled/disabled in-place.
+ if (previousOutput.httpsEnabled !== news.httpsEnabled) {
+ changes = true;
+ replaces.push("httpsEnabled");
+ }
+
+ return {
+ deleteBeforeReplace: deleteBeforeReplace,
+ replaces: replaces,
+ changes: changes
+ };
+ }
+
+ async create(inputs: DynamicProviderInputs): Promise<pulumi.dynamic.CreateResult> {
+ const cdnClient = await this.getCDNManagementClient();
+
+ const validationOutput = await cdnClient.endpoints.validateCustomDomain(
+ inputs.resourceGroupName,
+ inputs.profileName,
+ inputs.endpointName,
+ inputs.customDomainHostName);
+ if (!validationOutput.customDomainValidated) {
+ throw new Error(`Validation of custom domain failed with ${validationOutput.reason}`);
+ }
+
+ const result =
+ await cdnClient.customDomains.create(
+ inputs.resourceGroupName,
+ inputs.profileName,
+ inputs.endpointName,
+ this.name,
+ inputs.customDomainHostName);
+
+ if (inputs.httpsEnabled) {
+ await pulumi.log.info("Enabling HTTPS for the custom domain", undefined, undefined, true);
+
+ await cdnClient.customDomains.enableCustomHttps(
+ inputs.resourceGroupName,
+ inputs.profileName,
+ inputs.endpointName,
+ this.name);
+ }
+
+ const outs: DynamicProviderOutputs = {
+ ...result,
+ name: this.name,
+ resourceGroupName: inputs.resourceGroupName,
+ profileName: inputs.profileName,
+ endpointName: inputs.endpointName,
+ customDomainHostName: inputs.customDomainHostName,
+ httpsEnabled: inputs.httpsEnabled,
+ };
+
+ return { id: result.id!, outs: outs };
+ }
+
+ async read(id: string, props: DynamicProviderOutputs): Promise<pulumi.dynamic.ReadResult> {
+ const cdnClient = await this.getCDNManagementClient();
+ const result = await cdnClient.customDomains.get(props.resourceGroupName, props.profileName, props.endpointName, this.name);
+ return {
+ id: result.id,
+ props: { ...props, ...result },
+ };
+ }
+
+ async delete(id: string, props: DynamicProviderOutputs): Promise<void> {
+ const cdnClient = await this.getCDNManagementClient();
+ const result = await cdnClient.customDomains.deleteMethod(props.resourceGroupName, props.profileName, props.endpointName, this.name);
+ if (result._response.status >= 400) {
+ throw new Error("Error response received while trying to delete the custom domain.");
+ }
+ if (!result.resourceState) {
+ return;
+ }
+ if (result.resourceState !== 'Deleting') {
+ throw new Error(`Provisioning state of the custom domain was expected to be 'Deleting', but was ${result.resourceState}.`);
+ }
+
+ await pulumi.log.info(
+ "The request to delete was successful. However, it can take a minute or two to fully complete deletion.",
+ undefined,
+ undefined,
+ true);
+ }
+
+ /**
+ * The only thing that the update method really updates in the custom domain is the HTTPS enablement.
+ */
+ async update(id: string, currentOutputs: DynamicProviderOutputs, newInputs: DynamicProviderInputs): Promise<pulumi.dynamic.UpdateResult> {
+ const cdnClient = await this.getCDNManagementClient();
+ if (newInputs.httpsEnabled) {
+ await cdnClient.customDomains.enableCustomHttps(
+ newInputs.resourceGroupName,
+ newInputs.profileName,
+ newInputs.endpointName,
+ this.name);
+
+ currentOutputs.httpsEnabled = true;
+ return { outs: currentOutputs };
+ }
+
+ await cdnClient.customDomains.disableCustomHttps(
+ newInputs.resourceGroupName,
+ newInputs.profileName,
+ newInputs.endpointName,
+ this.name);
+
+ currentOutputs.httpsEnabled = false;
+ return { outs: currentOutputs };
+ }
+}
+
+/**
+ * CDNCustomDomainResource is a resource that can be used to create
+ * custom domains against Azure CDN resources.
+ * The Azure CDN resource itself must exist in order to create a custom domain for it.
+ *
+ * Outputs from the dynamic resource provider must be declared in the dynamic resource itself
+ * as `public readonly` members with the type `Output<T>`. These are automatically set by the dynamic
+ * provider engine. The names of these properties must match the names of the properties exactly as
+ * returned in the outputs of the dynamic resource provider functions.
+ */
+export class CDNCustomDomainResource extends pulumi.dynamic.Resource {
+ /**
+ * These are the same properties that were originally passed as inputs, but available as outputs
+ * for convenience. The names of these properties must match with `CustomDomainOptions`.
+ */
+ public readonly resourceGroupName: pulumi.Output<string>;
+ public readonly profileName: pulumi.Output<string>;
+ public readonly endpointName: pulumi.Output<string>;
+ public readonly customDomainHostName: pulumi.Output<string>;
+ public readonly httpsEnabled: pulumi.Output<boolean>;
+
+ // The following are properties set by the CDN rest client.
+ public readonly customHttpsProvisioningState: pulumi.Output<cdnManagement.CdnManagementModels.CustomHttpsProvisioningState>;
+ public readonly customHttpsProvisioningSubstate: pulumi.Output<cdnManagement.CdnManagementModels.CustomHttpsProvisioningSubstate>;
+ public readonly provisioningState: pulumi.Output<string>;
+ public readonly resourceState: pulumi.Output<cdnManagement.CdnManagementModels.CustomDomainResourceState>;
+ public readonly type: pulumi.Output<string>;
+
+ constructor(name: string, args: CustomDomainOptions, opts?: pulumi.CustomResourceOptions) {
+ super(new CDNCustomDomainResourceProvider(name), `azure:cdn:Endpoint:CustomDomains:${name}`, args, opts);
+ }
+}

ADD
azure-ts-dynamicresource/index.ts
+// Copyright 2016-2019, Pulumi Corporation. All rights reserved.
+
+import * as pulumi from "@pulumi/pulumi";
+import * as azure from "@pulumi/azure";
+
+import { CDNCustomDomainResource } from "./cdnCustomDomain";
+
+/**
+ * The location where our resource group and the resources under it will be created.
+ *
+ * To externalize this value, and make this configurable across environments/stacks,
+ * learn more at https://pulumi.io/reference/config/.
+ */
+const location = "West US";
+
+// Create an Azure Resource Group
+const resourceGroup = new azure.core.ResourceGroup("resourceGroup", {
+ location: location,
+});
+
+// Create an Azure resource (Storage Account) so we can point the CDN endpoint to it.
+const storageAccount = new azure.storage.Account("storageAccount", {
+ resourceGroupName: resourceGroup.name,
+ accountTier: "Standard",
+ accountReplicationType: "LRS",
+});
+
+/**
+ * Create a Blob container in the storage account,
+ * to store any static files. The CDN endpoint will be pointed at the
+ * endpoint for this blob container.
+ */
+const blobContainer = new azure.storage.Container("blobContainer", {
+ resourceGroupName: resourceGroup.name,
+ storageAccountName: storageAccount.name,
+ // Make each "blob" in the container publicly accessible.
+ // DO NOT set this property if you are going to store sensitive files!
+ containerAccessType: "blob",
+});
+
+const cdnProfile = new azure.cdn.Profile("cdnProfile", {
+ resourceGroupName: resourceGroup.name,
+ // Choose an appropriate SKU to use.
+ // https://docs.microsoft.com/en-us/azure/cdn/cdn-features
+ sku: "Standard_Akamai",
+});
+
+const cdnEndpoint = new azure.cdn.Endpoint("cdnEndpoint", {
+ /**
+ * Specify a well-known name for the endpoint name,
+ * so you can add a CNAME record for your custom domain
+ * pointing to this CDN endpoint to it.
+ *
+ * For example, the URL for this CDN endpoint when it is created
+ * would be `my-cdn-endpoint.azureedge.net`.
+ */
+ name: "my-cdn-endpoint",
+ resourceGroupName: resourceGroup.name,
+ profileName: cdnProfile.name,
+ isHttpsAllowed: true,
+ isHttpAllowed: false,
+ isCompressionEnabled: true,
+ originHostHeader: storageAccount.primaryBlobHost,
+ contentTypesToCompresses: [
+ "text/plain",
+ "text/html",
+ "text/css",
+ "text/javascript",
+ "application/x-javascript",
+ "application/javascript",
+ "application/json",
+ "application/xml",
+ "image/png",
+ "image/jpeg",
+ ],
+ origins: [
+ {
+ name: "cdn-origin",
+ hostName: storageAccount.primaryBlobHost,
+ httpsPort: 443,
+ }
+ ]
+});
+
+export const cdnEndpointUrl = pulumi.interpolate`https://${cdnEndpoint.hostName}`;
+
+export const cdnCustomDomainResource = new CDNCustomDomainResource("cdnCustomDomain", {
+ resourceGroupName: resourceGroup.name,
+ // Ensure that there is a CNAME record for mycompany.com pointing to my-cdn-endpoint.azureedge.net.
+ // You would do that in your domain registrar's portal.
+ customDomainHostName: "mycompany.com",
+ profileName: cdnProfile.name,
+ endpointName: cdnEndpoint.name,
+ /**
+ * This will enable HTTPS through Azure's one-click
+ * automated certificate deployment. The certificate is
+ * fully managed by Azure from provisioning to automatic renewal
+ * at no additional cost to you.
+ */
+ httpsEnabled: true,
+}, { parent: cdnEndpoint });

ADD
azure-ts-dynamicresource/package.json
+{
+ "name": "azure-ts-appservice",
+ "version": "1.0.0",
+ "devDependencies": {
+ "@types/node": "^10.3.3"
+ },
+ "dependencies": {
+ "@azure/arm-cdn": "^4.2.0",
+ "@azure/ms-rest-js": "^1.8.10",
+ "@azure/ms-rest-azure-js": "^1.3.7",
+ "@azure/ms-rest-nodeauth": "^1.1.1",
+ "@pulumi/azure": "latest",
+ "@pulumi/pulumi": "latest"
+ }
+}

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

Add an example of a dynamic resource provider that adds a custom domain to an Azure CDN endpoint. (#307)