Apollo Federation: MSA for GraphQL


1. What is Apollo Federation?

Apollo Federation은 여러 개의 서비스가 하나의 그래프 API를 구성하고, 이를 통해 클라이언트 애플리케이션에서 쉽게 데이터를 검색하고 조작할 수 있도록 하는 마이크로서비스 아키텍처를 지원하는 Apollo GraphQL의 기능 중 하나이다.

Apollo Federation is one of the features of Apollo GraphQL that supports a microservice architecture that allows multiple services to construct a single graph API, which makes it easy to retrieve and manipulate data in client applications.

Netflix에서 Apollo Federation을 이용해서 서비스 병목지점이 되는 거대한 GraphQL 서비스를 각 조직별로 관리할 수 있는 형태로 전환하는데 성공하였다. 이 내용에 대해서는 Youtube 및 Netflix 기술블로그에 자세히 안내되어 있다.

Netflix succeeded in using Apollo Federation to transform the huge GraphQL service, which becomes a service bottleneck, into a form that can be managed by each organization. This content is detailed in the YouTube and Netflix technology blogs.

2. Advantages of Apollo Federation

  1. 복잡한 데이터 모델링: Apollo Federation은 각 서비스를 독립적으로 개발하고 배포할 수 있도록 지원하며, 이를 통해 서비스를 더 작고 단순하게 유지할 수 있다. 또한, 여러 서비스 간에 공유되는 데이터 모델을 중앙에서 관리할 수 있으므로, 복잡한 데이터 모델링이 가능하다.
  2. 확장성: Apollo Federation은 수평적으로 확장할 수 있는 아키텍처를 지원한다. 새로운 서비스를 추가하거나 기존 서비스를 수정하더라도 다른 서비스에 영향을 미치지 않으므로, 서비스를 쉽게 확장할 수 있다.
  3. 유연성: Apollo Federation은 여러 프로그래밍 언어와 데이터베이스를 지원하며, 기존 마이크로서비스 아키텍처와 쉽게 통합될 수 있다.
  4. 성능: Apollo Federation은 여러 개의 서비스를 쉽게 연결할 수 있도록 지원하며, 불필요한 데이터 요청을 최소화하여 성능을 최적화한다.
  5. 개발 생산성: Apollo Federation은 GraphQL의 기능을 제공하므로, 개발자는 데이터를 쉽게 검색하고 조작할 수 있다. 또한, Apollo Studio와 같은 툴을 사용하여 쉽게 API를 문서화하고 모니터링할 수 있다.

Apollo Federation은 여러 microservices로 구성된 GraphQL 서비스를 단일 endpoint에서 통합해서 하나의 GraphQL로 제공해주는 API Gateway같은 역할을 해준다.

  1. Complex data modeling: Apollo Federation enables each service to be developed and distributed independently, allowing it to be smaller and simpler. In addition, since data models shared between multiple services can be centrally managed, complex data modeling is possible.
  2. Scalability: Apollo Federation supports horizontally scalable architectures. Adding new services or modifying existing services does not affect other services, so services can be easily expanded.
  3. Flexibility: Apollo Federation supports multiple programming languages and databases, and can be easily integrated with existing microservices architectures.
  4. Performance: Apollo Federation makes it easy to connect multiple services and optimizes performance by minimizing unnecessary data requests.
  5. Development Productivity: Apollo Federation provides GraphQL capabilities, so developers can easily search and manipulate data. In addition, tools such as Apollo Studio can be used to easily document and monitor APIs.

Apollo Federation acts as an API gateway that integrates GraphQL services consisting of multiple microservices from a single endpoint and provides them as a GraphQL.

3. Simple examples of Subgraphs and Gateway

