This post was originally published here

This is the fifth and final post in my series around setting up CI/CD for Azure API Management using Azure Resource Manager templates. We already created our API Management instance, added products, users and groups to the instance, and created unversioned and versioned APIs. In this final post, we will see how we can use linked ARM templates in combination with VSTS to deploy our solution all at once, and how this allows us to re-use existing templates to build up our API Management.

image

The posts in this series are the following, this list will be updated as the posts are being published.

So far we did each of these steps from their own repositories, which is great when you want to have different developers only working on their own parts of the total solution. However if you don’t need this type of granular security on your repositories, it probably makes more sense to have you entire API Management solution in a single repository. When working with ARM templates, they can quickly become quite large and cumbersome to maintain. To avoid this, we can split up the template into smaller templates, which each does it’s own piece of work, and link these together from a master template. This allows for re-use of templates and breaking them down into smaller pieces of functionality. For this post we will be creating a API Management instance, create products, users and groups for Contoso, and deploy a versioned API. We will be re-using the templates we created in the previous blogposts from this series for our content.

We will start by creating a new GIT repository in our VSTS project called Linked Template. This repository will hold the master and nested templates which will be used to roll out our API Management solution.

Create Linked Template repository

Create Linked Template repository

Once the repository has been created, we will clone it to our local machine, and add a folder called templates in the repository. In the templates folder, create a new file called instance.json, which will hold the following nested ARM template for the API Management instance. All the ARM templates in this post should be placed in the templates folder unless otherwise specified.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
      "APIManagementSku": {
        "type": "string",
        "defaultValue": "Developer"
      },
      "APIManagementSkuCapacity": {
        "type": "string",
        "defaultValue": "1"
      },
      "APIManagementInstanceName": {
        "type": "string",
        "defaultValue": "MyAPIManagementInstance"
      },
      "PublisherName": {
        "type": "string",
        "defaultValue": "Eldert Grootenboer"
      },
      "PublisherEmail": {
        "type": "string",
        "defaultValue": "me@mydomaintwo.com"
      }
    },
    "resources": [
      {
        "type": "Microsoft.ApiManagement/service",
        "name": "[parameters('APIManagementInstanceName')]",
        "apiVersion": "2017-03-01",
        "properties": {
          "publisherEmail": "[parameters('PublisherEmail')]",
          "publisherName": "[parameters('PublisherName')]",
          "notificationSenderEmail": "apimgmt-noreply@mail.windowsazure.com",
          "hostnameConfigurations": [],
          "additionalLocations": null,
          "virtualNetworkConfiguration": null,
          "customProperties": {
            "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls10": "False",
            "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls11": "False",
            "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Ssl30": "False",
            "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TripleDes168": "False",
            "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls10": "False",
            "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls11": "False",
            "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Ssl30": "False"
          },
          "virtualNetworkType": "None"
        },
        "resources": [],
        "sku": {
          "name": "[parameters('APIManagementSku')]",
          "capacity": "[parameters('APIManagementSkuCapacity')]"
        },
        "location": "[resourceGroup().location]",
        "tags": {},
        "scale": null
      }
    ]
  }

The next ARM template will hold the users of Contoso, add a users.json file to the same local repository and add the following template.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "APIManagementInstanceName": {
          "type": "string",
          "defaultValue": "MyAPIManagementInstance"
        }
    },
    "resources": [
        {
            "type": "Microsoft.ApiManagement/service/users",
            "name": "[concat(parameters('APIManagementInstanceName'), '/john-smith-contoso-com')]",
            "apiVersion": "2017-03-01",
            "scale": null,
            "properties": {
                "firstName": "John",
                "lastName": "Smith",
                "email": "john.smith@eldert.org",
                "state": "active",
                "note": "Developer working for Contoso, one of the consumers of our APIs",
                "confirmation": "invite"
            }
        }
    ]
  }

Now add a groups.json file to the repository containing the following ARM template, this will add the groups for Contoso.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "APIManagementInstanceName": {
          "type": "string",
          "defaultValue": "MyAPIManagementInstance"
        }
    },
    "resources": [
        {
            "type": "Microsoft.ApiManagement/service/groups",
            "name": "[concat(parameters('APIManagementInstanceName'), '/contosogroup')]",
            "apiVersion": "2017-03-01",
            "scale": null,
            "properties": {
                "displayName": "ContosoGroup",
                "description": "Group containing all developers and services from Contoso who will be consuming our APIs",
                "type": "custom",
                "externalId": null
            }
        }
    ]
  }

