Containerized Azure Functions with Azure DevOps

Published: Aug 26, 2022 by Isaac Johnson

Recently I had a discussion with some colleagues about Azure Functions leveraging containers. There was a good discussion on how to build and scale them leveraging Azure DevOps.

In this post we will go from start to finish creating a Python-based Azure Function. We will build it to a container and push to a private container registry (Harbor). We’ll then publish as a container using a secured container registry and then create and load an Azure Repo.

Next, we’ll setup Azure Pipelines, configuring with CICD and deployments. We’ll cover scaling issues with Approval Gates (using Environments), branch policies and multiple Deployment Slots. We’ll show how users could leverage a users branch and PreProd slot.

Lastly, we’ll dig into Alerting, Mentoring, Logging and Metrics using App Insights in Azure as well as cost considerations.

Setup

First, we need to install or update the Azure Function Toolset

Update the Azure Func tools

$ brew tap azure/functions
Running `brew update --auto-update`...
==> Auto-updated Homebrew!
Updated 3 taps (codefresh-io/cli, homebrew/core and derailed/k9s).
==> New Formulae
burst                       crytic-compile              ghorg                       linux-headers@5.15          rdb                         solc-select
cargo-crev                  dsda-doom                   git-sync                    lucky-commit                ripsecrets                  svt-av1
censys                      enex2notion                 go@1.18                     ouch                        rush-parallel               swiftdraw
cql-proxy                   gcc@11                      ksh93                       purescript-language-server  slither-analyzer
create-api                  gcem                        libvatek                    pymupdf                     snapcast

You have 30 outdated formulae installed.
You can upgrade them with brew upgrade
or list them with brew outdated.

==> Tapping azure/functions
Cloning into '/home/linuxbrew/.linuxbrew/Homebrew/Library/Taps/azure/homebrew-functions'...
remote: Enumerating objects: 607, done.
remote: Counting objects: 100% (205/205), done.
remote: Compressing objects: 100% (77/77), done.
remote: Total 607 (delta 150), reused 149 (delta 128), pack-reused 402
Receiving objects: 100% (607/607), 90.18 KiB | 2.05 MiB/s, done.
Resolving deltas: 100% (386/386), done.
Tapped 5 formulae (23 files, 193.5KB).

Then install with brew

$ brew install azure-functions-core-tools@4
==> Downloading https://functionscdn.azureedge.net/public/4.0.4736/Azure.Functions.Cli.linux-x64.4.0.4736.zip
######################################################################## 100.0%
==> Installing azure-functions-core-tools@4 from azure/functions

 Telemetry
 ---------
 The Azure Functions Core tools collect usage data in order to help us improve your experience.
 The data is anonymous and doesn't include any user specific or personal information. The data is collected by Microsoft.

 You can opt-out of telemetry by setting the FUNCTIONS_CORE_TOOLS_TELEMETRY_OPTOUT environment variable to '1' or 'true' using your favorite shell.
🍺  /home/linuxbrew/.linuxbrew/Cellar/azure-functions-core-tools@4/4.0.4736: 3,150 files, 401.7MB, built in 6 seconds
==> Running `brew cleanup azure-functions-core-tools@4`...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).

Lastly, link it

$ brew link --overwrite azure-functions-core-tools@4
Warning: Already linked: /home/linuxbrew/.linuxbrew/Cellar/azure-functions-core-tools@4/4.0.4736
To relink, run:
  brew unlink azure-functions-core-tools@4 && brew link azure-functions-core-tools@4

Add Python 3

we can use brew

$ brew install python
==> Downloading https://ghcr.io/v2/homebrew/core/gdbm/manifests/1.23
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/gdbm/blobs/sha256:7d5728174c3de6c048a233459a1b8ac9e8c53645ca14962d9a1deb60fd58a568
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:7d5728174c3de6c048a233459a1b8ac9e8c53645ca14962d9a1deb60fd58a568?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/mpdecimal/manifests/2.5.1
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/mpdecimal/blobs/sha256:c5d64a4dd47dc1b66887c0cecd884f0848a801cb2f684cde0f4664e709574067
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:c5d64a4dd47dc1b66887c0cecd884f0848a801cb2f684cde0f4664e709574067?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/ca-certificates/manifests/2022-07-19_1
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/ca-certificates/blobs/sha256:9e0df163364a5ae07f3ee2cf39083cd74bcb38eeb5250b706e1c02f878d8d632
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:9e0df163364a5ae07f3ee2cf39083cd74bcb38eeb5250b706e1c02f878d8d632?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/openssl/1.1/manifests/1.1.1q
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/openssl/1.1/blobs/sha256:abec715f01eb20edda202463ca91403e3fa767afcba0fe732ef8e072bb99d2fd
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:abec715f01eb20edda202463ca91403e3fa767afcba0fe732ef8e072bb99d2fd?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/zlib/manifests/1.2.12
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/zlib/blobs/sha256:23b1d8f0500bbccdf5cc466e7acbd7eddc40cd1465687239af423389abe4f46e
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:23b1d8f0500bbccdf5cc466e7acbd7eddc40cd1465687239af423389abe4f46e?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/sqlite/manifests/3.39.2
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/sqlite/blobs/sha256:b7c81a07dbc720fd921da3abb096363cc24d600105b85c46a010b58ce0883ee0
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:b7c81a07dbc720fd921da3abb096363cc24d600105b85c46a010b58ce0883ee0?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/xz/manifests/5.2.6
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/xz/blobs/sha256:607a3d993f45efe858d3e4f002603e323a1b1f0c87b4db6fb57d1280f479809d
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:607a3d993f45efe858d3e4f002603e323a1b1f0c87b4db6fb57d1280f479809d?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/expat/manifests/2.4.8
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/expat/blobs/sha256:db13166d6a5bd0e19f82e9cd19f4a951ffff40cdfc29197e8143780444d0c204
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:db13166d6a5bd0e19f82e9cd19f4a951ffff40cdfc29197e8143780444d0c204?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libxcrypt/manifests/4.4.28
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libxcrypt/blobs/sha256:84269a82afe1a9e94ba6828c983a4b8e65d8d672d36d88232daac832017ab327
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:84269a82afe1a9e94ba6828c983a4b8e65d8d672d36d88232daac832017ab327?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/python/3.10/manifests/3.10.6_1
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/python/3.10/blobs/sha256:809db3634bcdc3d3e5f60f9e0e74124fc7cb6f8c52085daa36acf118d2b64039
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:809db3634bcdc3d3e5f60f9e0e74124fc7cb6f8c52085daa36acf118d2b64039?se=2022-08-24T17%3
######################################################################## 100.0%
==> Installing dependencies for python@3.10: gdbm, mpdecimal, ca-certificates, openssl@1.1, zlib, sqlite, xz, expat and libxcrypt
==> Installing python@3.10 dependency: gdbm
==> Pouring gdbm--1.23.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/gdbm/1.23: 40 files, 1.3MB
==> Installing python@3.10 dependency: mpdecimal
==> Pouring mpdecimal--2.5.1.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/mpdecimal/2.5.1: 71 files, 2.4MB
==> Installing python@3.10 dependency: ca-certificates
==> Pouring ca-certificates--2022-07-19_1.all.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/ca-certificates/2022-07-19_1: 3 files, 238.2KB
==> Installing python@3.10 dependency: openssl@1.1
==> Pouring openssl@1.1--1.1.1q.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/openssl@1.1/1.1.1q: 8,411 files, 24.3MB
==> Installing python@3.10 dependency: zlib
==> Pouring zlib--1.2.12.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/zlib/1.2.12: 12 files, 464.3KB
==> Installing python@3.10 dependency: sqlite
==> Pouring sqlite--3.39.2.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/sqlite/3.39.2: 12 files, 5.2MB
==> Installing python@3.10 dependency: xz
==> Pouring xz--5.2.6.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/xz/5.2.6: 150 files, 2.4MB
==> Installing python@3.10 dependency: expat
==> Pouring expat--2.4.8.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/expat/2.4.8: 21 files, 841.7KB
==> Installing python@3.10 dependency: libxcrypt
==> Pouring libxcrypt--4.4.28.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/libxcrypt/4.4.28: 24 files, 361.8KB
==> Installing python@3.10
==> Pouring python@3.10--3.10.6_1.x86_64_linux.bottle.tar.gz
==> /home/linuxbrew/.linuxbrew/Cellar/python@3.10/3.10.6_1/bin/python3.10 -m ensurepip
==> /home/linuxbrew/.linuxbrew/Cellar/python@3.10/3.10.6_1/bin/python3.10 -m pip install -v --no-deps --no-index --upgrade --isolated --target=/home/linuxbrew/.linuxbre
==> Caveats
Python has been installed as
  /home/linuxbrew/.linuxbrew/bin/python3

Unversioned symlinks `python`, `python-config`, `pip` etc. pointing to
`python3`, `python3-config`, `pip3` etc., respectively, have been installed into
  /home/linuxbrew/.linuxbrew/opt/python@3.10/libexec/bin

You can install Python packages with
  pip3 install <package>
They will install into the site-package directory
  /home/linuxbrew/.linuxbrew/lib/python3.10/site-packages

tkinter is no longer included with this formula, but it is available separately:
  brew install python-tk@3.10

