Getting started¶
You need Docker (with the compose plugin) and a clone of the repository, for the example.
1. Get the base image¶
The image is built from the official openresty/openresty:1.31.1.1-resolute. It adds gqlc,
libgql.so and the Lua modules. It holds no schema: a gateway is an image built from it.
2. Run the example gateway¶
examples/docker is a blog API served by two REST services. A mock backend plays both
services, and memcached is the shared cache.
git clone https://github.com/abusi/gqlgw && cd gqlgw/examples/docker
GQLGW_IMAGE=ghcr.io/abusi/gqlgw:0.2.0 docker compose up --build # or, after make image: docker compose up --build
flowchart LR
you([curl]) -- ":8080 /graphql" --> gw[gateway]
you -- ":8191 /gqlgw_metrics" --> gw
gw -- "users + posts" --> be["backend (mock)"]
gw <-.-> mc[(memcached)]
Query it:
curl -s localhost:8080/graphql -H 'Content-Type: application/json' \
-d '{"query":"{ posts(limit: 3) { title author { name } } }"}'
{"data":{"posts":[{"title":"Post 1","author":{"name":"User 2"}},{"title":"Post 2","author":{"name":"User 3"}},{"title":"Post 3","author":{"name":"User 4"}}]}}
The example turns on the debug headers (GQLGW_DEBUG_HEADERS=true), which show what happened
behind the scenes. Add -i to the curl command to see them:
X-GQL-Rounds: 2 # two rounds: posts, then their authors
X-GQL-Subrequests: 2 # two upstream calls
X-GQL-Batches: 1 # the three authors in one batch call
X-GQL-Cache: l1=0;l2=0;l3=0;miss=3 # three author lookups, none cached yet
X-GQL-Doc-Cache: miss # the query text was parsed and validated
Run it again: X-GQL-Subrequests: 1. The authors now come from the cache, which is shared
through memcached by every gateway that uses it.
Prometheus metrics are on the admin port:
3. Build your own gateway¶
A gateway is three files: your schema, your gqlgw.toml, and a three-line Dockerfile.
# A gateway image: your schema and configuration on top of gqlgw.
#
# docker build -t my-gateway .
# docker run -p 8080:8080 -p 8191:8191 \
# -e USERS_URL=http://users:8080 -e POSTS_URL=http://posts:8080 my-gateway
# the base image: gqlgw:latest (make image), or a release: ghcr.io/abusi/gqlgw:<version>
ARG GQLGW_IMAGE=gqlgw:latest
FROM ${GQLGW_IMAGE}
COPY schema.graphqls gqlgw.toml /etc/gqlgw/
# the schema is compiled, and the configuration checked, when the image is built
RUN gqlc compile -c /etc/gqlgw/gqlgw.toml /etc/gqlgw/schema.graphqls
# gqlgw configuration (TOML 1.1).
#
# String values may use ${VAR}, ${VAR:-default} and ${VAR:?message}: they are
# read from the environment when the container starts (gqlc render).
[gateway]
listen = 8080
access_log = "/dev/stdout"
debug_headers = "${GQLGW_DEBUG_HEADERS:-false}"
[cache]
shm_size = "64m"
l2_ttl = 5
[cache.memcached]
host = "${MEMCACHED_HOST:-memcached}"
port = "${MEMCACHED_PORT:-11211}"
[metrics]
port = 8191
exclude_operations = ["^IntrospectionQuery$"]
# one section per @fetch(source: "...") of the schema
[sources.users]
url = "${USERS_URL:?set USERS_URL to the users service, for example http://users:8080}"
[sources.posts]
url = "${POSTS_URL:?set POSTS_URL to the posts service, for example http://posts:8080}"
forward_headers = ["Authorization", "Accept-Language"]
"""
Example schema: a blog served by two REST services (users, posts).
"""
schema {
query: Query
mutation: Mutation
}
type Query {
"A user by id."
user(id: ID!): User @fetch(source: "users", path: "/users/{args.id}")
"Latest posts."
posts(limit: Int = 20, tag: String): [Post!]!
@fetch(source: "posts", path: "/posts?limit={args.limit}&tag={args.tag}")
"Search anything."
search(q: String!): [SearchResult!]! @fetch(source: "posts", path: "/search?q={args.q}", select: "results")
}
type Mutation {
renameUser(id: ID!, name: String!): User
@fetch(source: "users", method: "PATCH", path: "/users/{args.id}", body: "{\"name\": {args.name}}")
createPost(input: PostInput!): Post
@fetch(source: "posts", method: "POST", path: "/posts", body: "{args.input}")
}
interface Node {
id: ID!
}
type User implements Node @cache(ttl: 300) {
id: ID!
name: String!
email: String @deprecated(reason: "Use contact.email")
createdAt: String @from(key: "created_at")
posts: [Post!]! @fetch(source: "posts", path: "/posts?author={parent.id}")
}
type Post implements Node {
id: ID!
title: String!
body: String
tags: [String!]!
authorId: ID! @from(key: "author_id")
author: User!
@fetch(source: "users", path: "/users/{parent.authorId}")
@batch(path: "/users?ids={keys}", match: "id", max: 50)
}
union SearchResult @discriminator(key: "kind") = User | Post
input PostInput {
title: String!
body: String
tags: [String!] = []
status: Status = DRAFT
}
enum Status {
DRAFT
PUBLISHED
ARCHIVED @deprecated
}
docker build --build-arg GQLGW_IMAGE=ghcr.io/abusi/gqlgw:0.2.0 -t my-gateway .
docker run --read-only --tmpfs /tmp -p 8080:8080 -p 8191:8191 \
-e USERS_URL=http://users:8080 -e POSTS_URL=http://posts:8080 my-gateway
In your own repository, write the base image directly: FROM ghcr.io/abusi/gqlgw:0.2.0.
What happens at each step:
flowchart LR
subgraph build ["docker build"]
sdl[schema.graphqls] --> compile["gqlc compile -c"]
toml[gqlgw.toml] --> compile
compile --> gqlb["schema.gqlb<br/>schema.routes.lua"]
end
subgraph start ["docker run"]
env(["environment<br/>USERS_URL, ..."]) --> render["gqlc render"]
render --> conf["gqlgw.conf<br/>gqlgw.lua"]
conf --> t["openresty -t"] --> run["openresty"]
end
gqlb --> render
- An invalid schema, or a
@fetch(source: "x")without a[sources.x], failsdocker build. - A missing variable (
${USERS_URL:?...}) stops the container at start, with the message written in the TOML.
Next steps¶
- Build and deploy pipeline: what each command checks and produces.
- SDL directives and gqlgw.toml: the references.
- Existing nginx / OpenResty: run gqlgw without Docker.