Intro to Lua and Openresty, Part 1: Hello World Examples

Posted on March 2, 2017

This post is part one of my “Intro to Lua and Openresty” series. For more info on the series (including scope, research notes, and the usual disclaimers), see the introductory post.

Execution environment

While there are a few ways you can run Lua code, we want to minimize the setup and administrative overhead. Docker helps keep the localhost clean, and the openresty images are a great way to get a fully functional lua environment. This set of images would also serve as a great base image when we get to building our own (later in the series). These images include luajit, which can run arbitrary lua code as an interpreted script. They also provide several flavors of the image, including a release on Alpine. Lua distributes modules with luarocks (package manager), and this is available on the alpine-fat flavor.

Hello World! in Lua

The canonical Hello World! in Lua is ridiculously simple:

ᐅ docker run -it --entrypoint /bin/sh openresty/openresty:alpine
/ # echo 'print("hello world!")' > hello.lua
/ #
/ # /usr/local/openresty/luajit/bin/luajit hello.lua
hello world!

So simple in fact, I skipped including it in the git repo with example code for the series. But there you have it :)

Hello World! in Openresty

OK, with that out of the way, let’s get into Openresty! We will start with the HTML version, and then do one with JSON.

the HTML Version

For these examples, we use content_by_lua to keep it simple and embed the lua code directly into nginx.conf:

worker_processes 1;
error_log error.log;
events {
    worker_connections 1024;
}
http {
    server {
        listen 8000;
        location / {
            default_type text/html;
            content_by_lua '
                ngx.say("<p>hello world!</p>")
            ';
        }
    }
}

ngx.say is provided by openresty and available in the nginx environment by default, so there is nothing to “import”.

If you are following along with the git repo, find the example here.

Let’s run it with the openresty:alpine docker image. We’ll use --volume to “mount” the nginx.conf into the container:

ᐅ docker run --name lua --rm --volume `pwd`:/usr/local/openresty/nginx/conf/ -p 127.0.0.1:8000:8000 openresty/openresty:alpine
nginx: the configuration file /usr/local/openresty/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/openresty/nginx/conf/nginx.conf test is successful

Let’s check it out:

ᐅ curl localhost:8000
<p>hello world!</p>

Yay!

Hello World - JSON Version

The nginx.conf this time…

worker_processes  1;
error_log error.log;
events {
    worker_connections 1024;
}
http {
    server {
        listen        8000;
        charset       utf-8;
        charset_types application/json;
        default_type  application/json;
        location / {
            content_by_lua '
                local cjson = require "cjson"
                ngx.status  = ngx.HTTP_OK
                ngx.say(cjson.encode({ status = true, foobar = "string" }))
                return ngx.exit(ngx.HTTP_OK)
            ';
        }
    }
}

Break down the Code

If you are following along with the git repo, find the example here.

Note that we’ve updated the content type spec to application/json. This is an nginx thing. It is nice that Openresty builds on the standard Nginx config directives.

This example uses content_by_lua to embed the lua code directly in the nginx.conf. There is also a directive that can load the lua code from a file, but for simplicity and clarity, the examples in this series will keep the code in nginx.conf.

cjson is a lua module for encoding and decoding JSON data. There are a couple other options, but this is included in openresty, so there is no need to install a module with luarocks (we’ll get there in time).

local cjson = require "cjson" is an import of sorts. You may also see examples that use require("cjson") instead. I am not sure which is more common, and it seems to be an arbitrary stylistic detail.

The JSON data we return in this example is { status = true, foobar = "string" }. Note the use of " for the string, while true is recognized as a boolean type. More info on cjson can be found in the lua-cjson manual.

Let’s see it run!

We use the same openresty docker image to run the JSON hello world:

ᐅ cd examples/02-hello-world-json
ᐅ docker run --name lua --rm --volume `pwd`:/usr/local/openresty/nginx/conf/ -p 127.0.0.1:8000:8000 openresty/openresty:alpine

Check it out…

ᐅ curl -i localhost:8000
HTTP/1.1 200 OK
Server: openresty/1.11.2.2
Date: Thu, 02 Mar 2017 15:43:20 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive

{"status":true,"foobar":"string"}

Drop into the docker container to debug…

If you need to debug, it’s nice to see what’s in the error log, for example:

ᐅ docker exec -it lua /bin/sh
/ #
/ # cat /usr/local/openresty/nginx/error.log
2017/03/02 15:40:13 [error] 5#5: *1 lua entry thread aborted: runtime error: content_by_lua(nginx.conf:18):3: attempt to index global 'cjson' (a nil value)
stack traceback:
coroutine 0:
        content_by_lua(nginx.conf:18): in function <content_by_lua(nginx.conf:18):1>, client: 172.17.0.1, server: , request: "GET / HTTP/1.1", host: "localhost:8000"

If you change the nginx.conf while Nginx is running, you can reload nginx to refresh the config in memory:

ᐅ docker exec -it lua /usr/local/openresty/nginx/sbin/nginx -s reload
2017/03/02 15:47:19 [notice] 7#7: signal process started

Makefile

We can combine the shell commands above into a Makefile to establish and codify a workflow for development and testing to add or modify code, eg:

run:
        docker run --name lua --rm --volume `pwd`:/usr/local/openresty/nginx/conf/ -p 127.0.0.1:8000:8000 openresty/openresty:alpine

reload:
        docker exec -it lua /usr/local/openresty/nginx/sbin/nginx -s reload

error-logs:
        docker exec -it lua cat /usr/local/openresty/nginx/error.log
logs:
        docker logs lua

get:
        curl -i localhost:8000

Using these make targets would look like:

ᐅ make run
docker run --name lua --rm --volume `pwd`:/usr/local/openresty/nginx/conf/ -p 127.0.0.1:8000:8000 openresty/openresty:alpine
ᐅ make get
curl -i localhost:8000
HTTP/1.1 200 OK
Server: openresty/1.11.2.2
Date: Thu, 23 Mar 2017 07:16:24 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive

{"status":true,"foobar":"string"}
ᐅ make logs
docker logs lua
172.17.0.1 - - [23/Mar/2017:07:16:24 +0000] "GET / HTTP/1.1" 200 45 "-" "curl/7.35.0"
ᐅ make reload
docker exec -it lua /usr/local/openresty/nginx/sbin/nginx -s reload
2017/03/23 07:17:20 [notice] 10#10: signal process started

Next Steps

Continue on to Part 2: JSON/POST Processing in Openresty.