mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
359 lines
9.7 KiB
TypeScript
359 lines
9.7 KiB
TypeScript
import * as k8s from '@pulumi/kubernetes';
|
|
import * as kx from '@pulumi/kubernetesx';
|
|
import * as pulumi from '@pulumi/pulumi';
|
|
import { isDefined } from './helpers';
|
|
import { normalizeEnv, PodBuilder } from './pod-builder';
|
|
import { ServiceSecret } from './secrets';
|
|
|
|
type ProbeConfig = Omit<
|
|
k8s.types.input.core.v1.Probe,
|
|
'httpGet' | 'exec' | 'grpc' | 'tcpSocket'
|
|
> & { endpoint: string };
|
|
|
|
function normalizeEnvSecrets(envSecrets?: Record<string, ServiceSecretBinding<any>>) {
|
|
return envSecrets
|
|
? Object.keys(envSecrets).map(name => ({
|
|
name,
|
|
valueFrom: {
|
|
secretKeyRef: {
|
|
name: envSecrets[name].secret.record.metadata.name,
|
|
key: envSecrets[name].key,
|
|
},
|
|
},
|
|
}))
|
|
: [];
|
|
}
|
|
|
|
export type ServiceSecretBinding<T extends Record<string, string>> = {
|
|
secret: ServiceSecret<T>;
|
|
key: keyof T | pulumi.Output<keyof T>;
|
|
};
|
|
|
|
export class ServiceDeployment {
|
|
private envSecrets: Record<string, ServiceSecretBinding<any>> = {};
|
|
|
|
constructor(
|
|
protected name: string,
|
|
protected options: {
|
|
imagePullSecret?: k8s.core.v1.Secret;
|
|
env?: kx.types.Container['env'];
|
|
args?: kx.types.Container['args'];
|
|
image: string;
|
|
port?: number;
|
|
serviceAccountName?: pulumi.Output<string>;
|
|
livenessProbe?: string | ProbeConfig;
|
|
readinessProbe?: string | ProbeConfig;
|
|
startupProbe?: string | ProbeConfig;
|
|
memoryLimit?: string;
|
|
volumes?: k8s.types.input.core.v1.Volume[];
|
|
volumeMounts?: k8s.types.input.core.v1.VolumeMount[];
|
|
/**
|
|
* Enables /metrics endpoint on port 10254
|
|
*/
|
|
exposesMetrics?: boolean;
|
|
replicas?: number;
|
|
pdb?: boolean;
|
|
autoScaling?: {
|
|
minReplicas?: number;
|
|
maxReplicas: number;
|
|
cpu: {
|
|
limit: string;
|
|
cpuAverageToScale: number;
|
|
};
|
|
};
|
|
availabilityOnEveryNode?: boolean;
|
|
command?: string[];
|
|
},
|
|
protected dependencies?: Array<pulumi.Resource | undefined | null>,
|
|
protected parent?: pulumi.Resource | null,
|
|
) {}
|
|
|
|
withSecret<T extends Record<string, string | pulumi.Output<string>>>(
|
|
envVar: string,
|
|
secret: ServiceSecret<T>,
|
|
key: keyof T,
|
|
) {
|
|
this.envSecrets[envVar] = { secret, key };
|
|
|
|
return this;
|
|
}
|
|
|
|
withConditionalSecret<T extends Record<string, string | pulumi.Output<string>>>(
|
|
enabled: boolean,
|
|
envVar: string,
|
|
secret: ServiceSecret<T> | null,
|
|
key: keyof T,
|
|
) {
|
|
if (enabled && secret) {
|
|
this.envSecrets[envVar] = { secret, key };
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
deployAsJob() {
|
|
const { pb } = this.createPod(true);
|
|
|
|
const job = new kx.Job(
|
|
this.name,
|
|
{
|
|
spec: pb.asJobSpec(),
|
|
},
|
|
{ dependsOn: this.dependencies?.filter(isDefined) },
|
|
);
|
|
|
|
return { job };
|
|
}
|
|
|
|
createPod(asJob: boolean) {
|
|
const port = this.options.port || 3000;
|
|
const additionalEnv: any[] = normalizeEnv(this.options.env);
|
|
const secretsEnv: any[] = normalizeEnvSecrets(this.envSecrets);
|
|
|
|
let startupProbe: k8s.types.input.core.v1.Probe | undefined = undefined;
|
|
let livenessProbe: k8s.types.input.core.v1.Probe | undefined = undefined;
|
|
let readinessProbe: k8s.types.input.core.v1.Probe | undefined = undefined;
|
|
|
|
if (this.options.livenessProbe) {
|
|
livenessProbe =
|
|
typeof this.options.livenessProbe === 'string'
|
|
? {
|
|
initialDelaySeconds: 10,
|
|
terminationGracePeriodSeconds: 60,
|
|
periodSeconds: 10,
|
|
failureThreshold: 5,
|
|
timeoutSeconds: 5,
|
|
httpGet: {
|
|
path: this.options.livenessProbe,
|
|
port,
|
|
},
|
|
}
|
|
: {
|
|
...this.options.livenessProbe,
|
|
httpGet: {
|
|
path: this.options.livenessProbe.endpoint,
|
|
port,
|
|
},
|
|
};
|
|
}
|
|
|
|
if (this.options.readinessProbe) {
|
|
readinessProbe =
|
|
typeof this.options.readinessProbe === 'string'
|
|
? {
|
|
initialDelaySeconds: 10,
|
|
periodSeconds: 15,
|
|
failureThreshold: 5,
|
|
timeoutSeconds: 5,
|
|
httpGet: {
|
|
path: this.options.readinessProbe,
|
|
port,
|
|
},
|
|
}
|
|
: {
|
|
...this.options.readinessProbe,
|
|
httpGet: {
|
|
path: this.options.readinessProbe.endpoint,
|
|
port,
|
|
},
|
|
};
|
|
}
|
|
|
|
if (this.options.startupProbe) {
|
|
startupProbe =
|
|
typeof this.options.startupProbe === 'string'
|
|
? {
|
|
initialDelaySeconds: 20,
|
|
periodSeconds: 30,
|
|
failureThreshold: 10,
|
|
timeoutSeconds: 10,
|
|
httpGet: {
|
|
path: this.options.startupProbe,
|
|
port,
|
|
},
|
|
}
|
|
: {
|
|
...this.options.startupProbe,
|
|
httpGet: {
|
|
path: this.options.startupProbe.endpoint,
|
|
port,
|
|
},
|
|
};
|
|
}
|
|
|
|
if (this.options.exposesMetrics) {
|
|
additionalEnv.push({ name: 'PROMETHEUS_METRICS', value: '1' });
|
|
}
|
|
|
|
const topologySpreadConstraints: k8s.types.input.core.v1.TopologySpreadConstraint[] = [];
|
|
|
|
if (this.options.availabilityOnEveryNode) {
|
|
// This will ensure that services that has >1 replicas will be scheduled on every available node
|
|
// and ensure that we are not exposed to downtime issues caused by node failures/restarts:
|
|
topologySpreadConstraints.push({
|
|
maxSkew: 1,
|
|
topologyKey: 'topology.kubernetes.io/zone',
|
|
whenUnsatisfiable: 'DoNotSchedule',
|
|
labelSelector: {
|
|
matchLabels: {
|
|
app: this.name,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
const resourcesLimits: Record<string, string> = {};
|
|
|
|
if (this.options?.autoScaling?.cpu.limit) {
|
|
resourcesLimits.cpu = this.options.autoScaling.cpu.limit;
|
|
}
|
|
|
|
if (this.options.memoryLimit) {
|
|
resourcesLimits.memory = this.options.memoryLimit;
|
|
}
|
|
|
|
const pb = new PodBuilder({
|
|
restartPolicy: asJob ? 'Never' : 'Always',
|
|
imagePullSecrets: this.options.imagePullSecret
|
|
? [{ name: this.options.imagePullSecret.metadata.name }]
|
|
: undefined,
|
|
terminationGracePeriodSeconds: 60,
|
|
volumes: this.options.volumes,
|
|
topologySpreadConstraints,
|
|
serviceAccountName: this.options.serviceAccountName,
|
|
containers: [
|
|
{
|
|
livenessProbe,
|
|
readinessProbe,
|
|
startupProbe,
|
|
volumeMounts: this.options.volumeMounts,
|
|
imagePullPolicy: 'Always',
|
|
env: [
|
|
{ name: 'PORT', value: String(port) },
|
|
{
|
|
name: 'POD_NAME',
|
|
valueFrom: {
|
|
fieldRef: {
|
|
fieldPath: 'metadata.name',
|
|
},
|
|
},
|
|
},
|
|
]
|
|
.concat(additionalEnv)
|
|
.concat(secretsEnv),
|
|
name: this.name,
|
|
image: this.options.image,
|
|
resources: {
|
|
limits: resourcesLimits,
|
|
},
|
|
args: this.options.args,
|
|
ports: {
|
|
http: port,
|
|
...(this.options.exposesMetrics
|
|
? {
|
|
metrics: 10_254,
|
|
}
|
|
: {}),
|
|
},
|
|
command: this.options.command,
|
|
},
|
|
],
|
|
});
|
|
|
|
return { pb };
|
|
}
|
|
|
|
deploy() {
|
|
const { pb } = this.createPod(false);
|
|
|
|
const metadata: k8s.types.input.meta.v1.ObjectMeta = {
|
|
annotations: {},
|
|
};
|
|
|
|
metadata.labels = {
|
|
app: this.name,
|
|
};
|
|
|
|
if (this.options.exposesMetrics) {
|
|
metadata.annotations = {
|
|
'prometheus.io/port': '10254',
|
|
'prometheus.io/path': '/metrics',
|
|
'prometheus.io/scrape': 'true',
|
|
};
|
|
}
|
|
|
|
const deployment = new kx.Deployment(
|
|
this.name,
|
|
{
|
|
spec: pb.asExtendedDeploymentSpec(
|
|
{
|
|
replicas: this.options.replicas ?? 1,
|
|
strategy: {
|
|
type: 'RollingUpdate',
|
|
rollingUpdate: {
|
|
maxSurge: this.options.replicas ?? 1,
|
|
maxUnavailable: 0,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
annotations: metadata.annotations,
|
|
labels: metadata.labels,
|
|
},
|
|
),
|
|
},
|
|
{
|
|
dependsOn: this.dependencies?.filter(isDefined),
|
|
parent: this.parent ?? undefined,
|
|
},
|
|
);
|
|
|
|
if (this.options.pdb) {
|
|
new k8s.policy.v1.PodDisruptionBudget(`${this.name}-pdb`, {
|
|
spec: {
|
|
minAvailable: 1,
|
|
selector: deployment.spec.selector,
|
|
},
|
|
});
|
|
}
|
|
|
|
const service = deployment.createService({});
|
|
|
|
if (this.options.autoScaling) {
|
|
new k8s.autoscaling.v2.HorizontalPodAutoscaler(
|
|
`${this.name}-autoscaler`,
|
|
{
|
|
apiVersion: 'autoscaling/v2',
|
|
kind: 'HorizontalPodAutoscaler',
|
|
metadata: {},
|
|
spec: {
|
|
scaleTargetRef: {
|
|
name: deployment.metadata.name,
|
|
kind: deployment.kind,
|
|
apiVersion: deployment.apiVersion,
|
|
},
|
|
metrics: [
|
|
{
|
|
type: 'Resource',
|
|
resource: {
|
|
name: 'cpu',
|
|
target: {
|
|
type: 'Utilization',
|
|
averageUtilization: this.options.autoScaling.cpu.cpuAverageToScale,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
minReplicas: this.options.autoScaling.minReplicas || this.options.replicas || 1,
|
|
maxReplicas: this.options.autoScaling.maxReplicas,
|
|
},
|
|
},
|
|
{
|
|
dependsOn: [deployment, service],
|
|
},
|
|
);
|
|
}
|
|
|
|
return { deployment, service };
|
|
}
|
|
}
|