The following ARM template will be used to add the product for Contoso, for this we are going to add products.json to the repository.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "APIManagementInstanceName": {
          "type": "string",
          "defaultValue": "MyAPIManagementInstance"
        }
    },
    "resources": [
        {
            "type": "Microsoft.ApiManagement/service/products",
            "name": "[concat(parameters('APIManagementInstanceName'), '/contosoproduct')]",
            "apiVersion": "2017-03-01",
            "scale": null,
            "properties": {
                "displayName": "ContosoProduct",
                "description": "Product which will apply the high-over policies for developers and services of Contoso.",
                "terms": null,
                "subscriptionRequired": true,
                "approvalRequired": true,
                "subscriptionsLimit": null,
                "state": "published"
            }
        }
    ]
  }

We have created our templates for the products and groups, so the next template, which will be called products-groups.json, will link the Contoso group to the Contoso product.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "APIManagementInstanceName": {
          "type": "string",
          "defaultValue": "MyAPIManagementInstance"
        }
    },
    "resources": [
        {
            "type": "Microsoft.ApiManagement/service/products/groups",
            "name": "[concat(parameters('APIManagementInstanceName'), '/contosoproduct/contosogroup')]",
            "apiVersion": "2017-03-01",
            "scale": null,
            "properties": {}
        }
    ]
  }

Likewise we will also link the Contoso users to the Contoso group using the following ARM template in a file called groups-users.json.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "APIManagementInstanceName": {
          "type": "string",
          "defaultValue": "MyAPIManagementInstance"
        }
    },
    "resources": [
        {
            "type": "Microsoft.ApiManagement/service/groups/users",
            "name": "[concat(parameters('APIManagementInstanceName'), '/contosogroup/john-smith-contoso-com')]",
            "apiVersion": "2017-03-01",
            "scale": null,
            "properties": {}
        }
    ]
  }

Next up are the policies for the product, for this add a file called policies.json to the repository.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
      "APIManagementInstanceName": {
        "type": "string",
        "defaultValue": "MyAPIManagementInstance"
      }
    },
    "resources": [
        {
            "type": "Microsoft.ApiManagement/service/products/policies",
            "name": "[concat(parameters('APIManagementInstanceName'), '/contosoproduct/policy')]",
            "apiVersion": "2017-03-01",
            "scale": null,
            "properties": {
                "policyContent": "<policies>rn  <inbound>rn    <base />rn    <rate-limit calls="20" renewal-period="60" />rn  </inbound>rn  <backend>rn    <base />rn  </backend>rn  <outbound>rn    <base />rn  </outbound>rn  <on-error>rn    <base />rn  </on-error>rn</policies>"
            }
        }
    ]
  }

Now lets add the subcription for the Contoso user, which contains the Contoso product, by adding a file called subscriptions.json.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
      "APIManagementInstanceName": {
        "type": "string",
        "defaultValue": "MyAPIManagementInstance"
      }
    },
    "resources": [
        {
            "type": "Microsoft.ApiManagement/service/subscriptions",
            "name": "[concat(parameters('APIManagementInstanceName'), '/5ae6ed2358c2795ab5aaba68')]",
            "apiVersion": "2017-03-01",
            "scale": null,
            "properties": {
                "userId": "[resourceId('Microsoft.ApiManagement/service/users', parameters('APIManagementInstanceName'), 'john-smith-contoso-com')]",
                "productId": "[resourceId('Microsoft.ApiManagement/service/products', parameters('APIManagementInstanceName'), 'contosoproduct')]",
                "displayName": "ContosoProduct subscription",
                "state": "active"
            }
        }
    ]
  }

Next we will create the version set which will hold the different versions of our versioned API. Add a file to the repository called versionedAPIVersionSet.json and add the following template to it.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "APIManagementInstanceName": {
          "type": "string",
          "defaultValue": "MyAPIManagementInstance"
        }
    },
    "resources": [
        {
            "name": "[concat(parameters('APIManagementInstanceName'), '/versionsetversionedapi')]",
            "type": "Microsoft.ApiManagement/service/api-version-sets",
            "apiVersion": "2017-03-01",
            "properties": {
                "description": "Version set for versioned API blog post ",
                "versionQueryName": "api-version",
                "displayName": "Versioned API",
                "versioningScheme": "query"
            }
        }
    ]
}

