GraphQL Federation

Introduction

Apollo Federation provides a mechanism to combine multiple GraphQL endpoints and schema into a single aggregate endpoint and composite schema. The basic principles of GraphQL federation are as follows:

  1. Collects the schemas from multiple endpoints.

  2. Combines the schemas into a single composite schema.

  3. Serves the single composite schema to the user completely hiding the inner dependencies.

  4. When a query is performed, the Federation Gateway calls each of the endpoints in a specific order and combines their responses.

Quick Start

Create a docker-compose.yaml that manages all the required services for an aggregate federated SWAPI GraphQL endpoint that joins Semantic Objects and a simple example Apollo Server:

  • Federation Gateway

  • Semantic Objects service (Loaded with SWAPI data and Schema)

  • Example Extended SWAPI Service

  • GraphDB (Loaded with SWAPI data)

  • MongoDB (for storing and managing the schema)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
version: '3.6'

services:

  extend-example:
    image: ontotext/platform-example-starwars-federation-service:3.1.4
    restart: always
    ports:
      - "4006:4006"

  apollo-gateway:
    image: ontotext/apollo-federation-gateway:3.1.4
    restart: always
    depends_on:
      - semantic-objects
      - extend-example
    environment:
      CACHE_MAX_AGE: '5'
      ENGINE_API_KEY: '...'
      SERVICE_DWELL: 10000
      POLL_INTERVAL: 30
      SECURITY_ENABLED: "false"
      # Service 1: semantic-objects
      GRAPHQL_SERVICE_0: "http://semantic-objects:8080/graphql"
      GRAPHQL_SERVICE_0_GTG: "http://semantic-objects:8080/__gtg"
      GRAPHQL_SERVICE_0_HEALTH: "http://semantic-objects:8080/__health"
      # Service 2: extend-example
      GRAPHQL_SERVICE_1: "http://extend-example:4006/graphql"
      GRAPHQL_SERVICE_1_GTG: "http://extend-example:4006/__gtg"
      GRAPHQL_SERVICE_1_HEALTH: "http://extend-example:4006/__health"
    ports:
      - 9000:80

  semantic-objects:
    image: ontotext/platform-soaas-service:3.1.4
    restart: always
    depends_on:
      - mongodb
      - graphdb
    environment:
      soml.storage.mongodb.endpoint: "mongodb://mongodb:27017"
      soml.storage.mongodb.database: "soaas"
      rbac.storage.mongodb.endpoint: "mongodb://mongodb:27017"
      rbac.storage.mongodb.database: "soaas"
      sparql.endpoint.address: "http://graphdb:7200/"
      sparql.endpoint.repository: "soaas"
      sparql.endpoint.executionMode: "subquery"
      sparql.optimizations.mutationMode: "READ_WRITE"
      graphql.mutation.enabled: "true"
      graphql.mutation.generation.options.ExpressionsDataGenerator.enabled: "true"
      graphql.federation.enabled: "true"
      security.enabled: "false"
      security.secret: "XXXXXXX"
      soml.healthcheckSeverity: "HIGH"
      storage.location: "/var/lib/soaas"
      platform.license.file: "/PLATFORM.license"
    volumes:
      - soaas_data:/var/lib/soaas
      - $PLATFORM_LICENSE_FILE:/PLATFORM.license:ro
    ports:
      - "9995:8080"

  graphdb:
    image: ontotext/graphdb:9.1.0-ee
    restart: always
    ports:
      - "9998:7200"
    environment:
      GDB_JAVA_OPTS: >-
        -Xmx2g -Xms2g
        -Denable-context-index=true
        -Dentity-pool-implementation=transactional
        -Dhealth.max.query.time.seconds=60
        -Dgraphdb.append.request.id.headers=true
        -Dgraphdb.workbench.importDirectory=/opt/graphdb/home/graphdb-import
    volumes:
      - graphdb_data:/opt/graphdb/home
      - ${GDB_LICENSE_PATH}:/opt/graphdb/home/conf/graphdb.license:ro

  mongodb:
    image: mongo:4.0
    restart: always
    hostname: mongodb
    ports:
      - "9997:27017"
    volumes:
      - mongodb_data:/data/db
    command: mongod --bind_ip_all --replSet soaas
    stop_grace_period: 30s

  graphiql:
    image: ontotext/platform-graphiql:3.1.4
    restart: always
    depends_on:
      - semantic-objects
      - apollo-gateway
    ports:
      - "9999:8080"
    environment:
      SECURITY_ENABLED: "false"
      # Points to the Apollo Gateway
      GRAPHQL_ENDPOINT: "http://localhost:9000/graphql"

