Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions deployment/config/public-graphql-api/router.config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# yaml-language-server: $schema=https://github.com/graphql-hive/router/releases/download/hive-router%2Fv0.0.39/router-config.schema.json
override_subgraph_urls:
graphql:
url:
expression: env("INTERNAL_GRAPHQL_URL")
telemetry:
hive:
token:
expression: env("HIVE_ACCESS_TOKEN")
target:
expression: env("HIVE_TARGET")
tracing:
enabled: true
endpoint: env("HIVE_TRACE_ENDPOINT")
tracing:
exporters:
- kind: otlp
enabled: true
protocol: http
endpoint:
expression: env("OPENTELEMETRY_COLLECTOR_ENDPOINT")
10 changes: 10 additions & 0 deletions deployment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { deploySchemaPolicy } from './services/policy';
import { deployPostgres } from './services/postgres';
import { deployProxy } from './services/proxy';
import { deployPublicGraphQLAPIGateway } from './services/public-graphql-api-gateway';
import { deployPublicGraphQLAPIRouter } from './services/public-graphql-api-router';
import { deployRedis } from './services/redis';
import { deployS3, deployS3AuditLog, deployS3Mirror } from './services/s3';
import { deploySchema } from './services/schema';
Expand Down Expand Up @@ -311,13 +312,22 @@ const publicGraphQLAPIGateway = deployPublicGraphQLAPIGateway({
otelCollector,
});

const publicGraphQLAPIRouter = deployPublicGraphQLAPIRouter({
environment,
graphql,
docker,
observability,
otelCollector,
});

const proxy = deployProxy({
observability,
app,
graphql,
usage,
environment,
publicGraphQLAPIGateway,
publicGraphQLAPIRouter,
otelCollector,
});

Expand Down
23 changes: 15 additions & 8 deletions deployment/services/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { GraphQL } from './graphql';
import { Observability } from './observability';
import { OTELCollector } from './otel-collector';
import { type PublicGraphQLAPIGateway } from './public-graphql-api-gateway';
import { type PublicGraphQLAPIRouter } from './public-graphql-api-router';
import { Usage } from './usage';