We will now add the template for the first version of the versioned API, by creating a file in the repository called versionedAPIv1.json.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "APIManagementInstanceName": {
          "type": "string",
          "defaultValue": "MyAPIManagementInstance"
        }
    },
    "resources": [
        {
            "type": "Microsoft.ApiManagement/service/apis",
            "name": "[concat(parameters('APIManagementInstanceName'), '/versioned-api')]",
            "apiVersion": "2017-03-01",
            "scale": null,
            "properties": {
                "displayName": "Versioned API",
                "apiRevision": "1",
                "description": "Wikipedia for Web APIs. Repository of API specs in OpenAPI(fka Swagger) 2.0 format.nn**Warning**: If you want to be notified about changes in advance please subscribe to our [Gitter channel](https://gitter.im/APIs-guru/api-models).nnClient sample: [[Demo]](https://apis.guru/simple-ui) [[Repo]](https://github.com/APIs-guru/simple-ui)n",
                "serviceUrl": "https://api.apis.guru/v2/",
                "path": "versioned-api",
                "protocols": [
                    "https"
                ],
                "authenticationSettings": null,
                "subscriptionKeyParameterNames": null,
                "apiVersion": "v1",
                "apiVersionSetId": "[concat(resourceId('Microsoft.ApiManagement/service', parameters('APIManagementInstanceName')), '/api-version-sets/versionsetversionedapi')]"
            }
        }
    ]
}

Operations

For the first version of the API we will now add a file called versionedAPIv1Operations.json which will hold the various operations of this API.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "APIManagementInstanceName": {
          "type": "string",
          "defaultValue": "MyAPIManagementInstance"
        }
    },
    "resources": [
        {
            "type": "Microsoft.ApiManagement/service/apis/operations",
            "name": "[concat(parameters('APIManagementInstanceName'), '/versioned-api/getMetrics')]",
            "apiVersion": "2017-03-01",
            "scale": null,
            "properties": {
                "displayName": "Get basic metrics",
                "method": "GET",
                "urlTemplate": "/metrics",
                "templateParameters": [],
                "description": "Some basic metrics for the entire directory.nJust stunning numbers to put on a front page and are intended purely for WoW effect :)n",
                "responses": [
                    {
                        "statusCode": 200,
                        "description": "OK",
                        "headers": []
                    }
                ],
                "policies": null
            }
        },
        {
            "type": "Microsoft.ApiManagement/service/apis/operations",
            "name": "[concat(parameters('APIManagementInstanceName'), '/versioned-api/listAPIs')]",
            "apiVersion": "2017-03-01",
            "scale": null,
            "properties": {
                "displayName": "List all APIs",
                "method": "GET",
                "urlTemplate": "/list",
                "templateParameters": [],
                "description": "List all APIs in the directory.nReturns links to OpenAPI specification for each API in the directory.nIf API exist in multiple versions `preferred` one is explicitly marked.nnSome basic info from OpenAPI spec is cached inside each object.nThis allows to generate some simple views without need to fetch OpenAPI spec for each API.n",
                "responses": [
                    {
                        "statusCode": 200,
                        "description": "OK",
                        "headers": []
                    }
                ],
                "policies": null
            }
        }
    ]
}

Policies

The policies for the first version of our API are next, lets add the versionedAPIv1Policies.json file for this.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "APIManagementInstanceName": {
          "type": "string",
          "defaultValue": "MyAPIManagementInstance"
        }
    },
    "resources": [
        {
            "type": "Microsoft.ApiManagement/service/apis/operations/policies",
            "name": "[concat(parameters('APIManagementInstanceName'), '/versioned-api/getMetrics/policy')]",
            "apiVersion": "2017-03-01",
            "scale": null,
            "properties": {
                "policyContent": "[concat('<!--rn    IMPORTANT:rn    - Policy elements can appear only within the <inbound>, <outbound>, <backend> section elements.rn    - Only the <forward-request> policy element can appear within the <backend> section element.rn    - To apply a policy to the incoming request (before it is forwarded to the backend service), place a corresponding policy element within the <inbound> section element.rn    - To apply a policy to the outgoing response (before it is sent back to the caller), place a corresponding policy element within the <outbound> section element.rn    - To add a policy position the cursor at the desired insertion point and click on the round button associated with the policy.rn    - To remove a policy, delete the corresponding policy statement from the policy document.rn    - Position the <base> element within a section element to inherit all policies from the corresponding section element in the enclosing scope.rn    - Remove the <base> element to prevent inheriting policies from the corresponding section element in the enclosing scope.rn    - Policies are applied in the order of their appearance, from the top down.rn-->rn<policies>rn  <inbound>rn    <base />rn    <set-backend-service base-url="https://api.apis.guru/v2/" />rn    <rewrite-uri template="/metrics.json" />rn  </inbound>rn  <backend>rn    <base />rn  </backend>rn  <outbound>rn    <base />rn  </outbound>rn  <on-error>rn    <base />rn  </on-error>rn</policies>')]"
            }
        },
        {
            "type": "Microsoft.ApiManagement/service/apis/operations/policies",
            "name": "[concat(parameters('APIManagementInstanceName'), '/versioned-api/listAPIs/policy')]",
            "apiVersion": "2017-03-01",
            "scale": null,
            "properties": {
                "policyContent": "[concat('<!--rn    IMPORTANT:rn    - Policy elements can appear only within the <inbound>, <outbound>, <backend> section elements.rn    - Only the <forward-request> policy element can appear within the <backend> section element.rn    - To apply a policy to the incoming request (before it is forwarded to the backend service), place a corresponding policy element within the <inbound> section element.rn    - To apply a policy to the outgoing response (before it is sent back to the caller), place a corresponding policy element within the <outbound> section element.rn    - To add a policy position the cursor at the desired insertion point and click on the round button associated with the policy.rn    - To remove a policy, delete the corresponding policy statement from the policy document.rn    - Position the <base> element within a section element to inherit all policies from the corresponding section element in the enclosing scope.rn    - Remove the <base> element to prevent inheriting policies from the corresponding section element in the enclosing scope.rn    - Policies are applied in the order of their appearance, from the top down.rn-->rn<policies>rn  <inbound>rn    <base />rn    <set-backend-service base-url="https://api.apis.guru/v2" />rn    <rewrite-uri template="/list.json" />rn  </inbound>rn  <backend>rn    <base />rn  </backend>rn  <outbound>rn    <base />rn  </outbound>rn  <on-error>rn    <base />rn  </on-error>rn</policies>')]"
            }
        }
    ]
}

And now we will add the second version of our versioned API as well, in a file called versionedAPIv2.json.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "APIManagementInstanceName": {
          "type": "string",
          "defaultValue": "MyAPIManagementInstance"
        }
    },
    "resources": [
        {
            "type": "Microsoft.ApiManagement/service/apis",
            "name": "[concat(parameters('APIManagementInstanceName'), '/versioned-api-v2')]",
            "apiVersion": "2017-03-01",
            "scale": null,
            "properties": {
                "displayName": "Versioned API",
                "apiRevision": "1",
                "description": "Wikipedia for Web APIs. Repository of API specs in OpenAPI(fka Swagger) 2.0 format.nn**Warning**: If you want to be notified about changes in advance please subscribe to our [Gitter channel](https://gitter.im/APIs-guru/api-models).nnClient sample: [[Demo]](https://apis.guru/simple-ui) [[Repo]](https://github.com/APIs-guru/simple-ui)n",
                "serviceUrl": "https://api.apis.guru/v2/",
                "path": "versioned-api",
                "protocols": [
                    "https"
                ],
                "authenticationSettings": null,
                "subscriptionKeyParameterNames": null,
                "apiVersion": "v2",
                "apiVersionSetId": "[concat(resourceId('Microsoft.ApiManagement/service', parameters('APIManagementInstanceName')), '/api-version-sets/versionsetversionedapi')]"
            }
        }
    ]
}

Operations

Add the operations for the second version of our API in a file called versionedAPIv2Operations.json.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "APIManagementInstanceName": {
          "type": "string",
          "defaultValue": "MyAPIManagementInstance"
        }
    },
    "resources": [
        {
            "type": "Microsoft.ApiManagement/service/apis/operations",
            "name": "[concat(parameters('APIManagementInstanceName'), '/versioned-api-v2/listAPIs')]",
            "apiVersion": "2017-03-01",
            "scale": null,
            "properties": {
                "displayName": "List all APIs",
                "method": "GET",
                "urlTemplate": "/list",
                "templateParameters": [],
                "description": "List all APIs in the directory.nReturns links to OpenAPI specification for each API in the directory.nIf API exist in multiple versions `preferred` one is explicitly marked.nnSome basic info from OpenAPI spec is cached inside each object.nThis allows to generate some simple views without need to fetch OpenAPI spec for each API.n",
                "responses": [
                    {
                        "statusCode": 200,
                        "description": "OK",
                        "headers": []
                    }
                ],
                "policies": null
            }
        }
    ]
}

Policies