See: https://docs.brew.sh/Homebrew-and-Python
==> Summary
🍺  /home/linuxbrew/.linuxbrew/Cellar/python@3.10/3.10.6_1: 2,665 files, 80.8MB
==> Running `brew cleanup python@3.10`...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).
==> Upgrading 9 dependents of upgraded formulae:
Disable this behaviour by setting HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).
freetype 2.12.0 -> 2.12.1, libxml2 2.9.13 -> 2.10.0, krb5 1.19.3 -> 1.20, libidn2 2.3.2 -> 2.3.3, unbound 1.15.0 -> 1.16.2, gnutls 3.7.4 -> 3.7.7, cups 2.4.1 -> 2.4.2, util-linux 2.38 -> 2.38.1, openjdk@11 11.0.14.1 -> 11.0.16.1
==> Downloading https://ghcr.io/v2/homebrew/core/icu4c/manifests/70.1
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/icu4c/blobs/sha256:04f36c9e1047fb1a9e1f1889eae2ade68d6518fb847a90e7947cc87ca94512ef
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:04f36c9e1047fb1a9e1f1889eae2ade68d6518fb847a90e7947cc87ca94512ef?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libxml2/manifests/2.10.0
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libxml2/blobs/sha256:bb066a12fb1493d3c5288d337c411e3e7a3472c54159c5e1f9a0b0f03746b496
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:bb066a12fb1493d3c5288d337c411e3e7a3472c54159c5e1f9a0b0f03746b496?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/krb5/manifests/1.20
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/krb5/blobs/sha256:17c3f6518fc7f836cd1bcc8ae0f2d8a8cc9d8ca063fa78d2faaf67158bf3318d
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:17c3f6518fc7f836cd1bcc8ae0f2d8a8cc9d8ca063fa78d2faaf67158bf3318d?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libidn2/manifests/2.3.3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libidn2/blobs/sha256:0ceff03509ea09a0784aa40b7b724f40857dfac8e9e34b36ee93570c57eb1780
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:0ceff03509ea09a0784aa40b7b724f40857dfac8e9e34b36ee93570c57eb1780?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libnghttp2/manifests/1.49.0
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libnghttp2/blobs/sha256:b39e519b439023b05ce2563549d3238f61e0765ad370a92590282f63865ffb22
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:b39e519b439023b05ce2563549d3238f61e0765ad370a92590282f63865ffb22?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/unbound/manifests/1.16.2
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/unbound/blobs/sha256:291a9a65af515036927268326e852f268507552cd73d62abe05dac1098b41812
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:291a9a65af515036927268326e852f268507552cd73d62abe05dac1098b41812?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/guile/manifests/3.0.8_1
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/guile/blobs/sha256:8e65b0d034e8231e73951fd3488843df1ac986f5bfe4f218d06ffd3fd347e275
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:8e65b0d034e8231e73951fd3488843df1ac986f5bfe4f218d06ffd3fd347e275?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libtasn1/manifests/4.19.0
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libtasn1/blobs/sha256:e994c7b8c16afb59368d8d09a3f193451c9deab1e4a83f8a94650e27674d9278
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:e994c7b8c16afb59368d8d09a3f193451c9deab1e4a83f8a94650e27674d9278?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/nettle/manifests/3.8.1
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/nettle/blobs/sha256:1901826420ae92f7998068673ec444d32550618f38ad1c074acf68be16b9b056
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:1901826420ae92f7998068673ec444d32550618f38ad1c074acf68be16b9b056?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/gnutls/manifests/3.7.7
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/gnutls/blobs/sha256:7c8d8948ac53ee9c67931e011b312fd30c9414c70a7065ff1de7e5e30e105366
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:7c8d8948ac53ee9c67931e011b312fd30c9414c70a7065ff1de7e5e30e105366?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/cups/manifests/2.4.2
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/cups/blobs/sha256:6ea637f9b6b17168bbcd63d9065b279485762585a35753050c384c18ce07093b
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:6ea637f9b6b17168bbcd63d9065b279485762585a35753050c384c18ce07093b?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/freetype/manifests/2.12.1
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/freetype/blobs/sha256:43be70d09e51402bb453d491d69021af20f0d0c5154092bd5571b365673d4e2f
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:43be70d09e51402bb453d491d69021af20f0d0c5154092bd5571b365673d4e2f?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/util-linux/manifests/2.38.1
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/util-linux/blobs/sha256:27177c2a258f719de1236fc0eafa5e45021f7997e332dd9d345c28d0c4354c36
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:27177c2a258f719de1236fc0eafa5e45021f7997e332dd9d345c28d0c4354c36?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/alsa-lib/manifests/1.2.7.2
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/alsa-lib/blobs/sha256:9e9409e572680975dd401de6d0466ebc5b8bf2832cd013f2c88fc2022d48addb
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:9e9409e572680975dd401de6d0466ebc5b8bf2832cd013f2c88fc2022d48addb?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/xorgproto/manifests/2022.2
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/xorgproto/blobs/sha256:7517927867f2f59362eb06cb2b5dda1241a8f2765603197aae24d85b086dc645
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:7517927867f2f59362eb06cb2b5dda1241a8f2765603197aae24d85b086dc645?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libxcb/manifests/1.15
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libxcb/blobs/sha256:993d37bb436fab0157ed5f3c031f9a18168053439e93edb4bdce9abe6e99373d
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:993d37bb436fab0157ed5f3c031f9a18168053439e93edb4bdce9abe6e99373d?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libx11/manifests/1.8.1
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libx11/blobs/sha256:1a0f30a63d5cd9f212f6999156baf0503dcc85c0c9b6ce4fe8a9116727a77a84
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:1a0f30a63d5cd9f212f6999156baf0503dcc85c0c9b6ce4fe8a9116727a77a84?se=2022-08-24T17%3
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/openjdk/11/manifests/11.0.16.1
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/openjdk/11/blobs/sha256:ea25b4dd142c423ef91de19c7b9a82f77ab10e09deb1e5e4332c60af82bd1ff5
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:ea25b4dd142c423ef91de19c7b9a82f77ab10e09deb1e5e4332c60af82bd1ff5?se=2022-08-24T17%3
######################################################################## 100.0%
==> Upgrading libxml2
  2.9.13 -> 2.10.0

==> Installing dependencies for libxml2: icu4c
==> Installing libxml2 dependency: icu4c
==> Pouring icu4c--70.1.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/icu4c/70.1: 261 files, 80.8MB
==> Installing libxml2
==> Pouring libxml2--2.10.0.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/libxml2/2.10.0: 200 files, 8.2MB
==> Running `brew cleanup libxml2`...
Removing: /home/linuxbrew/.linuxbrew/Cellar/libxml2/2.9.13... (282 files, 11.9MB)
==> Upgrading krb5
  1.19.3 -> 1.20

==> Pouring krb5--1.20.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/krb5/1.20: 165 files, 5.2MB
==> Running `brew cleanup krb5`...
Removing: /home/linuxbrew/.linuxbrew/Cellar/krb5/1.19.3... (165 files, 5.2MB)
==> Upgrading libidn2
  2.3.2 -> 2.3.3

==> Pouring libidn2--2.3.3.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/libidn2/2.3.3: 79 files, 1.2MB
==> Running `brew cleanup libidn2`...
Removing: /home/linuxbrew/.linuxbrew/Cellar/libidn2/2.3.2... (78 files, 1MB)
==> Upgrading unbound
  1.15.0 -> 1.16.2

==> Installing dependencies for unbound: libnghttp2
==> Installing unbound dependency: libnghttp2
==> Pouring libnghttp2--1.49.0.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/libnghttp2/1.49.0: 14 files, 830.4KB
==> Installing unbound
==> Pouring unbound--1.16.2.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/unbound/1.16.2: 59 files, 11.2MB
==> Running `brew cleanup unbound`...
Removing: /home/linuxbrew/.linuxbrew/Cellar/unbound/1.15.0... (59 files, 11.1MB)
==> Upgrading gnutls
  3.7.4 -> 3.7.7

==> Installing dependencies for gnutls: guile, libtasn1 and nettle
==> Installing gnutls dependency: guile
==> Pouring guile--3.0.8_1.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/guile/3.0.8_1: 846 files, 69.6MB
==> Installing gnutls dependency: libtasn1
==> Pouring libtasn1--4.19.0.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/libtasn1/4.19.0: 62 files, 653.5KB
==> Installing gnutls dependency: nettle
==> Pouring nettle--3.8.1.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/nettle/3.8.1: 90 files, 2.9MB
==> Installing gnutls
==> Pouring gnutls--3.7.7.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/gnutls/3.7.7: 1,292 files, 11.7MB
==> Running `brew cleanup gnutls`...
Removing: /home/linuxbrew/.linuxbrew/Cellar/gnutls/3.7.4... (1,288 files, 11.6MB)
==> Upgrading cups
  2.4.1 -> 2.4.2

==> Pouring cups--2.4.2.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/cups/2.4.2: 121 files, 9.6MB
==> Running `brew cleanup cups`...
Removing: /home/linuxbrew/.linuxbrew/Cellar/cups/2.4.1... (121 files, 9.6MB)
==> Upgrading freetype
  2.12.0 -> 2.12.1

==> Pouring freetype--2.12.1.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/freetype/2.12.1: 68 files, 2.8MB
==> Running `brew cleanup freetype`...
Removing: /home/linuxbrew/.linuxbrew/Cellar/freetype/2.12.0... (68 files, 2.8MB)
==> Upgrading util-linux
  2.38 -> 2.38.1

==> Pouring util-linux--2.38.1.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/util-linux/2.38.1: 398 files, 20.3MB
==> Running `brew cleanup util-linux`...
Removing: /home/linuxbrew/.linuxbrew/Cellar/util-linux/2.38... (398 files, 20.2MB)
Removing: /home/builder/.cache/Homebrew/util-linux--2.38... (6.8MB)
==> Upgrading openjdk@11
  11.0.14.1 -> 11.0.16.1

==> Installing dependencies for openjdk@11: alsa-lib, xorgproto, libxcb and libx11
==> Installing openjdk@11 dependency: alsa-lib
==> Pouring alsa-lib--1.2.7.2.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/alsa-lib/1.2.7.2: 143 files, 2.2MB
==> Installing openjdk@11 dependency: xorgproto
==> Pouring xorgproto--2022.2.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/xorgproto/2022.2: 268 files, 4.0MB
==> Installing openjdk@11 dependency: libxcb
==> Pouring libxcb--1.15.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/libxcb/1.15: 2,459 files, 6.7MB
==> Installing openjdk@11 dependency: libx11
==> Pouring libx11--1.8.1.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/libx11/1.8.1: 1,062 files, 8MB
==> Installing openjdk@11
==> Pouring openjdk@11--11.0.16.1.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/openjdk@11/11.0.16.1: 670 files, 311.4MB
==> Running `brew cleanup openjdk@11`...
Removing: /home/linuxbrew/.linuxbrew/Cellar/openjdk@11/11.0.14.1... (670 files, 309MB)
==> Checking for dependents of upgraded formulae...
==> No broken dependents found!
==> Caveats
==> python@3.10
Python has been installed as
  /home/linuxbrew/.linuxbrew/bin/python3

Unversioned symlinks `python`, `python-config`, `pip` etc. pointing to
`python3`, `python3-config`, `pip3` etc., respectively, have been installed into
  /home/linuxbrew/.linuxbrew/opt/python@3.10/libexec/bin

You can install Python packages with
  pip3 install <package>
They will install into the site-package directory
  /home/linuxbrew/.linuxbrew/lib/python3.10/site-packages

tkinter is no longer included with this formula, but it is available separately:
  brew install python-tk@3.10

See: https://docs.brew.sh/Homebrew-and-Python

In my case, the lack of Python2 made venv steps fail. I worked around by symlinking python3 to phython

$ ln -s /home/linuxbrew/.linuxbrew/bin/python3 /home/linuxbrew/.linuxbrew/bin/python

Next, we want to create a new Python Azure Function in a Python Virtual Environment (venv)

builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$ python -m venv .venv
builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$ source .venv/bin/activate
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$ func init --worker-runtime python --docker
Found Python version 3.8.10 (python3.8).
Writing requirements.txt
Writing .funcignore
Writing getting_started.md
Writing .gitignore
Writing host.json
Writing local.settings.json
Writing /home/builder/Workspaces/azfunctest/.vscode/extensions.json
Writing Dockerfile
Writing .dockerignore
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$

