aws / aws-appsync-community

The AWS AppSync community
https://aws.amazon.com/appsync
Apache License 2.0
506 stars 32 forks source link

AppSync with Apollo Federation/Gateway #35

Open Glen-Moonpig opened 5 years ago

Glen-Moonpig commented 5 years ago

Question:

Does AppSync work with Apollo Federation and Gateway? I have already used AppSync with Apollo Server using the schema stitching method, which is now deprecated. It looks like the new Apollo Gateway using federation requires some additional implementation from the AppSync side.

If this does not work currently are there any plans to implement this?

Glen-Moonpig commented 5 years ago

I spoke to Apollo about using AppSync with federation and they replied

For AWS, we would need to work with them on exposing the right entry points to AppSync, but hopefully that can be done!

jbailey2010 commented 5 years ago

Hi,

Thanks for the feedback! I'll note this down as a feature request and bring it back to the team.

david-novicki commented 5 years ago

@jbailey2010 Is there an update on this or a designated thread?

0xR commented 4 years ago

For now you can add a middleware service with https://github.com/0xR/graphql-transform-federation. This allows you to add any federation decorators to an existing appsync schema.

lwc commented 4 years ago

The killer feature here would be if AppSync could act as a federation gateway, removing the need to run Apollo server πŸ‘Œ

LmKupke commented 4 years ago

@jbailey2010 Any update on this?

jfbaro commented 4 years ago

Any update?

DavidWells commented 4 years ago

I'm also curious about this.

Is there a way to use AppSync as the federated gateway entry point?

MontoyaAndres commented 3 years ago

Any news? πŸ‘€πŸ‡¨πŸ‡΄

lostb1t commented 3 years ago

+1

markoff-s commented 3 years ago

+1

vaptex commented 3 years ago

Any update on this Federation and Gateway support in AppSync?

jakemitchellxyz commented 3 years ago

This is an important devops need for our team. We use amplify, which would be cool if it worked with

travishaby commented 3 years ago

Would love to see this! We're currently hacking a solution similar to one mentioned in a related thread with a bunch of namespaced GraphQL schemas uploaded to the same S3 bucket, which are then all being stitched together in a job and deployed to our "global" AppSync instance. A cleaner, AWS-native solution would be ideal!

ncremaschini commented 3 years ago

this is absolutely required!

malikalimoekhamedov commented 3 years ago

So needed

cazzer commented 3 years ago

I'm not going to use AppSync until it supports Federation.

sebastiansanio commented 3 years ago

This is a big issue for our company too, is it in the roadmap?

leandrosalo commented 3 years ago

This is a MUST for us.... we need an update

sweepy84 commented 3 years ago

Really need thi ps too, but I haven’t seen any updates in AppSync for a while so not getting hopes up.

confix commented 3 years ago

Unfortunately responses to almost any AppSync related issue seem to boil down to "I am going to note that down"... It seems like they want us to host ourselves... ditching AppSync for now...

gastonsilva commented 2 years ago

+1

patrickacioli commented 2 years ago

+1

kuzbida commented 2 years ago

+1

Z11 commented 2 years ago

+1 * 10000000

friendOfOurs commented 2 years ago

+1

saravind-hotstar commented 2 years ago

The killer feature here would be if AppSync could act as a federation gateway, removing the need to run Apollo server πŸ‘Œ

shawnmclean commented 2 years ago

Does anyone have a workaround for this? Multiple AppSync services with a gateway service? Can I drop Apollo Federation on top of this?

PTaylour commented 2 years ago

@shawnmclean I gave https://github.com/0xR/graphql-transform-federation a little go but failed to get it to work with latest versions of apollo server and appsync.

Fell back to using import { stitchSchemas } from '@graphql-tools/stitch'; which seems well suited to situations where you can't easily edit existing schema (eg when using appsync with amplify):

  const appSyncIntrospectionSchemaObject = JSON.parse(
    fs.readFileSync(
      path.join(__dirname, '../../src/graphql/schema.json'),
      'utf8',
    ),
  );

  const appsyncSubSchema: SubschemaConfig = {
    schema: buildClientSchema(appSyncIntrospectionSchemaObject.data),
    /**
     * Provide executor as this API is server from a different server to the gateway
     */
    executor: createRemoteExecutor(
      `https://<yourendpoint>.appsync-api.eu-west-1.amazonaws.com/graphql`,
    ),
  };

  // build the combined schema
  const gatewaySchema = stitchSchemas({
    subschemas: [appsyncSubSchema, ...otherSubSchemas],
  });

  const gateway = new ApolloServer({
    schema: gatewaySchema,
    context: (ctx) => ({
      headers: {
        'Content-Type': 'application/json',
        // TODO the JWT is too long to send from apollo studio. May want to try and optimise this anyway.
        Authorization: ctx.req.header('authorization'),
      },
    }),
  });

