Azure DevCamp Vienna - Docker on Azure

26 March 2019 - Azure, Container, Docker, CLI

Introduction

This blog container the material I used during Azure DevCamp Vienna 2019 at Microsoft.

Prerequisites

In order to follow along, you need the following prerequisites:

  1. Linux environment with
  2. Azure Subscription in which you are administrator (get one for free)

If you do not want to bother with installing the first prerequisite, you can use the Docker image I prepared for the training. If you have Docker installed (Docker Desktop works, too), run docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock rstropek/devcamp. If you want to build the image yourself, you can find the Dockerfile on GitHub.

I recommend using Visual Studio Code and its Azure CLI Plugin to work with the script mentioned in this lab.

Get Sources

You can get the sources mentioned in this sample on GitHub. Clone this repository on your machine. Note that the image rstropek/devcamp already contains the API’s source code in the /devcamp folder.

Using Docker locally

Here we build an ASP.NET Core Web API and run it locally in Docker Container. Note: Execute this script in the folder HotelsApi (/devcamp/HotelsApi in the image rstropek/devcamp).

  • Create a docker id on Docker Hub

  • Login Docker CLI: docker login

  • Set variables used in later scripts. Note: Replace rstropek in the following line with your docker id

DOCKER_ID="rstropek"
IMG="hotelsapi"
  • Build API: docker build -t $DOCKER_ID/$IMG .

  • The web API needs a SQL Server. Start SQL Server in Docker container.

docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=P@ssw0rd123' -p 1433:1433 -d --name sqldb mcr.microsoft.com/mssql/server`
  • Start API in Docker container and link it with SQL Server
docker run -it --rm -p 5000:80 --link sqldb:db -e Logging__LogLevel__Default=Information $DOCKER_ID/$IMG
  • Push API to Docker Hub: docker push $DOCKER_ID/$IMG

  • Open Docker Hub and verfiy that your image is available

Getting Started with Azure

  • Later, we will test WebHooks with Azure Container Registry. Create a WebHook test target on https://webhook.site and copy it’s URL (something like https://webhook.site/00000000-0000-0000-0000-000000000000).

  • Set some variables that we will use throughout this demo. Note that you have to replace some variables.

SUBSCRIPTION="Your Azure Subscription Name"
WHTARGET="Your webhook test URL from https://webhook.site"
LOCATION="westeurope"
IMG="hotelsapi"
RG="devcamp"
REG="devcamp19"
SKU="premium"
SP="devcamp19-sp"
PASSWD="P@ssw0rd123" # NEVER do that in real world!!!!
WEB="devcamp19-web"
LINUX_PLAN="devcamp19cplan"
LINUX_WEB_APP="devcamp19cweb" # Choose any unique name here (e.g. something with your name)
KV="vault$IMG"
AKS="devcamp19aks"
SQL="$RG-sql"
SQLPWD="P@ssw0rd123"
DB="Hotels"
AI=$RG-ai
  • Log in and select correct account
az login
az account set --subscription "$SUBSCRIPTION"
  • Create resource groups if it does not exist yet
if [ $(az group exists --name "$RG") == 'false' ]
then
    az group create --name "$RG" --location "$LOCATION"
fi
  • Create Azure SQL DB
if ! az sql server show --resource-group "$RG" --name "$SQL"
then
    # Create server
    az sql server create --resource-group $RG --name $SQL --location $LOCATION --admin-user dev --admin-password $SQLPWD

    # To make testing easier, we allow connections for all IP addresses. In real world, limit access accordingly
    az sql server firewall-rule create --resource-group $RG --server $SQL --name all --start-ip-address 0.0.0.0 --end-ip-address 255.255.255.255

    # Create DB
    az sql db create --server $SQL --resource-group $RG --name $DB
fi
  • Create application insights instance
if ! az resource show --resource-group $RG --resource-type Microsoft.Insights/components --name $AI
then
    az resource create --resource-group $RG --name $AI --resource-type Microsoft.Insights/components --location $LOCATION --properties '{"Application_Type":"web"}'

    # Get AI instrumentation key and store it in variable
    AIKEY=$(az resource show --resource-group $RG --resource-type Microsoft.Insights/components --name $AI | jq '.properties.InstrumentationKey' -r)
fi

Azure Container Registry (ACR)

  • Create Azure Container Registry if it does not exist yet
if [ $(az acr check-name --name "$REG" --query nameAvailable) == 'true' ]
then
    az acr create --resource-group "$RG" --name "$REG" --sku "$SKU" --admin-enabled
fi
# Get ID of ACR
ACRID=$(az acr show --name "$REG" --resource-group "$RG" --query "id" -o tsv)
  • You could use the following command to get the password of the admin account: az acr credential show --name "$REG" --query "passwords[0].value". Prefer service principals (as shown below) for production scenarios.

  • Log in with individual account of developer (for e.g. pushing containers using docker cli)