(.venv) builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$ func new --name HttpExample --template "HTTP trigger" --authlevel anonymous
Select a number for template:HTTP trigger
Function name: [HttpTrigger] Writing /home/builder/Workspaces/azfunctest/HttpExample/__init__.py
Writing /home/builder/Workspaces/azfunctest/HttpExample/function.json
The function "HttpExample" was created successfully from the "HTTP trigger" template.

Now we can test locally

(.venv) builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$ func start
Found Python version 3.8.10 (python3.8).

Azure Functions Core Tools
Core Tools Version:       4.0.4736 Commit hash: N/A  (64-bit)
Function Runtime Version: 4.8.1.18957


Functions:

        HttpExample: [GET,POST] http://localhost:7071/api/HttpExample

For detailed output, run func with --verbose flag.
[2022-08-24T17:04:15.580Z] Worker process started and initialized.
[2022-08-24T17:04:20.269Z] Host lock lease acquired by instance ID '0000000000000000000000009E0083EE'.

/content/images/2022/09/azfuncazdo-01.png

Containerizing

Ideally, we want to make a container from this code. Our create made a Dockerfile we can use.

First, let’s make a change we can see reflect in the webapp

$ cat HttpExample/__init__.py
import logging

import azure.functions as func


def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    name = req.params.get('name')
    if not name:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            name = req_body.get('name')

    if name:
        return func.HttpResponse(f"Hello, {name}. This HTTP triggered function executed successfully, Buddy.")
    else:
        return func.HttpResponse(
             "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response, Pal.",
             status_code=200
        )

Then we can test

/content/images/2022/09/azfuncazdo-02.png

We can now build it, listing a tag for our destination CR

(.venv) builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$ docker build --tag harbor.freshbrewed.science/freshbrewedprivate/azfunc01:v0.0.1 .
[+] Building 23.4s (9/9) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                               0.0s
 => => transferring dockerfile: 38B                                                                                                                                0.0s
 => [internal] load .dockerignore                                                                                                                                  0.0s
 => => transferring context: 34B                                                                                                                                   0.0s
 => [internal] load metadata for mcr.microsoft.com/azure-functions/python:4-python3.8                                                                              0.7s
 => [1/4] FROM mcr.microsoft.com/azure-functions/python:4-python3.8@sha256:35cbd79e52907195c94a9571f36861cb3351672b55cf047ccfc9f630af5838a9                       19.2s
 => => resolve mcr.microsoft.com/azure-functions/python:4-python3.8@sha256:35cbd79e52907195c94a9571f36861cb3351672b55cf047ccfc9f630af5838a9                        0.0s
 => => sha256:6f668c4783382b31feea0b50a736eb38cab248fa5d3ff50edb18361b97be3c30 11.33MB / 11.33MB                                                                   1.4s
 => => sha256:35cbd79e52907195c94a9571f36861cb3351672b55cf047ccfc9f630af5838a9 2.43kB / 2.43kB                                                                     0.0s
 => => sha256:e37ebf440f7f53eb0584605f7c63a95b42583a1372913da9061b6fdb7b535663 1.08MB / 1.08MB                                                                     0.5s
 => => sha256:547c70c5f43fe0afb45e8f303d43fabbe2fe180422df1985fcbb19cfebb977bf 10.05kB / 10.05kB                                                                   0.0s
 => => sha256:461246efe0a75316d99afdbf348f7063b57b0caeee8daab775f1f08152ea36f4 31.37MB / 31.37MB                                                                   2.3s
 => => sha256:33c68732c70263ef65d4fbec6081f5aece39eb923150090ae31d9fdc60a1c5a4 234B / 234B                                                                         0.6s
 => => sha256:0ac96ef71e90047fde11b18e309b2c288d1f2990bc1dae64599daf7a12b99433 3.17MB / 3.17MB                                                                     1.1s
 => => sha256:8580284b1cdff2df97e7faf50a9e1079e842b9ff198819da408bf63b61c5168a 64.08MB / 64.08MB                                                                   6.1s
 => => sha256:88ef5d024750f33b9586bc77e264dcf0246c3a9a8af7b4aabdfbb74f4d29cebd 94.36MB / 94.36MB                                                                  11.5s
 => => sha256:43ad0d503017ae61d13f564b2146806de23f69cfbea580cfc04c99706fef88fc 6.93MB / 6.93MB                                                                     3.1s
 => => extracting sha256:461246efe0a75316d99afdbf348f7063b57b0caeee8daab775f1f08152ea36f4                                                                          1.3s
 => => sha256:cf42f74e290d84acdc3c17af83076a371e299970ebc9d04a9fe5b7c2dffaf1b9 465B / 465B                                                                         3.3s
 => => sha256:5b5931ee77e735936408dd350bb7e5121a0463277ace4b1500583bacdb5a7ef7 174.03MB / 174.03MB                                                                14.4s
 => => extracting sha256:e37ebf440f7f53eb0584605f7c63a95b42583a1372913da9061b6fdb7b535663                                                                          0.1s
 => => extracting sha256:6f668c4783382b31feea0b50a736eb38cab248fa5d3ff50edb18361b97be3c30                                                                          0.5s
 => => extracting sha256:33c68732c70263ef65d4fbec6081f5aece39eb923150090ae31d9fdc60a1c5a4                                                                          0.0s
 => => extracting sha256:0ac96ef71e90047fde11b18e309b2c288d1f2990bc1dae64599daf7a12b99433                                                                          0.3s
 => => extracting sha256:8580284b1cdff2df97e7faf50a9e1079e842b9ff198819da408bf63b61c5168a                                                                          2.3s
 => => extracting sha256:88ef5d024750f33b9586bc77e264dcf0246c3a9a8af7b4aabdfbb74f4d29cebd                                                                          2.7s
 => => extracting sha256:43ad0d503017ae61d13f564b2146806de23f69cfbea580cfc04c99706fef88fc                                                                          0.2s
 => => extracting sha256:cf42f74e290d84acdc3c17af83076a371e299970ebc9d04a9fe5b7c2dffaf1b9                                                                          0.0s
 => => extracting sha256:5b5931ee77e735936408dd350bb7e5121a0463277ace4b1500583bacdb5a7ef7                                                                          4.2s
 => [internal] load build context                                                                                                                                  0.2s
 => => transferring context: 18.39MB                                                                                                                               0.2s
 => [2/4] COPY requirements.txt /                                                                                                                                  0.7s
 => [3/4] RUN pip install -r /requirements.txt                                                                                                                     2.3s
 => [4/4] COPY . /home/site/wwwroot                                                                                                                                0.2s
 => exporting to image                                                                                                                                             0.2s
 => => exporting layers                                                                                                                                            0.2s
 => => writing image sha256:b861a838894c09b2cb9b8247beef9d59d05499f697bf545977f1ff0e229f8a70                                                                       0.0s
 => => naming to harbor.freshbrewed.science/freshbrewedprivate/azfunc01:v0.0.1                                                                                     0.0s

Before we go on, let’s run our function as a container first

(.venv) builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$ docker run -p 8080:80 -it harbor.freshbrewed.science/freshbrewedprivate/azfunc01:v0.0.1
info: Host.Triggers.Warmup[0]
      Initializing Warmup Extension.
info: Host.Startup[503]
      Initializing Host. OperationId: '45b31053-ef5a-493b-9de1-1a0d88bd0c53'.
info: Host.Startup[504]
      Host initialization: ConsecutiveErrors=0, StartupCount=1, OperationId=45b31053-ef5a-493b-9de1-1a0d88bd0c53
