CloudRun Python Github and Apprise Notifications

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

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

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

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

At the first invokation, there is not much to see in metrics

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

If I look at the YAML

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

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.

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

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.

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

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

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

I’ll add a secret, GCLOUD_SERVICE_KEY, that is a GCP Service Account JSON, base64 encoded

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

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

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

I can verify by checking GCR

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

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

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

And we have it in GCR just fine

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

Unfortunately our SA doesnt have Cloud Run abilities

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

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

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

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

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

      - 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

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

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:

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

We can do a bit of cleanup on our GCR images as well.

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

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

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

Additionally, if I check my Cloud Run, I can see in the YAML it is using the updated image

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

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

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

Though in trying to hit the endpoint, I kept getting an error

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

I realized a mistake that was preventing it from running was a lack of authentication.

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

To fix this - to allow unauthenticated users to invoke the cloud run function - I went to the Permissions section

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

Then Added “allUsers” and for the role, chose Cloud Run Invoker

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

which prompted about “Allow Public Access”

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

I now see it’s switched back to “Allow unauthenticated”

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

Now upon reload, I can reach it

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

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&nbsp;Body'}]}
2022-09-20 22:17:34,937 - INFO - Sent SendGrid notification to isaac.johnson@gmail.com.

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

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".

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

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

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

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

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

Now when i hit https://hellofresh-q5jg7qcghq-uc.a.run.app/ I get an email

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

Teams

We can go to “connectors” on a teams channel

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

the configure an “incoming webhook”

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

and give it an image and name

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

which will give us our webhook url

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

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

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

My next step will be to add the string to Github as a secret

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

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

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

and I get an email

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

Costs

So far, the two Cloud Run instances have not cost much on my daily billing

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

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

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

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.

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

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.

gcp serverless cloudrun github apprise

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

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

Theme built by C.S. Rhymes