And add a file called versionedAPIv2Policies.json which will hold the policies for the second version of our versioned API.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "APIManagementInstanceName": {
          "type": "string",
          "defaultValue": "MyAPIManagementInstance"
        }
    },
    "resources": [
        {
            "type": "Microsoft.ApiManagement/service/apis/operations/policies",
            "name": "[concat(parameters('APIManagementInstanceName'), '/versioned-api-v2/listAPIs/policy')]",
            "apiVersion": "2017-03-01",
            "scale": null,
            "properties": {
                "policyContent": "[concat('<!--rn    IMPORTANT:rn    - Policy elements can appear only within the <inbound>, <outbound>, <backend> section elements.rn    - Only the <forward-request> policy element can appear within the <backend> section element.rn    - To apply a policy to the incoming request (before it is forwarded to the backend service), place a corresponding policy element within the <inbound> section element.rn    - To apply a policy to the outgoing response (before it is sent back to the caller), place a corresponding policy element within the <outbound> section element.rn    - To add a policy position the cursor at the desired insertion point and click on the round button associated with the policy.rn    - To remove a policy, delete the corresponding policy statement from the policy document.rn    - Position the <base> element within a section element to inherit all policies from the corresponding section element in the enclosing scope.rn    - Remove the <base> element to prevent inheriting policies from the corresponding section element in the enclosing scope.rn    - Policies are applied in the order of their appearance, from the top down.rn-->rn<policies>rn  <inbound>rn    <base />rn    <set-backend-service base-url="https://api.apis.guru/v2" />rn    <rewrite-uri template="/list.json" />rn  </inbound>rn  <backend>rn    <base />rn  </backend>rn  <outbound>rn    <base />rn  </outbound>rn  <on-error>rn    <base />rn  </on-error>rn</policies>')]"
            }
        }
    ]
}

And finally we will add the products-apis.json file, which will link the versions of our versioned API to the Contoso product.

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "APIManagementInstanceName": {
          "type": "string",
          "defaultValue": "MyAPIManagementInstance"
        }
    },
    "resources": [
        {
            "type": "Microsoft.ApiManagement/service/products/apis",
            "name": "[concat(parameters('APIManagementInstanceName'), '/contosoproduct/versioned-api')]",
            "apiVersion": "2017-03-01",
            "scale": null,
            "properties": {}
        },
        {
            "type": "Microsoft.ApiManagement/service/products/apis",
            "name": "[concat(parameters('APIManagementInstanceName'), '/contosoproduct/versioned-api-v2')]",
            "apiVersion": "2017-03-01",
            "scale": null,
            "properties": {}
        }
    ]
}

We have now created all of our linked templates, so lets create the master template in a file called master.json. Don’t place this file in the templates folder, but instead place it in the root directory of the repository. This template will call all our other templates, passing in parameters as required. This is also where we handle our dependOn dependencies.