info: Microsoft.Azure.WebJobs.Hosting.OptionsLoggingService[0]
      LoggerFilterOptions
      {
        "MinLevel": "None",
        "Rules": [
...

/content/images/2022/09/azfuncazdo-03.png

In order to push to a private container registry, we will need to login

$ docker login harbor.freshbrewed.science
Authenticating with existing credentials...
Login Succeeded

Note: If using ACR, you can use az acr login

Once logged in, we can push our built image

$ docker push harbor.freshbrewed.science/freshbrewedprivate/azfunc01:v0.0.1
The push refers to repository [harbor.freshbrewed.science/freshbrewedprivate/azfunc01]
8c52a6db274b: Pushed
5eee235dcd5c: Pushed
2414f8c3f220: Pushed
feb598154981: Pushed
77e70b00b417: Pushed
e0fdd6ff4866: Pushed
96bc2ff527d5: Pushed
f6f2492342c7: Pushed
d756ffe2a052: Pushed
b55536955a69: Pushed
89c513219c8b: Pushed
0255573f4829: Pushed
43b3c4e3001c: Pushed
v0.0.1: digest: sha256:ecee3ad8746a5a6abcd643a985c882bee6068a6122036bb40d39a15e97811cf6 size: 3055

Creating the Azure Function instance in Azure

Now that we have a containerized function locally, let’s create the App in Azure

$ az login

$ az group create --name AzFuncContainersRG --location centralus
{
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/AzFuncContainersRG",
  "location": "centralus",
  "managedBy": null,
  "name": "AzFuncContainersRG",
  "properties": {
    "provisioningState": "Succeeded"
  },
  "tags": null,
  "type": "Microsoft.Resources/resourceGroups"
}

Next, a storage account is needed

$ az storage account create --name idjfuncapptest1 --location centralus -g AzFuncContainersRG --sku Standard_LRS
{
  "accessTier": "Hot",
  "allowBlobPublicAccess": true,
  "allowCrossTenantReplication": null,
  "allowSharedKeyAccess": null,
  "azureFilesIdentityBasedAuthentication": null,
  "blobRestoreStatus": null,
  "creationTime": "2022-08-24T17:34:14.005428+00:00",
  "customDomain": null,
  "defaultToOAuthAuthentication": null,
  "enableHttpsTrafficOnly": true,
  "enableNfsV3": null,
  "encryption": {
    "encryptionIdentity": null,
    "keySource": "Microsoft.Storage",
    "keyVaultProperties": null,
    "requireInfrastructureEncryption": null,
    "services": {
      "blob": {
        "enabled": true,
        "keyType": "Account",
        "lastEnabledTime": "2022-08-24T17:34:14.130433+00:00"
      },
      "file": {
        "enabled": true,
        "keyType": "Account",
        "lastEnabledTime": "2022-08-24T17:34:14.130433+00:00"
      },
      "queue": null,
      "table": null
    }
  },
  "extendedLocation": null,
  "failoverInProgress": null,
  "geoReplicationStats": null,
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/AzFuncContainersRG/providers/Microsoft.Storage/storageAccounts/idjfuncapptest1",
  "identity": null,
  "immutableStorageWithVersioning": null,
  "isHnsEnabled": null,
  "keyCreationTime": {
    "key1": "2022-08-24T17:34:14.130433+00:00",
    "key2": "2022-08-24T17:34:14.130433+00:00"
  },
  "keyPolicy": null,
  "kind": "StorageV2",
  "largeFileSharesState": null,
  "lastGeoFailoverTime": null,
  "location": "centralus",
  "minimumTlsVersion": "TLS1_0",
  "name": "idjfuncapptest1",
  "networkRuleSet": {
    "bypass": "AzureServices",
    "defaultAction": "Allow",
    "ipRules": [],
    "resourceAccessRules": null,
    "virtualNetworkRules": []
  },
  "primaryEndpoints": {
    "blob": "https://idjfuncapptest1.blob.core.windows.net/",
    "dfs": "https://idjfuncapptest1.dfs.core.windows.net/",
    "file": "https://idjfuncapptest1.file.core.windows.net/",
    "internetEndpoints": null,
    "microsoftEndpoints": null,
    "queue": "https://idjfuncapptest1.queue.core.windows.net/",
    "table": "https://idjfuncapptest1.table.core.windows.net/",
    "web": "https://idjfuncapptest1.z19.web.core.windows.net/"
  },
  "primaryLocation": "centralus",
  "privateEndpointConnections": [],
  "provisioningState": "Succeeded",
  "publicNetworkAccess": null,
  "resourceGroup": "AzFuncContainersRG",
  "routingPreference": null,
  "sasPolicy": null,
  "secondaryEndpoints": null,
  "secondaryLocation": null,
  "sku": {
    "name": "Standard_LRS",
    "tier": "Standard"
  },
  "statusOfPrimary": "available",
  "statusOfSecondary": null,
  "tags": {},
  "type": "Microsoft.Storage/storageAccounts"
}

We now need to put this into a Premium Plan

$ az functionapp plan create -g AzFuncContainersRG --name myPremiumPlan --location centralus --number-of-workers
1 --sku EP1 --is-linux
{
  "freeOfferExpirationTime": null,
  "geoRegion": "Central US",
  "hostingEnvironmentProfile": null,
  "hyperV": false,
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/AzFuncContainersRG/providers/Microsoft.Web/serverfarms/myPremiumPlan",
  "isSpot": false,
  "isXenon": false,
  "kind": "elastic",
  "location": "centralus",
  "maximumElasticWorkerCount": 1,
  "maximumNumberOfWorkers": 0,
  "name": "myPremiumPlan",
  "numberOfSites": 0,
  "perSiteScaling": false,
  "provisioningState": "Succeeded",
  "reserved": true,
  "resourceGroup": "AzFuncContainersRG",
  "sku": {
    "capabilities": null,
    "capacity": 1,
    "family": "EP",
    "locations": null,
    "name": "EP1",
    "size": "EP1",
    "skuCapacity": null,
    "tier": "ElasticPremium"
  },
  "spotExpirationTime": null,
  "status": "Ready",
  "subscription": "d955c0ba-13dc-44cf-a29a-8fed74cbb22d",
  "systemData": null,
  "tags": null,
  "targetWorkerCount": 0,
  "targetWorkerSizeId": 0,
  "type": "Microsoft.Web/serverfarms",
  "workerTierName": null
}

I’ll need the SA connection string. We can pull it from the Portal or command line

$ az storage account show-connection-string -g AzFuncContainersRG --name idjfuncapptest1 --query connectionString --output tsv

However, we can just use backticks to pull it on the fly in our create statement below.

When we create the Az Func App, since we are using a private CR, we’ll need to pass in those credentials

$ az functionapp create --name MySampleApp082022 --storage-account idjfuncapptest1 -g AzFuncContainersRG --plan m
yPremiumPlan --deployment-container-image-name harbor.freshbrewed.science/freshbrewedprivate/azfunc01:v0.0.1 --docker-registry-server-user imagepuller --docker-registry
-server-password MYHARBORPASSWORD
No functions version specified so defaulting to 3. In the future, specifying a version will be required. To create a 3.x function you would pass in the flag `--functions-version 3`
Application Insights "MySampleApp082022" was created for this Function App. You can visit https://portal.azure.com/#resource/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/AzFuncContainersRG/providers/microsoft.insights/components/MySampleApp082022/overview to view your Application Insights component
{
  "availabilityState": "Normal",
  "clientAffinityEnabled": false,
  "clientCertEnabled": false,
  "clientCertExclusionPaths": null,
  "clientCertMode": "Required",
  "cloningInfo": null,
  "containerSize": 0,
  "customDomainVerificationId": "1B4C8E9BFA263939F5437F88623F1C0397DE707EB4D40A3AB2A7B071129D28ED",
  "dailyMemoryTimeQuota": 0,
  "defaultHostName": "mysampleapp082022.azurewebsites.net",
  "enabled": true,
  "enabledHostNames": [
    "mysampleapp082022.azurewebsites.net",
    "mysampleapp082022.scm.azurewebsites.net"
  ],
  "hostNameSslStates": [
    ....

Then update the appsettings with our storage account credentials

$ az functionapp config appsettings set --name MySampleApp082022 -g AzFuncContainersRG --settings AzureWebJobsSto
rage=`az storage account show-connection-string -g AzFuncContainersRG --name idjfuncapptest1 --query connectionString --output tsv`
[
  {
    "name": "MACHINEKEY_DecryptionKey",
    "slotSetting": false,
    "value": "616CDF0D3D834B9C5619233E93B1346F606121DFC0A1E38571716197A6C024E0"
  },
  {
    "name": "DOCKER_CUSTOM_IMAGE_NAME",
    "slotSetting": false,
    "value": "harbor.freshbrewed.science/freshbrewedprivate/azfunc01:v0.0.1"
  },
  ...

If we go to the front door, we can see the Func App splash page

/content/images/2022/09/azfuncazdo-04.png

Then hit the API endpoint

/content/images/2022/09/azfuncazdo-05.png

If we wanted to trigger updates of a single image (in the case of using “latest”), then we could trigger with a Webhook. To get the CICD URL webhook

$ az functionapp deployment container config --enable-cd --query CI_CD_URL --output tsv --name MySampleApp082022 -g AzFuncContainersRG
https://$MySampleApp082022:asdfasdfasdfasdfasdfasdfasdfasdfasdasdfasdf@mysampleapp082022.scm.azurewebsites.net/docker/hook

However, this is not the approach we will be using as we talk about setting up Azure DevOps below.

Azure DevOps

Let’s first create an Azure DevOps Repo.

First, I’ll see if there is one that would work well

$ az repos list --organization https://dev.azure.com/princessking/ --project IAC-Demo -o table
ID                                    Name              Default Branch    Project
------------------------------------  ----------------  ----------------  ---------
381490be-1d2d-4299-b6ca-4cf2fa11f5de  Dotnet-Core       master            IAC-Demo
0c972a74-2522-4263-99ea-5d6692f31065  Helm-Demo         master            IAC-Demo
678e6ec3-6d46-4c6d-94eb-2673e6d0a488  IAC-Demo          develop           IAC-Demo
0090ec49-5900-4867-8db4-c914c2263254  IaC-Packer        master            IAC-Demo
a5ed232b-97d2-4d16-aaae-63173144ab77  Nomad             master            IAC-Demo
7febee1e-1fa4-4327-a185-5e9fe6d7b3c7  VNC-Docker-Build  master            IAC-Demo
6907b550-9621-4e37-a91c-c04a5adc0c66  demo-repo         master            IAC-Demo
cea67316-1487-472e-8f97-405fae45ca61  ggg               master            IAC-Demo
b9a2f8b6-c62b-4bfa-b0fb-abd74576ff14  myPugApp          master            IAC-Demo
1136d941-7b25-4742-8dfc-362bc4c12be5  vnc-docker        master            IAC-Demo

I’ll create a new one

$ az repos create --name AzFuncAppCICD --organization https://dev.azure.com/princessking/ --project IAC-Demo
{
  "defaultBranch": null,
  "id": "54437cd4-5865-459d-93c5-851738c3e16d",
  "isDisabled": false,
  "isFork": null,
  "name": "AzFuncAppCICD",
  "parentRepository": null,
  "project": {
    "abbreviation": null,
    "defaultTeamImageUrl": null,
    "description": null,
    "id": "f5e16db0-35f6-493b-8d96-7516c115d4cc",
    "lastUpdateTime": "2020-09-17T10:47:48.847Z",
    "name": "IAC-Demo",
    "revision": 176,
    "state": "wellFormed",
    "url": "https://princessking.visualstudio.com/_apis/projects/f5e16db0-35f6-493b-8d96-7516c115d4cc",
    "visibility": "public"
  },
  "remoteUrl": "https://princessking.visualstudio.com/IAC-Demo/_git/AzFuncAppCICD",
  "size": 0,
  "sshUrl": "princessking@vs-ssh.visualstudio.com:v3/princessking/IAC-Demo/AzFuncAppCICD",
  "url": "https://princessking.visualstudio.com/f5e16db0-35f6-493b-8d96-7516c115d4cc/_apis/git/repositories/54437cd4-5865-459d-93c5-851738c3e16d",
  "validRemoteUrls": null,
  "webUrl": "https://princessking.visualstudio.com/IAC-Demo/_git/AzFuncAppCICD"
}

Then to push our local files to the repo we first turn the folder into a GIT repo

(.venv) builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$ git init
Initialized empty Git repository in /home/builder/Workspaces/azfunctest/.git/
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$ git checkout -b main
Switched to a new branch 'main'
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$ git add -A
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$ git commit -m "First"
[main (root-commit) 8aa3c6a] First
 10 files changed, 158 insertions(+)
 create mode 100644 .dockerignore
 create mode 100644 .funcignore
 create mode 100644 .gitignore
 create mode 100644 .vscode/extensions.json
 create mode 100644 Dockerfile
 create mode 100644 HttpExample/__init__.py
 create mode 100644 HttpExample/function.json
 create mode 100644 getting_started.md
 create mode 100644 host.json
 create mode 100644 requirements.txt

Followed by a push of the new repo into Azure Repos

(.venv) builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$ git remote add origin https://princessking.visualstudio.com/IAC-Demo/_git/AzFuncAppCICD
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$ git push -u origin --all
Username for 'https://princessking.visualstudio.com': isaac.johnson@gmail.com
Password for 'https://isaac.johnson@gmail.com@princessking.visualstudio.com':
Enumerating objects: 14, done.
Counting objects: 100% (14/14), done.
Delta compression using up to 16 threads
Compressing objects: 100% (11/11), done.
Writing objects: 100% (14/14), 3.01 KiB | 3.01 MiB/s, done.
Total 14 (delta 0), reused 0 (delta 0)
remote: Analyzing objects... (14/14) (6 ms)
remote: Storing packfile... done (81 ms)
remote: Storing index... done (121 ms)
To https://princessking.visualstudio.com/IAC-Demo/_git/AzFuncAppCICD
 * [new branch]      main -> main
Branch 'main' set up to track remote branch 'main' from 'origin'.

And now see the content updated in our Azure Repo

/content/images/2022/09/azfuncazdo-06.png

We can click “Setup Build” to start the Azure Pipeline wizard and choose the Python Func App template

/content/images/2022/09/azfuncazdo-07.png

and after selecting my subscription, I could pick the App and Working directory

/content/images/2022/09/azfuncazdo-08.png

When I click “Validate and Create”, it created the AzDO Template for me:

# Python Function App to Linux on Azure
# Build a Python function app and deploy it to Azure as a Linux function app.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/python

trigger:
- main

variables:
  # Azure Resource Manager connection created during pipeline creation
  azureSubscription: '82cb120f-b5a5-4a98-9277-d083ad7796a9'

  # Function app name
  functionAppName: 'MySampleApp082022'

  # Agent VM image name
  vmImageName: 'ubuntu-latest'

  # Working Directory
  workingDirectory: '$(System.DefaultWorkingDirectory)/'

stages:
- stage: Build
  displayName: Build stage

  jobs:
  - job: Build
    displayName: Build
    pool:
      vmImage: $(vmImageName)

    steps:
    - bash: |
        if [ -f extensions.csproj ]
        then
            dotnet build extensions.csproj --runtime ubuntu.16.04-x64 --output ./bin
        fi
      workingDirectory: $(workingDirectory)
      displayName: 'Build extensions'

    - task: UsePythonVersion@0
      displayName: 'Use Python 3.6'
      inputs:
        versionSpec: 3.6 # Functions V2 supports Python 3.6 as of today

    - bash: |
        pip install --target="./.python_packages/lib/site-packages" -r ./requirements.txt
      workingDirectory: $(workingDirectory)
      displayName: 'Install application dependencies'

    - task: ArchiveFiles@2
      displayName: 'Archive files'
      inputs:
        rootFolderOrFile: '$(workingDirectory)'
        includeRootFolder: false
        archiveType: zip
        archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
        replaceExistingArchive: true

    - publish: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
      artifact: drop

- stage: Deploy
  displayName: Deploy stage
  dependsOn: Build
  condition: succeeded()

  jobs:
  - deployment: Deploy
    displayName: Deploy
    environment: 'development'
    pool:
      vmImage: $(vmImageName)

    strategy:
      runOnce:
        deploy:

          steps:
          - task: AzureFunctionApp@1
            displayName: 'Azure functions app deploy'
            inputs:
              azureSubscription: '$(azureSubscription)'
              appType: functionAppLinux
              appName: $(functionAppName)
              package: '$(Pipeline.Workspace)/drop/$(Build.BuildId).zip'

Before we try this, it’s clear it will package a zip to upload to our app. This is not what we want.

If we check our App in the Azure Portal we can confirm our source is a container:

/content/images/2022/09/azfuncazdo-09.png

So instead of pushing zip archives to Azure, let us instead build a container and push to Harbor first

In order to do that, we first need to create a Docker Registry service connection we could use in our pipeline

/content/images/2022/09/azfuncazdo-10.png

and put in our details

/content/images/2022/09/azfuncazdo-11.png

We change the Pipeline YAML to

$ cat azure-pipelines.yml
# Python Function App to Linux on Azure
# Build a Python function app and deploy it to Azure as a Linux function app.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/python

trigger:
- main

variables:
  # Azure Resource Manager connection created during pipeline creation
  azureSubscription: '766a220a-2481-4b60-9612-7e761be6f2ab'

  # Function app name
  functionAppName: 'MySampleApp082022'

  # Agent VM image name
  vmImageName: 'ubuntu-latest'

  # Working Directory
  workingDirectory: '$(System.DefaultWorkingDirectory)/'

stages:
- stage: Build
  displayName: Build stage

  jobs:
  - job: Build
    displayName: Build
    pool:
      vmImage: $(vmImageName)

    steps:
    - bash: |
        if [ -f extensions.csproj ]
        then
            dotnet build extensions.csproj --runtime ubuntu.16.04-x64 --output ./bin
        fi
      workingDirectory: $(workingDirectory)
      displayName: 'Build extensions'

    - task: UsePythonVersion@0
      displayName: 'Use Python 3.6'
      inputs:
        versionSpec: 3.6 # Functions V2 supports Python 3.6 as of today

    - bash: |
        pip install --target="./.python_packages/lib/site-packages" -r ./requirements.txt
      workingDirectory: $(workingDirectory)
      displayName: 'Install application dependencies'

    - task: ArchiveFiles@2
      displayName: 'Archive files'
      inputs:
        rootFolderOrFile: '$(workingDirectory)'
        includeRootFolder: false
        archiveType: zip
        archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
        replaceExistingArchive: true

    - publish: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
      artifact: drop

    - task: Docker@2
      displayName: 'Build and Push to Harbor CR'
      inputs:
        containerRegistry: 'FBHarbor'
        repository: 'freshbrewedprivate/azfunc01'
        command: 'buildAndPush'
        Dockerfile: '**/Dockerfile'

- stage: Deploy
  displayName: Deploy stage
  dependsOn: Build
  condition: succeeded()

  jobs:
  - deployment: Deploy
    displayName: Deploy
    environment: 'development'
    pool:
      vmImage: $(vmImageName)

    strategy:
      runOnce:
        deploy:

          steps:
            - task: AzureFunctionAppContainer@1
              displayName: 'Update to new Harbor Image'
              inputs:
                azureSubscription: 'Pay-As-You-Go (d955c0ba-13dc-44cf-a29a-8fed74cbb22d)'
                appName: 'MySampleApp082022'
                deployToSlotOrASE: true
                resourceGroupName: 'AzFuncContainersRG'
                slotName: 'production'
                imageName: 'harbor.freshbrewed.science/freshbrewedprivate/azfunc01:$(Build.BuildId)'

We can save and run

/content/images/2022/09/azfuncazdo-12.png

And we can see it pushed to the Azure Function

/content/images/2022/09/azfuncazdo-13.png

While I do not see logs because we haven’t enabled logging on this container, we can see our container image via the logs

/content/images/2022/09/azfuncazdo-14.png

/ 420KB
2022-08-24T18:40:01.522Z INFO  - 032ba9bcf8d7 Pull complete
2022-08-24T18:40:01.682Z INFO  -  Digest: sha256:1c576b0c66749c4478917205dd8ff40c95e2adee8402548c9e148aad6e2ca591
2022-08-24T18:40:01.778Z INFO  -  Status: Downloaded newer image for harbor.freshbrewed.science/freshbrewedprivate/azfunc01:19663
2022-08-24T18:40:02.650Z INFO  - Pull Image successful, Time taken: 0 Minutes and 11 Seconds
2022-08-24T18:40:08.182Z INFO  - Starting container for site
2022-08-24T18:40:08.189Z INFO  - docker run -d --expose=80 --name mysampleapp082022_3_bb79f754 -e DOCKER_CUSTOM_IMAGE_NAME=harbor.freshbrewed.science/freshbrewedprivate/azfunc01:v0.0.1 -e WEBSITES_ENABLE_APP_SERVICE_STORAGE=false -e WEBSITE_SITE_NAME=MySampleApp082022 -e WEBSITE_AUTH_ENABLED=False -e PORT=80 -e WEBSITE_ROLE_INSTANCE_ID=0 -e WEBSITE_HOSTNAME=mysampleapp082022.azurewebsites.net -e WEBSITE_INSTANCE_ID=9e6fc5670940244f0d168017eb99604b10dcd4a6c49433f1427c2881112e8a6b -e WEBSITE_USE_DIAGNOSTIC_SERVER=False harbor.freshbrewed.science/freshbrewedprivate/azfunc01:19663  

Let’s prove this is really updating by testing our CICD

/content/images/2022/09/azfuncazdo-15.png

Which triggers a build

/content/images/2022/09/azfuncazdo-16.png

which deploys via pushing the container which updates the Function

Starting: Update to new Harbor Image
==============================================================================
Task         : Azure Functions for container
Description  : Update a function app with a Docker container
Version      : 1.208.0
Author       : Microsoft Corporation
Help         : https://aka.ms/azurefunctioncontainertroubleshooting
==============================================================================
Got service connection details for Azure App Service:'MySampleApp082022'
Trying to update App Service Configuration settings. Data: {"appCommandLine":null,"linuxFxVersion":"DOCKER|harbor.freshbrewed.science/freshbrewedprivate/azfunc01:19664"}
Updated App Service Configuration settings.
Restarting App Service: MySampleApp082022
App Service 'MySampleApp082022' restarted successfully.
Trying to update App Service Application settings. Data: {"WEBSITES_ENABLE_APP_SERVICE_STORAGE":"false"}
App Service Application settings are already present.
Successfully added release annotation to the Application Insight : MySampleApp082022
Successfully updated deployment History at https://mysampleapp082022.scm.azurewebsites.net/api/deployments/196641661367337244
App Service Application URL: http://mysampleapp082022.azurewebsites.net

/content/images/2022/09/azfuncazdo-17.png

I can see the container image was updated by looking in the Log

/content/images/2022/09/azfuncazdo-18.png

Of course, the proof is in seeing it running with the change

/content/images/2022/09/azfuncazdo-19.png

Adding slots

The model we have laid out works great if we are just pushing live changes every time.

However, one of the big values of Az Function Apps are the ability to have Deployments slots with a promotion path.

All apps get one (ie. “production”). If you have just one slot, asking Azure will list none

$ az functionapp deployment slot list --name MySampleApp082022 -g AzFuncContainersRG
[]

I’ll now create a “PreProd” slot. We use the Function name in lieu of source slot name when cloning from “production” (the first)

$ az functionapp deployment slot create -n MySampleApp082022 -g AzFuncContainersRG -s PreProd --configuration-source MySampleApp082022
{
  "availabilityState": "Normal",
  "clientAffinityEnabled": false,
  "clientCertEnabled": false,
  "clientCertExclusionPaths": null,
  "clientCertMode": "Required",
  "cloningInfo": null,
  "containerSize": 1536,
  "customDomainVerificationId": "1B4C8E9BFA263939F5437F88623F1C0397DE707EB4D40A3AB2A7B071129D28ED",
  "dailyMemoryTimeQuota": 0,
  "defaultHostName": "mysampleapp082022-preprod.azurewebsites.net",
  "enabled": true,
  "enabledHostNames": [
    "mysampleapp082022-preprod.azurewebsites.net",
    "mysampleapp082022-preprod.scm.azurewebsites.net"
  ],
  "hostNameSslStates": [
    {
      "hostType": "Standard",
      "ipBasedSslResult": null,
      "ipBasedSslState": "NotConfigured",
      "name": "mysampleapp082022-preprod.azurewebsites.net",
      "sslState": "Disabled",
      "thumbprint": null,
      "toUpdate": null,
      "toUpdateIpBasedSsl": null,
      "virtualIp": null
    },
    {
      "hostType": "Repository",
      "ipBasedSslResult": null,
      "ipBasedSslState": "NotConfigured",
      "name": "mysampleapp082022-preprod.scm.azurewebsites.net",
      "sslState": "Disabled",
      "thumbprint": null,
      "toUpdate": null,
      "toUpdateIpBasedSsl": null,
      "virtualIp": null
    }
  ],
  "hostNames": [
    "mysampleapp082022-preprod.azurewebsites.net"
  ],
  "hostNamesDisabled": false,
  "hostingEnvironmentProfile": null,
  "httpsOnly": false,
  "hyperV": false,
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/AzFuncContainersRG/providers/Microsoft.Web/sites/MySampleApp082022/slots/PreProd",
  "identity": null,
  "inProgressOperationId": null,
  "isDefaultContainer": null,
  "isXenon": false,
  "kind": "functionapp,linux,container",
  "lastModifiedTimeUtc": "2022-08-24T19:12:11.933333",
  "location": "Central US",
  "maxNumberOfWorkers": null,
  "name": "PreProd",
  "outboundIpAddresses": "20.98.185.248,20.98.188.19,20.98.188.42,20.98.188.129,20.98.188.248,20.98.189.29,20.118.48.4",
  "possibleOutboundIpAddresses": "20.98.184.96,20.98.184.223,20.98.184.247,20.98.185.11,20.98.185.102,20.98.185.119,20.98.185.248,20.98.188.19,20.98.188.42,20.98.188.129,20.98.188.248,20.98.189.29,20.98.189.31,20.98.189.41,20.98.189.52,20.98.189.115,20.98.189.122,20.98.189.136,20.98.189.237,20.98.189.240,20.98.190.46,20.98.190.149,20.98.190.253,20.98.191.177,20.118.48.4",
  "redundancyMode": "None",
  "repositorySiteName": "MySampleApp082022",
  "reserved": true,
  "resourceGroup": "AzFuncContainersRG",
  "scmSiteAlsoStopped": false,
  "serverFarmId": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/AzFuncContainersRG/providers/Microsoft.Web/serverfarms/myPremiumPlan",
  "siteConfig": {
    "acrUseManagedIdentityCreds": false,
    "acrUserManagedIdentityId": null,
    "alwaysOn": false,
    "antivirusScanEnabled": null,
    "apiDefinition": null,
    "apiManagementConfig": null,
    "appCommandLine": null,
    "appSettings": null,
    "autoHealEnabled": null,
    "autoHealRules": null,
    "autoSwapSlotName": null,
    "azureMonitorLogCategories": null,
    "azureStorageAccounts": null,
    "connectionStrings": null,
    "cors": null,
    "customAppPoolIdentityAdminState": null,
    "customAppPoolIdentityTenantState": null,
    "defaultDocuments": null,
    "detailedErrorLoggingEnabled": null,
    "documentRoot": null,
    "elasticWebAppScaleLimit": 0,
    "experiments": null,
    "fileChangeAuditEnabled": null,
    "ftpsState": null,
    "functionAppScaleLimit": null,
    "functionsRuntimeScaleMonitoringEnabled": null,
    "handlerMappings": null,
    "healthCheckPath": null,
    "http20Enabled": false,
    "http20ProxyFlag": null,
    "httpLoggingEnabled": null,
    "ipSecurityRestrictions": [
      {
        "action": "Allow",
        "description": "Allow all access",
        "headers": null,
        "ipAddress": "Any",
        "name": "Allow all",
        "priority": 2147483647,
        "subnetMask": null,
        "subnetTrafficTag": null,
        "tag": null,
        "vnetSubnetResourceId": null,
        "vnetTrafficTag": null
      }
    ],
    "ipSecurityRestrictionsDefaultAction": null,
    "javaContainer": null,
    "javaContainerVersion": null,
    "javaVersion": null,
    "keyVaultReferenceIdentity": null,
    "limits": null,
    "linuxFxVersion": "",
    "loadBalancing": null,
    "localMySqlEnabled": null,
    "logsDirectorySizeLimit": null,
    "machineKey": null,
    "managedPipelineMode": null,
    "managedServiceIdentityId": null,
    "metadata": null,
    "minTlsCipherSuite": null,
    "minTlsVersion": null,
    "minimumElasticInstanceCount": 0,
    "netFrameworkVersion": null,
    "nodeVersion": null,
    "numberOfWorkers": 1,
    "phpVersion": null,
    "powerShellVersion": null,
    "preWarmedInstanceCount": null,
    "publicNetworkAccess": null,
    "publishingPassword": null,
    "publishingUsername": null,
    "push": null,
    "pythonVersion": null,
    "remoteDebuggingEnabled": null,
    "remoteDebuggingVersion": null,
    "requestTracingEnabled": null,
    "requestTracingExpirationTime": null,
    "routingRules": null,
    "runtimeADUser": null,
    "runtimeADUserPassword": null,
    "scmIpSecurityRestrictions": [
      {
        "action": "Allow",
        "description": "Allow all access",
        "headers": null,
        "ipAddress": "Any",
        "name": "Allow all",
        "priority": 2147483647,
        "subnetMask": null,
        "subnetTrafficTag": null,
        "tag": null,
        "vnetSubnetResourceId": null,
        "vnetTrafficTag": null
      }
    ],
    "scmIpSecurityRestrictionsDefaultAction": null,
    "scmIpSecurityRestrictionsUseMain": null,
    "scmMinTlsVersion": null,
    "scmType": null,
    "sitePort": null,
    "storageType": null,
    "supportedTlsCipherSuites": null,
    "tracingOptions": null,
    "use32BitWorkerProcess": null,
    "virtualApplications": null,
    "vnetName": null,
    "vnetPrivatePortsCount": null,
    "vnetRouteAllEnabled": null,
    "webSocketsEnabled": null,
    "websiteTimeZone": null,
    "winAuthAdminState": null,
    "winAuthTenantState": null,
    "windowsFxVersion": null,
    "xManagedServiceIdentityId": null
  },
  "slotSwapStatus": null,
  "state": "Running",
  "suspendedTill": null,
  "systemData": null,
  "tags": null,
  "targetSwapSlot": null,
  "trafficManagerHostNames": null,
  "type": "Microsoft.Web/sites/slots",
  "usageState": "Normal"
}

I see it listed now

$ az functionapp deployment slot list --name MySampleApp082022 -g AzFuncContainersRG -o table
Name     Status    Plan
-------  --------  -------------
PreProd  Running   myPremiumPlan

We can also check the URL and see “PreProd” is now running

/content/images/2022/09/azfuncazdo-20.png

Using Deployment Slots with a Testing Stage

Let’s say we want to do some kind of test first in a stage.

Then let’s say that, for now, we are gating on actual “Production Approval”

First, we create a Production “Environment” in our Pipelines area that we will use for the approval gate

/content/images/2022/09/azfuncazdo-21.png

and give it a name

/content/images/2022/09/azfuncazdo-22.png

Next, I go to this Environment and use the 3-Dot menu to pick “Approvals and checks”

/content/images/2022/09/azfuncazdo-23.png

then we choose Approvals. We could also limit to a branch or business hours.

/content/images/2022/09/azfuncazdo-24.png

And we can give it Team details (a user or an AzDO Team), Instructions and even a timeout

/content/images/2022/09/azfuncazdo-25.png

We can now see our new Gate on “Production”

/content/images/2022/09/azfuncazdo-26.png

Now we just use that as an “environment” in our Azure DevOps YAML to trigger the gate.

$ cat azure-pipelines.yml
# Python Function App to Linux on Azure
# Build a Python function app and deploy it to Azure as a Linux function app.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/python

trigger:
- main

variables:
  # Azure Resource Manager connection created during pipeline creation
  azureSubscription: '766a220a-2481-4b60-9612-7e761be6f2ab'

  # Function app name
  functionAppName: 'MySampleApp082022'

  # Agent VM image name
  vmImageName: 'ubuntu-latest'

  # Working Directory
  workingDirectory: '$(System.DefaultWorkingDirectory)/'

stages:
- stage: Build
  displayName: Build stage

  jobs:
  - job: Build
    displayName: Build
    pool:
      vmImage: $(vmImageName)

    steps:
    - bash: |
        if [ -f extensions.csproj ]
        then
            dotnet build extensions.csproj --runtime ubuntu.16.04-x64 --output ./bin
        fi
      workingDirectory: $(workingDirectory)
      displayName: 'Build extensions'

    - task: UsePythonVersion@0
      displayName: 'Use Python 3.6'
      inputs:
        versionSpec: 3.6 # Functions V2 supports Python 3.6 as of today

    - bash: |
        pip install --target="./.python_packages/lib/site-packages" -r ./requirements.txt
      workingDirectory: $(workingDirectory)
      displayName: 'Install application dependencies'

    - task: ArchiveFiles@2
      displayName: 'Archive files'
      inputs:
        rootFolderOrFile: '$(workingDirectory)'
        includeRootFolder: false
        archiveType: zip
        archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
        replaceExistingArchive: true

    - publish: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
      artifact: drop

    - task: Docker@2
      displayName: 'Build and Push to Harbor CR'
      inputs:
        containerRegistry: 'FBHarbor'
        repository: 'freshbrewedprivate/azfunc01'
        command: 'buildAndPush'
        Dockerfile: '**/Dockerfile'

- stage: Deploy
  displayName: Deploy to PreProd
  dependsOn: Build
  condition: succeeded()

  jobs:
  - deployment: Deploy
    displayName: Deploy
    environment: 'development'
    pool:
      vmImage: $(vmImageName)

    strategy:
      runOnce:
        deploy:

          steps:
            - task: AzureFunctionAppContainer@1
              displayName: 'Update to new Harbor Image'
              inputs:
                azureSubscription: 'Pay-As-You-Go (d955c0ba-13dc-44cf-a29a-8fed74cbb22d)'
                appName: 'MySampleApp082022'
                deployToSlotOrASE: true
                resourceGroupName: 'AzFuncContainersRG'
                slotName: 'PreProd'
                imageName: 'harbor.freshbrewed.science/freshbrewedprivate/azfunc01:$(Build.BuildId)'

- stage: DeployPreProd
  displayName: Deploy to Production
  dependsOn: Deploy
  condition: succeeded()

  jobs:
  - deployment: Deploy
    displayName: Deploy
    environment: 'Production'
    pool:
      vmImage: $(vmImageName)

    strategy:
      runOnce:
        deploy:

          steps:
            - task: AzureFunctionAppContainer@1
              displayName: 'Update to new Harbor Image'
              inputs:
                azureSubscription: 'Pay-As-You-Go (d955c0ba-13dc-44cf-a29a-8fed74cbb22d)'
                appName: 'MySampleApp082022'
                deployToSlotOrASE: true
                resourceGroupName: 'AzFuncContainersRG'
                slotName: 'production'
                imageName: 'harbor.freshbrewed.science/freshbrewedprivate/azfunc01:$(Build.BuildId)'

We can now see the build has 3 stages

/content/images/2022/09/azfuncazdo-27.png

We can now see it’s deployed to PreProd, but Prod is waiting on approval

/content/images/2022/09/azfuncazdo-28.png

I can see the email

/content/images/2022/09/azfuncazdo-29.png

and click Review to review and approve

/content/images/2022/09/azfuncazdo-30.png

I can also see who approved it (such as when a Team is an approver and I want to know who clicked approve and when)

/content/images/2022/09/azfuncazdo-31.png

I should note that I can use Harbor to check the images. Note: the tags align with our AzDO build ID

/content/images/2022/09/azfuncazdo-32.png

From there I can do a security scan

/content/images/2022/09/azfuncazdo-33.png

Which returns the number of Vulnerabilities and their relative level

/content/images/2022/09/azfuncazdo-34.png

mouse over for details

/content/images/2022/09/azfuncazdo-35.png

We can then pick the image tag to see the list in the “Additions” section

/content/images/2022/09/azfuncazdo-36.png

Some CVEs just list affected versions

/content/images/2022/09/azfuncazdo-37.png

Others list Potential Mitigations

/content/images/2022/09/azfuncazdo-38.png

Mitigating Security Vulnerabilities in the scan

Let’s assume we like security (because we do!).

We can first double check that our image is deb/apt based with a quick interactive run

$ docker run -it harbor.freshbrewed.science/freshbrewedprivate/azfunc01:v0.0.1 /bin/bash
root@d83246ace4c5:/# apt update
Hit:1 http://deb.debian.org/debian bullseye InRelease
Get:2 http://deb.debian.org/debian-security bullseye-security InRelease [48.4 kB]
Get:3 http://deb.debian.org/debian bullseye-updates InRelease [44.1 kB]
Get:4 http://deb.debian.org/debian-security bullseye-security/main amd64 Packages [180 kB]
Hit:5 http://security.debian.org/debian-security jessie/updates InRelease
Get:6 http://deb.debian.org/debian bullseye-updates/main amd64 Packages.diff/Index [9483 B]
Get:7 http://deb.debian.org/debian bullseye-updates/main amd64 Packages T-2022-08-18-2019.35-F-2022-08-18-2019.35.pdiff [284 B]
Get:7 http://deb.debian.org/debian bullseye-updates/main amd64 Packages T-2022-08-18-2019.35-F-2022-08-18-2019.35.pdiff [284 B]
Get:8 https://packages.microsoft.com/debian/9/prod stretch InRelease [4009 B]
Get:9 https://packages.microsoft.com/debian/9/prod stretch/main amd64 Packages [219 kB]
Fetched 505 kB in 11s (47.9 kB/s)
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
8 packages can be upgraded. Run 'apt list --upgradable' to see them.
root@d83246ace4c5:/# apt upgrade -y
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
Calculating upgrade... Done
The following packages will be upgraded:
  curl libcurl4 libgnutls30 libtirpc-common libtirpc-dev libtirpc3 linux-libc-dev tzdata
8 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
Need to get 4055 kB of archives.
After this operation, 20.5 kB of additional disk space will be used.
Get:1 http://deb.debian.org/debian-security bullseye-security/main amd64 libgnutls30 amd64 3.7.1-5+deb11u2 [1341 kB]
Get:2 http://deb.debian.org/debian-security bullseye-security/main amd64 libtirpc-common all 1.3.1-1+deb11u1 [13.5 kB]
Get:3 http://deb.debian.org/debian-security bullseye-security/main amd64 libtirpc-dev amd64 1.3.1-1+deb11u1 [191 kB]
Get:4 http://deb.debian.org/debian-security bullseye-security/main amd64 libtirpc3 amd64 1.3.1-1+deb11u1 [84.1 kB]
Get:5 http://deb.debian.org/debian bullseye-updates/main amd64 tzdata all 2021a-1+deb11u5 [284 kB]
Get:6 http://deb.debian.org/debian-security bullseye-security/main amd64 curl amd64 7.74.0-1.3+deb11u2 [270 kB]
Get:7 http://deb.debian.org/debian-security bullseye-security/main amd64 libcurl4 amd64 7.74.0-1.3+deb11u2 [345 kB]
Get:8 http://deb.debian.org/debian-security bullseye-security/main amd64 linux-libc-dev amd64 5.10.136-1 [1528 kB]
Fetched 4055 kB in 0s (16.5 MB/s)
debconf: delaying package configuration, since apt-utils is not installed
(Reading database ... 14266 files and directories currently installed.)
Preparing to unpack .../libgnutls30_3.7.1-5+deb11u2_amd64.deb ...
Unpacking libgnutls30:amd64 (3.7.1-5+deb11u2) over (3.7.1-5+deb11u1) ...
Setting up libgnutls30:amd64 (3.7.1-5+deb11u2) ...
(Reading database ... 14266 files and directories currently installed.)
Preparing to unpack .../libtirpc-common_1.3.1-1+deb11u1_all.deb ...
Unpacking libtirpc-common (1.3.1-1+deb11u1) over (1.3.1-1) ...
Setting up libtirpc-common (1.3.1-1+deb11u1) ...
(Reading database ... 14266 files and directories currently installed.)
Preparing to unpack .../libtirpc-dev_1.3.1-1+deb11u1_amd64.deb ...
Unpacking libtirpc-dev:amd64 (1.3.1-1+deb11u1) over (1.3.1-1) ...
Preparing to unpack .../libtirpc3_1.3.1-1+deb11u1_amd64.deb ...
Unpacking libtirpc3:amd64 (1.3.1-1+deb11u1) over (1.3.1-1) ...
Setting up libtirpc3:amd64 (1.3.1-1+deb11u1) ...
(Reading database ... 14266 files and directories currently installed.)
Preparing to unpack .../tzdata_2021a-1+deb11u5_all.deb ...
Unpacking tzdata (2021a-1+deb11u5) over (2021a-1+deb11u4) ...
Preparing to unpack .../curl_7.74.0-1.3+deb11u2_amd64.deb ...
Unpacking curl (7.74.0-1.3+deb11u2) over (7.74.0-1.3+deb11u1) ...
Preparing to unpack .../libcurl4_7.74.0-1.3+deb11u2_amd64.deb ...
Unpacking libcurl4:amd64 (7.74.0-1.3+deb11u2) over (7.74.0-1.3+deb11u1) ...
Preparing to unpack .../linux-libc-dev_5.10.136-1_amd64.deb ...
Unpacking linux-libc-dev:amd64 (5.10.136-1) over (5.10.127-2) ...
Setting up linux-libc-dev:amd64 (5.10.136-1) ...
Setting up tzdata (2021a-1+deb11u5) ...

Current default time zone: 'Etc/UTC'
Local time is now:      Wed Aug 24 19:49:25 UTC 2022.
Universal Time is now:  Wed Aug 24 19:49:25 UTC 2022.
Run 'dpkg-reconfigure tzdata' if you wish to change it.

Setting up libtirpc-dev:amd64 (1.3.1-1+deb11u1) ...
Setting up libcurl4:amd64 (7.74.0-1.3+deb11u2) ...
Setting up curl (7.74.0-1.3+deb11u2) ...
Processing triggers for libc-bin (2.31-13+deb11u3) ...

So let’s add that to our Dockerfile.

(.venv) builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$ git diff Dockerfile
diff --git a/Dockerfile b/Dockerfile
index c9d066f..7294c07 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,4 +8,7 @@ ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
 COPY requirements.txt /
 RUN pip install -r /requirements.txt

-COPY . /home/site/wwwroot
\ No newline at end of file
+RUN apt update
+RUN apt upgrade -y
+
+COPY . /home/site/wwwroot

I’ll build and test locally

(.venv) builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$ docker build --tag harbor.freshbrewed.science/freshbrewedprivate/azfunc01:v0.0.2 .
[+] Building 18.2s (11/11) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                                                                                                              0.0s
 => => transferring dockerfile: 496B                                                                                                                                                                                                                              0.0s
 => [internal] load .dockerignore                                                                                                                                                                                                                                 0.0s
 => => transferring context: 34B                                                                                                                                                                                                                                  0.0s
 => [internal] load metadata for mcr.microsoft.com/azure-functions/python:4-python3.8                                                                                                                                                                             0.2s
 => [1/6] FROM mcr.microsoft.com/azure-functions/python:4-python3.8@sha256:35cbd79e52907195c94a9571f36861cb3351672b55cf047ccfc9f630af5838a9                                                                                                                       0.0s
 => [internal] load build context                                                                                                                                                                                                                                 0.1s
 => => transferring context: 204.36kB                                                                                                                                                                                                                             0.1s
 => CACHED [2/6] COPY requirements.txt /                                                                                                                                                                                                                          0.0s
 => CACHED [3/6] RUN pip install -r /requirements.txt                                                                                                                                                                                                             0.0s
 => [4/6] RUN apt update                                                                                                                                                                                                                                         11.5s
 => [5/6] RUN apt upgrade -y                                                                                                                                                                                                                                      5.8s
 => [6/6] COPY . /home/site/wwwroot                                                                                                                                                                                                                               0.3s
 => exporting to image                                                                                                                                                                                                                                            0.2s
 => => exporting layers                                                                                                                                                                                                                                           0.2s
 => => writing image sha256:444efcd26efa948f0b211965baa39c56c958b0e745775358c833ab8f7c590b8e                                                                                                                                                                      0.0s
 => => naming to harbor.freshbrewed.science/freshbrewedprivate/azfunc01:v0.0.2

/content/images/2022/09/azfuncazdo-39.png

Now I’ll add to source and push

(.venv) builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$ git add Dockerfile
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$ git commit -m "Auto-update OS to fix some CVEs"
[main 914aaab] Auto-update OS to fix some CVEs
 1 file changed, 4 insertions(+), 1 deletion(-)
(.venv) builder@DESKTOP-QADGF36:~/Workspaces/azfunctest$ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 16 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 363 bytes | 363.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Analyzing objects... (3/3) (44 ms)
remote: Storing packfile... done (58 ms)
remote: Storing index... done (49 ms)
To https://princessking.visualstudio.com/IAC-Demo/_git/AzFuncAppCICD
   6f4ad1f..914aaab  main -> main

The pipeline kicks in

/content/images/2022/09/azfuncazdo-40.png

After the build stage, I hopped over to Harbor and queued a scan

/content/images/2022/09/azfuncazdo-41.png

and I see we’ve reduced 46 criticals just by updating the OS in the image

/content/images/2022/09/azfuncazdo-42.png

Branch checks

Let’s add one more minor feature.

I really only want to consider going to prod if we are building on the mainline. Thus far, all my images have been built on “main”. But I could see users wanting to build and test on a “develop” or personal branch.

I’ll add an “isMain” and also trigger builds off of some other common branch paths. We can see the trigger updated, a new variable and lastly a condition added to the Prod stage

# Python Function App to Linux on Azure
# Build a Python function app and deploy it to Azure as a Linux function app.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/python

trigger:
- main
- users/*
- features/*
- hotfixes/*

variables:
  # Azure Resource Manager connection created during pipeline creation
  azureSubscription: '766a220a-2481-4b60-9612-7e761be6f2ab'

  # Function app name
  functionAppName: 'MySampleApp082022'

  # Agent VM image name
  vmImageName: 'ubuntu-latest'

  # Working Directory
  workingDirectory: '$(System.DefaultWorkingDirectory)/'
  
  isMain: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')]
  
stages:
- stage: Build
  displayName: Build stage

  jobs:
  - job: Build
    displayName: Build
    pool:
      vmImage: $(vmImageName)

    steps:
    - bash: |
        if [ -f extensions.csproj ]
        then
            dotnet build extensions.csproj --runtime ubuntu.16.04-x64 --output ./bin
        fi
      workingDirectory: $(workingDirectory)
      displayName: 'Build extensions'

    - task: UsePythonVersion@0
      displayName: 'Use Python 3.6'
      inputs:
        versionSpec: 3.6 # Functions V2 supports Python 3.6 as of today

    - bash: |
        pip install --target="./.python_packages/lib/site-packages" -r ./requirements.txt
      workingDirectory: $(workingDirectory)
      displayName: 'Install application dependencies'

    - task: ArchiveFiles@2
      displayName: 'Archive files'
      inputs:
        rootFolderOrFile: '$(workingDirectory)'
        includeRootFolder: false
        archiveType: zip
        archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
        replaceExistingArchive: true

    - publish: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
      artifact: drop

    - task: Docker@2
      displayName: 'Build and Push to Harbor CR'
      inputs:
        containerRegistry: 'FBHarbor'
        repository: 'freshbrewedprivate/azfunc01'
        command: 'buildAndPush'
        Dockerfile: '**/Dockerfile'

- stage: Deploy
  displayName: Deploy to PreProd
  dependsOn: Build
  condition: succeeded()

  jobs:
  - deployment: Deploy
    displayName: Deploy
    environment: 'development'
    pool:
      vmImage: $(vmImageName)

    strategy:
      runOnce:
        deploy:

          steps:
            - task: AzureFunctionAppContainer@1
              displayName: 'Update to new Harbor Image'
              inputs:
                azureSubscription: 'Pay-As-You-Go (d955c0ba-13dc-44cf-a29a-8fed74cbb22d)'
                appName: 'MySampleApp082022'
                deployToSlotOrASE: true
                resourceGroupName: 'AzFuncContainersRG'
                slotName: 'PreProd'
                imageName: 'harbor.freshbrewed.science/freshbrewedprivate/azfunc01:$(Build.BuildId)'          

- stage: DeployProd
  displayName: Deploy to Production
  dependsOn: Deploy
  condition: and(succeeded(), eq(variables.isMain, 'true'))

  jobs:
  - deployment: Deploy
    displayName: Deploy
    environment: 'Production'
    pool:
      vmImage: $(vmImageName)

    strategy:
      runOnce:
        deploy:

          steps:
            - task: AzureFunctionAppContainer@1
              displayName: 'Update to new Harbor Image'
              inputs:
                azureSubscription: 'Pay-As-You-Go (d955c0ba-13dc-44cf-a29a-8fed74cbb22d)'
                appName: 'MySampleApp082022'
                deployToSlotOrASE: true
                resourceGroupName: 'AzFuncContainersRG'
                slotName: 'production'
                imageName: 'harbor.freshbrewed.science/freshbrewedprivate/azfunc01:$(Build.BuildId)'          

Pushing the change queues a build

/content/images/2022/09/azfuncazdo-43.png

As an aside, if we chose to “Reject” a build

/content/images/2022/09/azfuncazdo-44.png

It will show as a rejection with a note of when and whom rejected it

/content/images/2022/09/azfuncazdo-45.png

Testing User Branch Builds

Let’s create a user branch in users to see how we can invoke a build (prior to PR) so i can test with PreProd

/content/images/2022/09/azfuncazdo-46.png

This will build because we have an active trigger on “users/*”

/content/images/2022/09/azfuncazdo-47.png

I can see my “I am Groot” change in “PreProd”

/content/images/2022/09/azfuncazdo-48.png

But as you see above, it did not attempt to trigger to Prod. For that, I would need to get code to main.

Pull Requests and Branch Policies

Going over Branch Policies in Azure Repos is a bit out of scope here, but arguably, you will want to create a Policy on at least main that would require a minimum number of reviewers or at least a Build Validation (a PR build)

/content/images/2022/09/azfuncazdo-49.png

I covered this some time ago in VSTS Security and Policies (part 1) and part 2.

Metrics

Because we are using all the power of Azure, we can use the ALM collected metrics to see trends like usage (Requests over time)

requests
| summarize totalCount=sum(itemCount) by bin(timestamp, 30m)
| render timechart

/content/images/2022/09/azfuncazdo-51.png

Perhaps we care about Failed Operations

requests
| where success == false
| summarize failedCount=sum(itemCount), impactedUsers=dcount(user_Id) by operation_Name
| order by failedCount desc

/content/images/2022/09/azfuncazdo-52.png

I’ll click “+ New alert rule”

There I could detail the limit (more than 1 in 5m)

/content/images/2022/09/azfuncazdo-53.png

Then I’ll leverage an existing action group. I can create that

/content/images/2022/09/azfuncazdo-55.png

then set the Roles to alert with an email

/content/images/2022/09/azfuncazdo-56.png

I could also add some optional actions. The Webhook is a good general purpose option

/content/images/2022/09/azfuncazdo-57.png

We can run a test to see how the alerts look

/content/images/2022/09/azfuncazdo-58.png

I can add custom emails by using the “Email/SMS message” step

/content/images/2022/09/azfuncazdo-64.png

and see it was added

/content/images/2022/09/azfuncazdo-65.png

When tested

/content/images/2022/09/azfuncazdo-66.png

which appears as such

/content/images/2022/09/azfuncazdo-67.png

If we go back to our alert rule flow, after picking the action group, we pick details and severity

/content/images/2022/09/azfuncazdo-68.png

Here is where I will pause. If we create the alert, it will cost us $1.50.

/content/images/2022/09/azfuncazdo-69.png

A more scalable approach is to monitor all our functions into an Azure App Insights space

/content/images/2022/09/azfuncazdo-70.png

And use --app-insights and --app-insights-key when creating our Azure Function to send it to a single Log Analytics Workspace.

We can look at App Insights to get a handle on usage

/content/images/2022/09/azfuncazdo-71.png

An application map

/content/images/2022/09/azfuncazdo-76.png

We can see some rather nice live Operational Dashboards from there

/content/images/2022/09/azfuncazdo-72.png

Including a stream that includes requests and failures

/content/images/2022/09/azfuncazdo-73.png

as well as Performance specifics

/content/images/2022/09/azfuncazdo-74.png

and a dashboard we can share and export

/content/images/2022/09/azfuncazdo-75.png

Note on cost

This can get expensive up over time. If you want to avoid high costs on your monitoring, change your data retention in Configure/”Usage and estimated costs”

/content/images/2022/09/azfuncazdo-77.png

For instance, i can reduce our retention from the default 90 days down to 30 days as we are only using this for alerting and high level metrics

/content/images/2022/09/azfuncazdo-78.png

Cleanup

The Azure Repos and Pipelines do not cost us money, but

$ az group list -o table | grep AzFuncContainersRG
AzFuncContainersRG          centralus       Succeeded

I can see cost analysis once some time has passed. This is because I bundled everything into a single RG. Initially we won’t see values

/content/images/2022/09/azfuncazdo-50.png

But after a day or two, we can see some results. As we are using a App Plan, we can see that Premium plan (to give us multiple slots) will add up over time

/content/images/2022/09/azfuncazdo-79.png

Most of the costs being the Plan and not really the functions or App Insights

/content/images/2022/09/azfuncazdo-80.png

Based on at least $1.72/day for the plan and $0.10 on storage, I’m likely looking at at least US$57 for this for the month.

/content/images/2022/09/azfuncazdo-81.png

However, this is far cheaper than running a full kubernetes stack for just a few containers and we have the advantage of App Insights and Deployment slots. For a business, this is a fair deal. However, for me, I’ll cleanup to avoid further costs.

Because I bundled everything into a Resource Group, the scrub up is clean

/content/images/2022/09/azfuncazdo-82.png

$ az group delete -g AzFuncContainersRG
Are you sure you want to perform this operation? (y/n): y

/content/images/2022/09/azfuncazdo-83.png

Summary

We showed how to create a Python based Azure Function with an HTTP trigger. We then built and tested locally, including using a container. Leveraging a private Container Registry (Harbor), we pushed the image then created a new App Service. The App Service required a Resource Group, Storage Account, and App Service Plan.

We then improved the model by adding a CICD Pipeline in Azure DevOps after storing our code into Azure Repos. We first pushed to our primary deployment slot (“production”) before moving on to create a second PreProd slot. We added Checks and Approvals using an Azure Pipelines Environment and lastly discussed private branches and Azure Repos Policy Checks.

We also explore App Insights and the various items monitored as well as ways to manage costs. As a Kubernetes cluster generally will run at least US$300 for a minimal production setup, the containerized function model is still cheaper than self-hosting in Kubernetes for up to 6 Applications. Once we get beyond that, from a cost perspective, transitioning to Kubernetes would provide a better value (on cost).

A quick note on Kubernetes costs

There are ways to reduce Kuberenetes spend however. Assuming we used DSv2 Series to back a three node cluster in West US, we would see that might run $306.60 for the compute layer

/content/images/2022/09/azfuncazdo-84.png

Howver, if you know you need this size for at least a year, you can pay up front ($2240.90) and get the same cluster for $186.74/mo.

/content/images/2022/09/azfuncazdo-85.png

Using Reserved Instances is a great way to reduce cloud spend (and AWS has similar reserved instances or lowering costs with spot instances)

azure functions devops AzureDevOps AppInsights

Have something to add? Feedback? Try our new forums

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes