Intro

I'm in the process of moving all my stuff from Azure to AWS. One of the things I currently run in Azure is a twitter bot that tweets every hour (it even made the local news a couple of years ago...).
Currently it's beeing triggered by a cron job on a VM. It has worked perfectly fine for 6 years now, but since I'm moving stuff and don't wan't to maintain a VM anymore, I decided to "convert it" to run in a AWS Lambda instead.

Solution

The bot is written in Python and has two pip dependencies, twitter and pytz.

Code changes

Before the code looked liked this:
tweet.py

from twitter import OAuth, Twitter
from datetime import datetime, time
import pytz, random

TOKEN = "SECRET"
TOKEN_KEY = "SECRET"
API_KEY = "SECRET"
API_SECRET = "SECRET"

MY_TZ = pytz.timezone('Europe/Stockholm')
now = datetime.now(MY_TZ).time()
retries = 0
def get_current_hour(timestamp):
    if timestamp.hour == 0 or timestamp.hour == 12:
        return 12
    return timestamp.hour % 12

def compose_tweet(timestamp):
    number_of_rings = get_current_hour(timestamp)
    # Creates a tweet, e.g. "BONG BONG BONG #03:00
    alert_sound = get_bell_sound(retries)
    tweet = " ".join([alert_sound] * number_of_rings)
    hashtag = "#%s:%s" %(str(timestamp.hour).zfill(2), str(timestamp.minute).zfill(2))
    return "%s %s" %(tweet, hashtag)

def send_tweet(tweet):
    global retries
    auth = OAuth(TOKEN, TOKEN_KEY, API_KEY, API_SECRET)
    t = Twitter(auth=auth)
    try:
        t.statuses.update(status=tweet)
        retries = 0
    except:
        retries += 1
        if retries <= 7:
            main()
        else:
            raise

def get_bell_sound(index):
    sounds = ('BONG', 'DONG', 'DING', 'BING-BONG', 'RING', 'PING', 'JINGLE', 'DING-DONG')
    return sounds[index]

def main(timestamp = now):
    send_tweet(compose_tweet(timestamp))

if __name__ == "__main__":
    main()

To make it "Lambda compatible", I only needed to add the following method

def lambda_handler(event, context):
    main()

The lambda_handler function is called by the lambda runtime. You get access to both the event that triggered the function and also a context. I don't need any of it though.

Packaging

Here the documentation lacked a little when it comes to how to deploy a python function with external (pip) dependencies. It wasn't that hard to figure out but it took me like 30 minutes that I could have spent on something else, so that's why I'm writing this post :).

Lambda supports uploading a zip file, so let's create one. It needs to contain the pip packages and the entrypoint, in our case the entrypoint is tweet.py.

The easiest way dealing with PIP imo is to have a requirements.txt file, so let's create one.

requirements.txt

twitter==1.18.0
pytz==2018.5

Here we have specified which packages and what version to use. I use quite old packages, I don't know if newer versions work and I don't have time to try it out either, so bare with me.

After creating a requirements.txt, we can install the packages to the current folder like this:

pip install -r requirements.txt -t .

This will create the following folder structure:
folderstructure
Now the only thing left to do is create the zip file.
I added the following files/folders to the zip file:
zipfile
Then I uploaded it by choosing Upload a .zip file
upload-aws
The UI will now look like this:
aws-uploaded
Here I've also specified the entrypoint for the function by entering tweet.lambda_handler in the Handler input field (tweet is the name of the file and lambda_handler is the name of the method).

Scheduling/triggering

Now the only thing that's left is to make the function run every hour (08:00, 09:00 and so on).
It's really easy, all we need to do is add a CloudWatch Event Trigger with the following schedule expression: cron(0 * * * ? *).

That's it, we're now tweeting from the (aws) cloud!

BONG BONG BONG BONG BONG BONG BONG BONG BONG BONG BONG BONG #12:00

— Domkyrkan (@skara_domkyrka) February 9, 2020