az acr login --name "$REG"
  • Create a service principal if it does not exist yet
if [ $(az ad sp list --display-name "$SP" | jq length) == '0' ]
then
    az ad sp create-for-rbac --name "$SP" --password "$PASSWD"
fi
# Get ID of service principal
SPAPPID=$(az ad sp list --display-name "$SP" --query "[].appId" -o tsv)
  • You could use the following command to delete the service principal: az ad sp delete --id "$SPAPPID"

  • Assign service principal the Contributor role so it can push images

if [ $(az role assignment list --assignee "$SPAPPID" --role Contributor --scope "$ACRID" | jq length) == '0' ]
then
    az role assignment create --assignee "$SPAPPID" --role Contributor --scope "$ACRID"
fi
  • Use usual docker login with service principal to authenticate at ACR with Docker CLI
docker login -u $SPAPPID -p $PASSWD $REG.azurecr.io
  • Push an image to ACR with Docker CLI
docker tag $DOCKER_ID/$IMG $REG.azurecr.io/$IMG
docker push $REG.azurecr.io/$IMG
  • Use ACR quick task to build an image in the cloud
az acr build --registry "$REG" --image $IMG:v2 .
  • Inspect storage limits
az acr show-usage --resource-group "$RG" --name "$REG" --output table
  • You could use the following statement to delete the image from ACR: az acr repository delete --name "$REG" --image $IMG:v1

  • Get a list of repositories and tags in the registry

az acr repository list --name $REG
az acr repository show-tags --repository $IMG --name $REG

Azure Container Instance (ACI)

  • Create Keyvault and store store service principal name, secret, and DB connection string
