Python + Terraform + Lambda + USD$ 0 = Twitter bot!

Alejandro Baltra
5 min readOct 22, 2020
Kinda like this

I’m sure you are all familiar with the classical inspirational-quote-over-nice-photograph template that became super popular with Tumblr’s rise. What if I told you there’s a bot out there doing exactly this, automatically creating and publishing them, all for absolutely zero dollars!

The Setup

Let’s start by defining how this is gonna work:

  • Get a picture from somewhere
  • Get a quote to slap on top of it
  • Publish it somewhere. I decided to put them on Twitter because why not.
  • Be as decent as possible, and by this I mean Terraform for all of our AWS resources and have a way to deploy it on one command.

After deciding to write this in Python (in part because I really like it but also because it’s one of the supported languages by AWS Lambda), it was time to find out where we are gonna be getting our pictures and quotes from. We could write our own inspirational text and go out into the wild and get some fantastic footage, but that requires creativity and effort. Let’s stay away from those.

With some googling I came by Lorem Picsum, which is meant to fill the same role as Lorem Impsum does, but for text. And they have an API! Awesome, image source found.

Next step are the quotes. I tried a few, and ended up going for Quotable because the barrier to entry is pretty much non existent and we don’t have to worry about API keys or rate limiting. We won’t be spamming it anyway.

Finally, we need to get a Twitter Developer Account, and register our application there. This will give us the credentials we’ll need to interact with Twitter using our code.

The code

With all the requirements gathered, let’s put some code behind it. We’ll use Tweepy to communicate with Twitter’s API, plain old requests for the photos and quotes API and Pillow to merge text and image and make our final inspirational poster.

Maybe the most involved parts are the creation of the poster and the posterior upload to Twitter.

For the image creation we need to make sure the text fits in the image and for that we have to calculate the full length of the quote. We also have to merge both quote and image and save them somewhere for later upload. Luckily there’s the /tmp folder and your Lambda Function is guaranteed to have access to it.

def be_aesthetic():
image = get_image()
inspo = get_inspo()
print(“Being aesthethic”)# well need to wrap long texts
wrapped_inspo = textwrap.wrap(inspo[‘content’], 50)
img = Image.open(BytesIO(image))
font = ImageFont.truetype(‘Caveat-Regular.ttf’, 40)
color = ‘rgb(255, 255, 255)’
draw = ImageDraw.Draw(img, ‘RGBA’)
# make sure it fits
base_y = random.randint(50, 550–45 * len(wrapped_inspo) — 45)
draw.rectangle([(0, 0), (800, 600)], (0, 0, 0, 125))
draw.text((50, base_y), “\n”.join(wrapped_inspo), fill=color, font=font)
draw.text((350, base_y + 45 * len(wrapped_inspo)), f”- {inspo[‘author’]}”, fill=color, font=font)
img.save(‘/tmp/aesthetic.jpg’)return inspo[‘author’]

Uploading to Twitter is pretty much just calling Tweepy with the right parameters:

