loading...
Google Cloud

Beep boop: Scheduling a twitter bot with Cloud Functions and Cloud KMS encrypted secrets

glasnt profile image Katie McLaughlin ・Updated on ・7 min read

BeepBoop

Sometimes, you just need a scheduled task to make the machine go 'beep boop'. With Google Cloud Scheduler, you can make parts of Google Cloud go 'beep boop' in a timely, but secure, manner.


In this example, we'll be creating a Twitter bot that tweets daily. This is based on the real-world example @python2sunset.

We'll be using the following components:

  • Cloud Scheduler, which targets a
  • Cloud Pub/Sub topic with a specific payload, which
  • Cloud Functions listens to, using
  • Cloud KMS encrypted environment variables.

In our example, we'll be posting to a Twitter account. You'll need a set of API keys to use with Twitter, which you can get by applying for a developer account and registering your app.


Table of contents:


Example code: beepboop

Let's work backwards through this list and start with the function:

This function accepts an event and context, which our Pub/Sub message payload will supply. We haven't created this payload yet, but we can make it anything we want. In this case, we're going to use a dictionary:

{"tweet": "🤖"}

Our event data will be a base64 encoded representation of this string, which we can decode and then safely parse to extract our data. We're only continuing on with our processing if we see a specific payload (a dictionary with a "tweet" key). Using this method we can tell our function to tweet any string, functionality we will mention later.

This function also has a third-party package dependency (python-twitter) which we'll need to ensure we include in our provisioning.


Configuring our actuators

Now we have our function code, we need the parts that make our function run (the 'actuators', "a device that causes a machine or other device to operate"). For that, we'll need to create a Pub/Sub topic and Scheduler job that will trigger our function. We'll name all of these literally, prepending our function name.

For each of these components, we'll link to the related section of the Google Cloud Platform Console with an an example screenshot, and the equivalent gcloud command. You can choose to use either or.

For the topic, we'll need a new Pub/Sub topic called beepboop-topic:

Creating a topic

gcloud pubsub topics create beepboop-topic 

For the job, we'll need a new Cloud Scheduler job:

  • called beepboop-job,
  • on a daily frequency (following cron standards),
  • in Greenwich Mean Time (GMT),
  • targeting Pub/Sub and the beepboop-topic topic we just created,
  • with our aforementioned payload {"tweet": "🤖"}

Creating a Cloud Scheduler Job

gcloud scheduler jobs create pubsub beepboop-job \
    --schedule "0 0 * * *"  \
    --topic beepboop-topic \
    --message-body "{'tweet': '🤖'}"

The schedule "0 0 * * *" means "daily at midnight" (minute 0, hour 0, every day of the month, every month of the year, and every day of the week).

Given our function passes through any value of "tweet", we could setup:

  • Tweeting "🤖" at midnight, and
  • tweeting "👾" at midday

by creating a second scheduled job, targeting the same topic, with these different payload values. This allows us to keep the one function code that performs multiple tasks ✨


Creating encrypted secrets

Before we get to the function, we need to do something about those four environment variables. They are super secret values that if exposed can have nasty consequences. In our case, people can tweet as us, access our account, and our direct messages.

So, we need to encrypt them. For this, we'll use Cloud KMS for encryption and decryption. There are other ways to do secrets in Google Cloud. If you're using another part of Google Cloud such as App Engine or Cloud Run, berglas (before v0.5.0) simplifies this process by wrapping around Cloud KMS. Newer versions of berglas use Secret Manager (a topic for another post).

We're going to create a key on a keyring, then encrypt our secrets, add these as environment variables for our function via the console, and decrypt them at run time.

First, we create a keyring to store our key on.

Creating a KMS Key Ring

gcloud kms keyrings create beepboop-keyring --location global

Then a key on our keyring:

Creating a KMS Key

gcloud kms keys create beepboop-key \
    --purpose encryption \
    --keyring beepboop-keyring \
    --location global

From here, we can encrypt our secrets. For this, we'll need the combination keyring/key identifier known as the resource. For our key, this resource will be projects/beepboop-project/locations/global/keyRings/beepboop-keyring/cryptoKeys/beepboop-key


For any project, location, keyring and key, this will be:

projects/PROJECT/locations/LOCATION/keyRings/KEYRING/cryptoKeys/KEY

You can generate the specific resource identifier for your key by:

  • going to the Key on the Keyring in the Google Cloud Platform Console, clicking the "⋮" icon, and selecting "Copy Resource ID"
  • using the gcloud CLI
    • gcloud kms keys list --location global --keyring beepboop-keyring --format="value(name)"

Now we have our key, we can encrypt our values. Since we have four values to encrypt, and later decrypt, we need to consider the most efficient way to do this.