{ 
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
      "TemplatesStorageAccount": {
        "type": "string",
        "defaultValue": "https://mystorageaccount.blob.core.windows.net/templates/"
      },
      "TemplatesStorageAccountSASToken": {
        "type": "string",
        "defaultValue": ""
      },
      "APIManagementSku": {
        "type": "string",
        "defaultValue": "Developer"
      },
      "APIManagementSkuCapacity": {
        "type": "string",
        "defaultValue": "1"
      },
      "APIManagementInstanceName": {
        "type": "string",
        "defaultValue": "MyAPIManagementInstance"
      },
      "PublisherName": {
        "type": "string",
        "defaultValue": "Eldert Grootenboer"
      },
      "PublisherEmail": {
        "type": "string",
        "defaultValue": "me@mydomaintwo.com"
      }
    },
    "resources": [
        {
           "apiVersion": "2017-05-10",
           "name": "instanceTemplate",
           "type": "Microsoft.Resources/deployments",
           "properties": {
             "mode": "Incremental",
             "templateLink": {
                "uri":"[concat(parameters('TemplatesStorageAccount'), '/instance.json', parameters('TemplatesStorageAccountSASToken'))]",
                "contentVersion":"1.0.0.0"
             },
             "parameters": {
                "APIManagementSku": {"value": "[parameters('APIManagementSku')]" },
                "APIManagementSkuCapacity": {"value": "[parameters('APIManagementSkuCapacity')]" },
                "APIManagementInstanceName": {"value": "[parameters('APIManagementInstanceName')]" },
                "PublisherName": {"value": "[parameters('PublisherName')]" },
                "PublisherEmail": {"value": "[parameters('PublisherEmail')]" }
              }
           }
        },
        {
           "apiVersion": "2017-05-10",
           "name": "usersTemplate",
           "type": "Microsoft.Resources/deployments",
           "properties": {
             "mode": "Incremental",
             "templateLink": {
                "uri":"[concat(parameters('TemplatesStorageAccount'), '/users.json', parameters('TemplatesStorageAccountSASToken'))]",
                "contentVersion":"1.0.0.0"
             },
             "parameters": {
                "APIManagementInstanceName": {"value": "[parameters('APIManagementInstanceName')]" }
              }
           },
           "dependsOn": [
            "[resourceId('Microsoft.Resources/deployments', 'instanceTemplate')]"
           ]
        },
        {
           "apiVersion": "2017-05-10",
           "name": "groupsTemplate",
           "type": "Microsoft.Resources/deployments",
           "properties": {
             "mode": "Incremental",
             "templateLink": {
                "uri":"[concat(parameters('TemplatesStorageAccount'), '/groups.json', parameters('TemplatesStorageAccountSASToken'))]",
                "contentVersion":"1.0.0.0"
             },
             "parameters": {
                "APIManagementInstanceName": {"value": "[parameters('APIManagementInstanceName')]" }
              }
           },
           "dependsOn": [
            "[resourceId('Microsoft.Resources/deployments', 'instanceTemplate')]"
           ]
        },
        {
           "apiVersion": "2017-05-10",
           "name": "productsTemplate",
           "type": "Microsoft.Resources/deployments",
           "properties": {
             "mode": "Incremental",
             "templateLink": {
                "uri":"[concat(parameters('TemplatesStorageAccount'), '/products.json', parameters('TemplatesStorageAccountSASToken'))]",
                "contentVersion":"1.0.0.0"
             },
             "parameters": {
                "APIManagementInstanceName": {"value": "[parameters('APIManagementInstanceName')]" }
              }
           },
           "dependsOn": [
            "[resourceId('Microsoft.Resources/deployments', 'instanceTemplate')]"
           ]
        },
        {
           "apiVersion": "2017-05-10",
           "name": "groupsUsersTemplate",
           "type": "Microsoft.Resources/deployments",
           "properties": {
             "mode": "Incremental",
             "templateLink": {
                "uri":"[concat(parameters('TemplatesStorageAccount'), '/groups-users.json', parameters('TemplatesStorageAccountSASToken'))]",
                "contentVersion":"1.0.0.0"
             },
             "parameters": {
                "APIManagementInstanceName": {"value": "[parameters('APIManagementInstanceName')]" }
              }
           },
           "dependsOn": [
            "[resourceId('Microsoft.Resources/deployments', 'groupsTemplate')]",
            "[resourceId('Microsoft.Resources/deployments', 'usersTemplate')]"
           ]
        },
        {
           "apiVersion": "2017-05-10",
           "name": "productsGroupsTemplate",
           "type": "Microsoft.Resources/deployments",
           "properties": {
             "mode": "Incremental",
             "templateLink": {
                "uri":"[concat(parameters('TemplatesStorageAccount'), '/products-groups.json', parameters('TemplatesStorageAccountSASToken'))]",
                "contentVersion":"1.0.0.0"
             },
             "parameters": {
                "APIManagementInstanceName": {"value": "[parameters('APIManagementInstanceName')]" }
              }
           },
           "dependsOn": [
            "[resourceId('Microsoft.Resources/deployments', 'productsTemplate')]",
            "[resourceId('Microsoft.Resources/deployments', 'groupsTemplate')]"
           ]
        },
        {
           "apiVersion": "2017-05-10",
           "name": "subscriptionsTemplate",
           "type": "Microsoft.Resources/deployments",
           "properties": {
             "mode": "Incremental",
             "templateLink": {
                "uri":"[concat(parameters('TemplatesStorageAccount'), '/subscriptions.json', parameters('TemplatesStorageAccountSASToken'))]",
                "contentVersion":"1.0.0.0"
             },
             "parameters": {
                "APIManagementInstanceName": {"value": "[parameters('APIManagementInstanceName')]" }
              }
           },
           "dependsOn": [
            "[resourceId('Microsoft.Resources/deployments', 'productsTemplate')]",
            "[resourceId('Microsoft.Resources/deployments', 'usersTemplate')]"
           ]
        },
        {
           "apiVersion": "2017-05-10",
           "name": "policiesTemplate",
           "type": "Microsoft.Resources/deployments",
           "properties": {
             "mode": "Incremental",
             "templateLink": {
                "uri":"[concat(parameters('TemplatesStorageAccount'), '/policies.json', parameters('TemplatesStorageAccountSASToken'))]",
                "contentVersion":"1.0.0.0"
             },
             "parameters": {
                "APIManagementInstanceName": {"value": "[parameters('APIManagementInstanceName')]" }
              }
           },
           "dependsOn": [
            "[resourceId('Microsoft.Resources/deployments', 'productsTemplate')]"
           ]
        },
        {
           "apiVersion": "2017-05-10",
           "name": "versionedAPIVersionSetTemplate",
           "type": "Microsoft.Resources/deployments",
           "properties": {
             "mode": "Incremental",
             "templateLink": {
                "uri":"[concat(parameters('TemplatesStorageAccount'), '/versionedAPIVersionSet.json', parameters('TemplatesStorageAccountSASToken'))]",
                "contentVersion":"1.0.0.0"
             },
             "parameters": {
                "APIManagementInstanceName": {"value": "[parameters('APIManagementInstanceName')]" }
              }
           },
           "dependsOn": [
            "[resourceId('Microsoft.Resources/deployments', 'instanceTemplate')]"
           ]
        },
        {
           "apiVersion": "2017-05-10",
           "name": "versionedAPIv1Template",
           "type": "Microsoft.Resources/deployments",
           "properties": {
             "mode": "Incremental",
             "templateLink": {
                "uri":"[concat(parameters('TemplatesStorageAccount'), '/versionedAPIv1.json', parameters('TemplatesStorageAccountSASToken'))]",
                "contentVersion":"1.0.0.0"
             },
             "parameters": {
                "APIManagementInstanceName": {"value": "[parameters('APIManagementInstanceName')]" }
              }
           },
           "dependsOn": [
            "[resourceId('Microsoft.Resources/deployments', 'versionedAPIVersionSetTemplate')]"
           ]
        },
        {
           "apiVersion": "2017-05-10",
           "name": "versionedAPIv1OperationsTemplate",
           "type": "Microsoft.Resources/deployments",
           "properties": {
             "mode": "Incremental",
             "templateLink": {
                "uri":"[concat(parameters('TemplatesStorageAccount'), '/versionedAPIv1Operations.json', parameters('TemplatesStorageAccountSASToken'))]",
                "contentVersion":"1.0.0.0"
             },
             "parameters": {
                "APIManagementInstanceName": {"value": "[parameters('APIManagementInstanceName')]" }
              }
           },
           "dependsOn": [
            "[resourceId('Microsoft.Resources/deployments', 'versionedAPIv1Template')]"
           ]
        },
        {
           "apiVersion": "2017-05-10",
           "name": "versionedAPIv1PoliciesTemplate",
           "type": "Microsoft.Resources/deployments",
           "properties": {
             "mode": "Incremental",
             "templateLink": {
                "uri":"[concat(parameters('TemplatesStorageAccount'), '/versionedAPIv1Policies.json', parameters('TemplatesStorageAccountSASToken'))]",
                "contentVersion":"1.0.0.0"
             },
             "parameters": {
                "APIManagementInstanceName": {"value": "[parameters('APIManagementInstanceName')]" }
              }
           },
           "dependsOn": [
            "[resourceId('Microsoft.Resources/deployments', 'versionedAPIv1Template')]",
            "[resourceId('Microsoft.Resources/deployments', 'versionedAPIv1OperationsTemplate')]"
           ]
        },
        {
           "apiVersion": "2017-05-10",
           "name": "versionedAPIv2Template",
           "type": "Microsoft.Resources/deployments",
           "properties": {
             "mode": "Incremental",
             "templateLink": {
                "uri":"[concat(parameters('TemplatesStorageAccount'), '/versionedAPIv2.json', parameters('TemplatesStorageAccountSASToken'))]",
                "contentVersion":"1.0.0.0"
             },
             "parameters": {
                "APIManagementInstanceName": {"value": "[parameters('APIManagementInstanceName')]" }
              }
           },
           "dependsOn": [
            "[resourceId('Microsoft.Resources/deployments', 'versionedAPIVersionSetTemplate')]"
           ]
        },
        {
           "apiVersion": "2017-05-10",
           "name": "versionedAPIv2OperationsTemplate",
           "type": "Microsoft.Resources/deployments",
           "properties": {
             "mode": "Incremental",
             "templateLink": {
                "uri":"[concat(parameters('TemplatesStorageAccount'), '/versionedAPIv2Operations.json', parameters('TemplatesStorageAccountSASToken'))]",
                "contentVersion":"1.0.0.0"
             },
             "parameters": {
                "APIManagementInstanceName": {"value": "[parameters('APIManagementInstanceName')]" }
              }
           },
           "dependsOn": [
            "[resourceId('Microsoft.Resources/deployments', 'versionedAPIv2Template')]"
           ]
        },
        {
           "apiVersion": "2017-05-10",
           "name": "versionedAPIv2PoliciesTemplate",
           "type": "Microsoft.Resources/deployments",
           "properties": {
             "mode": "Incremental",
             "templateLink": {
                "uri":"[concat(parameters('TemplatesStorageAccount'), '/versionedAPIv2Policies.json', parameters('TemplatesStorageAccountSASToken'))]",
                "contentVersion":"1.0.0.0"
             },
             "parameters": {
                "APIManagementInstanceName": {"value": "[parameters('APIManagementInstanceName')]" }
              }
           },
           "dependsOn": [
            "[resourceId('Microsoft.Resources/deployments', 'versionedAPIv2Template')]",
            "[resourceId('Microsoft.Resources/deployments', 'versionedAPIv2OperationsTemplate')]"
           ]
        },
        {
           "apiVersion": "2017-05-10",
           "name": "productsAPIsTemplate",
           "type": "Microsoft.Resources/deployments",
           "properties": {
             "mode": "Incremental",
             "templateLink": {
                "uri":"[concat(parameters('TemplatesStorageAccount'), '/products-apis.json', parameters('TemplatesStorageAccountSASToken'))]",
                "contentVersion":"1.0.0.0"
             },
             "parameters": {
                "APIManagementInstanceName": {"value": "[parameters('APIManagementInstanceName')]" }
              }
           },
           "dependsOn": [
            "[resourceId('Microsoft.Resources/deployments', 'productsTemplate')]",
            "[resourceId('Microsoft.Resources/deployments', 'versionedAPIv1Template')]",
            "[resourceId('Microsoft.Resources/deployments', 'versionedAPIv2Template')]"
           ]
        }
    ]
  }

