Published: Oct 6, 2022 by Isaac Johnson
So far we have setup a Python based Cloud Run function and added Unit Tests and a CICD Flow in Github. Our next step will show how we can use this containerized function in other providers such as Kubernetes and Azure Functions.
We’ll first show how to copy the built container into a private Harbor CR. Then, we’ll launch it into an on-prem K3s cluster and use port-forward to test. We’ll add Ingress with TLS and “secure” it with a private endpoint URL.
Lastly, we’ll walk through how to use the same container image for a linux-based Azure App Service Plan function app. We’ll wrap by exploring features of an Azure App Service (such as logs and metrics) and securing it with an IdP and OAuth flow.
Fetching and copying the container from GCP
login to gcloud and set the project id
$ gcloud auth login --no-launch-browser
Go to the following link in your browser:
https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=32555940559.apps.googleusercontent.com&redirect_uri=https%3A%2F%2Fsdk.cloud.google.com%2Fauthcode.html&scope=openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fappengine.admin+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fsqlservice.login+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcompute+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Faccounts.reauth&state=Eb3aThv0XbaR3q5rrUv2NLAzG531Js&prompt=consent&access_type=offline&code_challenge=W4DnsazV6C3zfFInkNseLLqBTcPVPp80uOZ9T2TvCrk&code_challenge_method=S256
Enter authorization code: 4/0ARtbsJry_1pCrLNJIRjUdnmz2Li31LESOJfmUXKReK9223fweJEDBcq4mdxBNCTvGcMEgQ
You are now logged in as [isaac.johnson@gmail.com].
Your current project is [myanthosproject2]. You can change this setting by running:
$ gcloud config set project PROJECT_ID
$ gcloud config set project myanthosproject2
Updated property [core/project].
I can now see the private repository I created for the python container that was used in Cloud Run
$ gcloud container images list
NAME
gcr.io/myanthosproject2/pythonfunction
Only listing images in gcr.io/myanthosproject2. Use --repository to list images in other repositories.
I then want to get the tags so we can pull the image
$ gcloud container images list-tags gcr.io/myanthosproject2/pythonfunction
DIGEST TAGS TIMESTAMP
04ce84892758 3123889903,latest 2022-09-25T17:22:47
e922947f7f73 3123855199 2022-09-25T17:11:19
39339292671d 3123705581 2022-09-25T16:19:41
ab345118d2cf 3120705034 2022-09-24T22:23:00
0fed933a89bc 3120492894 2022-09-24T20:40:52
9fae5133da44 3120464723 2022-09-24T20:28:35
89dbcb33f2f5 3120375478 2022-09-24T19:49:12
3f33a8798bad 3120328407 2022-09-24T19:31:48
396c2a5c2106 3120262228 2022-09-24T19:11:05
7e35d34b68fc 3120004317 2022-09-24T17:33:05
ec5afbec78d7 3119981975 2022-09-24T17:23:52
2ab22fd9487a 3102378241 2022-09-21T21:07:28
4d45c26cb4d8 3097404036 2022-09-21T06:25:25
3f55a4a8c394 3094842334 2022-09-20T21:44:49
9b7e1ac9bed4 3094553799 2022-09-20T20:18:45
16f41bd58fef 3093601599 2022-09-20T16:34:41
8cbe0f278f0a 3093537776 2022-09-20T16:22:00
0ca813da3f4e 3090046439 2022-09-20T07:25:41
25ae6cd8d542 3090008324 2022-09-20T07:13:27
6e636503301b 3089854983 2022-09-20T07:09:37
The docker pull command kept crashing
$ gcloud docker -- pull gcr.io/myanthosproject2/pythonfunction
WARNING: `gcloud docker` will not be supported for Docker client versions above 18.03.
As an alternative, use `gcloud auth configure-docker` to configure `docker` to
use `gcloud` as a credential helper, then use `docker` as you would for non-GCR
registries, e.g. `docker pull gcr.io/project-id/my-image`. Add
`--verbosity=error` to silence this warning: `gcloud docker
--verbosity=error -- pull gcr.io/project-id/my-image`.
See: https://cloud.google.com/container-registry/docs/support/deprecation-notices#gcloud-docker
ERROR: gcloud crashed (TypeError): a bytes-like object is required, not 'str'
If you would like to report this issue, please run the following command:
gcloud feedback
To check gcloud for common problems, please run the following command:
gcloud info --run-diagnostics
However, using the credential helper worked
$ gcloud auth configure-docker
Adding credentials for all GCR repositories.
WARNING: A long list of credential helpers may cause delays running 'docker build'. We recommend passing the registry name to configure only the registry you are using.
After update, the following will be written to your Docker config file located at [/home/builder/.docker/config.json]:
{
"credHelpers": {
"gcr.io": "gcloud",
"us.gcr.io": "gcloud",
"eu.gcr.io": "gcloud",
"asia.gcr.io": "gcloud",
"staging-k8s.gcr.io": "gcloud",
"marketplace.gcr.io": "gcloud"
}
}
Do you want to continue (Y/n)? y
Docker configuration file updated.
$ docker pull gcr.io/myanthosproject2/pythonfunction
Using default tag: latest
latest: Pulling from myanthosproject2/pythonfunction
31b3f1ad4ce1: Already exists
f335cc1597f2: Already exists
501b4d0d8bea: Already exists
abd735557fdf: Already exists
9358bdbbffdc: Already exists
0c3b8bec9940: Pull complete
9d60ad65a66a: Pull complete
f9e7527f6326: Pull complete
Digest: sha256:04ce848927589424cc8f972298745f8e11f94e3299ab2e31f8d9ccd78e4f0f41
Status: Downloaded newer image for gcr.io/myanthosproject2/pythonfunction:latest
gcr.io/myanthosproject2/pythonfunction:latest
I can now see it locally
$ docker images | head -3
REPOSITORY TAG IMAGE ID CREATED SIZE
gcr.io/myanthosproject2/pythonfunction latest 2c3fa45965c6 8 days ago 152MB
test latest d9611b409f42 13 days ago 147MB
My next step is to push it to my private CR.
I’ll tag it then push to Harbor
$ docker tag gcr.io/myanthosproject2/pythonfunction:latest harbor.freshbrewed.science/freshbrewedprivate/pythonfunction:latest
$ docker push harbor.freshbrewed.science/freshbrewedprivate/pythonfunction:latest
The push refers to repository [harbor.freshbrewed.science/freshbrewedprivate/pythonfunction]
9283b229a025: Pushed
526119217a7a: Pushed
4e86cf43e3a8: Pushed
95557c4c07af: Pushed
873602908422: Pushed
e4abe883350c: Pushed
95a02847aa85: Pushed
b45078e74ec9: Pushed
latest: digest: sha256:04ce848927589424cc8f972298745f8e11f94e3299ab2e31f8d9ccd78e4f0f41 size: 1996
I should note that there are ways to generate a service account key for GCR that one could use with image pull secrets (see this SO post).
I now see it in Harbor
Now let’s create a Deployment YAML with a Service definition
$ cat python.function.deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
generation: 3
labels:
app: python-crfunction
name: python-crfunction
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: python-crfunction
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
annotations:
dapr.io/app-id: python-crfunction
dapr.io/app-port: "5001"
dapr.io/enabled: "true"
creationTimestamp: null
labels:
app: python-crfunction
app.kubernetes.io/name: python-crfunction
spec:
containers:
- image: harbor.freshbrewed.science/freshbrewedprivate/pythonfunction:latest
imagePullPolicy: Always
name: python-crfunction
env:
- name: PORT
value: "8080"
ports:
- containerPort: 8080
protocol: TCP
name: httpsvcend
resources: {}
dnsPolicy: ClusterFirst
imagePullSecrets:
- name: myharborreg
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/instance: python-crfunction
app.kubernetes.io/name: python-crfunction
name: python-crfunction
namespace: default
spec:
internalTrafficPolicy: Cluster
ports:
- name: http
port: 80
protocol: TCP
targetPort: httpsvcend
selector:
app.kubernetes.io/name: python-crfunction
type: ClusterIP
and apply
$ kubectl apply -f python.function.deployment.yaml
deployment.apps/python-crfunction created
service/python-crfunction created
While I likely need to remove or tweak the dapr configuration (as it expects a listener on 5001), I can port-forward to the service
$ kubectl port-forward svc/python-crfunction 8080:80
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Handling connection for 8080
Handling connection for 8080
and invoke the container
and I can see that triggered an email
Clearly, I don’t wish to port-forward to hit this endpoint
First, I create a JSON for Route53
$ cat r53-pyfunction.json
{
"Comment": "CREATE python-crfunction fb.s A record",
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "python-crfunction.freshbrewed.science",
"Type": "A",
"TTL": 300,
"ResourceRecords": [
{
"Value": "73.242.50.46"
}
]
}
}
]
}
Then apply it
$ aws route53 change-resource-record-sets --hosted-zone-id Z39E8QFU0F9PZP --change-batch file://r53-pyfunction.json
{
"ChangeInfo": {
"Id": "/change/C027964413831YBZWYF7R",
"Status": "PENDING",
"SubmittedAt": "2022-10-05T14:18:19.412Z",
"Comment": "CREATE python-crfunction fb.s A record"
}
}
Now I can apply an Ingress to use it
$ cat python-crfunction.ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
kubernetes.io/tls-acme: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.org/client-max-body-size: "0"
nginx.org/proxy-connect-timeout: "600"
nginx.org/proxy-read-timeout: "600"
name: python-crfunction
namespace: default
spec:
ingressClassName: nginx
rules:
- host: python-crfunction.freshbrewed.science
http:
paths:
- backend:
service:
name: python-crfunction
port:
number: 80
path: /
pathType: ImplementationSpecific
tls:
- hosts:
- python-crfunction.freshbrewed.science
secretName: python-crfunction-tls
I likely do not need those other annotations, such as max body size. I use the “0” body size as well as the longer timeouts for addressing large payloads to some of my other services (such as large sonar deliveries or harbor image pushes).
We can apply the ingress;
$ kubectl apply -f python-crfunction.ingress.yaml
ingress.networking.k8s.io/python-crfunction created
Then I can see it applied
$ kubectl get ingress python-crfunction
NAME CLASS HOSTS ADDRESS PORTS AGE
python-crfunction nginx python-crfunction.freshbrewed.science 192.168.1.214,192.168.1.38,192.168.1.57,192.168.1.77 80, 443 6m28s
One minor issue is nginx isnt going to route to a failing service. And the service is unhappy because the pod keeps crashing
$ kubectl get pods | tail -n 1
python-crfunction-54487b6bb8-nklmb 1/2 CrashLoopBackOff 562 (4m44s ago) 26h
$ kubectl get deployments python-crfunction
NAME READY UP-TO-DATE AVAILABLE AGE
python-crfunction 0/1 1 0 26h
I’ll update the app port in the dapr annotations
$ kubectl get deployments python-crfunction -o yaml > python-crfunction.dep.yaml
$ kubectl get deployments python-crfunction -o yaml > python-crfunction.dep.yaml.bak
$ vi python-crfunction.dep.yaml
$ diff -c python-crfunction.dep.yaml python-crfunction.dep.yaml.bak
*** python-crfunction.dep.yaml 2022-10-05 09:41:31.009573000 -0500
--- python-crfunction.dep.yaml.bak 2022-10-05 09:39:08.399273000 -0500
***************
*** 29,35 ****
metadata:
annotations:
dapr.io/app-id: python-crfunction
! dapr.io/app-port: "8080"
dapr.io/enabled: "true"
creationTimestamp: null
labels:
--- 29,35 ----
metadata:
annotations:
dapr.io/app-id: python-crfunction
! dapr.io/app-port: "5001"
dapr.io/enabled: "true"
creationTimestamp: null
labels:
$ kubectl apply -f python-crfunction.dep.yaml
deployment.apps/python-crfunction configured
This then fixed the dapr sidecar
$ kubectl get deployments python-crfunction
NAME READY UP-TO-DATE AVAILABLE AGE
python-crfunction 1/1 1 1 26h
$ kubectl get pods python-crfunction-7d44797b8b-hgsmn
NAME READY STATUS RESTARTS AGE
python-crfunction-7d44797b8b-hgsmn 2/2 Running 0 3m32s
However, this creates a different issue. I now have it emailing me every time the URL loads. And moreover, there is no authentication
There are two ways I can think to handle this.
The first is to use nginx basic auth:
$ sudo apt install apache2-utils
$ htpasswd -c auth2 builder
New password:
Re-type new password:
Adding password for user builder
$ kubectl create secret generic pyfunc-basic-auth --from-file=auth2
secret/pyfunc-basic-auth created
Then I patch it in (actually i just deleted it and readded)
builder@DESKTOP-72D2D9T:~/Workspaces/jekyll-blog$ kubectl get ingress python-crfunction -o yaml > python-crfunction.ingress.yaml
builder@DESKTOP-72D2D9T:~/Workspaces/jekyll-blog$ kubectl get ingress python-crfunction -o yaml > python-crfunction.ingress.yaml.bak
builder@DESKTOP-72D2D9T:~/Workspaces/jekyll-blog$ kubectl delete ingress python-crfunction
ingress.networking.k8s.io "python-crfunction" deleted
builder@DESKTOP-72D2D9T:~/Workspaces/jekyll-blog$ vi python-crfunction.ingress.yaml
builder@DESKTOP-72D2D9T:~/Workspaces/jekyll-blog$ diff python-crfunction.ingress.yaml python-crfunction.ingress.yaml.bak
6,9d5
<
< nginx.ingress.kubernetes.io/auth-realm: Authentication Required
< nginx.ingress.kubernetes.io/auth-secret: pyfunc-basic-auth
< nginx.ingress.kubernetes.io/auth-type: basic
builder@DESKTOP-72D2D9T:~/Workspaces/jekyll-blog$ kubectl apply -f python-crfunction.ingress.yaml
ingress.networking.k8s.io/python-crfunction created
Unfortunately, because I’ve setup https redirect, it will skip the basic auth steps so we will never actually use these annotatations.
Another approach would be to use a more complicated path. We could do this with the Ingress definition.
$ diff -c python-crfunction.ingress.yaml python-crfunction.ingress.yaml.
bak
*** python-crfunction.ingress.yaml 2022-10-05 17:54:01.242819000 -0500
--- python-crfunction.ingress.yaml.bak 2022-10-05 17:53:25.630078000 -0500
***************
*** 33,39 ****
name: python-crfunction
port:
number: 80
! path: /12345asdfg
pathType: ImplementationSpecific
tls:
- hosts:
--- 33,39 ----
name: python-crfunction
port:
number: 80
! path: /
pathType: ImplementationSpecific
tls:
- hosts:
$ kubectl apply -f python-crfunction.ingress.yaml
ingress.networking.k8s.io/python-crfunction created
Now the base URL gives us a 404
That didn’t work. I actually tried many permutations. Fundamentally, I need to update the code to allow other routes
I added another route to main.py
$ git diff
diff --git a/main.py b/main.py
index c44517a..11252e8 100644
--- a/main.py
+++ b/main.py
@@ -17,6 +17,18 @@ def hello_world():
)
return "Hello hello {}!".format(name)
+@app.route("/asdf1234")
+def hello_world2():
+ name = os.environ.get("NAME", "World")
+ apobj = apprise.Apprise()
+ apobj.add('sendgrid://SENDGRIDTOKENHERE:isaac@freshbrewed.science/isaac.johnson@gmail.com')
+ apobj.add('msteams://MSTEAMSTOKENHERE')
+ apobj.notify(
+ body='Notified by Cloud Run Function',
+ title='From Python Cloud Run',
+ )
+ return "Hello hello {}!".format(name)
+
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
which triggered a build and deploy
Once it was pushed, I did a local docker pull from gcr.io, tag, and push back to harbor. Then I rotated the pod to see it pull a fresh image
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 27s default-scheduler Successfully assigned default/python-crfunction-7d44797b8b-9m5gm to builder-hp-elitebook-850-g2
Normal Pulling 27s kubelet Pulling image "harbor.freshbrewed.science/freshbrewedprivate/pythonfunction:latest"
Normal Pulled 14s kubelet Successfully pulled image "harbor.freshbrewed.science/freshbrewedprivate/pythonfunction:latest" in 13.365231702s
Normal Created 14s kubelet Created container python-crfunction
Normal Started 14s kubelet Started container python-crfunction
Normal Pulled 14s kubelet Container image "docker.io/daprio/daprd:1.8.2" already present on machine
Normal Created 14s kubelet Created container daprd
Normal Started 14s kubelet Started container daprd
Now when i apply an fresh ingress yaml:
$ cat python-crfunction.ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/app-root: /
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/add-base-url : "true"
kubernetes.io/tls-acme: "true"
name: python-crfunction
namespace: default
spec:
ingressClassName: nginx
rules:
- host: python-crfunction.freshbrewed.science
http:
paths:
- backend:
service:
name: python-crfunction
port:
number: 80
path: /asdf1234
pathType: Prefix
- backend:
service:
name: python-crfunction
port:
number: 80
path: /asdf1234/*
pathType: Prefix
tls:
- hosts:
- python-crfunction.freshbrewed.science
secretName: python-crfunction-tls
$ kubectl delete -f python-crfunction.ingress.yaml
ingress.networking.k8s.io "python-crfunction" deleted
$ kubectl apply -f python-crfunction.ingress.yaml
ingress.networking.k8s.io/python-crfunction created
Then I can hit the URL on the “secret” path
but not the short path
Azure Function
First, I’ll login to azure
$ az login --use-device-code
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code R57USFYFS to authenticate.
The following tenants don't contain accessible subscriptions. Use 'az login --allow-no-subscriptions' to have tenant level access.
...
Next, I’ll create a Resource Group to hold the storage account and function. Optionally, I could also create an ACR here as well
$ az group create --name pythonfunction-rg --location centralus
{
"id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/pythonfunction-rg",
"location": "centralus",
"managedBy": null,
"name": "pythonfunction-rg",
"properties": {
"provisioningState": "Succeeded"
},
"tags": null,
"type": "Microsoft.Resources/resourceGroups"
}
$ az storage account create --name ijfuncstorage1 --location centralus --resource-group pythonfunc
tion-rg --sku Standard_LRS
{
"accessTier": "Hot",
"allowBlobPublicAccess": true,
"allowCrossTenantReplication": null,
"allowSharedKeyAccess": null,
"allowedCopyScope": null,
"azureFilesIdentityBasedAuthentication": null,
"blobRestoreStatus": null,
"creationTime": "2022-10-06T13:47:17.943785+00:00",
...
The next step can cost us money if we don’t pay attention. Here we’ll create a Premium App Service Plan to run the workers
$ az functionapp plan create --resource-group pythonfunction-rg --name funcPremiumPlan --location
centralus --number-of-workers 1 --sku EP1 --is-linux
{
"elasticScaleEnabled": true,
"extendedLocation": null,
"freeOfferExpirationTime": null,
"geoRegion": "Central US",
"hostingEnvironmentProfile": null,
"hyperV": false,
"id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/pythonfunction-rg/providers/Microsoft.Web/serverfarms/funcPremiumPlan",
"isSpot": false,
"isXenon": false,
"kind": "elastic",
"kubeEnvironmentProfile": null,
"location": "centralus",
"maximumElasticWorkerCount": 1,
"maximumNumberOfWorkers": 0,
"name": "funcPremiumPlan",
"numberOfSites": 0,
"numberOfWorkers": 1,
"perSiteScaling": false,
"provisioningState": "Succeeded",
"reserved": true,
"resourceGroup": "pythonfunction-rg",
"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",
"tags": null,
"targetWorkerCount": 0,
"targetWorkerSizeId": 0,
"type": "Microsoft.Web/serverfarms",
"workerTierName": null,
"zoneRedundant": false
}
The last step is to deploy. You’ll see how we add a docker username and password so we can pull from our private registry
$ az functionapp create --name ijpythonfunction1 --storage-account ijfuncstorage1 --resource-group pythonfunction-rg --plan funcPremiumPlan --deployment-container-image-name harbor.freshbrewed.science/freshbrewedprivate/pythonfunction:latest --docker-registry-server-user imagepuller --docker-registry-server-password ******************
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 "ijpythonfunction1" was created for this Function App. You can visit https://portal.azure.com/#resource/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/pythonfunction-rg/providers/microsoft.insights/components/ijpythonfunction1/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": "ijpythonfunction1.azurewebsites.net",
"enabled": true,
"enabledHostNames": [
"ijpythonfunction1.azurewebsites.net",
"ijpythonfunction1.scm.azurewebsites.net"
],
"extendedLocation": null,
"hostNameSslStates": [
{
"hostType": "Standard",
"ipBasedSslResult": null,
"ipBasedSslState": "NotConfigured",
"name": "ijpythonfunction1.azurewebsites.net",
"sslState": "Disabled",
"thumbprint": null,
"toUpdate": null,
"toUpdateIpBasedSsl": null,
"virtualIp": null
},
{
"hostType": "Repository",
"ipBasedSslResult": null,
"ipBasedSslState": "NotConfigured",
"name": "ijpythonfunction1.scm.azurewebsites.net",
"sslState": "Disabled",
"thumbprint": null,
"toUpdate": null,
"toUpdateIpBasedSsl": null,
"virtualIp": null
}
],
"hostNames": [
"ijpythonfunction1.azurewebsites.net"
],
"hostNamesDisabled": false,
"hostingEnvironmentProfile": null,
"httpsOnly": false,
"hyperV": false,
"id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/pythonfunction-rg/providers/Microsoft.Web/sites/ijpythonfunction1",
"identity": null,
"inProgressOperationId": null,
"isDefaultContainer": null,
"isXenon": false,
"keyVaultReferenceIdentity": "SystemAssigned",
"kind": "functionapp,linux",
"lastModifiedTimeUtc": "2022-10-06T13:54:12.153333",
"location": "Central US",
"maxNumberOfWorkers": null,
"name": "ijpythonfunction1",
"outboundIpAddresses": "20.112.192.230,20.112.192.241,20.112.192.249,20.112.193.15,20.112.193.17,20.112.193.109,20.118.56.8",
"possibleOutboundIpAddresses": "20.112.192.180,20.112.192.185,20.112.192.189,20.112.192.203,20.112.192.204,20.112.192.214,20.112.192.230,20.112.192.241,20.112.192.249,20.112.193.15,20.112.193.17,20.112.193.109,20.112.193.136,20.112.193.171,20.112.193.173,20.112.193.182,20.112.193.188,20.112.193.197,20.112.193.200,20.112.193.217,20.112.193.220,20.112.193.223,20.112.193.234,20.112.193.238,20.118.56.8",
"publicNetworkAccess": null,
"redundancyMode": "None",
"repositorySiteName": "ijpythonfunction1",
"reserved": true,
"resourceGroup": "pythonfunction-rg",
"scmSiteAlsoStopped": false,
"serverFarmId": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/pythonfunction-rg/providers/Microsoft.Web/serverfarms/funcPremiumPlan",
"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": null,
"experiments": null,
"fileChangeAuditEnabled": null,
"ftpsState": null,
"functionAppScaleLimit": 0,
"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",
"storageAccountRequired": false,
"suspendedTill": null,
"tags": null,
"targetSwapSlot": null,
"trafficManagerHostNames": null,
"type": "Microsoft.Web/sites",
"usageState": "Normal",
"virtualNetworkSubnetId": null,
"vnetContentShareEnabled": false,
"vnetImagePullEnabled": false,
"vnetRouteAllEnabled": false
}
And we can test and see we are now hosted as an azure function app as well
We can also test our “secret” endpoint of asdf1234 and see it responds (but others like asdfasdf are ignored)
In Azure we can look at metrics and logs in app insights or basic metrics on the Function App itself, such as memory usage
We can use App Insights to check similar, such as memory usage or number of 404s returned
In some cases, you may be able to get a shell to the running container (I was not able to with this function container)
Add an Identity Provider
We can then choose an IdP such as Google, Facebook, Github and more. Here I’ll just use Microsoft
I’ll accept the default permission (read from Microsoft Graph)
And now we can see we require auth to hit the endpoint
If I use my work account, it routes through our MFA
and I’ll be rejected
But if I go back to my gmail account (The Microsoft account tied to this function), I have the choice to allow this App to see my identity
And upon accept, I’m redirected via the normal OAuth flow back to the app
Summary
We covered pulling the container we built for cloud run and using it in our local kubernetes cluster. This involved pushing to a different CR, then using a Deployment and Service definition paired with an Ingress one.
We covered updating our container to use a “secret” path that could a length token that could be rotated. There are other ways to secure this we will cover next time.
We setup the function app as an Azure Function app including showing how we pull from our own private container registry. Lastly, we checked out some of the features of Azure App Services.