To make our application twelve factor, we can use four separate environment variables. For each of our secret values, we can encrypt them using the same key, then decrypt them in our function. For the command-line deployment method, we can put all these values into a yaml file, which will then be processed for us. For the console method, we will have to copy the values in separately.

For ease of ensuring we don't get any encoding issues, since we're using strings, we'll use a method similar to our reference article, using base64 encoding -- but processing all the keys in bulk.

Using this small code snippet, we can take a yaml file of key-value pairs, and encrypt only the value, and returning a new set of key-encryptedvalue pairs:

Given a yaml file secrets.yaml, which contains one KEY_RESOURCE_NAME of your resource identifier, and any other number of key-value pairs, it will encrypt all of them (apart from the KEY_RESOURCE_NAME itself), and output the result into a secrets.yaml.enc file (the .enc extension is just a flag to us that this file is the encrypted version)

$ cat secrets.yaml
KEY_RESOURCE_NAME: "projects/beepboop-project/locations/global/keyRings/beepboop-keyring/cryptoKeys/beepboop-key"
CONSUMER_TOKEN: AAAAAAAAAAAAAAAAAAAAAAAA
CONSUMER_SECRET: YIJJJjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjj
ACCESS_TOKEN: 1111111111111111111-0oaaaaaaaaaaaaaaaaaaaaaaaa
ACCESS_SECRET: z4hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh

$ pip install google-cloud-kms pyyaml
$ python encrypt.py

$ cat secrets.yaml.enc
ACCESS_SECRET: CiQAgOOBPVxIGOZTU49QtgTmQwNKPS2qiXw6tLAWFRzaFy3WZy8S...==
ACCESS_TOKEN: CiQAgOOBPeaDhy/NP3rYYP5hc9yJnZZvw+kiIHMdbXWWQhknBQwS...==
CONSUMER_SECRET: CiQAgOOBPQm49gQd+N34gd9NyM7GoY8y//3n5qEqsbbLwxEVEv4SWAb...==
CONSUMER_TOKEN: CiQAgOOBPSp0zXOkD8q1+1tlIA7kWua0CkzncZapHOxhjIsRCS4SQgAb...==
KEY_RESOURCE_NAME: projects/python2sunset-countdown/locations/global/keyRings/beepboop-keyring/cryptoKeys/beepboop-key

Our keys may change order, but since we're referencing them by key name, the order doesn't matter.


Side note: if you want to save this setup for later in a git repo, do not commit these files. There is no point making these values secret only to share them on the public internet 😫. To ensure you can't accidentally add them, create a .gitignore file and add "secrets.yaml*" so you don't commit them. To ensure glcoud ignores these files too, add !include:.gitignore to your .gcloudignore file. This will ignore the contents of .gitignore!


To decrypt these secrets, we'll need to adjust our original function:

We'll also need to make sure we update our requirements.txt:

We'll also need to give our function permission to decrypt using this key. (We as an admin user have rights to do this, but our function, by default, does not have permission).

gcloud kms keys add-iam-policy-binding beepboop-key \
    --location global \
    --keyring beepboop-keyring \
    --role roles/cloudkms.cryptoKeyDecrypter \
    --member serviceAccount:beepboop-project@appspot.gserviceaccount.com

The serviceAccount here is the default service account for Cloud Functions. In our local python scripts and in the gcloud command line we act as an admin user, so we have permission to do a lot of things. Our function doesn't (and really shouldn't) have all the permissions we the admin have, so we need to be explicit.


Deploying our function

And finally for the function, we'll need a new Function:

  • called beepboop-function,
  • triggered on the topic beepboop-topic,
  • using our main.py and requirements.txt files from earlier,
  • executing the function beepboop, and
  • specifying our environment variables

Creating the function

Environment Variables

Important note: for the --source flag you specify a folder, and everything apart from the files mentioned in .gcloudignore are uploaded. Since we've been playing with secrets files we want to super make sure we don't upload anything we don't want to. So we should create a folder containing only our code and requirements. Put the main.py and requirements.txt files in their own code/ folder and the gcloud command below will work.

gcloud functions deploy beepboop-function \
    --trigger-topic beepboop-topic \
    --runtime python37 \
    --entry-point beepboop \
    --source code/ \
    --env-vars-file secrets.yaml.enc \
    --no-allow-unauthenticated

Both these methods will result in the same five environment variables.


To test this function, you can go back to the Cloud Scheduler, and click "Run Now":

Cloud Scheduler Job Listing

gcloud scheduler jobs run beepboop-job

If everything went to plan, we should have a response on twitter:

Beep boop tweet

Beep boop!

Discussion

pic
Editor guide