volumes:
  soaas_data:
  graphdb_data:
  mongodb_data:

The Ontotext Platform is available under a commercial time-based license. To obtain an evaluation licence, please contact the Ontotext team at info@ontotext.com and see the documentation on Setting up Licenses .

Use the following environment file .env to configure the location of the Ontotext Platform license and GraphDB license as well as:

1
2
3
4
5
6
7
# Path to the platform license file. Mounted in the semantic objects service.
# Note: It's best to use absolute paths, especially when running Docker in Windows
PLATFORM_LICENSE_FILE=~/.platform/platform.license

# Path to a license file that will be mounted in the GraphDB container.
# Note: It's best to use absolute paths, especially when running Docker in Windows
GDB_LICENSE_PATH=~/.platform/graphdb.license

Create a simple bash script setup.sh for starting everything, loading the SWAPI data and schema. You can also refer to the Semantic Objects getting started guide.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/usr/bin/env bash

# Home directory of this script
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

# Stop and remove previous setups
docker-compose -f ${DIR}/docker-compose.yaml down

# Download the images
docker-compose -f ${DIR}/docker-compose.yaml pull

# Start the services in the background
docker-compose -f ${DIR}/docker-compose.yaml up -d

# Wait a bit
echo "Sleeping for 30 seconds..."
sleep 30

# The host where platform is deployed and reachable
PLATFORM_HOST=localhost
# GraphDB address
GDB_ADDRESS=http://${PLATFORM_HOST}:9998
# Semantic objects service address
SOAAS_ADDRESS=http://${PLATFORM_HOST}:9995

set -eu

# Create repository in GraphDB
curl -X POST \
  -H "Content-Type: multipart/form-data" \
  -F "config=@repo.ttl" \
  ${GDB_ADDRESS}/rest/repositories/

# Load the data in GraphDB
curl -X POST \
    -H "Content-Type:application/x-turtle" \
    -T ${DIR}/data.ttl \
    ${GDB_ADDRESS}/repositories/soaas/statements

# Load the schema in SOaaS
curl -X POST \
    -H "Content-Type: text/yaml" \
    -H "Accept: application/ld+json" \
    -H 'X-Request-ID: GettingStartedTx01' \
    -T ${DIR}/schema.yaml \
    ${SOAAS_ADDRESS}/soml

# Bind the Schema
curl -X PUT -H 'X-Request-ID: BindingSchema' ${SOAAS_ADDRESS}/soml/swapi/soaas

echo -n "\n\nDone"

Download the GraphDB repository configuration, SWAPI data and SOML schema following the Semantic Objects getting started guide and place them in the same directory as the setup script. You should have the following files in the directory: data.ttl, repo.ttl and schema.yaml.

Run the set-up script. To start the services, load the data and build a federated aggregate GraphQL endpoint, use the following shell command:

chmod +x ./setup.sh && ./setup.sh

Note

The setup script and the .env file should be in the same directory as the docker-compose.yaml file.

Try out a GraphQL query against the federated aggregate schema using this curl command:

curl 'http://localhost:9000/graphql' \
      -H 'Accept: application/json' \
      -H 'Content-Type: application/json' \
      --data-binary '{"query":"query { planet(where: {ID: \"https://swapi.co/resource/planet/25\"}) { name diameter gravity } }"}' \
      --compressed

That returns the Planet Dantooine with the extended calculated gravity property.

{
  "data": {
    "planet": [
      {
        "name": "Dantooine",
        "diameter": 9830,
        "gravity": "1 standard"
      }
    ]
  }
}

Tip

You can also open the GraphiQL playground on http://localhost:9999/graphiql. It is configured to send queries against the Apollo Gateway.

Tutorial

You can find more details on how to use Semantic Objects in this tutorial.