먼저 앞으로 등장할 예제코드에 대한 설명하겠다.

  • 대부분의 기술블로그, Youtube 영상등에서는 express + apollo 기반의 코드로 설명을 하고 있다.
  • 현재 Node로 개발을 하는 많은 스타트업 회사에서 Nest.JS를 활용하고 있으므로, 앞으로 설명에서 나오는 소스코드들은 Nest.JS 기반으로 작성하도록 하겠다.
  • @nestjs/apollo, @apollo/subgraph, @nestjs/typeorm 를 활용한 코드이다.
  • typeorm 의 경우 active record pattern 으로 표현한다.
  • Apollo Federation v2를 사용했으며, 그러기 위해서는 Apollo Server v4가 필요하다.

First, I will explain the example code that will appear in the future.

  • Most technical blogs and YouTube videos are explained with express+apollo-based code.
  • Currently, many startup companies developing with Node are using Nest.JS, so we will write the source codes in the description based on Nest.JS.
  • The code uses @nestjs/apollo, @apollo/subgraph, and @nestjs/typeorm.
  • In the case of typeorm, it is expressed as active record pattern.
  • We used Apollo Federation v2, and we need Apollo Server v4.

  • Hospital Service Server (http://localhost:4001/graphql)
    • query hospitals를 제공
// schema.gql

type Hospital {
  id: ID
  name: String!
}

type Query {
  hospital(id: ID): [Hospital]
}

// hospital.resolver.ts

@Resolver(() => Hospital)
export class HospitalResolver  {
  @Query(() => [Hospital])
  async hospitals(@Args('id', {type: () => ID, , nullable: true}) id?: string) {
    return id ? Hospital.find({where: {id}) : Hospital.find();
  }
}
  • Doctor Service Server (http://localhost:4002/graphql)
    • query doctors를 제공
// schema.gql

type Doctor {
  id: ID
  name: String!
}

type Query {
  doctor(id: ID): [Doctor]
}

// doctor.resolver.ts

@Resolver(() => Doctor)
export class DoctorResolver  {
  @Query(() => [Doctor])
  async doctors(@Args('id', {type: () => ID, , nullable: true}) id?: string) {
    return id? Doctor.find({where: {id}) : Doctor.find();
  }
}

위와 같이 2개의 microservice가 운영중이라고 가정했을때

Assuming that two microservices are in operation as above,

  • Apollo Federation Gateway (http://localhost:3000/graphql)
const { ApolloServer } = require("apollo-server");
const { ApolloGateway, IntrospectAndCompose } = require("@apollo/gateway");

const supergraphSdl = new IntrospectAndCompose({
  subgraphs: [
    { name: "hospital", url: "http://localhost:4001/graphql" },
    { name: "doctor", url: "http://localhost:4002/graphql" },
  ],
});

const gateway = new ApolloGateway({
  supergraphSdl,
  __exposeQueryPlanExperimental: false,
});

(async () => {
  const server = new ApolloServer({
    gateway,
    engine: false,
    subscriptions: false,
  });

  server.listen().then(({ url }) => {
    console.log(`🚀 Server ready at ${url}`);
  });
})();

만약 header의 Authorization 을 각 서비스로 전달하려면 아래와 같이 수정해야 한다.

If you want to deliver the header’s Authorization to each service, you need to modify it as follows.

const { startStandaloneServer } = require('@apollo/server/standalone');
const { RemoteGraphQLDataSource } = require("@apollo/gateway");

class AuthenticatedDataSource extends RemoteGraphQLDataSource {
  willSendRequest({ request, context }) {
    request.http.headers.set('Authorization', context.authorization);
  }
}

const gateway = new ApolloGateway({
  supergraphSdl,
  buildService({ name, url }) {
    return new AuthenticatedDataSource({ name, url });
  }
});

(async () => {
  const server = new ApolloServer({ gateway });
  const { url } = await startStandaloneServer(server, {
    context: ({ req }) => 
			({ authorization: req.headers.authorization || '' })
  });
  console.log(`🚀  Server ready at ${url}`);
})();

Apollo Federation에서 위 2개 service를 subgraphs로 묶어서 실행할 경우 localhost:3000/graphql 로 접근하면

If Apollo Federation executes the above two services by combining them with subgraphs, access ‘localhost:3000/graphql’

  • query hospitals
  • query doctors

를 모두 제공한다.

하지만 이것만으로 GraphQL Schema를 merge했다고 하기에는 무언가 부족하다. hospital has many doctos 관계일 경우 hospital.doctors 또는 doctor.hospital 과 같이 2개의 subgraph를 하나의 query로 가져오지 못한다면 사용하기에 하나의 GraphQL로 서비스하는 것에 비해서 불편함이 크다.

However, this alone is not enough to merge GraphQL Schema. In the case of the hospital has any doctor relationship, if two subgraphs, such as ‘hospital.doctors’ or ‘doctor.hospital’, cannot be imported into one query, it is more inconvenient than serving with one GraphQL for use.

4. Relationship between Object Types Across Multiple Subgraphs

각각의 Subgraph에서 제공하는 Object Type간의 relation을 활용하여 하나의 query에서 실행하는 것도 가능핟. 그렇게하기 위해서는 GraphQL의 지시어(directive)를 활용해야 한다.

It is also possible to execute in one query by utilizing the relation between object types provided by each subgraph. To do so, GraphQL’s directives must be used.

사용가능한 directives는 아래에서 확인이 가능하다.

Available directives can be found below.

Subgraph를 넘어선 Object Type간의 relation은 아래와 같이 선언이 가능하다.

The relationship between object types beyond the subgraph can be declared as follows.

  • Doctor Service Server (http://localhost:4002/graphql)
type Doctor {
  id: ID
  name: String!
  hospital: Hospital
}

extend type Hospital @key(fields: "id") {
  id: ID! @external
  doctors: [Doctor]
}
  • Hospital Service Server (http://localhost:4001/graphql)
type Hospital @key(fields: "id") {
  id: ID
  name: String!
}

그런 다음 Doctor Service Server에서 HospitalResolver를 구현해주면 된다.

Then, implement HospitalResolver on the Doctor Service Server.

@Resolver(() => Hospital)
export class HospitalResolver extends HospitalBaseResolver {
  @ResolveField(() => [Doctor], {nullable: 'itemsAndList', defaultValue: []})
  doctors(@Parent() hospital: Hospital, @Context() context: any) {
    return Doctor.find({where: {hospitalId: hospital.id});
  }
}

하지만 위 구현은 N+1 쿼리가 발생할 수 있다. DataLoader를 이용해서 이 문제를 해결하려면 아래와 같이 구현해야 한다.

However, in the above implementation, an N+1 query may occur. To solve this problem using Data Loader, it should be implemented as follows.

export function hospitalDoctorLoader() {
  return new DataLoader<string, Doctor[]>(
    async (hospitalIds: readonly string[]) => {
      const doctors = await Doctor.find({where: {hospitalId: In([...hospitalIds])}});
      const hospitalDoctorMap = _.groupBy(doctors, row => row.hospitalId);
      return _.map(hospitalIds, hospitalId => hospitalDoctorMap[hospitalId]);
    }, {cache: false});
}

const dataLoaders = {
	hospitalDoctorLoader: hospitalDoctorLoader()
}

// GraphQL Module context에 dataloader 삽입

context: ({ extra }) => ({ extra, ...dataLoaders, }),

// hospital.resolver.ts

@Resolver(() => Hospital)
export class HospitalResolver extends HospitalBaseResolver {
  @ResolveField(() => [Doctor], {nullable: 'itemsAndList', defaultValue: []})
  doctors(@Parent() hospital: Hospital, @Context() context: any) {
    return context.hospitalDoctorLoader.load(hospital.id);
  }
}

Gateway를 다시 실행한 뒤 [localhost:300/graphql](http://localhost:300/graphql) 로 접속하여 hospital.doctors 를 쿼리해보자.

After running the gateway again, go to ‘localhost:300/graphql’ and query ‘hospital.doctors’.

query ExampleQuery {
  hospital(id: "id") {
    id
    name
    doctors {
      id
      name		
    }
  }
}

만약 2개의 Subgraph 서버에 log를 켜 두었다면 먼저 Hospital Service Server 에서 이전과 동일한 query가 실행된 다음 Doctor Service Server 가 실행되는 것을 확인 할 수 있다.

If log is turned on for two subgraph servers, it can be verified that ‘Hospital Service Server’ first executes the same query as before and then ‘Doctor Service Server’.

5. Precautions for Apollo Federation

Apollo Federation의 주의사항 및 각종 설정방법은 공식문서에서 확인이 가능하다. 지금까지 작업하면서 알아낸 것을 간단히 정리해보자면,

  • Subgraph에 동일한 Object Type이 선언되어 있으면 오류가 발생한다. 이 경우 @shareable 이라는 directive를 붙여주는 것으로 해결이 가능한데, 이걸 Object type에 적어줄 수도 있고, 각 field에다가 붙여줄수도 있다. 여러 subgraph에 선언된 @shareable 항목이 동일해야지만 오류가 발생하지 않는다.
  • 동일한 이름의 query, mutation이 여러 subgraph에 있더라도 오류가 발생하지 않는다. 만약 interface가 동일한 query, mutation이 여러개 있는 경우 어떤 것을 더 우선순위를 두어서 실행할지에 대한 설정은 없다. 현재 알아낸 정보로는 gateway에 선언된 subgraph 대표이름의 ABC 순서로 먼저오는 것이 실행되었다.
  • subscription 은 현재 지원되지 않는다. (2023.03기준) 하지만 앞으로 지원할 예정이며, 지금도 이걸 동작하게 하기 위해서 해야하는 작업들은 공식문서에 소개되어 있다.

The precautions and various setting methods of Apollo Federation can be checked in the official document. To summarize what we’ve learned so far,

  • If the same Object Type is declared in the subgraph, an error occurs. In this case, it can be solved by attaching a directive called ‘@shareable’, which can be written on the Object type or attached to each field. The ‘@shareable’ items declared in various subgraphs must be the same to prevent errors.
  • No error occurs even if the query, mutation of the same name is in multiple subgraphs. If there are multiple query, mutation with the same interface, there is no setting for which to prioritize. As for the information currently obtained, the ABC order of the representative name of the subgraph declared on the gateway was executed first.
  • Subscription is not currently supported. (As of 2023.03) However, it will be supported in the future, and the work that needs to be done to make it work is introduced in the official document.

6. Conclusion

서비스 규모가 커지면서 하나의 큰 GraphQL 서비스로 작업하기에 어려운 시점이 온다. 이 경우 Apollo Federation은 굉장히 좋은 해결책이 된다. 이미 Netflix에서도 동일한 고민을 하였으며, Netflix의 경우 Monolith GraphQL 서비스를 MSA화하는 작업을 진행하면서 Apollo Federation과 긴밀하게 작업을 하였으며 직접적으로도 기여를 했다. 우리도 처음 Apollo Federation을 검토할 때 예상되는 여러 문제점들이 있었는데, 그 문제들에 대해서 현재까지는 모두 해결방법이 존재하였다. 이미 Netflix에서도 동일한 고민을 하였으며, 그것을 해결하기 위해서 많은 기여를 한 것으로 이해된다.

As the size of the service grows, there is a difficult time to work with one large GraphQL service. In this case, Apollo Federation is a very good solution. Netflix already had the same concern, and Netflix worked closely with Apollo Federation and directly contributed to the process of turning MSA from the Monolith GraphQL service. We also had a number of expected problems when we first reviewed Apollo Federation, and so far, there have been solutions to all of them. It is understood that Netflix has already made the same concern, and has contributed a lot to solve it.

References

Tags

#GraphQL #ApolloFederation #APIGateway #MSA #Subgraph

이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)