Only prototyping at the moment, haven't tried this in production (and obviously no good if you want to use apollo federation)

ndejaco2 commented 2 years ago

A workaround for supporting federation using Apollo Gateway to federate across multiple AppSync GraphQL Apis is provided here: https://github.com/apollographql/apollo-federation-subgraph-compatibility/tree/main/implementations/appsync

joekendal commented 2 years ago

+1

flochaz commented 2 years ago

Here is an implementation of it : https://github.com/flochaz/federated-appsync-api-demo

azarbomi commented 2 years ago

Here is an implementation of it : https://github.com/flochaz/federated-appsync-api-demo

This blog was just posted on AWS (Feb. 1st 2022) as well to give another example.

https://aws.amazon.com/blogs/mobile/federation-appsync-subgraph/

Borduhh commented 2 years ago

Here is an implementation of it : https://github.com/flochaz/federated-appsync-api-demo

This blog was just posted on AWS (Feb. 1st 2022) as well to give another example.

https://aws.amazon.com/blogs/mobile/federation-appsync-subgraph/

We're using a variation of this to work with IAM roles where the Federation server runs on ECS and has access via IAM to all underlying AppSync graphs backed by microservices.

The big problems we had to overcome were:

Apollo Server

export const server = new ApolloServer({
  gateway: new ApolloGateway({
    supergraphSdl: superGraphSdlManager,
    buildService: ({ url }) => new AuthenticatedDataSource({ url }),
  }),
  healthCheckPath: '/healthcheck',
});

Custom Build Service

import { GraphQLDataSourceProcessOptions, RemoteGraphQLDataSource } from '@apollo/gateway';
import { GraphQLRequest } from 'apollo-server-types';
import { SignatureV4 } from '@aws-sdk/signature-v4';
import { Sha256 } from '@aws-crypto/sha256-js';
import { OutgoingHttpHeader } from 'http';
import { defaultProvider } from '@aws-sdk/credential-provider-node';
import { HttpRequest } from '@aws-sdk/protocol-http';