if ! az keyvault show --resource-group "$RG" --name "$KV"
then
    az keyvault create --resource-group "$RG" --name "$KV"
    # To delete: az keyvault delete --resource-group "$RG" --name "$KV"

    # Create service principal and store the generated password in Keyvault
    az keyvault secret set --vault-name $KV --name $REG-pull-PASSWD \
        --value $(az ad sp create-for-rbac --name $REG-pull2 --scopes $ACRID --role reader --query password --output tsv)
    # To delete: az ad sp delete --id $(az ad sp list --display-name "$REG-pull" --query "[].appId" -o tsv)

    # Store service principal's name in KeyVault
    az keyvault secret set --vault-name $KV --name $REG-pull-usr \
        --value $(az ad sp show --id http://$REG-pull2 --query appId --output tsv)

    # Store connection string in KeyVault
    az keyvault secret set --vault-name $KV --name $REG-sql-conn \
        --value "Server=$SQL.database.windows.net,1433;Initial Catalog=$DB;User ID=dev;Password=$SQLPWD;Persist Security Info=False;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
fi
  • Start container from image in ACR if it does not already exist. Note that we use ACI here. Other options would be App Service or AKS (Kubernetes)
if ! az container show --resource-group "$RG" --name "$WEB"
then
    # Note how we get service principal data from Keyvault
    az container create --resource-group "$RG" --name "$WEB" --image $REG.azurecr.io/$IMG --cpu 1 --memory 1 --ip-address public --ports 80 \
        --registry-login-server $REG.azurecr.io \
        --registry-username $(az keyvault secret show --vault-name $KV --name $REG-pull-usr --query value -o tsv) \
        --registry-password $(az keyvault secret show --vault-name $KV --name $REG-pull-PASSWD --query value -o tsv) \
        --secure-environment-variables "ConnectionStrings__DefaultConnection=$(az keyvault secret show --vault-name $KV --name $REG-sql-conn --query value -o tsv)" \
                                       "ApplicationInsights__InstrumentationKey=$AIKEY"

    # You could use the following statement to see logs
    # az container attach --resource-group "$RG" --name "$WEB"

    # You could use the following statement to delete the container
    # az container delete --resource-group "$RG" --name "$WEB" --yes
fi

ACR Integration Scenarios

  • Create a webhook so we get notified about container pushes
if ! az acr webhook show --registry "$REG" --name "webhook1$REG"
then
    az acr webhook create --registry "$REG" --name "webhook1$REG" --actions push --uri $WHTARGET

    # After creating the webhook, trigger a push or build to ACR and see how the webhook is triggered
fi

Note that we will not do the following demos in the workshop in Vienna because of limited time. However, I would like to include the scripts so interested people can try the demos at home:

  • Demo demo website that displays events happening in EventGrid
if output=$(az appservice plan show --resource-group "$RG" --name "$EVENT_SITE-plan") && [ -z "$output" ]
then
    az group deployment create --resource-group "$RG" \
        --template-uri "https://raw.githubusercontent.com/Azure-Samples/azure-event-grid-viewer/master/azuredeploy.json" \
        --parameters siteName=$EVENT_SITE hostingPlanName=$EVENT_SITE-plan
fi
  • Register EventGrid provider if not already registered
if [ $(az provider show --namespace Microsoft.EventGrid --query "registrationState" --output tsv) != "Registered" ]
then
    az provider register --namespace Microsoft.EventGrid
fi
  • Add EventGrid subscription if not already exists
if ! az eventgrid event-subscription show --name event-sub-acr --source-resource-id $ACRID
then
    az eventgrid event-subscription create --name event-sub-acr --source-resource-id $ACRID --endpoint $APP_ENDPOINT
    # az eventgrid event-subscription delete --name event-sub-acr --resource-id $ACRID
fi
  • After creating the subscription, trigger a push or build to ACR and see how the EventGrid message is sent

  • Create a task that listens to change in GitHub and rebuilds images if necessary

if ! az acr task show --registry "$REG" --name "$TASK"
then
    az acr task create --registry "$REG" --name "$TASK" --image $TASK: \
        --context $GHREPO --branch master --file Dockerfile --git-access-token $GHPAT
fi
  • Use the following command to manually trigger the task
az acr task run --registry "$REG" --name "$TASK"
  • Use the following command to see a list of task runs
az acr task list-runs --registry "$REG" --output table

Azure App Service

  • Create Linux app service plan if it does not exist
if output=$(az appservice plan show --resource-group "$RG" --name "$LINUX_PLAN") && [ -z "$output" ]
then
    az appservice plan create --name "$LINUX_PLAN" --resource-group "$RG" --sku b1 --is-linux
fi
  • Deploy container web app
if ! az webapp show --resource-group "$RG" --name "$LINUX_WEB_APP"
then
    # Create web app with dummy container image (will be changed later)
    az webapp create --resource-group "$RG" --plan "$LINUX_PLAN" --name "$LINUX_WEB_APP" \
        --deployment-container-image-name nginx:alpine

    # Use managed identity to allow access to Keyvault
    az webapp identity assign --resource-group "$RG" --name "$LINUX_WEB_APP"
    SPWEBAPP=$(az ad sp list --display-name "$LINUX_WEB_APP" --query "[].appId" -o tsv)
    az keyvault set-policy --resource-group $RG --name $KV --secret-permissions get --spn $SPWEBAPP

    # Get URL of connection string in Keyvault and store it in app settings
    CONNSTRINGVERSION=$(az keyvault secret list-versions --vault-name $KV --name $REG-sql-conn | jq '.[0].id' -r)
    az webapp config appsettings set --resource-group $RG --name $LINUX_WEB_APP \
        --settings "ConnectionStrings__DefaultConnection=@Microsoft.KeyVault(SecretUri=$CONNSTRINGVERSION)" \
                   "ApplicationInsights__InstrumentationKey=$AIKEY"

    # Set container of web app to correct image
    az webapp config container set --resource-group "$RG" --name "$LINUX_WEB_APP" \
        --docker-custom-image-name $REG.azurecr.io/$IMG \
        --docker-registry-server-user $(az keyvault secret show --vault-name $KV --name $REG-pull-usr --query value -o tsv) \
        --docker-registry-server-password $(az keyvault secret show --vault-name $KV --name $REG-pull-PASSWD --query value -o tsv)
fi

Azure Kubernetes Service (AKS)

  • Register Kubernetes provider if not already registered
if [ $(az provider show --namespace Microsoft.ContainerService --query "registrationState" --output tsv) != "Registered" ]
then
    az provider register --namespace Microsoft.ContainerService
fi
  • Create Kubernetes cluster
if ! az aks show --resource-group "$RG" --name "$AKS"
then
    az aks create --resource-group "$RG" --name "$AKS" --node-count 3 --generate-ssh-keys \
        --location westeurope \
        --client-secret $(az keyvault secret show --vault-name $KV --name $REG-pull-PASSWD --query value -o tsv) \
        --service-principal $(az keyvault secret show --vault-name $KV --name $REG-pull-usr --query value -o tsv)
fi
  • Connect kubectl with AKS: az aks get-credentials --resource-group "$RG" --name "$AKS"

  • Check connection to AKS: kubectl get nodes

  • Deploy demo API to Kubernetes

cat hotels.yaml \
  | sed "s//$(az keyvault secret show --vault-name $KV --name $REG-sql-conn --query value -o tsv)/g" \
  | sed "s//$AIKEY/g" \
  | kubectl apply -f -
  • Watch front-end service to get public IP: kubectl get service hotelsapi-svc --watch

  • Try to reach web app via public IP

  • Create a ClusterRoleBinding which gives the role dashboard-admin to the ServiceAccount

kubectl create clusterrolebinding kubernetes-dashboard -n kube-system --clusterrole=cluster-admin --serviceaccount=kube-system:kubernetes-dashboard
  • Start Kubernetes dashboard (CMD, not bash as we need to start a local browser)
az aks browse --resource-group "$RG" --name "$AKS"