visit
Please note, that your App Service Plan must be either at the Standard, Premium, or Isolated tier.
param registryName string
param registryLocation string
param registrySku string
resource containerRegistry 'Microsoft.ContainerRegistry/registries@2021-09-01' = {
name: registryName
location: registryLocation
sku: {
name: registrySku
}
identity: {
type: 'SystemAssigned'
}
}
param appServicePlanName string
param appServicePlanLocation string
param appServicePlanSkuName string
param appServicePlanCapacity int
resource appServicePlan 'Microsoft.Web/serverfarms@2021-02-01' = {
name: appServicePlanName
location: appServicePlanLocation
sku: {
name: appServicePlanSkuName
capacity: appServicePlanCapacity
}
kind: 'linux'
properties: {
reserved: true
}
}
output appServicePlanId string = appServicePlan.id
We're running our containers on Linux, so we'll need to provision a Linux App Service Plan. I've parameterized the App Plan SKU name, but as I mentioned before, deployment slots are only available at Standard or above plan. Since we're provisioning a Linux plan, we'll need to set the reserved
property to true.
@description('Name of the app service plan')
param appServiceName string
@description('Location of the app service plan')
param appServiceLocation string
@description('Name of the slot that we want to create in our App Service')
param appServiceSlotName string
@description('The Server Farm Id for our App Plan')
param serverFarmId string
@description('Name of the Azure Container Registry that this App will pull images from')
param acrName string
@description('The docker image and tag')
param dockerImageAndTag string = '/hellobluegreenwebapp:latest'
// This is the ACR Pull Role Definition Id: //docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#acrpull
var acrPullRoleDefinitionId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')
var appSettings = [
{
name: 'WEBSITES_ENABLE_APP_SERVICE_STORAGE'
value: 'false'
}
{
name: 'WEBSITES_PORT'
value: '80'
}
{
name: 'DOCKER_REGISTRY_SERVER_URL'
value: '//${containerRegistry.properties.loginServer}'
}
]
resource containerRegistry 'Microsoft.ContainerRegistry/registries@2021-09-01' existing = {
name: acrName
}
resource appService 'Microsoft.Web/sites@2021-02-01' = {
name: appServiceName
location: appServiceLocation
kind: 'app,linux,container'
properties: {
serverFarmId: serverFarmId
siteConfig: {
appSettings: appSettings
acrUseManagedIdentityCreds: true
linuxFxVersion: 'DOCKER|${containerRegistry.properties.loginServer}/${dockerImageAndTag}'
}
}
identity: {
type: 'SystemAssigned'
}
resource blueSlot 'slots' = {
name: appServiceSlotName
location: appServiceLocation
kind: 'app,linux,container'
properties: {
serverFarmId: serverFarmId
siteConfig: {
acrUseManagedIdentityCreds: true
appSettings: appSettings
}
}
identity: {
type: 'SystemAssigned'
}
}
}
resource appServiceAcrPullRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-08-01-preview' = {
scope: containerRegistry
name: guid(containerRegistry.id, appService.id, acrPullRoleDefinitionId)
properties: {
principalId: appService.identity.principalId
roleDefinitionId: acrPullRoleDefinitionId
principalType: 'ServicePrincipal'
}
}
resource appServiceSlotAcrPullRoleAssignment 'Microsoft.Authorization/roleAssignments@2020-08-01-preview' = {
scope: containerRegistry
name: guid(containerRegistry.id, appService::blueSlot.id, acrPullRoleDefinitionId)
properties: {
principalId: appService::blueSlot.identity.principalId
roleDefinitionId: acrPullRoleDefinitionId
principalType: 'ServicePrincipal'
}
}
subscriptionResourceId
.existing
. We reference the name of our registry using a parameter.
kind
property to app,linux,container
. This tells the App Service that we want to host Linux Containers.serverFarmId
to the Id of our Server Farm. We're setting this as a parameter, so if we wanted to use this module for other App Services, we could deploy it to another App Service Plan.appSettings
to the App Settings we defined earlier, the acrUseManagedIdentityCreds
property tells the App Service that we want to use the managed identity credentials to pull images from our container registry. We finally set the linuxFxVersion
to DOCKER|${containerRegistry.properties.loginServer}/${dockerImageAndTag}
. This sets the Linux App Framework and version.
We then create two role assignments that allow our App Service Blue and Green slot to pull images from our container registry. The roleDefinitionId
uses the AcrPull role definition we defined in our variable at the start and the scope
property assigns this role to the App Service over our Azure Container Registry.
With our modules created, we can now write up our main.bicep
file like so:
param webAppName string = uniqueString(resourceGroup().id)
param acrName string = toLower('acr${webAppName}')
param acrSku string
param appServicePlanName string = toLower('asp-${webAppName}')
param appServiceName string = toLower('asp-${webAppName}')
param appServicePlanSkuName string
param appServicePlanInstanceCount int
var appServiceSlotName = 'blue'
param location string = resourceGroup().location
module containerRegistry 'containerRegistry.bicep' = {
name: 'containerRegistry'
params: {
registryLocation: location
registryName: acrName
registrySku: acrSku
}
}
module appServicePlan 'appServicePlan.bicep' = {
name: 'appServicePlan'
params: {
appServicePlanLocation: location
appServicePlanName: appServicePlanName
appServicePlanSkuName: appServicePlanSkuName
appServicePlanCapacity: appServicePlanInstanceCount
}
}
module appService 'appService.bicep' = {
name: 'appService'
params: {
appServiceLocation: location
appServiceName: appServiceName
serverFarmId: appServicePlan.outputs.appServicePlanId
appServiceSlotName: appServiceSlotName
acrName: acrName
}
}
We'll also need a parameters file. We can create a parameters.json
file like so:
{
"$schema": "//schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"acrSku":{
"value": "Basic"
},
"appServicePlanSkuName": {
"value": "S1"
},
"appServicePlanInstanceCount": {
"value": 1
}
}
}
az group create -n <resource-group-name> -l <location>
az ad sp create-for-rbac --name yourApp --role owner --scopes /subscriptions/{subscription-id}/resourceGroups/exampleRG --sdk-auth
{
"clientId": "<GUID>",
"clientSecret": "<GUID>",
"subscriptionId": "<GUID>",
"tenantId": "<GUID>",
}
We need to save this JSON output as a Secret in our GitHub repo. You can do this by selecting Settings then Secrets and clicking on New to create the secret. We'll need to create the following secrets:
Secret |
Value |
---|---|
RESOURCE_GROUP | Name of the resource group that we're deploying resources to |
AZURE_CREDENTIALS | The entire JSON output that was generated as part of the service principal creation step |
name: Deploy Azure Infrastructure
on:
push:
paths:
- 'deploy/*'
workflow_dispatch:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run Bicep Linter
run: az bicep build --file ./deploy/main.bicep
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: azure/login@v1
name: Sign in to Azure
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- uses: azure/arm-deploy@v1
name: Run preflight validation
with:
deploymentName: ${{ github.run_number }}
resourceGroupName: ${{ secrets.AZURE_RG }}
template: ./deploy/main.bicep
parameters: ./deploy/parameters.json
deploymentMode: Validate
preview:
runs-on: ubuntu-latest
needs: [lint, validate]
steps:
- uses: actions/checkout@v2
- uses: azure/login@v1
name: Sign in to Azure
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- uses: Azure/cli@v1
name: Run what-if
with:
inlineScript: |
az deployment group what-if --resource-group ${{ secrets.AZURE_RG }} --template-file ./deploy/main.bicep --parameters ./deploy/parameters.json
deploy:
runs-on: ubuntu-latest
environment: Dev
needs: preview
steps:
- uses: actions/checkout@v2
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy Bicep File
uses: azure/arm-deploy@v1
with:
subscriptionId: ${{ secrets.AZURE_SUBSCRIPTION }}
resourceGroupName: ${{ secrets.AZURE_RG }}
template: ./deploy/main.bicep
parameters: ./deploy/parameters.json
failOnStdErr: false
environment
parameter in this stage. When we approve this deployment, the stage deploys our Bicep template using the parameters file to deploy our resources to Azure.
If we head into Azure, we can see that our resources have been created:
First, we need to get the resource Id of our container registry. We can do this by running the following command (Replace <registry-name>
with the name of your Azure Container Registry):
registryId=$(az acr show --name <registry-name> --query id --output tsv)
Now that we have our resource Id, we can use the following AZ CLI command to assign the AcrPush role (Replace <ClientId>
with the client ID of your service principal):
az role assignment create --assignee <ClientId> --scope $registryId --role AcrPush
Secret |
Value |
---|---|
REGISTRY_LOGIN_SERVER | The login server name of your registry (all lowercase). Example: |
REGISTRY_USERNAME |
The |
REGISTRY_PASSWORD |
The |
name: Build and Deploy Container Image to App Service
on:
workflow_dispatch:
defaults:
run:
working-directory: ./src
# Note: The use of :latest for the container image is not recommeded for production environments.
jobs:
build-container-image:
runs-on: ubuntu-latest
steps:
- name: 'Checkout GitHub Action'
uses: actions/checkout@main
- name: 'Login via Azure CLI'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: 'Build and Push Image to ACR'
uses: azure/docker-login@v1
with:
login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- run: |
docker build . -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/hellobluegreenwebapp:latest
docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/hellobluegreenwebapp:latest
One thing to note here is the image tag that we're using. This is a basic demo, so I'm just using latest
. For your container images, make sure you follow .
Once this has been built and pushed to our Azure Container Registry, we should be able to see our container image by navigating to our ACR, and searching for the image under Repositories
We then deploy our image to our Blue slot using the output variable we set as the App name, with the image that we've pushed to our ACR, and explicitly tell the task to deploy the image to our blue
slot:
deploy-to-blue-slot:
needs: build-container-image
runs-on: ubuntu-latest
steps:
- name: 'Checkout GitHub Action'
uses: actions/checkout@main
- name: 'Login via Azure CLI'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: 'Get App Name'
id: getwebappname
run: |
a=$(az webapp list -g ${{ secrets.AZURE_RG }} --query '[].{Name:name}' -o tsv)
echo "::set-output name=appName::$a"
- name: 'Deploy to Blue Slot'
uses: azure/webapps-deploy@v2
with:
app-name: ${{ steps.getwebappname.outputs.appName }}
images: ${{ secrets.REGISTRY_LOGIN_SERVER }}/hellobluegreenwebapp:latest
slot-name: 'blue'
//<name-of-app-service>-<slot-name>.azurewebsites.net
We can use this URL to navigate to the blue slot and see that our image has been successfully deployed:
If we navigate to our green slot, we see that nothing has been deployed yet! Let's swap the slots now.
swap-to-green-slot:
runs-on: ubuntu-latest
environment: Dev
needs: deploy-to-blue-slot
steps:
- name: 'Checkout GitHub Action'
uses: actions/checkout@main
- name: 'Login via Azure CLI'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: 'Get App Name'
id: getwebappname
run: |
a=$(az webapp list -g ${{ secrets.AZURE_RG }} --query '[].{Name:name}' -o tsv)
echo "::set-output name=appName::$a"
- name: 'Swap to green slot'
uses: Azure/cli@v1
with:
inlineScript: |
az webapp deployment slot swap --slot 'blue' --resource-group ${{ secrets.AZURE_RG }} --name ${{ steps.getwebappname.outputs.appName }}