export function deployProxy({
Expand All @@ -16,6 +17,7 @@ export function deployProxy({
environment,
observability,
publicGraphQLAPIGateway,
publicGraphQLAPIRouter,
otelCollector,
}: {
observability: Observability;
Expand All @@ -24,6 +26,7 @@ export function deployProxy({
app: App;
usage: Usage;
publicGraphQLAPIGateway: PublicGraphQLAPIGateway;
publicGraphQLAPIRouter: PublicGraphQLAPIRouter;
otelCollector: OTELCollector;
}) {
const { tlsIssueName } = new CertManager().deployCertManagerAndIssuer();
Expand Down Expand Up @@ -108,14 +111,6 @@ export function deployProxy({
},
])
.registerService({ record: environment.apiDns }, [
{
name: 'public-graphql-api',
path: '/graphql',
customRewrite: '/graphql',
service: publicGraphQLAPIGateway.service,
requestTimeout: '60s',
retriable: true,
},
{
name: 'otel-traces',
path: '/otel/v1/traces',
Expand All @@ -124,5 +119,17 @@ export function deployProxy({
requestTimeout: '60s',
retriable: true,
},
{
name: 'public-graphql-api',
path: '/graphql',
customRewrite: '/graphql',
// Here we split traffic between the two services: Hive Gateway and Hive Router
service: [
{ upstream: publicGraphQLAPIGateway.service, weight: 50 },
{ upstream: publicGraphQLAPIRouter.service, weight: 50 },
Comment on lines +128 to +129
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Starting with a 50/50 traffic split for a new service is quite aggressive, especially as testing on dev/staging is not yet complete. It would be safer to start with a much smaller percentage for the new Hive Router, for example 5% or 10%, and then gradually increase it. This canary release approach would minimize the potential impact if any issues arise with the new router.

Suggested change
{ upstream: publicGraphQLAPIGateway.service, weight: 50 },
{ upstream: publicGraphQLAPIRouter.service, weight: 50 },
{ upstream: publicGraphQLAPIGateway.service, weight: 95 },
{ upstream: publicGraphQLAPIRouter.service, weight: 5 },

],
requestTimeout: '60s',
retriable: true,
},
]);
}
7 changes: 1 addition & 6 deletions deployment/services/public-graphql-api-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@ import { type OTELCollector } from './otel-collector';
*/
const dockerImage = 'ghcr.io/graphql-hive/gateway:2.1.19';

const gatewayConfigDirectory = path.resolve(
__dirname,
'..',
'config',
'public-graphql-api-gateway',
);
const gatewayConfigDirectory = path.resolve(__dirname, '..', 'config', 'public-graphql-api');

// On global scope to fail early in case of a read error
const gatewayConfigPath = path.join(gatewayConfigDirectory, 'gateway.config.ts');
Expand Down
116 changes: 116 additions & 0 deletions deployment/services/public-graphql-api-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import * as fs from 'fs';
import * as path from 'path';
import * as kx from '@pulumi/kubernetesx';
import * as pulumi from '@pulumi/pulumi';
import { serviceLocalEndpoint } from '../utils/local-endpoint';
import { ServiceSecret } from '../utils/secrets';
import { ServiceDeployment } from '../utils/service-deployment';
import { type Docker } from './docker';
import { type Environment } from './environment';
import { type GraphQL } from './graphql';
import { type Observability } from './observability';
import { type OTELCollector } from './otel-collector';

/**
* Hive Router Docker Image Version
*/
const dockerImage = 'ghcr.io/graphql-hive/router:0.0.39';
const configDirectory = path.resolve(__dirname, '..', 'config', 'public-graphql-api');

// On global scope to fail early in case of a read error
const configPath = path.join(configDirectory, 'router.config.yaml');
const routerConfigFile = fs.readFileSync(configPath, 'utf-8');

export function deployPublicGraphQLAPIRouter(args: {
environment: Environment;
graphql: GraphQL;
docker: Docker;
observability: Observability;
otelCollector: OTELCollector;
}) {
const apiConfig = new pulumi.Config('api');

// Note: The persisted documents cdn endpoint can also be used for reading the contract schema
const cdnEndpoint =
apiConfig.requireObject<Record<string, string>>('env')['HIVE_PERSISTED_DOCUMENTS_CDN_ENDPOINT'];

if (!cdnEndpoint) {
throw new Error("Missing cdn endpoint variable 'HIVE_PERSISTED_DOCUMENTS_CDN_ENDPOINT'.");
}

const hiveConfig = new pulumi.Config('hive');
const hiveConfigSecrets = new ServiceSecret('hive-router-tracing-secret', {
otelTraceAccessToken: hiveConfig.requireSecret('otelTraceAccessToken'),
});

const supergraphEndpoint = cdnEndpoint + '/contracts/public';

// Note: The persisted documents access key is also valid for reading the supergraph
const publicGraphQLAPISecret = new ServiceSecret('public-graphql-api-router-secret', {
cdnAccessKeyId: apiConfig.requireSecret('hivePersistedDocumentsCdnAccessKeyId'),
});

const routerConfigDirAbsolutePath = '/config/';
const routerConfigFileName = 'router.yaml';

const configMap = new kx.ConfigMap('public-graphql-api-router-config', {
data: {
[routerConfigFileName]: routerConfigFile,
},
});

const mountName = 'router-config';

return new ServiceDeployment(
'public-graphql-api-router',
{
imagePullSecret: args.docker.secret,
image: dockerImage,
replicas: args.environment.podsConfig.general.replicas,
availabilityOnEveryNode: true,
env: {
ROUTER_CONFIG_FILE_PATH: `${routerConfigDirAbsolutePath}${routerConfigFileName}`,
INTERNAL_GRAPHQL_URL: serviceLocalEndpoint(args.graphql.service).apply(
value => `${value}/graphql-public`,
),
HIVE_CDN_ENDPOINT: supergraphEndpoint,
HIVE_TARGET: hiveConfig.require('target'),
OPENTELEMETRY_COLLECTOR_ENDPOINT: args.observability.tracingEndpoint ?? '',
HIVE_TRACE_ENDPOINT: serviceLocalEndpoint(args.otelCollector.service).apply(
value => `${value}/v1/traces`,
),
},
port: 4000,
volumes: [
{
name: mountName,
configMap: {
name: configMap.metadata.name,
},
},
],
volumeMounts: [
{
mountPath: routerConfigDirAbsolutePath,
name: mountName,
readOnly: true,
},
],
readinessProbe: '/readiness',
livenessProbe: '/health',
startupProbe: {
endpoint: '/health',
initialDelaySeconds: 60,
failureThreshold: 10,
periodSeconds: 15,
timeoutSeconds: 15,
},
},
[args.graphql.deployment, args.graphql.service],
)
.withSecret('HIVE_CDN_KEY', publicGraphQLAPISecret, 'cdnAccessKeyId')
.withSecret('HIVE_ACCESS_TOKEN', hiveConfigSecrets, 'otelTraceAccessToken')
.deploy();
}

export type PublicGraphQLAPIRouter = ReturnType<typeof deployPublicGraphQLAPIRouter>;
29 changes: 22 additions & 7 deletions deployment/utils/reverse-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ import { helmChart } from './helm';
// prettier-ignore
export const CONTOUR_CHART = helmChart('https://raw.githubusercontent.com/bitnami/charts/refs/heads/index/bitnami/', 'contour', '20.0.3');

type SingleUpstream = k8s.core.v1.Service;
type WeightBasedUpstream = Array<{ upstream: k8s.core.v1.Service; weight: number }>;

function isWeightBasedUpstream(
upstream: SingleUpstream | WeightBasedUpstream,
): upstream is WeightBasedUpstream {
return Array.isArray(upstream);
}

export class Proxy {
private lbService: Output<k8s.core.v1.Service> | null = null;

Expand Down Expand Up @@ -80,7 +89,7 @@ export class Proxy {
routes: {
name: string;
path: string;
service: k8s.core.v1.Service;
service: SingleUpstream | WeightBasedUpstream;
requestTimeout?: `${number}s` | 'infinity';
idleTimeout?: `${number}s`;
retriable?: boolean;
Expand Down Expand Up @@ -147,12 +156,18 @@ export class Proxy {
prefix: route.path,
},
],
services: [
{
name: route.service.metadata.name,
port: route.service.spec.ports[0].port,
},
],
services: isWeightBasedUpstream(route.service)
? route.service.map(serviceDef => ({
name: serviceDef.upstream.metadata.name,
port: serviceDef.upstream.spec.ports[0].port,
weight: serviceDef.weight,
}))
: [
{
name: route.service.metadata.name,
port: route.service.spec.ports[0].port,
},
],
// https://projectcontour.io/docs/1.29/config/request-routing/#session-affinity
loadBalancerPolicy: {
strategy: 'Cookie',
Expand Down
Loading