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'.
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
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": [
...
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
Then hit the API endpoint
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
We can click “Setup Build” to start the Azure Pipeline wizard and choose the Python Func App template
and after selecting my subscription, I could pick the App and Working directory
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:
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
and put in our details
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
And we can see it pushed to the Azure Function
While I do not see logs because we haven’t enabled logging on this container, we can see our container image via the logs
/ 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
Which triggers a build
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
I can see the container image was updated by looking in the Log
Of course, the proof is in seeing it running with the change
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
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
and give it a name
Next, I go to this Environment and use the 3-Dot menu to pick “Approvals and checks”
then we choose Approvals. We could also limit to a branch or business hours.
And we can give it Team details (a user or an AzDO Team), Instructions and even a timeout
We can now see our new Gate on “Production”
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
We can now see it’s deployed to PreProd, but Prod is waiting on approval
I can see the email
and click Review to review and approve
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)
I should note that I can use Harbor to check the images. Note: the tags align with our AzDO build ID
From there I can do a security scan
Which returns the number of Vulnerabilities and their relative level
mouse over for details
We can then pick the image tag to see the list in the “Additions” section
Some CVEs just list affected versions
Others list Potential Mitigations
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
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
After the build stage, I hopped over to Harbor and queued a scan
and I see we’ve reduced 46 criticals just by updating the OS in the image
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
As an aside, if we chose to “Reject” a build
It will show as a rejection with a note of when and whom rejected it
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
This will build because we have an active trigger on “users/*”
I can see my “I am Groot” change in “PreProd”
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)
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
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
I’ll click “+ New alert rule”
There I could detail the limit (more than 1 in 5m)
Then I’ll leverage an existing action group. I can create that
then set the Roles to alert with an email
I could also add some optional actions. The Webhook is a good general purpose option
We can run a test to see how the alerts look
I can add custom emails by using the “Email/SMS message” step
and see it was added
When tested
which appears as such
If we go back to our alert rule flow, after picking the action group, we pick details and severity
Here is where I will pause. If we create the alert, it will cost us $1.50.
A more scalable approach is to monitor all our functions into an Azure App Insights space
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
An application map
We can see some rather nice live Operational Dashboards from there
Including a stream that includes requests and failures
as well as Performance specifics
and a dashboard we can share and export
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”
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
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
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
Most of the costs being the Plan and not really the functions or App Insights
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.
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
$ az group delete -g AzFuncContainersRG
Are you sure you want to perform this operation? (y/n): y
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
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.
Using Reserved Instances is a great way to reduce cloud spend (and AWS has similar reserved instances or lowering costs with spot instances)