As you will notice, the first parameter is a path to a container in a storage account, so be sure to create this container. You could even do this from the build pipeline, but for this blog post we will do it manually.

Create storage account

Create storage account

Create container

Create container

We should now have a repository filled with ARM templates, so commit and push these to the remote repository.

Overview of ARM template files

Overview of ARM template files

Switch to our project in VSTS, and create a build pipeline called API Management CI-CD ARM-CI – Linked Template to validate the ARM template. Remember to enable the continous integration trigger, so our build runs every time we do a push to our repository.

Create build template for Linked Template repository

Create build template for Linked Template repository

In the build definition, we will add a Azure File Copy step, which will copy our linked templates to the blob container we just created, so update the settings of the step accordingly.

Set Azure File Copy steps properties

Set Azure File Copy steps properties

In the output of this step we will get back the URI and SAS token for our container which we will need in our ARM template, so make sure to update the variables here.

Set output names so we can use these in our ARM template

The other steps in the build pipeline are just as described in the first post in this series, except that we now use the variables from the Azure Copy File step in our Azure Resource Group Deployment step to update the location and SAS token of the storage account, and we include an extra Publish Built Artifacts step.

The complete build pipeline for validation

The complete build pipeline for validation

Note that in first the Publish Built Artifacts step we just publish the master.json files.

