Heroku is a webservice where users can run simple web applications for free for a limited number of hours per month. The obvious approach would be to run Functions as a Service to match resource supply from the server side and resource requirement from the end users' side. Such an implementation of a serverless application "sleeps" as long as there is no need for it. Once a user requests the function's results, the function is started and consumes only resources during its execution. However, Heroku does not offer the option to deploy serverless applications off-the-shelf to bill the customer per second.
In the following tutorial, we describe a way on how to use Heroku's one-off dynos, which are usually not addressable via HTTP requests, to process Functions as a Service with arguments provided via environment variables.
- Prerequisites
- Architecture
- Create One-off Dyno for Serverless Processing
- Trigger HTTP Request
- Retrieve Log Session
- Use With Frontend
- Copyright and License
To complete this tutorial, you will need:
- Around 15 minutes
- Free account on heroku.com and access to it through web browser
- Working installation of Heroku CLI where you are already logged in
- Shell with
curl
- Some basic understanding of programming languages
We want to create a serverless function on Heroku which only starts and executes some code when it is requested. Therefore, it should not consume any resources when it is not used. Applications on Heroku are managed within app containers which are called dynos. One dyno configuration is the one-off dyno which basically has the required functionality for a Function as a Service. However, one-off dynos are not addressable via HTTP requests and we want to show how to circumvent this issue.
We want to create a post request via Heroku's Platform API to start a one-off dyno on which a simple Python script reads the environment variables provided by the post request. These environment variables can be considered as the function's arguments. If the functions return value is required, it can be read from the function logs.
To show that the caller of the function does not have to be in the same network or network region, we host a static website on GitHub which you can use to generate calls to your own one-off dyno. This static website creates a post request to invoke your one-off dyno and shows the logs.
Executing the tutorial does not result in any additional cost as a Heroku account does not cost any fee. Heroku offers some free computing resources which should be sufficient for this tutorial. However, if you request a very high amount of computing resources, be aware that Heroku might charge you some fees.
The heart of our serverless application is a one-off dyno which only starts and executes some code when it is requested.
The folder one-off-dyno includes a quite minimal setup required for a one-off-dyno.
- The
Procfile
file specifies to reach the dyno via the nameserverless
and what to execute on the command line when it is started. We decided to run a Python script. You can also implement some other code which finds to an end (no specific framework needed). - We chose to use a Python script for our processing logic which can be modified to suit your needs.
- As we chose to execute a Python script, we need have a
requirements.txt
. If there are no dependencies which the system has to install before executing the script, this file can also be empty.
The following paragraphs describe on how to implement these files and run them as a one-off dyno on Heroku.
The following function in pseudocode is an enhanced "hello world" version and should be run as a service. If we supply
a NAME
, it greets the name. Otherwise, it greets the world.
function hello_world(NAME)
if (NAME is set and non-empty) then
name = NAME;
else
name = 'World';
end if
return 'Hello ' + name + '!';
end function
The following Python implementation of the previous function does not use traditional function parameters. Instead, we have to request the values from the environment variables. We can also not use return statements and have to print the results to the console and read them from the logs later on. If we do not need any return values, we can even omit to read the function logs.
if 'NAME' in os.environ and len(os.environ['NAME'].strip()) > 0:
name = os.environ['NAME'].strip()
else:
name = 'World'
print('Hello ' + name + '!')
We add the Python implementation with the required import
statement (see example
for reference) to the new folder example-app
for the Heroku app.
To create a working solution on Heroku, we have to create a Procfile
in the folder example-app
to tell Heroku what
to do when we try to start our one-off dyno.
A Procfile
is quite simple: It should be called Procfile
and after an identifier, it tells Heroku what to execute
on the commandline. Our identifier is serverless
, this is how our one-off dyno can be reached later on. We then tell
Heroku to run our newly created Python script serverless-task.py
.
serverless: python serverless-task.py
Do not forget to also create an empty requirements.txt
in the app folder.
We now create an application in our Heroku account. At first, we have to initialise a Git repository with the programme
code as Heroku usually manages deployments with Git. We do this by simply initialising a Git repo in example-app
and
then adding and committing the code to it.
$ cd example-app
$ git init
$ git add .
$ git commit -m "Initial commit"
Since the names of all Heroku apps are in a global namespace, lots of names are already taken and we cannot suggest a name. The Heroku CLI can be used to easily create a Heroku app for an initialised Git repository with an available name. Besides an app with a random name on the Heroku platform, this command results in creating a Heroku remote for the Git repository, i.e. a remote version of the repository on Heroku's servers.
$ heroku create
When having a Git repository with the relevant programme code and a linked app on the Heroku platform, you just have to push the code to Heroku.
$ git push heroku master
Read more about pushing code to Heroku in Heroku's Dev Center.
Even though Heroku tries to find an appropriate buildpack and deploys the programme code, it cannot be reached via
the web address of this app as only dynos of the type web can receive HTTP requests. However, you can already try to
call the one-off dyno via the Heroku CLI: We just have to tell Heroku to run
the dyno which we defined in the
Procfile
.
$ heroku run serverless
If you implement the function from above, you will see Hello World!
on the console as we did not set any environment
variable.
Subsequently, we will explain how the request to trigger an HTTP request to the one-off dyno is composed. If you already feel confident enough with the Heroku CLI, you can skip this section and proceed with the final request.
The Heroku Platform API offers an option to create dynos with a POST request,
which can be used to start a one-off dyno. We just have to insert the name of the app for $APP_NAME
.
$ curl -X POST https://api.heroku.com/apps/$APP_NAME/dynos
This POST request on its own, however, would not succeed. We have to specify the API's version in the header.
$ curl -X POST https://api.heroku.com/apps/$APP_NAME/dynos \
-H "Accept: application/vnd.heroku+json; version=3"
Additionaly, we have to authenticate the caller (ourselves). One easy way of authentication is through an API key which
we get from the Heroku CLI. We directly store it in the variable $token
which we can then use as a Bearer token.
$ TOKEN=$(heroku auth:token)
$ curl -X POST https://api.heroku.com/apps/$APP_NAME/dynos \
-H "Accept: application/vnd.heroku+json; version=3" \
-H "Authorization: Bearer $TOKEN"
However, this request still does not specify which dyno to start. Similar to the command we ran on the Heroku CLI, we
also want to inform Heroku that it should run
a specific command. The command should be the dyno defined in the
Procfile
: serverless
. As we pass these data in JSON format, we also have to add this information to the header.
$ curl -X POST https://api.heroku.com/apps/$APP_NAME/dynos \
-H "Accept: application/vnd.heroku+json; version=3" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"command": "serverless",
"type": "run"
}'
Finally, we can also set the environment variables in the body of the request and can therefore achieve arguments of the function.
Request
$ curl -X POST https://api.heroku.com/apps/$APP_NAME/dynos \
-H "Accept: application/vnd.heroku+json; version=3" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"command": "serverless",
"type": "run",
"env": {
"NAME": "Daniela"
}
}'
Response
{
...
"name": "run.8361",
...
}
The result of the function, however, is not included in the response of the request. To get the result, we have to read the log of the dyno.
When we have invoked the command on Heroku, the API will respond with a JSON object containing the name of the dyno in which our command is being executed (see above).
To stream the log generated by the dyno, we can create a log session that will connect to the log stream of the
specified dyno and stream the result.
We include the parameters "source": "app"
and "tail": true
to specify that we only want the logs generated by the
command itself and that the ongoing logs should be streamed.
Request
$ curl -n -X POST https://api.heroku.com/apps/$APP_NAME/log-sessions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.heroku+json; version=3" \
-d '{
"dyno": "run.8361",
"source": "app",
"tail": true
}'
Response
{
...
"logplex_url": "<LOGPLEX_URL>",
...
}
The response from creating the log session will include a URL to the log session, specified under the key logplex_url
.
This URL can be opened in your browser or fetch it with curl
. It does not require additional authentication.
$ curl <LOGPLEX_URL>
As the caller does not have to be in the same network or network region, we implemented an example form on GitHub pages which you can use to try out your one-off dyno and see how Heroku can be used to implement serverless Functions as a Service. You just have to provide the name of your Heroku app, your Heroku API key, the name of your dyno and the name which should be used in the Python function above. It will return the logs of the app in which you can see the return value.
You can also see a description on how we managed to implement the calling site of the one-off dyno.
Thank you for reading this tutorial ⭐!
If you have valuable feedback you want to share, create a comment on this issue to inform us. If you have any questions or other thoughts, do not be afraid to create a new issue.
Copyright © 2021, Axel Pettersson and Felix Seifert
This tutorial is free. It is licensed under the GNU GPL version 3. That means you are free to use this tutorial for any purpose; free to study and modify this tutorial to suit your needs; and free to share this tutorial or your modifications with anyone. If you share this tutorial or your modifications, you must grant the recipients the same freedoms. To be more specific: you must share the texts and the source code under the same license. For details see https://www.gnu.org/licenses/gpl-3.0.html