Limitations

  • Currently, SOaaS cannot extend types defined in other endpoints. We will address this in future releases.

  • The @key directive for all types is set to their ID, although the Apollo Federation server supports each field to be used as key.

  • The current Apollo Gateway implementation (ontotext/apollo-federation-gateway) will not update the aggregated schema if an endpoint’s schema change. So changing the schema of a federated endpoint requires a Gateway restart.

  • The Apollo Gateway follows the GraphQL standard and terminates null-valued mandatory objects - something that the Platform does not. This means that, for the same query, you might get different results depending on whether you execute it against the Gateway or SOaaS directly.

Administration

Monitoring

Health Check

The Apollo federation gateway has an aggregate health check that checks all configured federated services for health status.

The __health endpoint can be accessed at http://{federationhost}:{federation_port}/__health.

An example response is as follows, and you can see the Semantic Objects health response aggregated with the extended service:

{
  "status": "OK",
  "id": "apollo-federation-gateway",
  "healthChecks": [
    {
      "status": "OK",
      "healthChecks": [
        {
          "status": "OK",
          "id": "1100",
          "name": "MongoDB Checks",
          "type": "mongo",
          "impact": "MongoDB operating normally and collection available.",
          "troubleshooting": "http://semantic-objects:8080/__trouble",
          "description": "MongoDB checks.",
          "message": "MongoDB operating normally and collection available."
        },
        {
          "status": "OK",
          "id": "1200",
          "name": "SPARQL Checks",
          "type": "sparql",
          "impact": "SPARQL Endpoint operating normally, writable and populated with data.",
          "troubleshooting": "http://semantic-objects:8080/__trouble",
          "description": "SPARQL Endpoint checks.",
          "message": "SPARQL Endpoint operating normally, writable and populated with data."
        },
        {
          "status": "OK",
          "id": "1300",
          "name": "SOML Checks",
          "type": "soml",
          "impact": "SOML bound, service operating normally.",
          "troubleshooting": "http://semantic-objects:8080/__trouble",
          "description": "SOML checks.",
          "message": "SOML bound, service operating normally."
        },
        {
          "status": "OK",
          "id": "1350",
          "type": "soml-rbac",
          "impact": "Security is disabled. SOML RBAC healthcheck is inactive.",
          "troubleshooting": "http://semantic-objects:8080/__trouble",
          "description": "SOML RBAC checks.",
          "message": "Security is disabled. SOML RBAC healthcheck is inactive."
        },
        {
          "status": "OK",
          "id": "1400",
          "name": "Query Service",
          "type": "queryService",
          "impact": "Query Service operating normally.",
          "troubleshooting": "http://semantic-objects:8080/__trouble",
          "description": "Query Service checks.",
          "message": "Query Service operating normally."
        },
        {
          "status": "OK",
          "id": "1500",
          "name": "Mutations Service",
          "type": "mutationService",
          "impact": "Mutation Service operating normally.",
          "troubleshooting": "http://semantic-objects:8080/__trouble",
          "description": "Mutation Service checks.",
          "message": "Mutation Service operating normally."
        }
      ]
    },
    {
      "status": "OK",
      "id": "example/1",
      "name": "Example Health Check",
      "type": "graphql",
      "healthChecks": []
    }
  ]
}

Good to Go

The Apollo federation gateway has an aggregate good to go check that checks all configured federated services for good to go status.

The __gtg endpoint can be accessed at http://{federationhost}:{federation_port}/__gtg.

An example response is as follows:

{
  "gtg": "OK",
  "message": "Federation Gateway operating as expected"
}

Configuration

Federation Gateway

The federation gateway is provided as a docker container. The following docker-compose YAML describes the various ENV variables that control the gateway:

  • GRAPHQL_SERVICE_N: GraphQL endpoint URL address to include within the aggregate schema

  • GRAPHQL_SERVICE_N_GTG: GraphQL endpoint good to go endpoint

  • GRAPHQL_SERVICE_N_HEALTH: GraphQL endpoint health endpoint

  • CACHE_MAX_AGE: cache header max age

  • ENGINE_API_KEY: in case you want to register the federation services with an Apollo Graph Manager

  • SERVICE_DWELL: dwell time between federated GraphQL endpoint health checks

  • POLL_INTERVAL: the interval between federated GraphQL endpoint health checks (start-up)

Example docker-compose.yaml configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
version: '3.6'