def share_inspo(author):
auth = tweepy.OAuthHandler(
os.getenv(“TWITTER_CONSUMER_KEY”),
os.getenv(“TWITTER_CONSUMER_SECRET”)
)
auth.set_access_token(
os.getenv(“TWITTER_ACCESS_TOKEN”),
os.getenv(“TWITTER_ACCESS_TOKEN_SECRET”)
)
api = tweepy.API(auth)
print(“Tweeting”)
author = author.replace(“ “, “”).replace(“‘“, “”)
media = api.media_upload(“/tmp/aesthetic.jpg”)
api.update_status(media_ids=[media.media_id], status=f”#inspiration #quote #{author}”)

The code itself is pretty self explanatory and publicly accessible in Github. Feel free to go in and take a peek.

The infrastructure

We will be running this as a Lambda function in AWS once a day at some hour. This means that we’ll also need a way to trigger it and for that we’ll rely on Cloudwatch Event Rules, which is a glorified crontab for AWS.

The terraform for this is quite simple

resource “aws_cloudwatch_event_rule” “aesthetic_cw_event” {
name = “send-inspo”
description = “Tweets inspiration quotes every 24 hrs”
schedule_expression = “cron(0 22 * * ? *)”
}
resource “aws_cloudwatch_event_target” “tweet_every_day” {
rule = aws_cloudwatch_event_rule.aesthetic_cw_event.name
target_id = “lambda”
arn = aws_lambda_function.aesthetic_lambda.arn
}

The one caveat here would be that we need to give the Cloudwatch event permissions to trigger our Lambda function:

resource “aws_lambda_permission” “allow_cloudwatch_to_call_lambda” {
statement_id = “AllowExecutionFromCloudWatch”
action = “lambda:InvokeFunction”
function_name = aws_lambda_function.aesthetic_lambda.function_name
principal = “events.amazonaws.com”
source_arn = aws_cloudwatch_event_rule.aesthetic_cw_event.arn
}

With our rule in place, let’s look at our Lambda definition

resource “aws_iam_role” “iam_for_lambda” {
name = “iam_for_lambda”
assume_role_policy = <<EOF
{
“Version”: “2012–10–17”,
“Statement”: [
{
“Action”: “sts:AssumeRole”,
“Principal”: {
“Service”: “lambda.amazonaws.com”
},
“Effect”: “Allow”,
“Sid”: “”
}
]
}
EOF
}
resource “aws_lambda_function” “aesthetic_lambda” {
filename = “${path.root}/../dist/function.zip”
function_name = “so-aesthetic”
role = aws_iam_role.iam_for_lambda.arn
handler = “main.handler”
source_code_hash = filebase64sha256(“${path.root}/../dist/function.zip”)runtime = “python3.8”
timeout = 60
environment {
variables = {
TWITTER_CONSUMER_KEY = var.TWITTER_CONSUMER_KEY,
TWITTER_CONSUMER_SECRET = var.TWITTER_CONSUMER_SECRET,
TWITTER_ACCESS_TOKEN = var.TWITTER_ACCESS_TOKEN,
TWITTER_ACCESS_TOKEN_SECRET = var.TWITTER_ACCESS_TOKEN_SECRET
}
}
}

Cool! Notice we are referencing a file name in the dist folder. We’ll be creating it through our deployment process in a bit, no sweat.

The deployment

I really didn’t want to be updating stuff in a console and anything more complicated than one step I would definitely forget, so we have to keep it simple. Thank God Makefiles exist.

Let’s create simple one that will package our Python code. And just for shits and giggles it will also run our Terraform. Yes, I am using TF to handle the function code as well; I am very aware there are better and smarter ways to do it, but in all honestly, we don’t need better nor smarter ;)

SHELL := /bin/bashbuild:
python3.8 -m virtualenv src/venv; \
source src/venv/bin/activate; \
pip install -r src/requirements.txt -t dist/.; \
cp src/main.py dist/; \
cp src/Caveat-Regular.ttf dist/; \
cd dist; \
zip -r function.zip .;
deploy:
terraform init terraform; \
terraform apply -var-file=terraform/env.auto.tfvars terraform;
clean:
rm -rf dist/*
all: build deploy clean

The build process sets up a virtualenv where it can install all the dependencies and then zips everything together. Whenever you use extra dependencies in a Lambda Function, you need to package them as part of your function ZIP. This will mean you probably won’t be able to use the Web UI to edit your Lambda code, but then again, why would you?

Fat-shaming my Lambda. Not cool, AWS.

With this, getting the latest version of our code deployed is as simple as running make all

Conclusion

This little bot has been running in AWS for about 3 months, costing exactly $0 dollars.

Literally nothing

And it took an afternoon to make. It was pretty fun.

Make sure you follow Inspobot for that daily dose of inspiration to brighten your day!

--

--

Alejandro Baltra

Chilean Software Developer living in the USA. Se habla español.