export default class AuthenticatedDataSource extends RemoteGraphQLDataSource {
  /**
   * Adds the necessary IAM Authorization headers for AppSync requests
   * @param request The request to Authorize
   * @returns The headers to pass through to the request
   */
  private async getAWSCustomHeaders(request: GraphQLRequest): Promise<{
    [key: string]: OutgoingHttpHeader | undefined;
  }> {
    const { http, ...requestWithoutHttp } = request;

    if (!http) return {};

    const url = new URL(http.url);

    // If the graph service is not AppSync, we should not sign these request.
    if (!url.host.match(/appsync-api/)) return {};

    const httpRequest = new HttpRequest({
      hostname: url.hostname,
      path: url.pathname,
      method: http.method,
      headers: {
        Host: url.host,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(requestWithoutHttp),
    });

    const signedRequest = await new SignatureV4({
      region: 'us-east-1',
      credentials: defaultProvider(),
      service: 'appsync',
      sha256: Sha256,
    }).sign(httpRequest);

    return signedRequest.headers || {};
  }

  /**
   * Customize the request to AppSync
   * @param options The options to send with the request
   */
  public async willSendRequest({ request }: GraphQLDataSourceProcessOptions) {
    const customHeaders = await this.getAWSCustomHeaders(request);

    if (customHeaders)
      Object.keys(customHeaders).forEach((h) => {
        request.http?.headers.set(h, customHeaders[h] as string);
      });
  }
}

We currently are passing our services in via environment variable that is auto-generated using CDK.

spacegraym3 commented 2 years ago

Has anyone tried following these instructions from AWS?

https://aws.amazon.com/blogs/mobile/federation-appsync-subgraph/

flochaz commented 2 years ago

@spacegraym3 , I'm the author, how can I help, what's the matter ?

sandunsameera commented 1 year ago

@flochaz im getting Validation error of type FieldUndefined: Field '_service' in type 'Query' is undefined @ '_service' this error when trying to access appsync with my federation gateway. Any idea why this error comes?

flochaz commented 1 year ago

@sandunsameera , What your AppSync model looks like ?

sandunsameera commented 1 year ago

@sandunsameera , What your AppSync model looks like ?

its what generated by appsync @flochaz

type Collection {
    category: String
    contractAddress: String
    id: String!
}

type CollectionConnection {
    items: [Collection]
    nextToken: String
}

input CreateCollectionInput {
    category: String
    contractAddress: String
}

input CreateP8Nmdev2OrderInput {
    id: String!
    asset: String
    marketId: String
    price: String
    timestamp: Int
    user: String
}

input DeleteCollectionInput {
    id: String!
}

input DeleteP8Nmdev2OrderInput {
    id: String!
}

type Mutation {
    createCollection(input: CreateCollectionInput!): Collection
    updateCollection(input: UpdateCollectionInput!): Collection
    deleteCollection(input: DeleteCollectionInput!): Collection
    createP8Nmdev2Order(input: CreateP8Nmdev2OrderInput!): P8Nmdev2Order
    updateP8Nmdev2Order(input: UpdateP8Nmdev2OrderInput!): P8Nmdev2Order
    deleteP8Nmdev2Order(input: DeleteP8Nmdev2OrderInput!): P8Nmdev2Order
}

type P8Nmdev2Order {
    id: String!
    asset: String
    marketId: String
    price: String
    timestamp: Int
    user: String
}

type P8Nmdev2OrderConnection {
    items: [P8Nmdev2Order]
    nextToken: String
}

type Query {
    collection(id: String!): Collection
    listCollections(filter: TableCollectionFilterInput, limit: Int, nextToken: String): CollectionConnection
    queryCollectionsByContractAddressIndex(contractAddress: String!, first: Int, after: String): CollectionConnection
    queryCollectionsByCategoryIndex(category: String!, first: Int, after: String): CollectionConnection
    getP8Nmdev2Order(id: String!): P8Nmdev2Order
    listP8Nmdev2Orders(filter: TableP8Nmdev2OrderFilterInput, limit: Int, nextToken: String): P8Nmdev2OrderConnection
    queryP8Nmdev2OrdersByMarketIdIndex(marketId: String!, first: Int, after: String): P8Nmdev2OrderConnection
    queryP8Nmdev2OrdersByUserIndex(user: String!, first: Int, after: String): P8Nmdev2OrderConnection
    queryP8Nmdev2OrdersByAssetIndex(asset: String!, first: Int, after: String): P8Nmdev2OrderConnection
    queryP8Nmdev2OrdersByMarketIdTimestampIndex(marketId: String!, first: Int, after: String): P8Nmdev2OrderConnection
}

type Subscription {
    onCreateCollection(category: String, contractAddress: String, id: String): Collection
        @aws_subscribe(mutations: ["createCollection"])
    onUpdateCollection(category: String, contractAddress: String, id: String): Collection
        @aws_subscribe(mutations: ["updateCollection"])
    onDeleteCollection(category: String, contractAddress: String, id: String): Collection
        @aws_subscribe(mutations: ["deleteCollection"])
    onCreateP8Nmdev2Order(
        id: String,
        asset: String,
        marketId: String,
        price: String,
        timestamp: Int
    ): P8Nmdev2Order
        @aws_subscribe(mutations: ["createP8Nmdev2Order"])
    onUpdateP8Nmdev2Order(
        id: String,
        asset: String,
        marketId: String,
        price: String,
        timestamp: Int
    ): P8Nmdev2Order
        @aws_subscribe(mutations: ["updateP8Nmdev2Order"])
    onDeleteP8Nmdev2Order(
        id: String,
        asset: String,
        marketId: String,
        price: String,
        timestamp: Int
    ): P8Nmdev2Order
        @aws_subscribe(mutations: ["deleteP8Nmdev2Order"])
}

input TableBooleanFilterInput {
    ne: Boolean
    eq: Boolean
}

input TableCollectionFilterInput {
    category: TableStringFilterInput
    contractAddress: TableStringFilterInput
    id: TableStringFilterInput
}

input TableFloatFilterInput {
    ne: Float
    eq: Float
    le: Float
    lt: Float
    ge: Float
    gt: Float
    contains: Float
    notContains: Float
    between: [Float]
}

input TableIDFilterInput {
    ne: ID
    eq: ID
    le: ID
    lt: ID
    ge: ID
    gt: ID
    contains: ID
    notContains: ID
    between: [ID]
    beginsWith: ID
}

input TableIntFilterInput {
    ne: Int
    eq: Int
    le: Int
    lt: Int
    ge: Int
    gt: Int
    contains: Int
    notContains: Int
    between: [Int]
}

input TableP8Nmdev2OrderFilterInput {
    id: TableStringFilterInput
    asset: TableStringFilterInput
    marketId: TableStringFilterInput
    price: TableStringFilterInput
    timestamp: TableIntFilterInput
    user: TableStringFilterInput
}

input TableStringFilterInput {
    ne: String
    eq: String
    le: String
    lt: String
    ge: String
    gt: String
    contains: String
    notContains: String
    between: [String]
    beginsWith: String
}

input UpdateCollectionInput {
    category: String
    contractAddress: String
    id: String!
}

input UpdateP8Nmdev2OrderInput {
    id: String!
    asset: String
    marketId: String
    price: String
    timestamp: Int
    user: String
}

type _Service {
    sdl: String
}

Is there is a way we can verify this schema is federate compatible?

flochaz commented 1 year ago

AppSync don't generate the model. Are you using Amplify ? if so make sure you still add (as mentioned in the blog post) the _service: _Service! query in your Query section. You will need as well to specify the associated resolver. All that make me think that a custom amplify transformer would be useful ... Adding to my TODO list.

cyril36 commented 11 months ago

Here is an implementation of it : https://github.com/flochaz/federated-appsync-api-demo

This blog was just posted on AWS (Feb. 1st 2022) as well to give another example. https://aws.amazon.com/blogs/mobile/federation-appsync-subgraph/

We're using a variation of this to work with IAM roles where the Federation server runs on ECS and has access via IAM to all underlying AppSync graphs backed by microservices.

The big problems we had to overcome were:

