Open Glen-Moonpig opened 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!
Hi,
Thanks for the feedback! I'll note this down as a feature request and bring it back to the team.
@jbailey2010 Is there an update on this or a designated thread?
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.
The killer feature here would be if AppSync could act as a federation gateway, removing the need to run Apollo server π
@jbailey2010 Any update on this?
Any update?
I'm also curious about this.
Is there a way to use AppSync as the federated gateway entry point?
Any news? ππ¨π΄
+1
+1
Any update on this Federation and Gateway support in AppSync?
This is an important devops need for our team. We use amplify, which would be cool if it worked with
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!
this is absolutely required!
So needed
I'm not going to use AppSync until it supports Federation.
This is a big issue for our company too, is it in the roadmap?
This is a MUST for us.... we need an update
Really need thi ps too, but I havenβt seen any updates in AppSync for a while so not getting hopes up.
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...
+1
+1
+1
+1 * 10000000
+1
The killer feature here would be if AppSync could act as a federation gateway, removing the need to run Apollo server π
Does anyone have a workaround for this? Multiple AppSync services with a gateway service? Can I drop Apollo Federation on top of this?
@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)
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
+1
Here is an implementation of it : https://github.com/flochaz/federated-appsync-api-demo
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/
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:
IntrospectAndCompose
to utilize retries and caching a built schema for scaling. 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.
Has anyone tried following these instructions from AWS?
https://aws.amazon.com/blogs/mobile/federation-appsync-subgraph/
@spacegraym3 , I'm the author, how can I help, what's the matter ?
@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?
@sandunsameera , What your AppSync model looks like ?
@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?
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.
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
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?