services:

  apollo-gateway:
    image: ontotext/apollo-federation-gateway:3.1.4
    environment:
      # Common configurations
      CACHE_MAX_AGE: '5'
      ENGINE_API_KEY: '...'
      SERVICE_DWELL: 10000
      POLL_INTERVAL: 30
      FORWARD_HEADERS: "X-Request-ID, Authorization"

      # Service 1: semantic-objects
      GRAPHQL_SERVICE_0: "http://semantic-objects:8080/graphql"
      GRAPHQL_SERVICE_0_GTG: "http://semantic-objects:8080/__gtg"
      GRAPHQL_SERVICE_0_HEALTH: "http://semantic-objects:8080/__health"

      # Service 2: extend-example
      GRAPHQL_SERVICE_1: "http://extend-example:4005/graphql"
      GRAPHQL_SERVICE_1_GTG: "http://extend-example:4005/__gtg"
      GRAPHQL_SERVICE_1_HEALTH: "http://extend-example:4005/__health"

      # Enable and configure the security
      SECURITY_ENABLED: "true"
      FUSION_APPLICATION_ID: "0afceebe-cfc6-4ddf-81b9-a7fd658b30d4"
      FUSION_PROTOCOL: "http"
      FUSION_HOST: "localhost"
      FUSION_PORT: "9011"
      FUSION_USERNAME: "federation"
      FUSION_PASSWORD: "federation"
    ports:
      - 9000:80
Federation Gateway Authorization

If the Platform (Semantic Objects) has security enabled the federation gateway must authenticate and retrieve an Authorization JWT token.

This token is used by the Gateway to make authorized requests to federated services to retrieve each GraphQL _service.sdl GraphQL schema.

After that, the gateway builds the aggregate schema and discards the token.

Subsequent GraphQL query and mutation requests will use the client’s Authorization header.

The following docker-compose YAML describes the various ENV variables that control the gateway schema aggregation authorization:

  • SECURITY_ENABLED: true - schema building uses FusionAuth supplied authorization tokens, otherwise no authorization

  • FUSION_APPLICATION_ID: The FusionAuth Application UUID for the Platform, e.g., “0afceebe-cfc6-4ddf-81b9-a7fd658b30d4”

  • FUSION_PROTOCOL: The protocol to use when connecting to the FusionAuth REST API “http” | “https”

  • FUSION_HOST: The server host to use when connecting to the FusionAuth REST API e.g. “localhost”

  • FUSION_PORT: The TCP/IP port to use when connecting to the FusionAuth REST API e.g. “9011”

  • FUSION_USERNAME: The FusionAuth username to use when authenticating e.g. “federation”

  • FUSION_PASSWORD: The FusionAuth password to use when authentication e.g. “federation”

  • FORWARD_HEADERS: The HTTP headers the Gateway should forward to federated GraphQL endpoints “X-Request-ID, Authorization”

As mentioned above, in order for the Federation Gateway to retrieve the SDL, it needs a user with sufficient rights to do so. So first, add the following role to the SOML (it provides gives read permissions to the SDL):

1
2
3
4
5
6
7
rbac:
  roles:
    Federation:
      description: "Federation service role"
      actions: [
        "_Service/sdl/read"
      ]

The next step is to add the user in FusionAuth. The user should have role Federation and username and password matching the ones defined in FUSION_USERNAME and FUSION_PASSWORD environment variables.

If you are using the OPCTL tool to provision the security, add the following to the configuration yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
security:
  roles:
    - name: Federation
      superRole: false
      default: false
  users:
    - username: federation
      email: federation@example.com
      password: federation
      roles:
        - Federation

Once you have a configured user and role, the Federation Gateway will be able to operate properly.

Federation Correlation

The Federation Gateway will generate X-Request-ID correlation ids if none are present on requests. X-Request-ID is always logged and forwarded to federated GraphQL endpoints.

This ensures that a GraphQL query or mutation can be traced and correlated across the Gateway, Federated GraphQL services and indeed GraphDB.

The dockerfile by default should include the following headers for forwarding:

  • FORWARD_HEADERS: “X-Request-ID, Authorization”

X-Request-ID is the Platform default correlation id HTTP header.

Semantic Objects Service

SOaaS supports the Apollo Federation protocol and can be a part of an Apollo Federation (with some known limitations).

To make the SOaaS endpoint federation-ready, you need to enable the following property:

graphql.federation.enabled=true

This will add all necessary additions to the GraphQL schema.