Published: Sep 27, 2022 by Isaac Johnson
Recently, I’ve had need to sort out serverless notification endpoints for a variety of needs. While there are plenty of ways to run serverless functions in GCP, I figured it would be a good chance to sort out creating a Cloud Run function then using the Apprise framework to send out notifications.
Because I like to go many places at once, we’ll start with Cloud Run, but then move to Knative in GKE and lastly to an Azure Function.
Creating a Cloud Run function
I have the habit of dancing between many GCP Projects. So first, I’ll set the project I intend to use
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ export PROJECTID=myanthosproject2
builder@DESKTOP-QADGF36:~/Workspaces/jekyll-blog$ gcloud config set project $PROJECTID
Updated property [core/project].
Then I’ll create a dir for our project and the initial main.py
builder@DESKTOP-QADGF36:~/Workspaces$ mkdir hellofresh
builder@DESKTOP-QADGF36:~/Workspaces$ cd hellofresh/
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ vi main.py
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ cat main.py
import os
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello_world():
name = os.environ.get("NAME", "World")
return "Hello {}!".format(name)
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
I’ll need some requirements for pip to install
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ vi requirements.txt
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ cat requirements.txt
Flask==2.1.0
gunicorn==20.1.0
And lastly, the Dockerfile
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ cat Dockerfile
# Use the official lightweight Python image.
# https://hub.docker.com/_/python
FROM python:3.10-slim
# Allow statements and log messages to immediately appear in the Knative logs
ENV PYTHONUNBUFFERED True
# Copy local code to the container image.
ENV APP_HOME /app
WORKDIR $APP_HOME
COPY . ./
# Install production dependencies.
RUN pip install --no-cache-dir -r requirements.txt
# Run the web service on container startup. Here we use the gunicorn
# webserver, with one worker process and 8 threads.
# For environments with multiple CPU cores, increase the number of workers
# to be equal to the cores available.
# Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling.
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app
Lastly, we need the dockerignore file
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ cat .dockerignore
Dockerfile
README.md
*.pyc
*.pyo
*.pyd
__pycache__
.pytest_cache
We can now build and deploy from source
$ gcloud run deploy hellofresh
Deploying from source. To deploy a container use [--image]. See https://cloud.google.com/run/docs/deploying-source-code for more details.
Source code location (/home/builder/Workspaces/hellofresh):
Next time, use `gcloud run deploy --source .` to deploy the current directory.
API [run.googleapis.com] not enabled on project [511842454269]. Would you like to enable and retry (this will take a few minutes)? (y/N)? y
Enabling service [run.googleapis.com] on project [511842454269]...
Operation "operations/acf.p2-511842454269-52748631-3cb9-4880-8d32-442599a0bbd0" finished successfully.
Please specify a region:
[1] asia-east1
[2] asia-east2
[3] asia-northeast1
[4] asia-northeast2
[5] asia-northeast3
[6] asia-south1
[7] asia-south2
[8] asia-southeast1
[9] asia-southeast2
[10] australia-southeast1
[11] australia-southeast2
[12] europe-central2
[13] europe-north1
[14] europe-southwest1
[15] europe-west1
[16] europe-west2
[17] europe-west3
[18] europe-west4
[19] europe-west6
[20] europe-west8
[21] europe-west9
[22] me-west1
[23] northamerica-northeast1
[24] northamerica-northeast2
[25] southamerica-east1
[26] southamerica-west1
[27] us-central1
[28] us-east1
[29] us-east4
[30] us-east5
[31] us-south1
[32] us-west1
[33] us-west2
[34] us-west3
[35] us-west4
[36] cancel
Please enter numeric choice or text value (must exactly match list item): 29
To make this the default region, run `gcloud config set run/region us-east4`.
Deploying from source requires an Artifact Registry Docker repository to store built containers. A repository named [cloud-run-source-deploy] in region [us-east4] will be created.
Do you want to continue (Y/n)? y
This command is equivalent to running `gcloud builds submit --tag [IMAGE] /home/builder/Workspaces/hellofresh` and `gcloud run deploy hellofresh --image [IMAGE]`
Allow unauthenticated invocations to [hellofresh] (y/N)? y
Building using Dockerfile and deploying container to Cloud Run service [hellofresh] in project [myanthosproject2] region [us-east4]
⠶ Building and deploying new service... Creating Container Repository.
⠶ Creating Container Repository...
. Uploading sources...
. Building Container...
. Creating Revision...
. Routing traffic...
. Setting IAM Policy...
This is now building and creating a few things; a cloud-run instance that can build containers from source, a Google Artifact Registry (I like to call “GaaaaaaaaaaRRRRR!” because it’s a silly acronym), then a Cloud Run instance.
When I first built, I got the following error
Building using Dockerfile and deploying container to Cloud Run service [hellofresh] in project [myanthosproject2] region [us-east4]
⠧ Building and deploying new service... Uploading sources.
✓ Creating Container Repository...
✓ Uploading sources...
⠧ Building and deploying new service... Uploading sources.
X Building and deploying new service... Building Container.
. Routing traffic...
. Setting IAM Policy...
- Building Container... Logs are available at [https://console.cloud.google.com/cloud-build/builds/b5346dd9-27c7-4666-a5c5-42798a8b914b?project=511842454269].
Enabling service [cloudbuild.googleapis.com] on project [511842454269]...
Operation "operations/acf.p2-511842454269-fd3a3ab5-c3f1-4216-b101-c55cf364f0b5" finished successfully.
Deployment failed
ERROR: (gcloud.run.deploy) Build failed; check build logs for details
The error bascially says the CloudBuild service doesnt have access to a storage bucket
I’ll add the CloudBuild user to the IAM policy
$ gcloud projects add-iam-policy-binding $PROJECTID --member "serviceAccount:511842454269@cloudbuild.gserviceaccount.com" --role "roles/storage.objectAdmin"
Updated IAM policy for project [myanthosproject2].
bindings:
- members:
- serviceAccount:service-511842454269@gcp-sa-servicemesh.iam.gserviceaccount.com
role: roles/anthosservicemesh.serviceAgent
... snip
Then try again
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ gcloud run deploy hellofresh
Deploying from source. To deploy a container use [--image]. See https://cloud.google.com/run/docs/deploying-source-code for more details.
Source code location (/home/builder/Workspaces/hellofresh):
Next time, use `gcloud run deploy --source .` to deploy the current directory.
Please specify a region:
[1] asia-east1
[2] asia-east2
[3] asia-northeast1
[4] asia-northeast2
[5] asia-northeast3
[6] asia-south1
[7] asia-south2
[8] asia-southeast1
[9] asia-southeast2
[10] australia-southeast1
[11] australia-southeast2
[12] europe-central2
[13] europe-north1
[14] europe-southwest1
[15] europe-west1
[16] europe-west2
[17] europe-west3
[18] europe-west4
[19] europe-west6
[20] europe-west8
[21] europe-west9
[22] me-west1
[23] northamerica-northeast1
[24] northamerica-northeast2
[25] southamerica-east1
[26] southamerica-west1
[27] us-central1
[28] us-east1
[29] us-east4
[30] us-east5
[31] us-south1
[32] us-west1
[33] us-west2
[34] us-west3
[35] us-west4
[36] cancel
Please enter numeric choice or text value (must exactly match list item): 29
To make this the default region, run `gcloud config set run/region us-east4`.
This command is equivalent to running `gcloud builds submit --tag [IMAGE] /home/builder/Workspaces/hellofresh` and `gcloud run deploy hellofresh --image [IMAGE]`
Allow unauthenticated invocations to [hellofresh] (y/N)? y
Building using Dockerfile and deploying container to Cloud Run service [hellofresh] in project [myanthosproject2] region [us-east4]
✓ Building and deploying new service... Done.
✓ Uploading sources...
✓ Building Container... Logs are available at [https://console.cloud.google.com/cloud-build/builds/b5c5e4bb-a137-4950-9ff0-0736dcb1d245?project=511842454269].
✓ Creating Revision... Revision deployment finished. Checking container health.
✓ Routing traffic...
✓ Setting IAM Policy...
Done.
Service [hellofresh] revision [hellofresh-00001-xox] has been deployed and is serving 100 percent of traffic.
Service URL: https://hellofresh-q5jg7qcghq-uk.a.run.app
Which we can see is running
At the first invokation, there is not much to see in metrics
If I look at the YAML
I can see the image is hosted at us-east4-docker.pkg.dev/myanthosproject2/cloud-run-source-deploy/hellofresh@sha256:e943ba360345c684c65cf5e6d14934f22038d5a7a2048912ded0fe3e4711709b
I can go to GaaaaaRRRRR and see the image.
What I really love is they not only stick me for higher costs on GAR over GCR, I also get a huge advert at the top to enable scanning for just US$0.26 per image. How fantastic.
In fairness, ACR had a scanning option, Microsoft Defender for Container Registries, but it has since been deprecated and it billed on a per hour basis by cores of your ACR host (0.00095/vCore/hour.. just under a penny an hour a core). ECR has free basic scanning but Enhanced scanning with “Inspector” can run $0.11 for the first image and $0.01 for each subsequent scan
Updating the Container Directly
Let us say that instead of paying extra for cloud build, I may wish to just build and push to GaaaaaaRRRRR locally, or via an AzDO or Github Actions Runner.
Let’s create a Github Repo first with the code and move from there.
Next, I need to make my local file system into a Git Repo before I can push it
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git init
Initialized empty Git repository in /home/builder/Workspaces/hellofresh/.git/
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git checkout -b main
Switched to a new branch 'main'
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
.dockerignore
Dockerfile
main.py
requirements.txt
nothing added to commit but untracked files present (use "git add" to track)
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git add -A
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git commit -m "Initial Commit"
[main (root-commit) 3e83d16] Initial Commit
4 files changed, 45 insertions(+)
create mode 100644 .dockerignore
create mode 100644 Dockerfile
create mode 100644 main.py
create mode 100644 requirements.txt
I’ll add a quick readme
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ echo "# cloudFunctionPython" >> README.md
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git add README.md
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git commit -m "Readme"
[main 76b34b4] Readme
1 file changed, 1 insertion(+)
create mode 100644 README.md
I can then add the remote origin and push
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git remote add origin https://github.com/idjohnson/cloudFunctionPython.git
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git push -u origin main
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 16 threads
Compressing objects: 100% (7/7), done.
Writing objects: 100% (9/9), 1.33 KiB | 1.33 MiB/s, done.
Total 9 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), done.
To https://github.com/idjohnson/cloudFunctionPython.git
* [new branch] main -> main
Branch 'main' set up to track remote branch 'main' from 'origin'.
Now that we have files in the repo, we can create a github runners file
When creating the workflow, we can choose to build on every push:
name: GitHub Actions Docker Build
on: push
jobs:
HostedActions:
runs-on: ubuntu-latest
steps:
...
Or we could do it just when the Dockerfile is updated
name: GitHub Actions Docker Build
on:
push:
paths:
- "**/Dockerfile"
- "Dockerfile"
jobs:
HostedActions:
runs-on: ubuntu-latest
steps:
...
For now, I’ll do the former.
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ cat .github/workflows/github-actions.yml
name: GitHub Actions Docker Build
on: push
jobs:
HostedActions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a $ event."
- run: echo "🐧 This job is now running on a $ server hosted by GitHub!"
- run: echo "🔎 The name of your branch is $ and your repository is $."
- name: Check out repository code
uses: actions/checkout@v2
- run: echo "💡 The $ repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: Build Dockerfile
run: |
docker build -t $GITHUB_RUN_ID .
docker images
I can add and push, which should create the action for me
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git add .github/
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git commit -m "first workflow"
[main d4e1684] first workflow
1 file changed, 19 insertions(+)
create mode 100644 .github/workflows/github-actions.yml
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git push
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 16 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (5/5), 739 bytes | 739.00 KiB/s, done.
Total 5 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To https://github.com/idjohnson/cloudFunctionPython.git
76b34b4..d4e1684 main -> main
I can now see it built a tag, albeit the repository name was set as the tagid
I’ll add a secret, GCLOUD_SERVICE_KEY
, that is a GCP Service Account JSON, base64 encoded
Then add a new step to my workflow to push to GaaaaaaRRRRR
- uses: RafikFarhad/push-to-gcr-github-action@v4.1
with:
gcloud_service_key: $ # can be base64 encoded or plain text
registry: gcr.io
project_id: myanthosproject2
image_name: pythonfunction
image_tag: latest,$
dockerfile: ./Dockerfile
context: .
This now pushes to GCR
I can verify by checking GCR
And, of course, by using GCR over GaaaaaRRRR we save roughly 4x the cost
Here we can push to GCR then use the standard GCP GH Actions to auth and update CloudRun
- uses: RafikFarhad/push-to-gcr-github-action@v4.1
with:
gcloud_service_key: $ # can be base64 encoded or plain text
registry: gcr.io
project_id: myanthosproject2
image_name: pythonfunction
image_tag: latest,$
dockerfile: ./Dockerfile
context: .
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v0'
with:
credentials_json: '$'
- id: 'deploy'
uses: 'google-github-actions/deploy-cloudrun@v0'
with:
service: 'hellofresh'
image: 'gcr.io/myanthosproject2/pythonfunction:$'
This presented our first access error to sort out.
We can see that we built and pushed the image just fine
And we have it in GCR just fine
Unfortunately our SA doesnt have Cloud Run abilities
I’ll add cloud run admin and developer to the SA role
$ gcloud projects add-iam-policy-binding $PROJECTID --member "serviceAccount:511842454269@cloudbuild.gserviceaccount.com" --role "roles/run.admin"
Updated IAM policy for project [myanthosproject2].
bindings:
- members:
...snip...
$ gcloud projects add-iam-policy-binding $PROJECTID --member "serviceAccount:511842454269@cloudbuild.gserviceaccount.com" --role "roles/run.developer"
Updated IAM policy for project [myanthosproject2].
bindings:
- members:
...snip...
Then I can re-run the jobs to see if that fixed our issue
Even adding the compute SA which Cloud Run uses didnt solve it
$ gcloud projects add-iam-policy-binding $PROJECTID --member "serviceAccount:511842454269-compute@developer.gserviceaccount.com" --role "roles/run.developer"
Updated IAM policy for project [myanthosproject2].
bindings:
I noticed the namespace used the project ID, not name, so i updated the YAML and tried again
- uses: RafikFarhad/push-to-gcr-github-action@v4.1
with:
gcloud_service_key: $ # can be base64 encoded or plain text
registry: gcr.io
project_id: myanthosproject2
image_name: pythonfunction
image_tag: latest,$
dockerfile: ./Dockerfile
context: .
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v0'
with:
credentials_json: '$'
- id: 'deploy'
uses: 'google-github-actions/deploy-cloudrun@v0'
with:
service: 'namespaces/511842454269/services/hellofresh'
image: 'gcr.io/myanthosproject2/pythonfunction:$'
But still a fail
I tried the command locally, perhaps I have a syntax issue or region mistake. That, however, worked
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ gcloud run deploy namespaces/511842454269/services/hellofresh --image gcr.io/myanthosproject2/pythonfunction:3090008324 --quiet --platform managed --region us-central1 --project myanthosproject2 --format json
Deploying container to Cloud Run service [hellofresh] in project [511842454269] region [us-central1]
✓ Deploying new service... Done.
✓ Creating Revision... Revision deployment finished. Checking container health.
✓ Routing traffic...
Done.
Service [hellofresh] revision [hellofresh-00001-ciz] has been deployed and is serving 100 percent of traffic.
Service URL: https://hellofresh-q5jg7qcghq-uc.a.run.app
{
"apiVersion": "serving.knative.dev/v1",
"kind": "Service",
"metadata": {
"annotations": {
"client.knative.dev/user-image": "gcr.io/myanthosproject2/pythonfunction:3090008324",
"run.googleapis.com/client-name": "gcloud",
"run.googleapis.com/client-version": "397.0.0",
"run.googleapis.com/ingress": "all",
"run.googleapis.com/ingress-status": "all",
"serving.knative.dev/creator": "isaac.johnson@gmail.com",
"serving.knative.dev/lastModifier": "isaac.johnson@gmail.com"
},
"creationTimestamp": "2022-09-20T12:14:37.169079Z",
"generation": 1,
"labels": {
"cloud.googleapis.com/location": "us-central1"
},
"name": "hellofresh",
"namespace": "511842454269",
"resourceVersion": "AAXpGscZdp0",
"selfLink": "/apis/serving.knative.dev/v1/namespaces/511842454269/services/hellofresh",
"uid": "4f2fba78-c821-4dbd-bf5d-7841a7e7a573"
},
"spec": {
"template": {
"metadata": {
"annotations": {
"autoscaling.knative.dev/maxScale": "100",
"client.knative.dev/user-image": "gcr.io/myanthosproject2/pythonfunction:3090008324",
"run.googleapis.com/client-name": "gcloud",
"run.googleapis.com/client-version": "397.0.0"
},
"name": "hellofresh-00001-ciz"
},
"spec": {
"containerConcurrency": 80,
"containers": [
{
"image": "gcr.io/myanthosproject2/pythonfunction:3090008324",
"ports": [
{
"containerPort": 8080,
"name": "http1"
}
],
"resources": {
"limits": {
"cpu": "1000m",
"memory": "512Mi"
}
}
}
],
"serviceAccountName": "511842454269-compute@developer.gserviceaccount.com",
"timeoutSeconds": 300
}
},
"traffic": [
{
"latestRevision": true,
"percent": 100
}
]
},
"status": {
"address": {
"url": "https://hellofresh-q5jg7qcghq-uc.a.run.app"
},
"conditions": [
{
"lastTransitionTime": "2022-09-20T12:15:02.117343Z",
"status": "True",
"type": "Ready"
},
{
"lastTransitionTime": "2022-09-20T12:15:01.509056Z",
"status": "True",
"type": "ConfigurationsReady"
},
{
"lastTransitionTime": "2022-09-20T12:15:02.309533Z",
"status": "True",
"type": "RoutesReady"
}
],
"latestCreatedRevisionName": "hellofresh-00001-ciz",
"latestReadyRevisionName": "hellofresh-00001-ciz",
"observedGeneration": 1,
"traffic": [
{
"latestRevision": true,
"percent": 100,
"revisionName": "hellofresh-00001-ciz"
}
],
"url": "https://hellofresh-q5jg7qcghq-uc.a.run.app"
}
}
That’s when I realized the mistake.
The service account I setup as a secret was none of the above.
builder@anna-MacBookAir:~$ cat sa-storage-key.json | grep client_email
"client_email": "gcpbucketsa@myanthosproject2.iam.gserviceaccount.com",
I added the missing roles
$ gcloud projects add-iam-policy-binding $PROJECTID --member "serviceAccount:gcpbucketsa@myanthosproject2.iam.gserviceaccount.com" --role "roles/run.developer"
Updated IAM policy for project [myanthosproject2].
bindings:
....
$ gcloud projects add-iam-policy-binding $PROJECTID --member "serviceAccount:gcpbucketsa@myanthosproject2.iam.gserviceaccount.com" --role "roles/iam.serviceAccountUser"
Updated IAM policy for project [myanthosproject2].
bindings:
Now it works:
We can do a bit of cleanup on our GCR images as well.
We can find the untagged images
$ gcloud container images list-tags gcr.io/myanthosproject2/pythonfunction --filter='-tags:*' --format="get(
digest)"
sha256:a0570af4ee69c7215298eac7d66c6a9351e43a1bcd5df5e700c9e9b9b8627b0b
sha256:11451bd748ed968648c541c1c1153d15b2eb20df4796f308e2b6c7c8e1da01dd
sha256:3627f25c6ac7aaa11e6a1bc2d604035860a3d7c90ce3e5346e88fd9cf82a3ff8
sha256:6bd9f95848e2dea1bd40faea038494d05dfe04f19157f2d914497fe317fde3a7
sha256:ef3c1f3749640b92ced5ed56373fb2f1e6d65a461fa62a623021c3198ea070fe
sha256:807620bfa06f1e9e3cfa4e5d78ddd4fd35c4ae82f1bb1d857dfe6cefeda6d9ed
sha256:adbb37456a3927faf599f8d7778da31a9639eb9a7913b74c49c5b47b2746db48
sha256:e184c1345b0e2d8bf8269d92113133a56ba5a6754c8f1595ce1fd38fdba5071e
Then pass that to a delete command to remove them (next time i’ll add --quiet
to the end)
$ gcloud container images list-tags gcr.io/myanthosproject2/pythonfunction --filter='-tags:*' --format="get(
digest)" | xargs -L1 -I {} -n 1 gcloud container images delete gcr.io/myanthosproject2/pythonfunction@{}
Digests:
- gcr.io/myanthosproject2/pythonfunction@sha256:a0570af4ee69c7215298eac7d66c6a9351e43a1bcd5df5e700c9e9b9b8627b0b
This operation will delete the tags and images identified by the digests above.
Do you want to continue (Y/n)?
Deleted [gcr.io/myanthosproject2/pythonfunction@sha256:a0570af4ee69c7215298eac7d66c6a9351e43a1bcd5df5e700c9e9b9b8627b0b].
Digests:
- gcr.io/myanthosproject2/pythonfunction@sha256:11451bd748ed968648c541c1c1153d15b2eb20df4796f308e2b6c7c8e1da01dd
This operation will delete the tags and images identified by the digests above.
Do you want to continue (Y/n)?
Deleted [gcr.io/myanthosproject2/pythonfunction@sha256:11451bd748ed968648c541c1c1153d15b2eb20df4796f308e2b6c7c8e1da01dd].
Digests:
- gcr.io/myanthosproject2/pythonfunction@sha256:3627f25c6ac7aaa11e6a1bc2d604035860a3d7c90ce3e5346e88fd9cf82a3ff8
This operation will delete the tags and images identified by the digests above.
Do you want to continue (Y/n)?
Deleted [gcr.io/myanthosproject2/pythonfunction@sha256:3627f25c6ac7aaa11e6a1bc2d604035860a3d7c90ce3e5346e88fd9cf82a3ff8].
Digests:
- gcr.io/myanthosproject2/pythonfunction@sha256:6bd9f95848e2dea1bd40faea038494d05dfe04f19157f2d914497fe317fde3a7
This operation will delete the tags and images identified by the digests above.
Do you want to continue (Y/n)?
Deleted [gcr.io/myanthosproject2/pythonfunction@sha256:6bd9f95848e2dea1bd40faea038494d05dfe04f19157f2d914497fe317fde3a7].
Digests:
- gcr.io/myanthosproject2/pythonfunction@sha256:ef3c1f3749640b92ced5ed56373fb2f1e6d65a461fa62a623021c3198ea070fe
This operation will delete the tags and images identified by the digests above.
Do you want to continue (Y/n)?
Deleted [gcr.io/myanthosproject2/pythonfunction@sha256:ef3c1f3749640b92ced5ed56373fb2f1e6d65a461fa62a623021c3198ea070fe].
Digests:
- gcr.io/myanthosproject2/pythonfunction@sha256:807620bfa06f1e9e3cfa4e5d78ddd4fd35c4ae82f1bb1d857dfe6cefeda6d9ed
This operation will delete the tags and images identified by the digests above.
Do you want to continue (Y/n)?
Deleted [gcr.io/myanthosproject2/pythonfunction@sha256:807620bfa06f1e9e3cfa4e5d78ddd4fd35c4ae82f1bb1d857dfe6cefeda6d9ed].
Digests:
- gcr.io/myanthosproject2/pythonfunction@sha256:adbb37456a3927faf599f8d7778da31a9639eb9a7913b74c49c5b47b2746db48
This operation will delete the tags and images identified by the digests above.
Do you want to continue (Y/n)?
Deleted [gcr.io/myanthosproject2/pythonfunction@sha256:adbb37456a3927faf599f8d7778da31a9639eb9a7913b74c49c5b47b2746db48].
Digests:
- gcr.io/myanthosproject2/pythonfunction@sha256:e184c1345b0e2d8bf8269d92113133a56ba5a6754c8f1595ce1fd38fdba5071e
This operation will delete the tags and images identified by the digests above.
Do you want to continue (Y/n)?
Deleted [gcr.io/myanthosproject2/pythonfunction@sha256:e184c1345b0e2d8bf8269d92113133a56ba5a6754c8f1595ce1fd38fdba5071e].
and we can see it cleaned up a bit
Additionally, if I check my Cloud Run, I can see in the YAML it is using the updated image
Adding Apprise
Apprise is a framework for enabling notifications to a variety of systems.
I’ll want to add apprise
to requirements.txt
$ cat requirements.txt
Flask==2.1.0
gunicorn==20.1.0
apprise==1.0.0
I can then apprise with an example notification
$ cat main.py
import os
import apprise
from flask import Flask
app = Flask(__name__)
apobj = apprise.Apprise()
apobj.add('mailto://tristan.cormac.moriarty:MYPASSWORD@gmail.com')
@app.route("/")
def hello_world():
name = os.environ.get("NAME", "World")
apobj.notify(
body='Notified by Cloud Run Function',
title='From Python Cloud Run',
)
return "Hello {}!".format(name)
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
I can then test a build locally to ensure I didn’t do any mistakes
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ docker build -t test .
[+] Building 11.5s (10/10) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 847B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 105B 0.0s
=> [internal] load metadata for docker.io/library/python:3.10-slim 1.9s
=> [auth] library/python:pull token for registry-1.docker.io 0.0s
=> [1/4] FROM docker.io/library/python:3.10-slim@sha256:6de22c9cf887098265b7614582b00641c0c8c6735af538d0f267d6bb457634f1 4.2s
=> => resolve docker.io/library/python:3.10-slim@sha256:6de22c9cf887098265b7614582b00641c0c8c6735af538d0f267d6bb457634f1 0.0s
=> => sha256:31b3f1ad4ce1f369084d0f959813c51df0ca17d9877d5ee88c2db6ff88341430 31.40MB / 31.40MB 1.7s
=> => sha256:f335cc1597f2f2d13ceea1c9b386aa1ac28efc46906a0a9cf1b4e368ec33a62a 1.08MB / 1.08MB 0.2s
=> => sha256:501b4d0d8bea6f36e0132899acecf241b4fc7f91117be870fee91069d93f2384 12.10MB / 12.10MB 1.2s
=> => sha256:6de22c9cf887098265b7614582b00641c0c8c6735af538d0f267d6bb457634f1 1.86kB / 1.86kB 0.0s
=> => sha256:7e8d32d9e8c20a3626146a932ecf98c3fc2a4b1100b008193b835acc2ab88018 1.37kB / 1.37kB 0.0s
=> => sha256:d7971c18b18e6df42f1c9af0ae06a658f82698f831b6c2300db25063d52b2a7c 7.50kB / 7.50kB 0.0s
=> => sha256:abd735557fdf93d385293000b9bb82151a0de2c674ff621a56484ea49bc7de4c 232B / 232B 0.3s
=> => sha256:9358bdbbffdc0f5b9e4cdb442d5676d421068183f5558979a2450ef59af16e4f 3.34MB / 3.34MB 0.8s
=> => extracting sha256:31b3f1ad4ce1f369084d0f959813c51df0ca17d9877d5ee88c2db6ff88341430 1.4s
=> => extracting sha256:f335cc1597f2f2d13ceea1c9b386aa1ac28efc46906a0a9cf1b4e368ec33a62a 0.1s
=> => extracting sha256:501b4d0d8bea6f36e0132899acecf241b4fc7f91117be870fee91069d93f2384 0.4s
=> => extracting sha256:abd735557fdf93d385293000b9bb82151a0de2c674ff621a56484ea49bc7de4c 0.0s
=> => extracting sha256:9358bdbbffdc0f5b9e4cdb442d5676d421068183f5558979a2450ef59af16e4f 0.2s
=> [internal] load build context 0.1s
=> => transferring context: 47.61kB 0.1s
=> [2/4] WORKDIR /app 0.2s
=> [3/4] COPY . ./ 0.0s
=> [4/4] RUN pip install --no-cache-dir -r requirements.txt 4.9s
=> exporting to image 0.2s
=> => exporting layers 0.2s
=> => writing image sha256:d9611b409f42339296b5e94acda4875c9a27abc2cf170ff3bf332a362add9557 0.0s
=> => naming to docker.io/library/test
Now I’ll test it live
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git add main.py
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git add requirements.txt
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git commit -m "Updates"
[main 6e79d1a] Updates
2 files changed, 9 insertions(+)
builder@DESKTOP-QADGF36:~/Workspaces/hellofresh$ git push
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 16 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 670 bytes | 670.00 KiB/s, done.
Total 4 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To https://github.com/idjohnson/cloudFunctionPython.git
e6bc2a7..6e79d1a main -> main
Though in trying to hit the endpoint, I kept getting an error
I realized a mistake that was preventing it from running was a lack of authentication.
To fix this - to allow unauthenticated users to invoke the cloud run function - I went to the Permissions section
Then Added “allUsers” and for the role, chose Cloud Run Invoker
which prompted about “Allow Public Access”
I now see it’s switched back to “Allow unauthenticated”
Now upon reload, I can reach it
One mistake I then made was checking in credentials on a public repo which required me to rotate passwords and make the repo private (then recreate this whole thing)
It took a bit to tease out the proper syntax. I escaped characters in passwords when needed, but some of the tests showed MFA getting in the way
builder@DESKTOP-72D2D9T:~/Workspaces/cloudFunctionPython$ apprise -vvv -t "Test Message" -b "Test Body" mailto://nulubez:XXXXXXXXXXXXXXXX@hotmail.com
2022-09-20 22:04:37,292 - INFO - Applying Microsoft Hotmail Defaults
2022-09-20 22:04:37,293 - DEBUG - Loaded E-Mail URL: mailtos://nulubez:****@hotmail.com/?from=nulubez%40hotmail.com&mode=starttls&smtp=smtp-mail.outlook.com&user=nulubez%40hotmail.com&format=html&overflow=upstream&rto=4.0&cto=15&verify=yes
2022-09-20 22:04:37,293 - DEBUG - Using selector: EpollSelector
2022-09-20 22:04:37,294 - INFO - Notifying 1 service(s) asynchronously.
2022-09-20 22:04:37,297 - DEBUG - Email From: Apprise Notifications <nulubez@hotmail.com>
2022-09-20 22:04:37,297 - DEBUG - Email To: nulubez@hotmail.com
2022-09-20 22:04:37,297 - DEBUG - Login ID: nulubez@hotmail.com
2022-09-20 22:04:37,297 - DEBUG - Delivery: smtp-mail.outlook.com:587
2022-09-20 22:04:37,298 - DEBUG - Connecting to remote SMTP server...
2022-09-20 22:04:37,409 - DEBUG - Securing connection with STARTTLS...
2022-09-20 22:04:37,573 - DEBUG - Applying user credentials...
2022-09-20 22:04:46,863 - WARNING - A Connection error occurred sending Email notification to smtp-mail.outlook.com.
2022-09-20 22:04:46,863 - DEBUG - Socket Exception: (535, b'5.7.139 Authentication unsuccessful, the user credentials were incorrect. [CH2PR19CA0010.namprd19.prod.outlook.com]')
builder@DESKTOP-72D2D9T:~/Workspaces/cloudFunctionPython$ apprise -vvv -t "Test Message" -b "Test Body" mailto://nulubez%40hotmail.com:XXXXXXXXXXXXXXXX@hotmail.com
2022-09-20 22:05:31,307 - INFO - Applying Microsoft Hotmail Defaults
2022-09-20 22:05:31,307 - DEBUG - Loaded E-Mail URL: mailtos://nulubez:****@hotmail.com/?from=nulubez%40hotmail.com&mode=starttls&smtp=smtp-mail.outlook.com&user=nulubez%40hotmail.com%40hotmail.com&format=html&overflow=upstream&rto=4.0&cto=15&verify=yes
2022-09-20 22:05:31,307 - DEBUG - Using selector: EpollSelector
2022-09-20 22:05:31,308 - INFO - Notifying 1 service(s) asynchronously.
2022-09-20 22:05:31,309 - DEBUG - Email From: Apprise Notifications <nulubez@hotmail.com>
2022-09-20 22:05:31,309 - DEBUG - Email To: nulubez@hotmail.com
2022-09-20 22:05:31,309 - DEBUG - Login ID: nulubez@hotmail.com@hotmail.com
2022-09-20 22:05:31,309 - DEBUG - Delivery: smtp-mail.outlook.com:587
2022-09-20 22:05:31,310 - DEBUG - Connecting to remote SMTP server...
2022-09-20 22:05:31,427 - DEBUG - Securing connection with STARTTLS...
2022-09-20 22:05:31,586 - DEBUG - Applying user credentials...
2022-09-20 22:05:36,676 - WARNING - A Connection error occurred sending Email notification to smtp-mail.outlook.com.
2022-09-20 22:05:36,676 - DEBUG - Socket Exception: (535, b'5.7.3 Authentication unsuccessful [CH2PR04CA0025.namprd04.prod.outlook.com]')
However, sendgrid seemed to access my credentials
builder@DESKTOP-72D2D9T:~/Workspaces/cloudFunctionPython$ apprise -vvv -t "Test Message" -b "Test Body" sendgrid://SG.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX:isaac@freshbrewed.science/isaac.johnson@gmail.com
2022-09-20 22:17:34,287 - DEBUG - Loaded SendGrid URL: sendgrid://S...U:isaac@freshbrewed.science/isaac.johnson%40gmail.com?format=html&overflow=upstream&rto=4.0&cto=4.0&verify=yes
2022-09-20 22:17:34,287 - DEBUG - Using selector: EpollSelector
2022-09-20 22:17:34,288 - INFO - Notifying 1 service(s) asynchronously.
2022-09-20 22:17:34,289 - DEBUG - SendGrid POST URL: https://api.sendgrid.com/v3/mail/send (cert_verify=True)
2022-09-20 22:17:34,289 - DEBUG - SendGrid Payload: {'personalizations': [{'to': [{'email': 'isaac.johnson@gmail.com'}]}], 'from': {'email': 'isaac@freshbrewed.science'}, 'subject': 'Test Message', 'content': [{'type': 'text/html', 'value': 'Test Body'}]}
2022-09-20 22:17:34,937 - INFO - Sent SendGrid notification to isaac.johnson@gmail.com.
And once I figured out that hotmail just needs an “Application Password” which you can create in the advanced security settings (for things like XBox), then it worked
builder@DESKTOP-72D2D9T:~/Workspaces/cloudFunctionPython$ apprise -vvv -t "Test Message" -b "Test Body" mailto://nulubez:XXXXXXXXXXXXXXXX@hotmail.com
2022-09-20 22:20:03,724 - INFO - Applying Microsoft Hotmail Defaults
2022-09-20 22:20:03,725 - DEBUG - Loaded E-Mail URL: mailtos://nulubez:****@hotmail.com/?from=nulubez%40hotmail.com&mode=starttls&smtp=smtp-mail.outlook.com&user=nulubez%40hotmail.com&format=html&overflow=upstream&rto=4.0&cto=15&verify=yes
2022-09-20 22:20:03,725 - DEBUG - Using selector: EpollSelector
2022-09-20 22:20:03,726 - INFO - Notifying 1 service(s) asynchronously.
2022-09-20 22:20:03,728 - DEBUG - Email From: Apprise Notifications <nulubez@hotmail.com>
2022-09-20 22:20:03,728 - DEBUG - Email To: nulubez@hotmail.com
2022-09-20 22:20:03,729 - DEBUG - Login ID: nulubez@hotmail.com
2022-09-20 22:20:03,729 - DEBUG - Delivery: smtp-mail.outlook.com:587
2022-09-20 22:20:03,729 - DEBUG - Connecting to remote SMTP server...
2022-09-20 22:20:03,860 - DEBUG - Securing connection with STARTTLS...
2022-09-20 22:20:04,014 - DEBUG - Applying user credentials...
2022-09-20 22:20:07,072 - INFO - Sent Email notification to "nulubez@hotmail.com".
And from Hotmail to my Gmail
builder@DESKTOP-72D2D9T:~/Workspaces/cloudFunctionPython$ apprise -vvv -t "Test Message" -b "Test Body" mailto://nulubez:XXXXXXXXXXXXXXXX@hotmail.com/?to=isaac.johnson@gmail.com
2022-09-20 22:20:28,367 - INFO - Applying Microsoft Hotmail Defaults
2022-09-20 22:20:28,367 - DEBUG - Loaded E-Mail URL: mailtos://nulubez:****@hotmail.com/isaac.johnson%40gmail.com?from=nulubez%40hotmail.com&mode=starttls&smtp=smtp-mail.outlook.com&user=nulubez%40hotmail.com&format=html&overflow=upstream&rto=4.0&cto=15&verify=yes
2022-09-20 22:20:28,368 - DEBUG - Using selector: EpollSelector
2022-09-20 22:20:28,368 - INFO - Notifying 1 service(s) asynchronously.
2022-09-20 22:20:28,371 - DEBUG - Email From: Apprise Notifications <nulubez@hotmail.com>
2022-09-20 22:20:28,372 - DEBUG - Email To: isaac.johnson@gmail.com
2022-09-20 22:20:28,372 - DEBUG - Login ID: nulubez@hotmail.com
2022-09-20 22:20:28,372 - DEBUG - Delivery: smtp-mail.outlook.com:587
2022-09-20 22:20:28,372 - DEBUG - Connecting to remote SMTP server...
2022-09-20 22:20:28,545 - DEBUG - Securing connection with STARTTLS...
2022-09-20 22:20:28,667 - DEBUG - Applying user credentials...
2022-09-20 22:20:30,394 - INFO - Sent Email notification to "isaac.johnson@gmail.com".
So we can use it in our build, I’ll save the real API key to a Github Actions secret just as we did earlier with the GCLOUD json key
I’ll then update my main.py to use a keyword
def hello_world():
name = os.environ.get("NAME", "World")
apobj = apprise.Apprise()
apobj.add('sendgrid://SENDGRIDTOKENHERE:isaac@freshbrewed.science/isaac.johnson@gmail.com')
apobj.notify(
body='Notified by Cloud Run Function',
title='From Python Cloud Run',
)
return "Hello hello {}!".format(name)
and the workflow to replace it inline
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: Replace Secret Tokens in Main
run: |
sed -i 's/SENDGRIDTOKENHERE/$/g' main.py
- uses: RafikFarhad/push-to-gcr-github-action@v4.1
with:
...snip...
I’ll push a build
Now when i hit https://hellofresh-q5jg7qcghq-uc.a.run.app/
I get an email
Teams
We can go to “connectors” on a teams channel
the configure an “incoming webhook”
and give it an image and name
which will give us our webhook url
We can see the msteams usage
$ apprise -vvv -t "Test Message" -b "Test Body" msteams://prince
ssking/ccef6213-asdf-asdf-asdf-4885a61efd4e@92a2e5a9-asdf-asdf-asdf-8af9712c16d5/27db93674c1449608bdb4dfb9b07beff/26a39c
32-7dcf-48b5-b24c-adda8d65d0c4
2022-09-21 19:04:52,052 - DEBUG - Loaded MSTeams URL: msteams://princessking/c...5/2...f/2...4/?image=yes&format=markdown&overflow=upstream&rto=4.0&cto=4.0&verify=yes
2022-09-21 19:04:52,052 - DEBUG - Using selector: EpollSelector
2022-09-21 19:04:52,053 - INFO - Notifying 1 service(s) asynchronously.
2022-09-21 19:04:52,054 - DEBUG - MSTeams POST URL: https://princessking.webhook.office.com/webhookb2/ccef6213-asdf-asdf-asdf-4885a61efd4e@92a2e5a9-asdf-asfd-asdf-8af9712c16d5/IncomingWebhook/27db93674c1449608bdb4dfb9b07beff/26a39c32-asfd-asfd-asfd-adda8d65d0c4 (cert_verify=True)
2022-09-21 19:04:52,056 - DEBUG - MSTeams Payload: {'@type': 'MessageCard', '@context': 'https://schema.org/extensions', 'summary': 'Apprise Notifications', 'themeColor': '#3AA3E3', 'sections': [{'activityImage': 'https://github.com/caronc/apprise/raw/master/apprise/assets/themes/default/apprise-info-72x72.png', 'activityTitle': 'Test Message', 'text': 'Test Body'}]}
2022-09-21 19:04:54,921 - INFO - Sent MSTeams notification.
which we see works just dandy
My next step will be to add the string to Github as a secret
then on the workflow YAML, add another replace. Because the teams string is basically “ORG/TOKENA/TOKENB/TOKENC”, we need to escape “/”
e.g.
princessking\/ccef6213-asdf-asfd-asfd-4885a61efd4e@92a2e5a9-asfd-asdf-asfd-8af9712c16d5\/27db93674c1449608bdb4dfb9b07beff\/26a39c32-asdf-asdf-asdf-adda8d65d0c4
then update the workflow
- name: Replace Secret Tokens in Main
run: |
sed -i 's/SENDGRIDTOKENHERE/$/g' main.py
sed -i 's/MSTEAMSTOKENHERE/$/g' main.py
and add a line to your python main.py
@app.route("/")
def hello_world():
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)
Now when I hit the URL https://hellofresh-q5jg7qcghq-uc.a.run.app/
I see an update in Teams
and I get an email
Costs
So far, the two Cloud Run instances have not cost much on my daily billing
If we look at Cloud Run, the first 180k vCPU seconds are free (fancy way of saying the first 50 hours) and we get 2m requests a month
This just means with two cloud run instances, I likely will start incurring charges (leaving them on all the time) in the next couple days
While I pulled out Cloud Build to use Github, the fact is that Cloud Build, for the smaller class instance, would be essentially free as the first two hours of use a day are free for Cloud Build.
Summary
We showed two easy ways to build and deploy a Cloud Run function written in python. We then moved the deployment of the container to a Github Action that would update Cloud Run with the new image. Next, we added Apprise and showed two different ways to setup Notifications (Email and Teams updates). Lastly, we talked briefly about costs.
The advantage of building a container that essentially has no outside dependencies (as you saw, we baked the credentials into the image at build time) is that we could use this for Cloud Run, Cloud Functions or even move to another cloud provider and run the container in AWS as a Lambda or Azure as an Azure Function. Moreover, all it would take is to create a basic deployment YAML to run this containerized service locally in our Kubernetes cluster.