  • Wrapping IntrospectAndCompose to utilize retries and caching a built schema for scaling.
  • Create a custom buildService to allow for passing IAM roles.

Apollo Server

export const server = new ApolloServer({
  gateway: new ApolloGateway({
    supergraphSdl: superGraphSdlManager,
    buildService: ({ url }) => new AuthenticatedDataSource({ url }),
  }),
  healthCheckPath: '/healthcheck',
});

Custom Build Service

import { GraphQLDataSourceProcessOptions, RemoteGraphQLDataSource } from '@apollo/gateway';
import { GraphQLRequest } from 'apollo-server-types';
import { SignatureV4 } from '@aws-sdk/signature-v4';
import { Sha256 } from '@aws-crypto/sha256-js';
import { OutgoingHttpHeader } from 'http';
import { defaultProvider } from '@aws-sdk/credential-provider-node';
import { HttpRequest } from '@aws-sdk/protocol-http';

export default class AuthenticatedDataSource extends RemoteGraphQLDataSource {
  /**
   * Adds the necessary IAM Authorization headers for AppSync requests
   * @param request The request to Authorize
   * @returns The headers to pass through to the request
   */
  private async getAWSCustomHeaders(request: GraphQLRequest): Promise<{
    [key: string]: OutgoingHttpHeader | undefined;
  }> {
    const { http, ...requestWithoutHttp } = request;

    if (!http) return {};

    const url = new URL(http.url);

    // If the graph service is not AppSync, we should not sign these request.
    if (!url.host.match(/appsync-api/)) return {};

    const httpRequest = new HttpRequest({
      hostname: url.hostname,
      path: url.pathname,
      method: http.method,
      headers: {
        Host: url.host,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(requestWithoutHttp),
    });

    const signedRequest = await new SignatureV4({
      region: 'us-east-1',
      credentials: defaultProvider(),
      service: 'appsync',
      sha256: Sha256,
    }).sign(httpRequest);

    return signedRequest.headers || {};
  }

  /**
   * Customize the request to AppSync
   * @param options The options to send with the request
   */
  public async willSendRequest({ request }: GraphQLDataSourceProcessOptions) {
    const customHeaders = await this.getAWSCustomHeaders(request);

    if (customHeaders)
      Object.keys(customHeaders).forEach((h) => {
        request.http?.headers.set(h, customHeaders[h] as string);
      });
  }
}

We currently are passing our services in via environment variable that is auto-generated using CDK.

I am deploying my subgraph in a private lambda url (not accessible from internet without IAM auth) and graph federation on public lambda (accessible by anybody) But i was struggling with the signaturev4 as we dont get much help from the error output on what we are missing .

but thank to you @Borduhh , my nightmare is over. This solution works perfectly for me (using lambda as service). I was missing the body field (as i saw in the doc that it was optional) so as a reminder, put all the fields that are in this example if you want make the signaturev4 working Thank you