Only publish master template in first Publish Build Artifacts step

Only publish master template in first Publish Build Artifacts step

While in the second Publish Built Artifacts step we publish the linked templates.

Publish templates folder in second Publish Build Artifacts step

Publish templates folder in second Publish Build Artifacts step

Once done, make sure to save and queue the build definition.

We will now create the deployment pipeline using a new release definition called API Management Linked Template, which again is almost the same as described in the first post of this series, except we will now include the Azure Copy File step just like in the build pipeline to copy our linked templates to the templates container.

Use output from build pipeline as artifact

Use output from build pipeline as artifact

Do not forget to enable the Continuous deployment trigger so this deployment is triggered each time the build runs successfully. In the Test environment, add a Azure File Copy step to copy the linked template files to our templates container, and set variables containing the URI and SAS token.

Set properties for Azure Copy Files step

Set properties for Azure Copy Files step

In the Azure Resource Group Deployment step use the variables from the Azure File Copy step to set the location and SAS token of the container.

Use URI and SAS token from previous step

Use URI and SAS token from previous step

Clone the test environment and set the cloned environment up as production environment, do remember to include an approval step.

Approval should be given before deploying to production

We have now finished our complete CI/CD pipeline, if we make push any changes to our repository it will trigger the build which does validation of the ARM templates, and then uses them to deploy or update our complete API Management instance including all users, groups, products and APIs which we defined in our templates.

API Management instance created including users, groups, products and APIs

API Management instance created including users, groups, products and APIs