Refactor deployment code (#4138)

This commit is contained in:
Dotan Simha 2024-03-04 14:56:12 +02:00 committed by GitHub
parent 445a2e5e5a
commit df0310dce6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 1184 additions and 1045 deletions

View file

@ -1,5 +1,4 @@
import * as pulumi from '@pulumi/pulumi';
import * as random from '@pulumi/random';
import { deployApp } from './services/app';
import { deployStripeBilling } from './services/billing';
import { deployCFBroker } from './services/cf-broker';
@ -8,41 +7,32 @@ import { deployClickhouse } from './services/clickhouse';
import { deployCloudFlareSecurityTransform } from './services/cloudflare-security';
import { deployDatabaseCleanupJob } from './services/database-cleanup';
import { deployDbMigrations } from './services/db-migrations';
import { configureDocker } from './services/docker';
import { deployEmails } from './services/emails';
import { prepareEnvironment } from './services/environment';
import { configureGithubApp } from './services/github';
import { deployGraphQL } from './services/graphql';
import { deployKafka } from './services/kafka';
import { deployMetrics } from './services/observability';
import { deploySchemaPolicy } from './services/policy';
import { deployPostgres } from './services/postgres';
import { deployProxy } from './services/proxy';
import { deployRateLimit } from './services/rate-limit';
import { deployRedis } from './services/redis';
import { deployS3 } from './services/s3';
import { deploySchema } from './services/schema';
import { configureSentry } from './services/sentry';
import { deploySentryEventsMonitor } from './services/sentry-events';
import { configureSlackApp } from './services/slack-app';
import { deploySuperTokens } from './services/supertokens';
import { deployTokens } from './services/tokens';
import { deployUsage } from './services/usage';
import { deployUsageEstimation } from './services/usage-estimation';
import { deployUsageIngestor } from './services/usage-ingestor';
import { deployWebhooks } from './services/webhooks';
import { DeploymentEnvironment } from './types';
import { configureZendesk } from './services/zendesk';
import { optimizeAzureCluster } from './utils/azure-helpers';
import { createDockerImageFactory } from './utils/docker-images';
import { isDefined, isProduction } from './utils/helpers';
// eslint-disable-next-line no-process-env
process.env.PULUMI_K8S_SUPPRESS_HELM_HOOK_WARNINGS = '1';
optimizeAzureCluster();
const dockerConfig = new pulumi.Config('docker');
const dockerImages = createDockerImageFactory({
registryHostname: dockerConfig.require('registryUrl'),
imagesPrefix: dockerConfig.require('imagesPrefix'),
});
const imagePullSecret = dockerImages.createRepositorySecret(
dockerConfig.requireSecret('registryAuthBase64'),
);
import { isDefined } from './utils/helpers';
// eslint-disable-next-line no-process-env
const imagesTag = process.env.DOCKER_IMAGE_TAG as string;
@ -51,250 +41,202 @@ if (!imagesTag) {
throw new Error(`DOCKER_IMAGE_TAG env variable is not set.`);
}
optimizeAzureCluster();
const docker = configureDocker();
const envName = pulumi.getStack();
const commonConfig = new pulumi.Config('common');
const appDns = 'app';
const rootDns = commonConfig.require('dnsZone');
const appHostname = `${appDns}.${rootDns}`;
const heartbeatsConfig = new pulumi.Config('heartbeats');
const emailConfig = new pulumi.Config('email');
const r2Config = new pulumi.Config('r2');
const s3Config = {
endpoint: r2Config.require('endpoint'),
bucketName: r2Config.require('bucketName'),
accessKeyId: r2Config.requireSecret('accessKeyId'),
secretAccessKey: r2Config.requireSecret('secretAccessKey'),
};
const deploymentEnv: DeploymentEnvironment = {
ENVIRONMENT: envName,
NODE_ENV: 'production',
DEPLOYED_DNS: appHostname,
};
deploySentryEventsMonitor({ envName, imagePullSecret });
const sentry = configureSentry();
const environment = prepareEnvironment({
release: imagesTag,
environment: envName,
rootDns: new pulumi.Config('common').require('dnsZone'),
});
deploySentryEventsMonitor({ docker, environment, sentry });
deployMetrics({ envName });
const cdnAuthPrivateKey = commonConfig.requireSecret('cdnAuthPrivateKey');
const clickhouse = deployClickhouse();
const postgres = deployPostgres();
const redis = deployRedis({ environment });
const kafka = deployKafka();
const s3 = deployS3();
const cdn = deployCFCDN({
envName,
rootDns,
s3Config,
release: imagesTag,
s3,
sentry,
environment,
});
const cfBroker = deployCFBroker({
envName,
rootDns,
release: imagesTag,
const broker = deployCFBroker({
environment,
sentry,
});
const redisApi = deployRedis({ deploymentEnv });
const kafkaApi = deployKafka();
const clickhouseApi = deployClickhouse();
// eslint-disable-next-line no-process-env
const shouldCleanDatabase = process.env.CLEAN_DATABASE === 'true';
const databaseCleanupJob = shouldCleanDatabase ? deployDatabaseCleanupJob({ deploymentEnv }) : null;
const databaseCleanupJob = shouldCleanDatabase ? deployDatabaseCleanupJob({ environment }) : null;
// eslint-disable-next-line no-process-env
const forceRunDbMigrations = process.env.FORCE_DB_MIGRATIONS === 'true';
const dbMigrations = deployDbMigrations({
clickhouse: clickhouseApi,
kafka: kafkaApi,
deploymentEnv,
image: dockerImages.getImageId('storage', imagesTag),
imagePullSecret,
clickhouse,
docker,
postgres,
s3,
cdn,
environment,
image: docker.factory.getImageId('storage', imagesTag),
force: forceRunDbMigrations,
dependencies: [databaseCleanupJob].filter(isDefined),
s3: s3Config,
cdnAuthPrivateKey,
});
const tokensApi = deployTokens({
image: dockerImages.getImageId('tokens', imagesTag),
release: imagesTag,
deploymentEnv,
const tokens = deployTokens({
image: docker.factory.getImageId('tokens', imagesTag),
environment,
dbMigrations,
redis: redisApi,
docker,
postgres,
redis,
heartbeat: heartbeatsConfig.get('tokens'),
imagePullSecret,
sentry,
});
const webhooksApi = deployWebhooks({
image: dockerImages.getImageId('webhooks', imagesTag),
imagePullSecret,
release: imagesTag,
deploymentEnv,
redis: redisApi,
const webhooks = deployWebhooks({
image: docker.factory.getImageId('webhooks', imagesTag),
environment,
heartbeat: heartbeatsConfig.get('webhooks'),
broker: cfBroker,
broker,
docker,
redis,
sentry,
});
const emailsApi = deployEmails({
image: dockerImages.getImageId('emails', imagesTag),
imagePullSecret,
release: imagesTag,
deploymentEnv,
redis: redisApi,
email: {
token: emailConfig.requireSecret('token'),
from: emailConfig.require('from'),
messageStream: emailConfig.require('messageStream'),
},
// heartbeat: heartbeatsConfig.get('emails'),
const emails = deployEmails({
image: docker.factory.getImageId('emails', imagesTag),
docker,
environment,
redis,
sentry,
});
const usageEstimationApi = deployUsageEstimation({
image: dockerImages.getImageId('usage-estimator', imagesTag),
imagePullSecret,
release: imagesTag,
deploymentEnv,
clickhouse: clickhouseApi,
const usageEstimator = deployUsageEstimation({
image: docker.factory.getImageId('usage-estimator', imagesTag),
docker,
environment,
clickhouse,
dbMigrations,
sentry,
});
const billingApi = deployStripeBilling({
image: dockerImages.getImageId('stripe-billing', imagesTag),
imagePullSecret,
release: imagesTag,
deploymentEnv,
const billing = deployStripeBilling({
image: docker.factory.getImageId('stripe-billing', imagesTag),
docker,
postgres,
environment,
dbMigrations,
usageEstimator: usageEstimationApi,
usageEstimator,
sentry,
});
const rateLimitApi = deployRateLimit({
image: dockerImages.getImageId('rate-limit', imagesTag),
imagePullSecret,
release: imagesTag,
deploymentEnv,
const rateLimit = deployRateLimit({
image: docker.factory.getImageId('rate-limit', imagesTag),
docker,
environment,
dbMigrations,
usageEstimator: usageEstimationApi,
emails: emailsApi,
usageEstimator,
emails,
postgres,
sentry,
});
const usageApi = deployUsage({
image: dockerImages.getImageId('usage', imagesTag),
imagePullSecret,
release: imagesTag,
deploymentEnv,
tokens: tokensApi,
kafka: kafkaApi,
const usage = deployUsage({
image: docker.factory.getImageId('usage', imagesTag),
docker,
environment,
tokens,
kafka,
dbMigrations,
rateLimit: rateLimitApi,
rateLimit,
sentry,
});
const usageIngestorApi = deployUsageIngestor({
image: dockerImages.getImageId('usage-ingestor', imagesTag),
imagePullSecret,
release: imagesTag,
clickhouse: clickhouseApi,
kafka: kafkaApi,
deploymentEnv,
const usageIngestor = deployUsageIngestor({
image: docker.factory.getImageId('usage-ingestor', imagesTag),
docker,
clickhouse,
kafka,
environment,
dbMigrations,
heartbeat: heartbeatsConfig.get('usageIngestor'),
sentry,
});
const schemaApi = deploySchema({
image: dockerImages.getImageId('schema', imagesTag),
imagePullSecret,
release: imagesTag,
deploymentEnv,
redis: redisApi,
broker: cfBroker,
const schema = deploySchema({
image: docker.factory.getImageId('schema', imagesTag),
docker,
environment,
redis,
broker,
sentry,
});
const schemaPolicyApi = deploySchemaPolicy({
image: dockerImages.getImageId('policy', imagesTag),
imagePullSecret,
release: imagesTag,
deploymentEnv,
const schemaPolicy = deploySchemaPolicy({
image: docker.factory.getImageId('policy', imagesTag),
docker,
environment,
sentry,
});
const supertokensApiKey = new random.RandomPassword('supertokens-api-key', {
length: 31,
special: false,
});
const supertokens = deploySuperTokens(postgres, { dependencies: [dbMigrations] }, environment);
const zendesk = configureZendesk({ environment });
const githubApp = configureGithubApp();
const slackApp = configureSlackApp();
const oauthConfig = new pulumi.Config('oauth');
const githubConfig = {
clientId: oauthConfig.requireSecret('githubClient'),
clientSecret: oauthConfig.requireSecret('githubSecret'),
};
const googleConfig = {
clientId: oauthConfig.requireSecret('googleClient'),
clientSecret: oauthConfig.requireSecret('googleSecret'),
};
const supertokens = deploySuperTokens(
{ apiKey: supertokensApiKey.result },
{ dependencies: [dbMigrations] },
deploymentEnv,
);
const zendeskConfig = new pulumi.Config('zendesk');
const graphqlApi = deployGraphQL({
clickhouse: clickhouseApi,
image: dockerImages.getImageId('server', imagesTag),
imagePullSecret,
release: imagesTag,
deploymentEnv,
tokens: tokensApi,
webhooks: webhooksApi,
schema: schemaApi,
schemaPolicy: schemaPolicyApi,
const graphql = deployGraphQL({
postgres,
environment,
clickhouse,
image: docker.factory.getImageId('server', imagesTag),
docker,
tokens,
webhooks,
schema,
schemaPolicy,
dbMigrations,
redis: redisApi,
usage: usageApi,
cdnAuthPrivateKey,
redis,
usage,
cdn,
usageEstimator: usageEstimationApi,
rateLimit: rateLimitApi,
billing: billingApi,
emails: emailsApi,
supertokensConfig: {
apiKey: supertokensApiKey.result,
endpoint: supertokens.localEndpoint,
},
s3Config,
zendeskConfig: isProduction(deploymentEnv)
? {
subdomain: zendeskConfig.require('subdomain'),
username: zendeskConfig.require('username'),
password: zendeskConfig.requireSecret('password'),
}
: null,
usageEstimator,
rateLimit,
billing,
emails,
supertokens,
s3,
zendesk,
githubApp,
sentry,
});
const app = deployApp({
deploymentEnv,
graphql: graphqlApi,
environment,
graphql,
dbMigrations,
image: dockerImages.getImageId('app', imagesTag),
imagePullSecret,
release: imagesTag,
supertokensConfig: {
apiKey: supertokensApiKey.result,
endpoint: supertokens.localEndpoint,
},
githubConfig,
googleConfig,
emailsEndpoint: emailsApi.localEndpoint,
zendeskSupport: isProduction(deploymentEnv),
image: docker.factory.getImageId('app', imagesTag),
docker,
supertokens,
emails,
zendesk,
billing,
github: githubApp,
slackApp,
sentry,
});
const proxy = deployProxy({
appHostname,
app,
graphql: graphqlApi,
usage: usageApi,
deploymentEnv,
graphql,
usage,
environment,
});
deployCloudFlareSecurityTransform({
@ -315,12 +257,12 @@ deployCloudFlareSecurityTransform({
ignoredHosts: ['cdn.graphql-hive.com', 'cdn.staging.graphql-hive.com'],
});
export const graphqlApiServiceId = graphqlApi.service.id;
export const usageApiServiceId = usageApi.service.id;
export const usageIngestorApiServiceId = usageIngestorApi.service.id;
export const tokensApiServiceId = tokensApi.service.id;
export const schemaApiServiceId = schemaApi.service.id;
export const webhooksApiServiceId = webhooksApi.service.id;
export const graphqlApiServiceId = graphql.service.id;
export const usageApiServiceId = usage.service.id;
export const usageIngestorApiServiceId = usageIngestor.service.id;
export const tokensApiServiceId = tokens.service.id;
export const schemaApiServiceId = schema.service.id;
export const webhooksApiServiceId = webhooks.service.id;
export const appId = app.deployment.id;
export const publicIp = proxy!.status.loadBalancer.ingress[0].ip;

View file

@ -1,61 +1,72 @@
import * as k8s from '@pulumi/kubernetes';
import * as pulumi from '@pulumi/pulumi';
import { DeploymentEnvironment } from '../types';
import { isProduction } from '../utils/helpers';
import { serviceLocalEndpoint } from '../utils/local-endpoint';
import { ServiceSecret } from '../utils/secrets';
import { ServiceDeployment } from '../utils/service-deployment';
import { StripeBilling } from './billing';
import { DbMigrations } from './db-migrations';
import { Docker } from './docker';
import { Emails } from './emails';
import { Environment } from './environment';
import { GitHubApp } from './github';
import { GraphQL } from './graphql';
const appConfig = new pulumi.Config('app');
const commonConfig = new pulumi.Config('common');
const githubAppConfig = new pulumi.Config('ghapp');
const appEnv = appConfig.requireObject<Record<string, string>>('env');
const commonEnv = commonConfig.requireObject<Record<string, string>>('env');
import { Sentry } from './sentry';
import { SlackApp } from './slack-app';
import { Supertokens } from './supertokens';
import { Zendesk } from './zendesk';
export type App = ReturnType<typeof deployApp>;
class AppOAuthSecret extends ServiceSecret<{
clientId: string | pulumi.Output<string>;
clientSecret: string | pulumi.Output<string>;
}> {}
export function deployApp({
deploymentEnv,
graphql,
dbMigrations,
release,
image,
supertokensConfig,
googleConfig,
githubConfig,
imagePullSecret,
emailsEndpoint,
zendeskSupport,
supertokens,
docker,
emails,
zendesk,
github,
slackApp,
billing,
sentry,
environment,
}: {
environment: Environment;
image: string;
release: string;
deploymentEnv: DeploymentEnvironment;
graphql: GraphQL;
dbMigrations: DbMigrations;
imagePullSecret: k8s.core.v1.Secret;
supertokensConfig: {
endpoint: pulumi.Output<string>;
apiKey: pulumi.Output<string>;
};
googleConfig: {
clientId: pulumi.Output<string>;
clientSecret: pulumi.Output<string>;
};
githubConfig: {
clientId: pulumi.Output<string>;
clientSecret: pulumi.Output<string>;
};
emailsEndpoint: pulumi.Output<string>;
zendeskSupport: boolean;
docker: Docker;
supertokens: Supertokens;
emails: Emails;
zendesk: Zendesk;
github: GitHubApp;
slackApp: SlackApp;
billing: StripeBilling;
sentry: Sentry;
}) {
const appConfig = new pulumi.Config('app');
const appEnv = appConfig.requireObject<Record<string, string>>('env');
const oauthConfig = new pulumi.Config('oauth');
const githubOAuthSecret = new AppOAuthSecret('oauth-github', {
clientId: oauthConfig.requireSecret('githubClient'),
clientSecret: oauthConfig.requireSecret('githubSecret'),
});
const googleOAuthSecret = new AppOAuthSecret('oauth-google', {
clientId: oauthConfig.requireSecret('googleClient'),
clientSecret: oauthConfig.requireSecret('googleSecret'),
});
return new ServiceDeployment(
'app',
{
image,
replicas: isProduction(deploymentEnv) ? 3 : 1,
imagePullSecret,
replicas: environment.isProduction ? 3 : 1,
imagePullSecret: docker.secret,
readinessProbe: '/api/health',
livenessProbe: '/api/health',
startupProbe: {
@ -66,138 +77,38 @@ export function deployApp({
timeoutSeconds: 15,
},
availabilityOnEveryNode: true,
env: [
{ name: 'DEPLOYED_DNS', value: deploymentEnv.DEPLOYED_DNS },
{ name: 'NODE_ENV', value: 'production' },
{
name: 'ENVIRONMENT',
value: deploymentEnv.ENVIRONMENT,
},
{
name: 'RELEASE',
value: release,
},
{ name: 'SENTRY_DSN', value: commonEnv.SENTRY_DSN },
{ name: 'SENTRY', value: commonEnv.SENTRY_ENABLED },
{
name: 'GRAPHQL_ENDPOINT',
value: serviceLocalEndpoint(graphql.service).apply(s => `${s}/graphql`),
},
{
name: 'SERVER_ENDPOINT',
value: serviceLocalEndpoint(graphql.service),
},
{
name: 'APP_BASE_URL',
value: `https://${deploymentEnv.DEPLOYED_DNS}/`,
},
{
name: 'INTEGRATION_SLACK',
value: '1',
},
{
name: 'INTEGRATION_SLACK_CLIENT_ID',
value: appEnv.SLACK_CLIENT_ID,
},
{
name: 'INTEGRATION_SLACK_CLIENT_SECRET',
value: appEnv.SLACK_CLIENT_SECRET,
},
{
name: 'INTEGRATION_GITHUB_APP_NAME',
value: githubAppConfig.require('name'),
},
{
name: 'STRIPE_PUBLIC_KEY',
value: appEnv.STRIPE_PUBLIC_KEY,
},
{
name: 'GA_TRACKING_ID',
value: appEnv.GA_TRACKING_ID,
},
{
name: 'CRISP_WEBSITE_ID',
value: appEnv.CRISP_WEBSITE_ID,
},
{
name: 'DOCS_URL',
value: 'https://the-guild.dev/graphql/hive/docs',
},
{
name: 'GRAPHQL_PERSISTED_OPERATIONS',
value: '1',
},
{
name: 'ZENDESK_SUPPORT',
value: zendeskSupport ? '1' : '0',
},
//
// AUTH
//
{
name: 'SUPERTOKENS_CONNECTION_URI',
value: supertokensConfig.endpoint,
},
{
name: 'SUPERTOKENS_API_KEY',
value: supertokensConfig.apiKey,
},
{
name: 'EMAILS_ENDPOINT',
value: emailsEndpoint,
},
// GitHub
{
name: 'AUTH_GITHUB',
value: '1',
},
{
name: 'AUTH_GITHUB_CLIENT_ID',
value: githubConfig.clientId,
},
{
name: 'AUTH_GITHUB_CLIENT_SECRET',
value: githubConfig.clientSecret,
},
// Google
{
name: 'AUTH_GOOGLE',
value: '1',
},
{
name: 'AUTH_GOOGLE_CLIENT_ID',
value: googleConfig.clientId,
},
{
name: 'AUTH_GOOGLE_CLIENT_SECRET',
value: googleConfig.clientSecret,
},
{
name: 'AUTH_REQUIRE_EMAIL_VERIFICATION',
value: '1',
},
{
name: 'AUTH_ORGANIZATION_OIDC',
value: '1',
},
//
// Migrations
//
{
name: 'MEMBER_ROLES_DEADLINE',
value: appEnv.MEMBER_ROLES_DEADLINE,
},
],
env: {
...environment.envVars,
SENTRY: sentry.enabled ? '1' : '0',
GRAPHQL_ENDPOINT: serviceLocalEndpoint(graphql.service).apply(s => `${s}/graphql`),
SERVER_ENDPOINT: serviceLocalEndpoint(graphql.service),
APP_BASE_URL: `https://${environment.appDns}/`,
INTEGRATION_SLACK: '1',
INTEGRATION_GITHUB_APP_NAME: github.name,
GA_TRACKING_ID: appEnv.GA_TRACKING_ID,
DOCS_URL: 'https://the-guild.dev/graphql/hive/docs',
GRAPHQL_PERSISTED_OPERATIONS: '1',
ZENDESK_SUPPORT: zendesk.enabled ? '1' : '0',
SUPERTOKENS_CONNECTION_URI: supertokens.localEndpoint,
EMAILS_ENDPOINT: serviceLocalEndpoint(emails.service),
AUTH_GITHUB: '1',
AUTH_GOOGLE: '1',
AUTH_REQUIRE_EMAIL_VERIFICATION: '1',
AUTH_ORGANIZATION_OIDC: '1',
MEMBER_ROLES_DEADLINE: appEnv.MEMBER_ROLES_DEADLINE,
},
port: 3000,
},
[graphql.service, graphql.deployment, dbMigrations],
).deploy();
)
.withSecret('INTEGRATION_SLACK_CLIENT_ID', slackApp.secret, 'clientId')
.withSecret('INTEGRATION_SLACK_CLIENT_SECRET', slackApp.secret, 'clientSecret')
.withSecret('STRIPE_PUBLIC_KEY', billing.secret, 'stripePublicKey')
.withSecret('SUPERTOKENS_API_KEY', supertokens.secret, 'apiKey')
.withSecret('AUTH_GITHUB_CLIENT_ID', githubOAuthSecret, 'clientId')
.withSecret('AUTH_GITHUB_CLIENT_SECRET', githubOAuthSecret, 'clientSecret')
.withSecret('AUTH_GOOGLE_CLIENT_ID', googleOAuthSecret, 'clientId')
.withSecret('AUTH_GOOGLE_CLIENT_SECRET', googleOAuthSecret, 'clientSecret')
.withConditionalSecret(sentry.enabled, 'SENTRY_DSN', sentry.secret, 'dsn')
.deploy();
}

View file

@ -1,66 +1,77 @@
import { parse } from 'pg-connection-string';
import * as k8s from '@pulumi/kubernetes';
import * as pulumi from '@pulumi/pulumi';
import { DeploymentEnvironment } from '../types';
import { isProduction } from '../utils/helpers';
import { serviceLocalEndpoint } from '../utils/local-endpoint';
import { ServiceSecret } from '../utils/secrets';
import { ServiceDeployment } from '../utils/service-deployment';
import { DbMigrations } from './db-migrations';
import { Docker } from './docker';
import { Environment } from './environment';
import { Postgres } from './postgres';
import { Sentry } from './sentry';
import { UsageEstimator } from './usage-estimation';
const billingConfig = new pulumi.Config('billing');
const commonConfig = new pulumi.Config('common');
const commonEnv = commonConfig.requireObject<Record<string, string>>('env');
const apiConfig = new pulumi.Config('api');
export type StripeBillingService = ReturnType<typeof deployStripeBilling>;
class StripeSecret extends ServiceSecret<{
stripePrivateKey: pulumi.Output<string> | string;
stripePublicKey: string | pulumi.Output<string>;
}> {}
export function deployStripeBilling({
deploymentEnv,
environment,
dbMigrations,
usageEstimator,
image,
release,
imagePullSecret,
docker,
postgres,
sentry,
}: {
usageEstimator: UsageEstimator;
image: string;
release: string;
deploymentEnv: DeploymentEnvironment;
environment: Environment;
dbMigrations: DbMigrations;
imagePullSecret: k8s.core.v1.Secret;
docker: Docker;
postgres: Postgres;
sentry: Sentry;
}) {
const rawConnectionString = apiConfig.requireSecret('postgresConnectionString');
const connectionString = rawConnectionString.apply(rawConnectionString =>
parse(rawConnectionString),
);
return new ServiceDeployment(
const billingConfig = new pulumi.Config('billing');
const stripeSecret = new StripeSecret('stripe', {
stripePrivateKey: billingConfig.requireSecret('stripePrivateKey'),
stripePublicKey: billingConfig.require('stripePublicKey'),
});
const { deployment, service } = new ServiceDeployment(
'stripe-billing',
{
image,
imagePullSecret,
replicas: isProduction(deploymentEnv) ? 3 : 1,
imagePullSecret: docker.secret,
replicas: environment.isProduction ? 3 : 1,
readinessProbe: '/_readiness',
livenessProbe: '/_health',
startupProbe: '/_health',
env: {
...deploymentEnv,
...commonEnv,
SENTRY: commonEnv.SENTRY_ENABLED,
RELEASE: release,
...environment.envVars,
SENTRY: sentry.enabled ? '1' : '0',
USAGE_ESTIMATOR_ENDPOINT: serviceLocalEndpoint(usageEstimator.service),
STRIPE_SECRET_KEY: billingConfig.requireSecret('stripePrivateKey'),
POSTGRES_HOST: connectionString.apply(connection => connection.host ?? ''),
POSTGRES_PORT: connectionString.apply(connection => connection.port || '5432'),
POSTGRES_PASSWORD: connectionString.apply(connection => connection.password ?? ''),
POSTGRES_USER: connectionString.apply(connection => connection.user ?? ''),
POSTGRES_DB: connectionString.apply(connection => connection.database ?? ''),
POSTGRES_SSL: connectionString.apply(connection => (connection.ssl ? '1' : '0')),
},
exposesMetrics: true,
port: 4000,
},
[dbMigrations, usageEstimator.service, usageEstimator.deployment],
).deploy();
)
.withSecret('STRIPE_SECRET_KEY', stripeSecret, 'stripePrivateKey')
.withSecret('POSTGRES_HOST', postgres.secret, 'host')
.withSecret('POSTGRES_PORT', postgres.secret, 'port')
.withSecret('POSTGRES_USER', postgres.secret, 'user')
.withSecret('POSTGRES_PASSWORD', postgres.secret, 'password')
.withSecret('POSTGRES_DB', postgres.secret, 'database')
.withSecret('POSTGRES_SSL', postgres.secret, 'ssl')
.withConditionalSecret(sentry.enabled, 'SENTRY_DSN', sentry.secret, 'dsn')
.deploy();
return {
deployment,
service,
secret: stripeSecret,
};
}
export type StripeBilling = ReturnType<typeof deployStripeBilling>;

View file

@ -1,35 +1,40 @@
import * as pulumi from '@pulumi/pulumi';
import { CloudflareBroker } from '../utils/cloudflare';
import { isProduction } from '../utils/helpers';
import { ServiceSecret } from '../utils/secrets';
import { Environment } from './environment';
import { Sentry } from './sentry';
const commonConfig = new pulumi.Config('common');
const cfConfig = new pulumi.Config('cloudflareCustom');
const observabilityConfig = new pulumi.Config('observability');
const commonEnv = commonConfig.requireObject<Record<string, string>>('env');
export class CloudFlareBrokerSecret extends ServiceSecret<{
secretSignature: string | pulumi.Output<string>;
baseUrl: string | pulumi.Output<string>;
}> {}
export type Broker = ReturnType<typeof deployCFBroker>;
export function deployCFBroker({
rootDns,
envName,
release,
environment,
sentry,
}: {
rootDns: string;
envName: string;
release: string;
environment: Environment;
sentry: Sentry;
}) {
const commonConfig = new pulumi.Config('common');
const cfConfig = new pulumi.Config('cloudflareCustom');
const observabilityConfig = new pulumi.Config('observability');
const cfBrokerSignature = commonConfig.requireSecret('cfBrokerSignature');
const broker = new CloudflareBroker({
envName,
envName: environment.envName,
zoneId: cfConfig.require('zoneId'),
// We can't cdn for staging env, since CF certificate only covers
// one level of subdomains. See: https://community.cloudflare.com/t/ssl-handshake-error-cloudflare-proxy/175088
// So for staging env, we are going to use `broker-staging` instead of `broker.staging`.
cdnDnsRecord: isProduction(envName) ? `broker.${rootDns}` : `broker-${rootDns}`,
cdnDnsRecord: environment.isProduction
? `broker.${environment.rootDns}`
: `broker-${environment.rootDns}`,
secretSignature: cfBrokerSignature,
sentryDsn: commonEnv.SENTRY_DSN,
release,
sentryDsn: sentry.enabled && sentry.secret ? sentry.secret.raw.dsn : '',
release: environment.release,
loki: observabilityConfig.getBoolean('enabled')
? {
endpoint: observabilityConfig.require('lokiEndpoint'),
@ -38,5 +43,16 @@ export function deployCFBroker({
}
: null,
});
return broker.deploy();
const deployedBroker = broker.deploy();
const secret = new CloudFlareBrokerSecret('cloudflare-broker', {
secretSignature: cfBrokerSignature,
baseUrl: deployedBroker.workerBaseUrl,
});
return {
broker: deployedBroker,
secret,
};
}

View file

@ -1,41 +1,51 @@
import * as pulumi from '@pulumi/pulumi';
import { CloudflareCDN } from '../utils/cloudflare';
import { isProduction } from '../utils/helpers';
const commonConfig = new pulumi.Config('common');
const cfConfig = new pulumi.Config('cloudflareCustom');
const commonEnv = commonConfig.requireObject<Record<string, string>>('env');
import { ServiceSecret } from '../utils/secrets';
import { Environment } from './environment';
import { S3 } from './s3';
import { Sentry } from './sentry';
export type CDN = ReturnType<typeof deployCFCDN>;
export class CDNSecret extends ServiceSecret<{
authPrivateKey: string | pulumi.Output<string>;
baseUrl: string | pulumi.Output<string>;
}> {}
export function deployCFCDN({
rootDns,
release,
envName,
s3Config,
environment,
s3,
sentry,
}: {
rootDns: string;
envName: string;
release: string;
s3Config: {
endpoint: string;
bucketName: string;
accessKeyId: pulumi.Output<string>;
secretAccessKey: pulumi.Output<string>;
};
environment: Environment;
s3: S3;
sentry: Sentry;
}) {
const cfConfig = new pulumi.Config('cloudflareCustom');
const cdn = new CloudflareCDN({
envName,
envName: environment.envName,
zoneId: cfConfig.require('zoneId'),
// We can't cdn for staging env, since CF certificate only covers
// one level of subdomains. See: https://community.cloudflare.com/t/ssl-handshake-error-cloudflare-proxy/175088
// So for staging env, we are going to use `cdn-staging` instead of `cdn.staging`.
cdnDnsRecord: isProduction(envName) ? `cdn.${rootDns}` : `cdn-${rootDns}`,
sentryDsn: commonEnv.SENTRY_DSN,
release,
s3Config,
cdnDnsRecord: environment.isProduction
? `cdn.${environment.rootDns}`
: `cdn-${environment.rootDns}`,
sentryDsn: sentry.enabled && sentry.secret ? sentry.secret?.raw.dsn : '',
release: environment.release,
s3,
});
return cdn.deploy();
const deployedCdn = cdn.deploy();
const cdnConfig = new pulumi.Config('cdn');
const secret = new CDNSecret('cdn', {
authPrivateKey: cdnConfig.requireSecret('authPrivateKey'),
baseUrl: deployedCdn.workerBaseUrl,
});
return {
cdn: deployedCdn,
secret,
};
}

View file

@ -1,30 +1,28 @@
import * as pulumi from '@pulumi/pulumi';
const clickhouseConfig = new pulumi.Config('clickhouse');
import { ServiceSecret } from '../utils/secrets';
export type Clickhouse = ReturnType<typeof deployClickhouse>;
type ClickhouseConfig = {
protocol: pulumi.Output<string> | string;
host: pulumi.Output<string> | string;
port: pulumi.Output<string> | string;
username: pulumi.Output<string> | string;
password: pulumi.Output<string>;
};
export class ClickhouseConnectionSecret extends ServiceSecret<{
host: string | pulumi.Output<string>;
port: string | pulumi.Output<string>;
username: string | pulumi.Output<string>;
password: string | pulumi.Output<string>;
protocol: string | pulumi.Output<string>;
}> {}
function getRemoteClickhouseConfig(): ClickhouseConfig {
return {
export function deployClickhouse() {
const clickhouseConfig = new pulumi.Config('clickhouse');
const secret = new ClickhouseConnectionSecret('clickhouse', {
host: clickhouseConfig.require('host'),
port: clickhouseConfig.require('port'),
username: clickhouseConfig.require('username'),
password: clickhouseConfig.requireSecret('password'),
protocol: clickhouseConfig.require('protocol'),
};
}
});
export function deployClickhouse() {
return {
config: getRemoteClickhouseConfig(),
secret,
deployment: null,
service: null,
};

View file

@ -1,13 +1,12 @@
import * as k8s from '@pulumi/kubernetes';
import * as pulumi from '@pulumi/pulumi';
import { DeploymentEnvironment } from '../types';
import { isProduction } from '../utils/helpers';
import { ServiceDeployment } from '../utils/service-deployment';
import { Environment } from './environment';
const apiConfig = new pulumi.Config('api');
export function deployDatabaseCleanupJob(options: { deploymentEnv: DeploymentEnvironment }) {
if (isProduction(options.deploymentEnv)) {
export function deployDatabaseCleanupJob(options: { environment: Environment }) {
if (options.environment.isProduction) {
throw new Error('Database cleanup job is not allowed in "production" environment!');
}

View file

@ -1,75 +1,47 @@
import { parse } from 'pg-connection-string';
import * as k8s from '@pulumi/kubernetes';
import * as pulumi from '@pulumi/pulumi';
import { DeploymentEnvironment } from '../types';
import { ServiceDeployment } from '../utils/service-deployment';
import { CDN } from './cf-cdn';
import { Clickhouse } from './clickhouse';
import { Kafka } from './kafka';
const apiConfig = new pulumi.Config('api');
import { Docker } from './docker';
import { Environment } from './environment';
import { Postgres } from './postgres';
import { S3 } from './s3';
export type DbMigrations = ReturnType<typeof deployDbMigrations>;
export function deployDbMigrations({
deploymentEnv,
environment,
clickhouse,
kafka,
s3,
image,
imagePullSecret,
dependencies,
force,
s3,
cdnAuthPrivateKey,
docker,
postgres,
cdn,
}: {
deploymentEnv: DeploymentEnvironment;
docker: Docker;
postgres: Postgres;
clickhouse: Clickhouse;
kafka: Kafka;
s3: S3;
cdn: CDN;
environment: Environment;
image: string;
imagePullSecret: k8s.core.v1.Secret;
dependencies?: pulumi.Resource[];
force?: boolean;
s3: {
accessKeyId: string | pulumi.Output<string>;
secretAccessKey: string | pulumi.Output<string>;
endpoint: string | pulumi.Output<string>;
bucketName: string | pulumi.Output<string>;
};
cdnAuthPrivateKey: pulumi.Output<string>;
}) {
const rawConnectionString = apiConfig.requireSecret('postgresConnectionString');
const connectionString = rawConnectionString.apply(rawConnectionString =>
parse(rawConnectionString),
);
const { job } = new ServiceDeployment(
'db-migrations',
{
imagePullSecret,
imagePullSecret: docker.secret,
image,
env: {
POSTGRES_HOST: connectionString.apply(connection => connection.host ?? ''),
POSTGRES_PORT: connectionString.apply(connection => connection.port || '5432'),
POSTGRES_PASSWORD: connectionString.apply(connection => connection.password ?? ''),
POSTGRES_USER: connectionString.apply(connection => connection.user ?? ''),
POSTGRES_DB: connectionString.apply(connection => connection.database ?? ''),
POSTGRES_SSL: connectionString.apply(connection => (connection.ssl ? '1' : '0')),
...environment.envVars,
MIGRATOR: 'up',
CLICKHOUSE_MIGRATOR: 'up',
CLICKHOUSE_HOST: clickhouse.config.host,
CLICKHOUSE_PORT: clickhouse.config.port,
CLICKHOUSE_USERNAME: clickhouse.config.username,
CLICKHOUSE_PASSWORD: clickhouse.config.password,
CLICKHOUSE_PROTOCOL: clickhouse.config.protocol,
CLICKHOUSE_MIGRATOR_GRAPHQL_HIVE_CLOUD: '1',
KAFKA_BROKER: kafka.config.endpoint,
TS_NODE_TRANSPILE_ONLY: 'true',
RUN_S3_LEGACY_CDN_KEY_IMPORT: '1',
S3_ACCESS_KEY_ID: s3.accessKeyId,
S3_SECRET_ACCESS_KEY: s3.secretAccessKey,
S3_ENDPOINT: s3.endpoint,
S3_BUCKET_NAME: s3.bucketName,
CDN_AUTH_PRIVATE_KEY: cdnAuthPrivateKey,
...deploymentEnv,
// Change to this env var will lead to force rerun of the migration job
// Since K8s job are immutable, we can't edit or ask K8s to re-run a Job, so we are doing a
// pseudo change to an env var, which causes Pulumi to re-create the Job.
@ -78,7 +50,24 @@ export function deployDbMigrations({
},
[clickhouse.deployment, clickhouse.service, ...(dependencies || [])],
clickhouse.service,
).deployAsJob();
)
.withSecret('POSTGRES_HOST', postgres.secret, 'host')
.withSecret('POSTGRES_PORT', postgres.secret, 'port')
.withSecret('POSTGRES_USER', postgres.secret, 'user')
.withSecret('POSTGRES_PASSWORD', postgres.secret, 'password')
.withSecret('POSTGRES_DB', postgres.secret, 'database')
.withSecret('POSTGRES_SSL', postgres.secret, 'ssl')
.withSecret('CLICKHOUSE_HOST', clickhouse.secret, 'host')
.withSecret('CLICKHOUSE_PORT', clickhouse.secret, 'port')
.withSecret('CLICKHOUSE_USERNAME', clickhouse.secret, 'username')
.withSecret('CLICKHOUSE_PASSWORD', clickhouse.secret, 'password')
.withSecret('CLICKHOUSE_PROTOCOL', clickhouse.secret, 'protocol')
.withSecret('S3_ACCESS_KEY_ID', s3.secret, 'accessKeyId')
.withSecret('S3_SECRET_ACCESS_KEY', s3.secret, 'secretAccessKey')
.withSecret('S3_BUCKET_NAME', s3.secret, 'bucket')
.withSecret('S3_ENDPOINT', s3.secret, 'endpoint')
.withSecret('CDN_AUTH_PRIVATE_KEY', cdn.secret, 'authPrivateKey')
.deployAsJob();
return job;
}

View file

@ -0,0 +1,21 @@
import * as pulumi from '@pulumi/pulumi';
import { createDockerImageFactory } from '../utils/docker-images';
export function configureDocker() {
const dockerConfig = new pulumi.Config('docker');
const dockerImages = createDockerImageFactory({
registryHostname: dockerConfig.require('registryUrl'),
imagesPrefix: dockerConfig.require('imagesPrefix'),
});
const imagePullSecret = dockerImages.createRepositorySecret(
dockerConfig.requireSecret('registryAuthBase64'),
);
return {
secret: imagePullSecret,
factory: dockerImages,
};
}
export type Docker = ReturnType<typeof configureDocker>;

View file

@ -1,53 +1,50 @@
import * as k8s from '@pulumi/kubernetes';
import * as pulumi from '@pulumi/pulumi';
import { DeploymentEnvironment } from '../types';
import { isProduction } from '../utils/helpers';
import { serviceLocalEndpoint } from '../utils/local-endpoint';
import { ServiceSecret } from '../utils/secrets';
import { ServiceDeployment } from '../utils/service-deployment';
import { Docker } from './docker';
import { Environment } from './environment';
import { Redis } from './redis';
const commonConfig = new pulumi.Config('common');
const commonEnv = commonConfig.requireObject<Record<string, string>>('env');
import { Sentry } from './sentry';
export type Emails = ReturnType<typeof deployEmails>;
class PostmarkSecret extends ServiceSecret<{
token: pulumi.Output<string> | string;
from: string;
messageStream: string;
}> {}
export function deployEmails({
deploymentEnv,
environment,
redis,
heartbeat,
email,
release,
image,
imagePullSecret,
docker,
sentry,
}: {
release: string;
environment: Environment;
image: string;
deploymentEnv: DeploymentEnvironment;
redis: Redis;
docker: Docker;
heartbeat?: string;
email: {
token: pulumi.Output<string>;
from: string;
messageStream: string;
};
imagePullSecret: k8s.core.v1.Secret;
sentry: Sentry;
}) {
const emailConfig = new pulumi.Config('email');
const postmarkSecret = new PostmarkSecret('postmark', {
token: emailConfig.requireSecret('token'),
from: emailConfig.require('from'),
messageStream: emailConfig.require('messageStream'),
});
const { deployment, service } = new ServiceDeployment(
'emails-service',
{
imagePullSecret,
imagePullSecret: docker.secret,
env: {
...deploymentEnv,
...commonEnv,
SENTRY: commonEnv.SENTRY_ENABLED,
RELEASE: release,
REDIS_HOST: redis.config.host,
REDIS_PORT: String(redis.config.port),
REDIS_PASSWORD: redis.config.password,
EMAIL_FROM: email.from,
...environment.envVars,
SENTRY: sentry.enabled ? '1' : '0',
EMAIL_PROVIDER: 'postmark',
EMAIL_PROVIDER_POSTMARK_TOKEN: email.token,
EMAIL_PROVIDER_POSTMARK_MESSAGE_STREAM: email.messageStream,
HEARTBEAT_ENDPOINT: heartbeat ?? '',
},
readinessProbe: '/_readiness',
@ -55,10 +52,18 @@ export function deployEmails({
startupProbe: '/_health',
exposesMetrics: true,
image,
replicas: isProduction(deploymentEnv) ? 3 : 1,
replicas: environment.isProduction ? 3 : 1,
},
[redis.deployment, redis.service],
).deploy();
)
.withSecret('REDIS_HOST', redis.secret, 'host')
.withSecret('REDIS_PORT', redis.secret, 'port')
.withSecret('REDIS_PASSWORD', redis.secret, 'password')
.withSecret('EMAIL_FROM', postmarkSecret, 'from')
.withSecret('EMAIL_PROVIDER_POSTMARK_TOKEN', postmarkSecret, 'token')
.withSecret('EMAIL_PROVIDER_POSTMARK_MESSAGE_STREAM', postmarkSecret, 'messageStream')
.withConditionalSecret(sentry.enabled, 'SENTRY_DSN', sentry.secret, 'dsn')
.deploy();
return { deployment, service, localEndpoint: serviceLocalEndpoint(service) };
}

View file

@ -0,0 +1,45 @@
import { Config, Output } from '@pulumi/pulumi';
import { ServiceSecret } from '../utils/secrets';
export class DataEncryptionSecret extends ServiceSecret<{
encryptionPrivateKey: string | Output<string>;
}> {}
export function prepareEnvironment(input: {
release: string;
rootDns: string;
environment: string;
}) {
const commonConfig = new Config('common');
const encryptionSecret = new DataEncryptionSecret('data-encryption', {
encryptionPrivateKey: commonConfig.requireSecret('encryptionSecret'),
});
const env =
input.environment === 'production' || input.environment === 'prod'
? 'production'
: input.environment;
const appDns = `app.${input.rootDns}`;
return {
envVars: {
NODE_ENV: 'production',
LOG_LEVEL: 'debug',
DEPLOYED_DNS: appDns,
ENVIRONMENT: input.environment,
RELEASE: input.release,
},
envName: env,
isProduction: env === 'production',
isStaging: env === 'staging',
isDev: env === 'dev',
encryptionSecret,
release: input.release,
appDns,
rootDns: input.rootDns,
};
}
export type Environment = ReturnType<typeof prepareEnvironment>;

View file

@ -0,0 +1,22 @@
import { Config, Output } from '@pulumi/pulumi';
import { ServiceSecret } from '../utils/secrets';
class GitHubIntegrationSecret extends ServiceSecret<{
appId: string | Output<string>;
privateKey: string | Output<string>;
}> {}
export function configureGithubApp() {
const githubAppConfig = new Config('ghapp');
const githubSecret = new GitHubIntegrationSecret('gitub-app', {
appId: githubAppConfig.require('id'),
privateKey: githubAppConfig.requireSecret('key'),
});
return {
name: githubAppConfig.require('name'),
secret: githubSecret,
};
}
export type GitHubApp = ReturnType<typeof configureGithubApp>;

View file

@ -1,40 +1,34 @@
import { parse } from 'pg-connection-string';
import * as k8s from '@pulumi/kubernetes';
import * as pulumi from '@pulumi/pulumi';
import { Output } from '@pulumi/pulumi';
import { DeploymentEnvironment } from '../types';
import { isProduction } from '../utils/helpers';
import { serviceLocalEndpoint } from '../utils/local-endpoint';
import { ServiceDeployment } from '../utils/service-deployment';
import { StripeBillingService } from './billing';
import { CDN } from './cf-cdn';
import { Clickhouse } from './clickhouse';
import { DbMigrations } from './db-migrations';
import { Docker } from './docker';
import { Emails } from './emails';
import { Environment } from './environment';
import { GitHubApp } from './github';
import { SchemaPolicy } from './policy';
import { Postgres } from './postgres';
import { RateLimitService } from './rate-limit';
import { Redis } from './redis';
import { S3 } from './s3';
import { Schema } from './schema';
import { Sentry } from './sentry';
import { Supertokens } from './supertokens';
import { Tokens } from './tokens';
import { Usage } from './usage';
import { UsageEstimator } from './usage-estimation';
import { Webhooks } from './webhooks';
const commonConfig = new pulumi.Config('common');
const cloudflareConfig = new pulumi.Config('cloudflare');
const apiConfig = new pulumi.Config('api');
const githubAppConfig = new pulumi.Config('ghapp');
const commonEnv = commonConfig.requireObject<Record<string, string>>('env');
const apiEnv = apiConfig.requireObject<Record<string, string>>('env');
import { Zendesk } from './zendesk';
export type GraphQL = ReturnType<typeof deployGraphQL>;
export function deployGraphQL({
clickhouse,
release,
image,
deploymentEnv,
environment,
tokens,
webhooks,
schema,
@ -47,152 +41,135 @@ export function deployGraphQL({
rateLimit,
billing,
emails,
supertokensConfig,
s3Config,
zendeskConfig,
imagePullSecret,
cdnAuthPrivateKey,
supertokens,
s3,
zendesk,
docker,
postgres,
githubApp,
sentry,
}: {
release: string;
githubApp: GitHubApp;
postgres: Postgres;
image: string;
clickhouse: Clickhouse;
deploymentEnv: DeploymentEnvironment;
environment: Environment;
tokens: Tokens;
webhooks: Webhooks;
schema: Schema;
schemaPolicy: SchemaPolicy;
redis: Redis;
cdn: CDN;
cdnAuthPrivateKey: Output<string>;
s3: S3;
usage: Usage;
usageEstimator: UsageEstimator;
dbMigrations: DbMigrations;
rateLimit: RateLimitService;
billing: StripeBillingService;
emails: Emails;
supertokensConfig: {
endpoint: Output<string>;
apiKey: Output<string>;
};
s3Config: {
endpoint: string;
bucketName: string;
accessKeyId: Output<string>;
secretAccessKey: Output<string>;
};
zendeskConfig: {
username: string;
password: Output<string>;
subdomain: string;
} | null;
imagePullSecret: k8s.core.v1.Secret;
supertokens: Supertokens;
zendesk: Zendesk;
docker: Docker;
sentry: Sentry;
}) {
const rawConnectionString = apiConfig.requireSecret('postgresConnectionString');
const connectionString = rawConnectionString.apply(rawConnectionString =>
parse(rawConnectionString),
);
const apiConfig = new pulumi.Config('api');
const apiEnv = apiConfig.requireObject<Record<string, string>>('env');
return new ServiceDeployment(
'graphql-api',
{
imagePullSecret,
image,
replicas: isProduction(deploymentEnv) ? 3 : 1,
pdb: true,
readinessProbe: '/_readiness',
livenessProbe: '/_health',
startupProbe: {
endpoint: '/_health',
initialDelaySeconds: 60,
failureThreshold: 10,
periodSeconds: 15,
timeoutSeconds: 15,
return (
new ServiceDeployment(
'graphql-api',
{
imagePullSecret: docker.secret,
image,
replicas: environment.isProduction ? 3 : 1,
pdb: true,
readinessProbe: '/_readiness',
livenessProbe: '/_health',
startupProbe: {
endpoint: '/_health',
initialDelaySeconds: 60,
failureThreshold: 10,
periodSeconds: 15,
timeoutSeconds: 15,
},
availabilityOnEveryNode: true,
env: {
...environment.envVars,
...apiEnv,
SENTRY: sentry.enabled ? '1' : '0',
REQUEST_LOGGING: '0', // disabled
BILLING_ENDPOINT: serviceLocalEndpoint(billing.service),
TOKENS_ENDPOINT: serviceLocalEndpoint(tokens.service),
WEBHOOKS_ENDPOINT: serviceLocalEndpoint(webhooks.service),
SCHEMA_ENDPOINT: serviceLocalEndpoint(schema.service),
SCHEMA_POLICY_ENDPOINT: serviceLocalEndpoint(schemaPolicy.service),
HIVE_USAGE_ENDPOINT: serviceLocalEndpoint(usage.service),
RATE_LIMIT_ENDPOINT: serviceLocalEndpoint(rateLimit.service),
EMAILS_ENDPOINT: serviceLocalEndpoint(emails.service),
USAGE_ESTIMATOR_ENDPOINT: serviceLocalEndpoint(usageEstimator.service),
WEB_APP_URL: `https://${environment.appDns}`,
CDN_CF: '1',
HIVE: '1',
HIVE_REPORTING: '1',
HIVE_USAGE: '1',
HIVE_REPORTING_ENDPOINT: 'http://0.0.0.0:4000/graphql',
ZENDESK_SUPPORT: zendesk.enabled ? '1' : '0',
INTEGRATION_GITHUB: '1',
SUPERTOKENS_CONNECTION_URI: supertokens.localEndpoint,
AUTH_ORGANIZATION_OIDC: '1',
GRAPHQL_PERSISTED_OPERATIONS_PATH: './persisted-operations.json',
},
exposesMetrics: true,
port: 4000,
},
availabilityOnEveryNode: true,
env: {
...apiEnv,
...deploymentEnv,
...apiConfig.requireObject<Record<string, string>>('env'),
...commonEnv,
RELEASE: release,
SENTRY: commonEnv.SENTRY_ENABLED,
// Logging
REQUEST_LOGGING: '0', // disabled
// ClickHouse
CLICKHOUSE_PROTOCOL: clickhouse.config.protocol,
CLICKHOUSE_HOST: clickhouse.config.host,
CLICKHOUSE_PORT: clickhouse.config.port,
CLICKHOUSE_USERNAME: clickhouse.config.username,
CLICKHOUSE_PASSWORD: clickhouse.config.password,
// Redis
REDIS_HOST: redis.config.host,
REDIS_PORT: String(redis.config.port),
REDIS_PASSWORD: redis.config.password,
// PG
POSTGRES_HOST: connectionString.apply(connection => connection.host ?? ''),
POSTGRES_PORT: connectionString.apply(connection => connection.port || '5432'),
POSTGRES_PASSWORD: connectionString.apply(connection => connection.password ?? ''),
POSTGRES_USER: connectionString.apply(connection => connection.user ?? ''),
POSTGRES_DB: connectionString.apply(connection => connection.database ?? ''),
POSTGRES_SSL: connectionString.apply(connection => (connection.ssl ? '1' : '0')),
// S3
S3_ENDPOINT: s3Config.endpoint,
S3_ACCESS_KEY_ID: s3Config.accessKeyId,
S3_SECRET_ACCESS_KEY: s3Config.secretAccessKey,
S3_BUCKET_NAME: s3Config.bucketName,
BILLING_ENDPOINT: serviceLocalEndpoint(billing.service),
TOKENS_ENDPOINT: serviceLocalEndpoint(tokens.service),
WEBHOOKS_ENDPOINT: serviceLocalEndpoint(webhooks.service),
SCHEMA_ENDPOINT: serviceLocalEndpoint(schema.service),
SCHEMA_POLICY_ENDPOINT: serviceLocalEndpoint(schemaPolicy.service),
WEB_APP_URL: `https://${deploymentEnv.DEPLOYED_DNS}`,
// CDN
CDN_CF: '1',
CDN_CF_BASE_URL: cdn.workerBaseUrl,
CDN_AUTH_PRIVATE_KEY: cdnAuthPrivateKey,
// Hive
HIVE: '1',
HIVE_REPORTING: '1',
HIVE_USAGE: '1',
HIVE_USAGE_ENDPOINT: serviceLocalEndpoint(usage.service),
HIVE_REPORTING_ENDPOINT: 'http://0.0.0.0:4000/graphql',
// Zendesk
...(zendeskConfig
? {
ZENDESK_SUPPORT: '1',
ZENDESK_USERNAME: zendeskConfig.username,
ZENDESK_PASSWORD: zendeskConfig.password,
ZENDESK_SUBDOMAIN: zendeskConfig.subdomain,
}
: {
ZENDESK_SUPPORT: '0',
}),
//
USAGE_ESTIMATOR_ENDPOINT: serviceLocalEndpoint(usageEstimator.service),
INTEGRATION_GITHUB: '1',
INTEGRATION_GITHUB_APP_ID: githubAppConfig.require('id'),
INTEGRATION_GITHUB_APP_PRIVATE_KEY: githubAppConfig.requireSecret('key'),
RATE_LIMIT_ENDPOINT: serviceLocalEndpoint(rateLimit.service),
EMAILS_ENDPOINT: serviceLocalEndpoint(emails.service),
ENCRYPTION_SECRET: commonConfig.requireSecret('encryptionSecret'),
// Auth
SUPERTOKENS_CONNECTION_URI: supertokensConfig.endpoint,
SUPERTOKENS_API_KEY: supertokensConfig.apiKey,
AUTH_ORGANIZATION_OIDC: '1',
// Various
GRAPHQL_PERSISTED_OPERATIONS_PATH: './persisted-operations.json',
},
exposesMetrics: true,
port: 4000,
},
[
dbMigrations,
redis.deployment,
redis.service,
clickhouse.deployment,
clickhouse.service,
rateLimit.deployment,
rateLimit.service,
],
).deploy();
[
dbMigrations,
redis.deployment,
redis.service,
clickhouse.deployment,
clickhouse.service,
rateLimit.deployment,
rateLimit.service,
],
)
// GitHub App
.withSecret('INTEGRATION_GITHUB_APP_ID', githubApp.secret, 'appId')
.withSecret('INTEGRATION_GITHUB_APP_PRIVATE_KEY', githubApp.secret, 'privateKey')
// Clickhouse
.withSecret('CLICKHOUSE_HOST', clickhouse.secret, 'host')
.withSecret('CLICKHOUSE_PORT', clickhouse.secret, 'port')
.withSecret('CLICKHOUSE_USERNAME', clickhouse.secret, 'username')
.withSecret('CLICKHOUSE_PASSWORD', clickhouse.secret, 'password')
.withSecret('CLICKHOUSE_PROTOCOL', clickhouse.secret, 'protocol')
// Redis
.withSecret('REDIS_HOST', redis.secret, 'host')
.withSecret('REDIS_PORT', redis.secret, 'port')
.withSecret('REDIS_PASSWORD', redis.secret, 'password')
// PG
.withSecret('POSTGRES_HOST', postgres.secret, 'host')
.withSecret('POSTGRES_PORT', postgres.secret, 'port')
.withSecret('POSTGRES_USER', postgres.secret, 'user')
.withSecret('POSTGRES_PASSWORD', postgres.secret, 'password')
.withSecret('POSTGRES_DB', postgres.secret, 'database')
.withSecret('POSTGRES_SSL', postgres.secret, 'ssl')
// CDN
.withSecret('CDN_AUTH_PRIVATE_KEY', cdn.secret, 'authPrivateKey')
.withSecret('CDN_CF_BASE_URL', cdn.secret, 'baseUrl')
// S3
.withSecret('S3_ACCESS_KEY_ID', s3.secret, 'accessKeyId')
.withSecret('S3_SECRET_ACCESS_KEY', s3.secret, 'secretAccessKey')
.withSecret('S3_BUCKET_NAME', s3.secret, 'bucket')
.withSecret('S3_ENDPOINT', s3.secret, 'endpoint')
// Supertokens
.withSecret('SUPERTOKENS_API_KEY', supertokens.secret, 'apiKey')
// Zendesk
.withConditionalSecret(zendesk.enabled, 'ZENDESK_SUBDOMAIN', zendesk.secret, 'subdomain')
.withConditionalSecret(zendesk.enabled, 'ZENDESK_USERNAME', zendesk.secret, 'username')
.withConditionalSecret(zendesk.enabled, 'ZENDESK_PASSWORD', zendesk.secret, 'password')
// Sentry
.withConditionalSecret(sentry.enabled, 'SENTRY_DSN', sentry.secret, 'dsn')
// Other
.withSecret('ENCRYPTION_SECRET', environment.encryptionSecret, 'encryptionPrivateKey')
.deploy()
);
}

View file

@ -1,27 +1,34 @@
import * as pulumi from '@pulumi/pulumi';
import { ServiceSecret } from '../utils/secrets';
export type Kafka = ReturnType<typeof deployKafka>;
export class KafkaSecret extends ServiceSecret<{
ssl: '0' | '1' | pulumi.Output<'0' | '1'>;
saslUsername: string | pulumi.Output<string>;
saslPassword: string | pulumi.Output<string>;
endpoint: string | pulumi.Output<string>;
}> {}
export function deployKafka() {
const eventhubConfig = new pulumi.Config('eventhub');
const secret = new KafkaSecret('kafka', {
ssl: '1',
saslUsername: '$ConnectionString',
saslPassword: eventhubConfig.requireSecret('key'),
endpoint: eventhubConfig.require('endpoint'),
});
return {
connectionEnv: {
KAFKA_SSL: '1',
KAFKA_SASL_MECHANISM: 'plain',
KAFKA_CONCURRENCY: '1',
KAFKA_SASL_USERNAME: '$ConnectionString',
KAFKA_SASL_PASSWORD: eventhubConfig.requireSecret('key'),
} as Record<string, pulumi.Output<string> | string>,
secret,
config: {
endpoint: eventhubConfig.require('endpoint'),
saslMechanism: 'plain',
concurrency: '1',
bufferSize: eventhubConfig.require('bufferSize'),
bufferInterval: eventhubConfig.require('bufferInterval'),
bufferDynamic: eventhubConfig.require('bufferDynamic'),
topic: eventhubConfig.require('topic'),
consumerGroup: eventhubConfig.require('consumerGroup'),
},
service: null,
deployment: null,
};
}

View file

@ -2,11 +2,13 @@ import * as pulumi from '@pulumi/pulumi';
import { Observability } from '../utils/observability';
import { deployGrafana } from './grafana';
const observabilityConfig = new pulumi.Config('observability');
export function deployMetrics(config: { envName: string }) {
const observabilityConfig = new pulumi.Config('observability');
if (!observabilityConfig.getBoolean('enabled')) {
return;
return {
enabled: false,
};
}
const observability = new Observability(config.envName, {
@ -25,5 +27,6 @@ export function deployMetrics(config: { envName: string }) {
return {
observability: observability.deploy(),
grafana: deployGrafana(config.envName),
enabled: true,
};
}

View file

@ -1,39 +1,35 @@
import * as k8s from '@pulumi/kubernetes';
import * as pulumi from '@pulumi/pulumi';
import { DeploymentEnvironment } from '../types';
import { isProduction } from '../utils/helpers';
import { ServiceDeployment } from '../utils/service-deployment';
const commonConfig = new pulumi.Config('common');
const commonEnv = commonConfig.requireObject<Record<string, string>>('env');
import { Docker } from './docker';
import { Environment } from './environment';
import { Sentry } from './sentry';
export type SchemaPolicy = ReturnType<typeof deploySchemaPolicy>;
export function deploySchemaPolicy({
deploymentEnv,
release,
environment,
image,
imagePullSecret,
docker,
sentry,
}: {
image: string;
release: string;
deploymentEnv: DeploymentEnvironment;
imagePullSecret: k8s.core.v1.Secret;
environment: Environment;
docker: Docker;
sentry: Sentry;
}) {
return new ServiceDeployment('schema-policy-service', {
image,
imagePullSecret,
imagePullSecret: docker.secret,
env: {
...deploymentEnv,
...commonEnv,
SENTRY: commonEnv.SENTRY_ENABLED,
RELEASE: release,
...environment.envVars,
SENTRY: sentry.enabled ? '1' : '0',
},
readinessProbe: '/_readiness',
livenessProbe: '/_health',
startupProbe: '/_health',
exposesMetrics: true,
replicas: isProduction(deploymentEnv) ? 3 : 1,
replicas: environment.isProduction ? 3 : 1,
pdb: true,
}).deploy();
})
.withConditionalSecret(sentry.enabled, 'SENTRY_DSN', sentry.secret, 'dsn')
.deploy();
}

View file

@ -0,0 +1,39 @@
import { parse } from 'pg-connection-string';
import * as pulumi from '@pulumi/pulumi';
import { ServiceSecret } from '../utils/secrets';
export class PostgresConnectionSecret extends ServiceSecret<{
host: string | pulumi.Output<string>;
port: string | pulumi.Output<string>;
user: string | pulumi.Output<string>;
password: string | pulumi.Output<string>;
database: string | pulumi.Output<string>;
ssl: '0' | '1' | pulumi.Output<'0' | '1'>;
connectionString: pulumi.Output<string>;
connectionStringPostgresql: pulumi.Output<string>;
}> {}
export function deployPostgres() {
const postgresConfig = new pulumi.Config('postgres');
const rawConnectionString = postgresConfig.requireSecret('connectionString');
const connectionString = rawConnectionString.apply(rawConnectionString =>
parse(rawConnectionString),
);
const secret = new PostgresConnectionSecret('postgres', {
connectionString: rawConnectionString,
connectionStringPostgresql: rawConnectionString.apply(str =>
str.replace('postgres://', 'postgresql://'),
),
host: connectionString.apply(connection => connection.host ?? ''),
port: connectionString.apply(connection => connection.port || '5432'),
user: connectionString.apply(connection => connection.user ?? ''),
password: connectionString.apply(connection => connection.password ?? ''),
database: connectionString.apply(connection => connection.database ?? ''),
ssl: connectionString.apply(connection => (connection.ssl ? '1' : '0')),
});
return { secret };
}
export type Postgres = ReturnType<typeof deployPostgres>;

View file

@ -1,34 +1,31 @@
import * as pulumi from '@pulumi/pulumi';
import { DeploymentEnvironment } from '../types';
import { CertManager } from '../utils/cert-manager';
import { isProduction } from '../utils/helpers';
import { Proxy } from '../utils/reverse-proxy';
import { App } from './app';
import { Environment } from './environment';
import { GraphQL } from './graphql';
import { Usage } from './usage';
const commonConfig = new pulumi.Config('common');
export function deployProxy({
appHostname,
graphql,
app,
usage,
deploymentEnv,
environment,
}: {
deploymentEnv: DeploymentEnvironment;
appHostname: string;
environment: Environment;
graphql: GraphQL;
app: App;
usage: Usage;
}) {
const { tlsIssueName } = new CertManager().deployCertManagerAndIssuer();
const commonConfig = new pulumi.Config('common');
return new Proxy(tlsIssueName, {
address: commonConfig.get('staticIp'),
aksReservedIpResourceGroup: commonConfig.get('aksReservedIpResourceGroup'),
})
.deployProxy({ replicas: isProduction(deploymentEnv) ? 3 : 1 })
.registerService({ record: appHostname }, [
.deployProxy({ replicas: environment.isProduction ? 3 : 1 })
.registerService({ record: environment.appDns }, [
{
name: 'app',
path: '/',

View file

@ -1,71 +1,62 @@
import { parse } from 'pg-connection-string';
import * as k8s from '@pulumi/kubernetes';
import * as pulumi from '@pulumi/pulumi';
import { DeploymentEnvironment } from '../types';
import { isProduction } from '../utils/helpers';
import { serviceLocalEndpoint } from '../utils/local-endpoint';
import { ServiceDeployment } from '../utils/service-deployment';
import { DbMigrations } from './db-migrations';
import { Docker } from './docker';
import { Emails } from './emails';
import { Environment } from './environment';
import { Postgres } from './postgres';
import { Sentry } from './sentry';
import { UsageEstimator } from './usage-estimation';
const rateLimitConfig = new pulumi.Config('rateLimit');
const commonConfig = new pulumi.Config('common');
const commonEnv = commonConfig.requireObject<Record<string, string>>('env');
const apiConfig = new pulumi.Config('api');
export type RateLimitService = ReturnType<typeof deployRateLimit>;
export function deployRateLimit({
deploymentEnv,
environment,
dbMigrations,
usageEstimator,
emails,
release,
image,
imagePullSecret,
docker,
postgres,
sentry,
}: {
usageEstimator: UsageEstimator;
deploymentEnv: DeploymentEnvironment;
environment: Environment;
dbMigrations: DbMigrations;
emails: Emails;
release: string;
image: string;
imagePullSecret: k8s.core.v1.Secret;
docker: Docker;
postgres: Postgres;
sentry: Sentry;
}) {
const rawConnectionString = apiConfig.requireSecret('postgresConnectionString');
const connectionString = rawConnectionString.apply(rawConnectionString =>
parse(rawConnectionString),
);
return new ServiceDeployment(
'rate-limiter',
{
imagePullSecret,
replicas: isProduction(deploymentEnv) ? 3 : 1,
imagePullSecret: docker.secret,
replicas: environment.isProduction ? 3 : 1,
readinessProbe: '/_readiness',
livenessProbe: '/_health',
startupProbe: '/_health',
env: {
...deploymentEnv,
...commonEnv,
SENTRY: commonEnv.SENTRY_ENABLED,
LIMIT_CACHE_UPDATE_INTERVAL_MS: rateLimitConfig.require('updateIntervalMs'),
RELEASE: release,
...environment.envVars,
SENTRY: sentry.enabled ? '1' : '0',
LIMIT_CACHE_UPDATE_INTERVAL_MS: environment.isProduction ? '60000' : '86400000',
USAGE_ESTIMATOR_ENDPOINT: serviceLocalEndpoint(usageEstimator.service),
EMAILS_ENDPOINT: serviceLocalEndpoint(emails.service),
POSTGRES_HOST: connectionString.apply(connection => connection.host ?? ''),
POSTGRES_PORT: connectionString.apply(connection => connection.port || '5432'),
POSTGRES_PASSWORD: connectionString.apply(connection => connection.password ?? ''),
POSTGRES_USER: connectionString.apply(connection => connection.user ?? ''),
POSTGRES_DB: connectionString.apply(connection => connection.database ?? ''),
POSTGRES_SSL: connectionString.apply(connection => (connection.ssl ? '1' : '0')),
WEB_APP_URL: `https://${deploymentEnv.DEPLOYED_DNS}/`,
WEB_APP_URL: `https://${environment.appDns}/`,
},
exposesMetrics: true,
port: 4000,
image,
},
[dbMigrations, usageEstimator.service, usageEstimator.deployment],
).deploy();
)
.withSecret('POSTGRES_HOST', postgres.secret, 'host')
.withSecret('POSTGRES_PORT', postgres.secret, 'port')
.withSecret('POSTGRES_USER', postgres.secret, 'user')
.withSecret('POSTGRES_PASSWORD', postgres.secret, 'password')
.withSecret('POSTGRES_DB', postgres.secret, 'database')
.withSecret('POSTGRES_SSL', postgres.secret, 'ssl')
.withConditionalSecret(sentry.enabled, 'SENTRY_DSN', sentry.secret, 'dsn')
.deploy();
}

View file

@ -1,19 +1,25 @@
import * as pulumi from '@pulumi/pulumi';
import { DeploymentEnvironment } from '../types';
import { isProduction } from '../utils/helpers';
import { serviceLocalHost } from '../utils/local-endpoint';
import { Redis as RedisStore } from '../utils/redis';
import { ServiceSecret } from '../utils/secrets';
import { Environment } from './environment';
const redisConfig = new pulumi.Config('redis');
export class RedisSecret extends ServiceSecret<{
password: string | pulumi.Output<string>;
host: string | pulumi.Output<string>;
port: string | pulumi.Output<string>;
}> {}
export type Redis = ReturnType<typeof deployRedis>;
export function deployRedis({ deploymentEnv }: { deploymentEnv: DeploymentEnvironment }) {
const redisPassword = redisConfig.require('password');
export function deployRedis(input: { environment: Environment }) {
const redisPassword = redisConfig.requireSecret('password');
const redisApi = new RedisStore({
password: redisPassword,
}).deploy({
limits: isProduction(deploymentEnv)
limits: input.environment.isProduction
? {
memory: '800Mi',
cpu: '1000m',
@ -24,13 +30,17 @@ export function deployRedis({ deploymentEnv }: { deploymentEnv: DeploymentEnviro
},
});
const host = serviceLocalHost(redisApi.service);
const port = String(redisApi.port);
const secret = new RedisSecret('redis', {
password: redisConfig.requireSecret('password'),
host,
port,
});
return {
deployment: redisApi.deployment,
service: redisApi.service,
config: {
host: serviceLocalHost(redisApi.service),
port: redisApi.port,
password: redisPassword,
},
secret,
};
}

24
deployment/services/s3.ts Normal file
View file

@ -0,0 +1,24 @@
import * as pulumi from '@pulumi/pulumi';
import { ServiceSecret } from '../utils/secrets';
export class S3Secret extends ServiceSecret<{
accessKeyId: string | pulumi.Output<string>;
secretAccessKey: string | pulumi.Output<string>;
endpoint: string | pulumi.Output<string>;
bucket: string | pulumi.Output<string>;
}> {}
export function deployS3() {
const r2Config = new pulumi.Config('r2');
const secret = new S3Secret('cloudflare-r2', {
endpoint: r2Config.require('endpoint'),
bucket: r2Config.require('bucketName'),
accessKeyId: r2Config.requireSecret('accessKeyId'),
secretAccessKey: r2Config.requireSecret('secretAccessKey'),
});
return { secret };
}
export type S3 = ReturnType<typeof deployS3>;

View file

@ -1,49 +1,38 @@
import * as k8s from '@pulumi/kubernetes';
import * as pulumi from '@pulumi/pulumi';
import { DeploymentEnvironment } from '../types';
import { isProduction } from '../utils/helpers';
import { ServiceDeployment } from '../utils/service-deployment';
import type { Broker } from './cf-broker';
import { Docker } from './docker';
import { Environment } from './environment';
import { Redis } from './redis';
const commonConfig = new pulumi.Config('common');
const commonEnv = commonConfig.requireObject<Record<string, string>>('env');
import { Sentry } from './sentry';
export type Schema = ReturnType<typeof deploySchema>;
export function deploySchema({
deploymentEnv,
environment,
redis,
broker,
release,
image,
imagePullSecret,
docker,
sentry,
}: {
image: string;
release: string;
deploymentEnv: DeploymentEnvironment;
environment: Environment;
redis: Redis;
broker: Broker;
imagePullSecret: k8s.core.v1.Secret;
docker: Docker;
sentry: Sentry;
}) {
return new ServiceDeployment(
'schema-service',
{
image,
imagePullSecret,
imagePullSecret: docker.secret,
availabilityOnEveryNode: true,
env: {
...deploymentEnv,
...commonEnv,
SENTRY: commonEnv.SENTRY_ENABLED,
RELEASE: release,
REDIS_HOST: redis.config.host,
REDIS_PORT: String(redis.config.port),
REDIS_PASSWORD: redis.config.password,
ENCRYPTION_SECRET: commonConfig.requireSecret('encryptionSecret'),
...environment.envVars,
SENTRY: sentry.enabled ? '1' : '0',
REQUEST_BROKER: '1',
REQUEST_BROKER_ENDPOINT: broker.workerBaseUrl,
REQUEST_BROKER_SIGNATURE: broker.secretSignature,
SCHEMA_CACHE_POLL_INTERVAL_MS: '150',
SCHEMA_CACHE_TTL_MS: '65000' /* 65s */,
SCHEMA_CACHE_SUCCESS_TTL_MS: '86400000' /* 24h */,
@ -53,9 +42,17 @@ export function deploySchema({
livenessProbe: '/_health',
startupProbe: '/_health',
exposesMetrics: true,
replicas: isProduction(deploymentEnv) ? 3 : 1,
replicas: environment.isProduction ? 3 : 1,
pdb: true,
},
[redis.deployment, redis.service],
).deploy();
)
.withSecret('REDIS_HOST', redis.secret, 'host')
.withSecret('REDIS_PORT', redis.secret, 'port')
.withSecret('REDIS_PASSWORD', redis.secret, 'password')
.withSecret('REQUEST_BROKER_ENDPOINT', broker.secret, 'baseUrl')
.withSecret('REQUEST_BROKER_SIGNATURE', broker.secret, 'secretSignature')
.withSecret('ENCRYPTION_SECRET', environment.encryptionSecret, 'encryptionPrivateKey')
.withConditionalSecret(sentry.enabled, 'SENTRY_DSN', sentry.secret, 'dsn')
.deploy();
}

View file

@ -1,16 +1,17 @@
import * as k8s from '@pulumi/kubernetes';
import * as pulumi from '@pulumi/pulumi';
import { ServiceDeployment } from '../utils/service-deployment';
import { Docker } from './docker';
import { Environment } from './environment';
import { Sentry } from './sentry';
export function deploySentryEventsMonitor(config: {
envName: string;
imagePullSecret: k8s.core.v1.Secret;
environment: Environment;
docker: Docker;
sentry: Sentry;
}) {
const namepsacesToTrack = ['default', 'observability', 'contour', 'cert-manager'];
const commonConfig = new pulumi.Config('common');
const commonEnv = commonConfig.requireObject<Record<string, string>>('env');
if (commonEnv.SENTRY_DSN) {
if (config.sentry.enabled && config.sentry.secret) {
let serviceAccount = new k8s.core.v1.ServiceAccount('sentry-k8s-agent', {
metadata: {
name: 'sentry-k8s-agent',
@ -71,14 +72,16 @@ export function deploySentryEventsMonitor(config: {
return new ServiceDeployment('sentry-events-monitor', {
image: 'ghcr.io/the-guild-org/sentry-kubernetes:d9f489ced1a8eeff4a08c7e2bce427a1545dbbbd',
imagePullSecret: config.imagePullSecret,
imagePullSecret: config.docker.secret,
serviceAccountName: serviceAccount.metadata.name,
env: {
SENTRY_DSN: commonEnv.SENTRY_DSN,
SENTRY_ENVIRONMENT: config.envName === 'dev' ? 'development' : config.envName,
SENTRY_ENVIRONMENT:
config.environment.envName === 'dev' ? 'development' : config.environment.envName,
SENTRY_K8S_WATCH_NAMESPACES: namepsacesToTrack.join(','),
},
}).deploy();
})
.withSecret('SENTRY_DSN', config.sentry.secret, 'dsn')
.deploy();
} else {
console.warn('SENTRY_DSN is not set, skipping Sentry events monitor deployment');

View file

@ -0,0 +1,29 @@
import { Config, Output } from '@pulumi/pulumi';
import { ServiceSecret } from '../utils/secrets';
export class SentrySecret extends ServiceSecret<{
dsn: string | Output<string>;
}> {}
export function configureSentry() {
const sentryConfig = new Config('sentry');
const isEnabled = sentryConfig.requireBoolean('enabled');
if (isEnabled) {
const secret = new SentrySecret('sentry', {
dsn: sentryConfig.requireSecret('dsn'),
});
return {
enabled: true,
secret,
};
}
return {
enabled: false,
secret: null,
};
}
export type Sentry = ReturnType<typeof configureSentry>;

View file

@ -0,0 +1,22 @@
import { Config, Output } from '@pulumi/pulumi';
import { ServiceSecret } from '../utils/secrets';
class SlackIntegrationSecret extends ServiceSecret<{
clientId: string | Output<string>;
clientSecret: string | Output<string>;
}> {}
export function configureSlackApp() {
const slackConfig = new Config('slack');
const secret = new SlackIntegrationSecret('slack-app', {
clientId: slackConfig.require('clientId'),
clientSecret: slackConfig.requireSecret('clientSecret'),
});
return {
secret,
};
}
export type SlackApp = ReturnType<typeof configureSlackApp>;

View file

@ -1,18 +1,30 @@
import * as kx from '@pulumi/kubernetesx';
import * as pulumi from '@pulumi/pulumi';
import { Output } from '@pulumi/pulumi';
import { DeploymentEnvironment } from '../types';
import { isProduction } from '../utils/helpers';
import * as random from '@pulumi/random';
import { serviceLocalEndpoint } from '../utils/local-endpoint';
import { ServiceSecret } from '../utils/secrets';
import { Environment } from './environment';
import { Postgres } from './postgres';
export class SupertokensSecret extends ServiceSecret<{
apiKey: string | pulumi.Output<string>;
}> {}
export function deploySuperTokens(
{ apiKey }: { apiKey: Output<string> },
postgres: Postgres,
resourceOptions: {
dependencies: pulumi.Resource[];
},
deploymentEnv: DeploymentEnvironment,
environment: Environment,
) {
const apiConfig = new pulumi.Config('api');
const supertokensApiKey = new random.RandomPassword('supertokens-api-key', {
length: 31,
special: false,
}).result;
const secret = new SupertokensSecret('supertokens', {
apiKey: supertokensApiKey,
});
const port = 3567;
const pb = new kx.PodBuilder({
@ -56,10 +68,18 @@ export function deploySuperTokens(
},
env: {
POSTGRESQL_TABLE_NAMES_PREFIX: 'supertokens',
POSTGRESQL_CONNECTION_URI: apiConfig
.requireSecret('postgresConnectionString')
.apply(str => str.replace('postgres://', 'postgresql://')),
API_KEYS: apiKey,
POSTGRESQL_CONNECTION_URI: {
secretKeyRef: {
name: postgres.secret.record.metadata.name,
key: 'connectionStringPostgresql',
},
},
API_KEYS: {
secretKeyRef: {
name: secret.record.metadata.name,
key: 'apiKey',
},
},
},
},
],
@ -68,7 +88,7 @@ export function deploySuperTokens(
const deployment = new kx.Deployment(
'supertokens',
{
spec: pb.asDeploymentSpec({ replicas: isProduction(deploymentEnv) ? 3 : 1 }),
spec: pb.asDeploymentSpec({ replicas: environment.isProduction ? 3 : 1 }),
},
{
dependsOn: resourceOptions.dependencies,
@ -81,5 +101,8 @@ export function deploySuperTokens(
deployment,
service,
localEndpoint: serviceLocalEndpoint(service),
secret,
};
}
export type Supertokens = ReturnType<typeof deploySuperTokens>;

View file

@ -1,68 +1,60 @@
import { parse } from 'pg-connection-string';
import * as k8s from '@pulumi/kubernetes';
import * as pulumi from '@pulumi/pulumi';
import { DeploymentEnvironment } from '../types';
import { isProduction } from '../utils/helpers';
import { ServiceDeployment } from '../utils/service-deployment';
import { DbMigrations } from './db-migrations';
import { Docker } from './docker';
import { Environment } from './environment';
import { Postgres } from './postgres';
import { Redis } from './redis';
const commonConfig = new pulumi.Config('common');
const apiConfig = new pulumi.Config('api');
const commonEnv = commonConfig.requireObject<Record<string, string>>('env');
import { Sentry } from './sentry';
export type Tokens = ReturnType<typeof deployTokens>;
export function deployTokens({
deploymentEnv,
environment,
dbMigrations,
redis,
heartbeat,
image,
release,
imagePullSecret,
docker,
postgres,
redis,
sentry,
}: {
image: string;
release: string;
deploymentEnv: DeploymentEnvironment;
environment: Environment;
dbMigrations: DbMigrations;
redis: Redis;
heartbeat?: string;
imagePullSecret: k8s.core.v1.Secret;
docker: Docker;
redis: Redis;
postgres: Postgres;
sentry: Sentry;
}) {
const rawConnectionString = apiConfig.requireSecret('postgresConnectionString');
const connectionString = rawConnectionString.apply(rawConnectionString =>
parse(rawConnectionString),
);
return new ServiceDeployment(
'tokens-service',
{
imagePullSecret,
imagePullSecret: docker.secret,
readinessProbe: '/_readiness',
livenessProbe: '/_health',
startupProbe: '/_health',
exposesMetrics: true,
availabilityOnEveryNode: true,
replicas: isProduction(deploymentEnv) ? 3 : 1,
replicas: environment.isProduction ? 3 : 1,
image,
env: {
...deploymentEnv,
...commonEnv,
SENTRY: commonEnv.SENTRY_ENABLED,
POSTGRES_HOST: connectionString.apply(connection => connection.host ?? ''),
POSTGRES_PORT: connectionString.apply(connection => connection.port || '5432'),
POSTGRES_PASSWORD: connectionString.apply(connection => connection.password ?? ''),
POSTGRES_USER: connectionString.apply(connection => connection.user ?? ''),
POSTGRES_DB: connectionString.apply(connection => connection.database ?? ''),
POSTGRES_SSL: connectionString.apply(connection => (connection.ssl ? '1' : '0')),
REDIS_HOST: redis.config.host,
REDIS_PORT: String(redis.config.port),
REDIS_PASSWORD: redis.config.password,
RELEASE: release,
...environment.envVars,
SENTRY: sentry.enabled ? '1' : '0',
HEARTBEAT_ENDPOINT: heartbeat ?? '',
},
},
[dbMigrations],
).deploy();
)
.withSecret('POSTGRES_HOST', postgres.secret, 'host')
.withSecret('POSTGRES_PORT', postgres.secret, 'port')
.withSecret('POSTGRES_USER', postgres.secret, 'user')
.withSecret('POSTGRES_PASSWORD', postgres.secret, 'password')
.withSecret('POSTGRES_DB', postgres.secret, 'database')
.withSecret('POSTGRES_SSL', postgres.secret, 'ssl')
.withSecret('REDIS_HOST', redis.secret, 'host')
.withSecret('REDIS_PORT', redis.secret, 'port')
.withSecret('REDIS_PASSWORD', redis.secret, 'password')
.withConditionalSecret(sentry.enabled, 'SENTRY_DSN', sentry.secret, 'dsn')
.deploy();
}

View file

@ -1,54 +1,50 @@
import * as k8s from '@pulumi/kubernetes';
import * as pulumi from '@pulumi/pulumi';
import { DeploymentEnvironment } from '../types';
import { isProduction } from '../utils/helpers';
import { ServiceDeployment } from '../utils/service-deployment';
import { Clickhouse } from './clickhouse';
import { DbMigrations } from './db-migrations';
const commonConfig = new pulumi.Config('common');
const commonEnv = commonConfig.requireObject<Record<string, string>>('env');
import { Docker } from './docker';
import { Environment } from './environment';
import { Sentry } from './sentry';
export type UsageEstimator = ReturnType<typeof deployUsageEstimation>;
export function deployUsageEstimation({
image,
imagePullSecret,
release,
deploymentEnv,
docker,
environment,
clickhouse,
dbMigrations,
sentry,
}: {
image: string;
imagePullSecret: k8s.core.v1.Secret;
release: string;
deploymentEnv: DeploymentEnvironment;
docker: Docker;
environment: Environment;
clickhouse: Clickhouse;
dbMigrations: DbMigrations;
sentry: Sentry;
}) {
return new ServiceDeployment(
'usage-estimator',
{
image,
imagePullSecret,
replicas: isProduction(deploymentEnv) ? 3 : 1,
imagePullSecret: docker.secret,
replicas: environment.isProduction ? 3 : 1,
readinessProbe: '/_readiness',
livenessProbe: '/_health',
startupProbe: '/_health',
env: {
...deploymentEnv,
...commonEnv,
SENTRY: commonEnv.SENTRY_ENABLED,
CLICKHOUSE_PROTOCOL: clickhouse.config.protocol,
CLICKHOUSE_HOST: clickhouse.config.host,
CLICKHOUSE_PORT: clickhouse.config.port,
CLICKHOUSE_USERNAME: clickhouse.config.username,
CLICKHOUSE_PASSWORD: clickhouse.config.password,
RELEASE: release,
...environment.envVars,
SENTRY: sentry.enabled ? '1' : '0',
},
exposesMetrics: true,
port: 4000,
},
[dbMigrations],
).deploy();
)
.withSecret('CLICKHOUSE_HOST', clickhouse.secret, 'host')
.withSecret('CLICKHOUSE_PORT', clickhouse.secret, 'port')
.withSecret('CLICKHOUSE_USERNAME', clickhouse.secret, 'username')
.withSecret('CLICKHOUSE_PASSWORD', clickhouse.secret, 'password')
.withSecret('CLICKHOUSE_PROTOCOL', clickhouse.secret, 'protocol')
.withConditionalSecret(sentry.enabled, 'SENTRY_DSN', sentry.secret, 'dsn')
.deploy();
}

View file

@ -1,57 +1,43 @@
import * as k8s from '@pulumi/kubernetes';
import * as pulumi from '@pulumi/pulumi';
import { DeploymentEnvironment } from '../types';
import { isProduction, isStaging } from '../utils/helpers';
import { ServiceDeployment } from '../utils/service-deployment';
import { Clickhouse } from './clickhouse';
import { DbMigrations } from './db-migrations';
import { Docker } from './docker';
import { Environment } from './environment';
import { Kafka } from './kafka';
const commonConfig = new pulumi.Config('common');
const commonEnv = commonConfig.requireObject<Record<string, string>>('env');
const clickHouseConfig = new pulumi.Config('clickhouse');
import { Sentry } from './sentry';
export type UsageIngestor = ReturnType<typeof deployUsageIngestor>;
export function deployUsageIngestor({
deploymentEnv,
environment,
clickhouse,
kafka,
dbMigrations,
heartbeat,
image,
release,
imagePullSecret,
docker,
sentry,
}: {
image: string;
release: string;
deploymentEnv: DeploymentEnvironment;
environment: Environment;
clickhouse: Clickhouse;
kafka: Kafka;
dbMigrations: DbMigrations;
heartbeat?: string;
imagePullSecret: k8s.core.v1.Secret;
docker: Docker;
sentry: Sentry;
}) {
const clickHouseConfig = new pulumi.Config('clickhouse');
const numberOfPartitions = 16;
const replicas = isProduction(deploymentEnv) ? 6 : 1;
const cpuLimit = isProduction(deploymentEnv) ? '600m' : '300m';
const maxReplicas = isProduction(deploymentEnv) ? numberOfPartitions : 2;
const clickhouseEnv = {
CLICKHOUSE_PROTOCOL: clickhouse.config.protocol,
CLICKHOUSE_HOST: clickhouse.config.host,
CLICKHOUSE_PORT: clickhouse.config.port,
CLICKHOUSE_USERNAME: clickhouse.config.username,
CLICKHOUSE_PASSWORD: clickhouse.config.password,
CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MS: '30000', // flush data after max 30 seconds
CLICKHOUSE_ASYNC_INSERT_MAX_DATA_SIZE: '200000000', // flush data when the buffer reaches 200MB
};
const replicas = environment.isProduction ? 6 : 1;
const cpuLimit = environment.isProduction ? '600m' : '300m';
const maxReplicas = environment.isProduction ? numberOfPartitions : 2;
// Require migrationV2DataIngestionStartDate only in production and staging
// Remove it once we are done with migration.
const clickHouseMigrationV2DataIngestionStartDate =
isProduction(deploymentEnv) || isStaging(deploymentEnv)
environment.isProduction || environment.isStaging
? clickHouseConfig.require('migrationV2DataIngestionStartDate')
: '';
@ -59,21 +45,20 @@ export function deployUsageIngestor({
'usage-ingestor-service',
{
image,
imagePullSecret,
imagePullSecret: docker.secret,
replicas,
readinessProbe: '/_readiness',
livenessProbe: '/_health',
availabilityOnEveryNode: true,
env: {
...deploymentEnv,
...commonEnv,
SENTRY: commonEnv.SENTRY_ENABLED,
...clickhouseEnv,
...kafka.connectionEnv,
KAFKA_BROKER: kafka.config.endpoint,
...environment.envVars,
SENTRY: sentry.enabled ? '1' : '0',
CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MS: '30000', // flush data after max 30 seconds
CLICKHOUSE_ASYNC_INSERT_MAX_DATA_SIZE: '200000000', // flush data when the buffer reaches 200MB
KAFKA_SASL_MECHANISM: kafka.config.saslMechanism,
KAFKA_CONCURRENCY: kafka.config.concurrency,
KAFKA_TOPIC: kafka.config.topic,
KAFKA_CONSUMER_GROUP: kafka.config.consumerGroup,
RELEASE: release,
HEARTBEAT_ENDPOINT: heartbeat ?? '',
MIGRATION_V2_INGEST_AFTER_UTC: clickHouseMigrationV2DataIngestionStartDate,
},
@ -88,12 +73,17 @@ export function deployUsageIngestor({
maxReplicas,
},
},
[
clickhouse.deployment,
clickhouse.service,
dbMigrations,
kafka.deployment,
kafka.service,
].filter(Boolean),
).deploy();
[clickhouse.deployment, clickhouse.service, dbMigrations].filter(Boolean),
)
.withSecret('CLICKHOUSE_HOST', clickhouse.secret, 'host')
.withSecret('CLICKHOUSE_PORT', clickhouse.secret, 'port')
.withSecret('CLICKHOUSE_USERNAME', clickhouse.secret, 'username')
.withSecret('CLICKHOUSE_PASSWORD', clickhouse.secret, 'password')
.withSecret('CLICKHOUSE_PROTOCOL', clickhouse.secret, 'protocol')
.withSecret('KAFKA_SASL_USERNAME', kafka.secret, 'saslUsername')
.withSecret('KAFKA_SASL_PASSWORD', kafka.secret, 'saslPassword')
.withSecret('KAFKA_SSL', kafka.secret, 'ssl')
.withSecret('KAFKA_BROKER', kafka.secret, 'endpoint')
.withConditionalSecret(sentry.enabled, 'SENTRY_DSN', sentry.secret, 'dsn')
.deploy();
}

View file

@ -1,41 +1,38 @@
import * as k8s from '@pulumi/kubernetes';
import * as pulumi from '@pulumi/pulumi';
import { DeploymentEnvironment } from '../types';
import { isProduction } from '../utils/helpers';
import { serviceLocalEndpoint } from '../utils/local-endpoint';
import { ServiceDeployment } from '../utils/service-deployment';
import { DbMigrations } from './db-migrations';
import { Docker } from './docker';
import { Environment } from './environment';
import { Kafka } from './kafka';
import { RateLimitService } from './rate-limit';
import { Sentry } from './sentry';
import { Tokens } from './tokens';
const commonConfig = new pulumi.Config('common');
const commonEnv = commonConfig.requireObject<Record<string, string>>('env');
export type Usage = ReturnType<typeof deployUsage>;
export function deployUsage({
deploymentEnv,
environment,
tokens,
kafka,
dbMigrations,
rateLimit,
image,
release,
imagePullSecret,
docker,
sentry,
}: {
image: string;
release: string;
deploymentEnv: DeploymentEnvironment;
environment: Environment;
tokens: Tokens;
kafka: Kafka;
dbMigrations: DbMigrations;
rateLimit: RateLimitService;
imagePullSecret: k8s.core.v1.Secret;
docker: Docker;
sentry: Sentry;
}) {
const replicas = isProduction(deploymentEnv) ? 3 : 1;
const cpuLimit = isProduction(deploymentEnv) ? '600m' : '300m';
const maxReplicas = isProduction(deploymentEnv) ? 6 : 2;
const replicas = environment.isProduction ? 3 : 1;
const cpuLimit = environment.isProduction ? '600m' : '300m';
const maxReplicas = environment.isProduction ? 6 : 2;
const kafkaBufferDynamic =
kafka.config.bufferDynamic === 'true' || kafka.config.bufferDynamic === '1' ? '1' : '0';
@ -43,24 +40,22 @@ export function deployUsage({
'usage-service',
{
image,
imagePullSecret,
imagePullSecret: docker.secret,
replicas,
readinessProbe: '/_readiness',
livenessProbe: '/_health',
startupProbe: '/_health',
availabilityOnEveryNode: true,
env: {
...deploymentEnv,
...commonEnv,
...kafka.connectionEnv,
SENTRY: commonEnv.SENTRY_ENABLED,
REQUEST_LOGGING: '0', // disabled
KAFKA_BROKER: kafka.config.endpoint,
...environment.envVars,
SENTRY: sentry.enabled ? '1' : '0',
REQUEST_LOGGING: '0',
KAFKA_BUFFER_SIZE: kafka.config.bufferSize,
KAFKA_SASL_MECHANISM: kafka.config.saslMechanism,
KAFKA_CONCURRENCY: kafka.config.concurrency,
KAFKA_BUFFER_INTERVAL: kafka.config.bufferInterval,
KAFKA_BUFFER_DYNAMIC: kafkaBufferDynamic,
KAFKA_TOPIC: kafka.config.topic,
RELEASE: release,
TOKENS_ENDPOINT: serviceLocalEndpoint(tokens.service),
RATE_LIMIT_ENDPOINT: serviceLocalEndpoint(rateLimit.service),
},
@ -81,8 +76,12 @@ export function deployUsage({
tokens.service,
rateLimit.deployment,
rateLimit.service,
kafka.deployment,
kafka.service,
].filter(Boolean),
).deploy();
)
.withSecret('KAFKA_SASL_USERNAME', kafka.secret, 'saslUsername')
.withSecret('KAFKA_SASL_PASSWORD', kafka.secret, 'saslPassword')
.withSecret('KAFKA_SSL', kafka.secret, 'ssl')
.withSecret('KAFKA_BROKER', kafka.secret, 'endpoint')
.withConditionalSecret(sentry.enabled, 'SENTRY_DSN', sentry.secret, 'dsn')
.deploy();
}

View file

@ -1,57 +1,53 @@
import * as k8s from '@pulumi/kubernetes';
import * as pulumi from '@pulumi/pulumi';
import { DeploymentEnvironment } from '../types';
import { isProduction } from '../utils/helpers';
import { ServiceDeployment } from '../utils/service-deployment';
import type { Broker } from './cf-broker';
import { Docker } from './docker';
import { Environment } from './environment';
import { Redis } from './redis';
const commonConfig = new pulumi.Config('common');
const commonEnv = commonConfig.requireObject<Record<string, string>>('env');
import { Sentry } from './sentry';
export type Webhooks = ReturnType<typeof deployWebhooks>;
export function deployWebhooks({
deploymentEnv,
redis,
environment,
heartbeat,
broker,
image,
release,
imagePullSecret,
docker,
redis,
sentry,
}: {
image: string;
release: string;
deploymentEnv: DeploymentEnvironment;
redis: Redis;
broker: Broker;
environment: Environment;
heartbeat?: string;
imagePullSecret: k8s.core.v1.Secret;
docker: Docker;
broker: Broker;
redis: Redis;
sentry: Sentry;
}) {
return new ServiceDeployment(
'webhooks-service',
{
imagePullSecret,
imagePullSecret: docker.secret,
env: {
...deploymentEnv,
...commonEnv,
SENTRY: commonEnv.SENTRY_ENABLED,
...environment.envVars,
SENTRY: sentry.enabled ? '1' : '0',
HEARTBEAT_ENDPOINT: heartbeat ?? '',
RELEASE: release,
REDIS_HOST: redis.config.host,
REDIS_PORT: String(redis.config.port),
REDIS_PASSWORD: redis.config.password,
REQUEST_BROKER: '1',
REQUEST_BROKER_ENDPOINT: broker.workerBaseUrl,
REQUEST_BROKER_SIGNATURE: broker.secretSignature,
},
readinessProbe: '/_readiness',
livenessProbe: '/_health',
startupProbe: '/_health',
exposesMetrics: true,
replicas: isProduction(deploymentEnv) ? 3 : 1,
replicas: environment.isProduction ? 3 : 1,
image,
},
[redis.deployment, redis.service],
).deploy();
)
.withSecret('REDIS_HOST', redis.secret, 'host')
.withSecret('REDIS_PORT', redis.secret, 'port')
.withSecret('REDIS_PASSWORD', redis.secret, 'password')
.withSecret('REQUEST_BROKER_ENDPOINT', broker.secret, 'baseUrl')
.withSecret('REQUEST_BROKER_SIGNATURE', broker.secret, 'secretSignature')
.withConditionalSecret(sentry.enabled, 'SENTRY_DSN', sentry.secret, 'dsn')
.deploy();
}

View file

@ -0,0 +1,33 @@
import { Config, Output } from '@pulumi/pulumi';
import { ServiceSecret } from '../utils/secrets';
import { Environment } from './environment';
export class ZendeskSecret extends ServiceSecret<{
subdomain: string | Output<string>;
username: string | Output<string>;
password: string | Output<string>;
}> {}
export function configureZendesk(input: { environment: Environment }) {
if (!input.environment.isProduction) {
return {
enabled: false,
secret: null,
};
}
const zendeskConfig = new Config('zendesk');
const secret = new ZendeskSecret('zendesk', {
subdomain: zendeskConfig.require('subdomain'),
username: zendeskConfig.require('username'),
password: zendeskConfig.requireSecret('password'),
});
return {
enabled: true,
secret,
};
}
export type Zendesk = ReturnType<typeof configureZendesk>;

View file

@ -1,11 +1,5 @@
import * as pulumi from '@pulumi/pulumi';
export interface DeploymentEnvironment {
ENVIRONMENT: string;
NODE_ENV: string;
DEPLOYED_DNS: string;
}
export interface RegistryConfig {
registry: string;
registryToken: pulumi.Output<string>;

View file

@ -2,6 +2,7 @@ import { readFileSync } from 'fs';
import { resolve } from 'path';
import * as cf from '@pulumi/cloudflare';
import * as pulumi from '@pulumi/pulumi';
import { S3 } from '../services/s3';
export class CloudflareCDN {
constructor(
@ -9,14 +10,9 @@ export class CloudflareCDN {
envName: string;
zoneId: string;
cdnDnsRecord: string;
sentryDsn: string;
sentryDsn: string | pulumi.Output<string>;
release: string;
s3Config: {
endpoint: string;
bucketName: string;
accessKeyId: pulumi.Output<string>;
secretAccessKey: pulumi.Output<string>;
};
s3: S3;
},
) {}
@ -78,19 +74,19 @@ export class CloudflareCDN {
},
{
name: 'S3_ENDPOINT',
text: this.config.s3Config.endpoint,
text: this.config.s3.secret.raw.endpoint,
},
{
name: 'S3_ACCESS_KEY_ID',
text: this.config.s3Config.accessKeyId,
text: this.config.s3.secret.raw.accessKeyId,
},
{
name: 'S3_SECRET_ACCESS_KEY',
text: this.config.s3Config.secretAccessKey,
text: this.config.s3.secret.raw.secretAccessKey,
},
{
name: 'S3_BUCKET_NAME',
text: this.config.s3Config.bucketName,
text: this.config.s3.secret.raw.bucket,
},
],
});
@ -118,7 +114,7 @@ export class CloudflareBroker {
zoneId: string;
cdnDnsRecord: string;
secretSignature: pulumi.Output<string>;
sentryDsn: string;
sentryDsn: string | pulumi.Output<string>;
release: string;
loki: null | {
endpoint: string;

View file

@ -1,21 +1,3 @@
import { DeploymentEnvironment } from '../types';
export function isProduction(deploymentEnv: DeploymentEnvironment | string): boolean {
return isDeploymentEnvironment(deploymentEnv)
? deploymentEnv.ENVIRONMENT === 'production' || deploymentEnv.ENVIRONMENT === 'prod'
: deploymentEnv === 'production' || deploymentEnv === 'prod';
}
export function isStaging(deploymentEnv: DeploymentEnvironment | string): boolean {
return isDeploymentEnvironment(deploymentEnv)
? deploymentEnv.ENVIRONMENT === 'staging'
: deploymentEnv === 'staging';
}
export function isDeploymentEnvironment(value: any): value is DeploymentEnvironment {
return value && typeof value === 'object' && typeof value['ENVIRONMENT'] === 'string';
}
export function isDefined<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}

View file

@ -1,5 +1,6 @@
import * as k8s from '@pulumi/kubernetes';
import * as kx from '@pulumi/kubernetesx';
import { Output } from '@pulumi/pulumi';
import { getLocalComposeConfig } from './local-config';
import { normalizeEnv, PodBuilder } from './pod-builder';
@ -9,7 +10,7 @@ export class Redis {
constructor(
protected options: {
env?: kx.types.Container['env'];
password: string;
password: Output<string>;
},
) {}
@ -17,7 +18,7 @@ export class Redis {
const redisService = getLocalComposeConfig().service('redis');
const name = 'redis-store';
const env = normalizeEnv(this.options.env ?? {}).concat([
const env: k8s.types.input.core.v1.EnvVar[] = normalizeEnv(this.options.env ?? {}).concat([
{
name: 'REDIS_PASSWORD',
value: this.options.password,
@ -30,24 +31,28 @@ export class Redis {
},
},
},
]);
] satisfies k8s.types.input.core.v1.EnvVar[]);
const cm = new kx.ConfigMap('redis-scripts', {
data: {
'readiness.sh': `#!/bin/bash
response=$(timeout -s SIGTERM 3 $1 redis-cli -h localhost -a ${this.options.password} -p ${PORT} ping)
if [ "$response" != "PONG" ]; then
echo "$response"
exit 1
fi
`,
'liveness.sh': `#!/bin/bash
response=$(timeout -s SIGTERM 3 $1 redis-cli -h localhost -a ${this.options.password} -p ${PORT} ping)
if [ "$response" != "PONG" ] && [ "$response" != "LOADING Redis is loading the dataset in memory" ]; then
echo "$response"
exit 1
fi
`,
'readiness.sh': this.options.password.apply(
p => `#!/bin/bash
response=$(timeout -s SIGTERM 3 $1 redis-cli -h localhost -a ${p} -p ${PORT} ping)
if [ "$response" != "PONG" ]; then
echo "$response"
exit 1
fi
`,
),
'liveness.sh': this.options.password.apply(
p => `#!/bin/bash
response=$(timeout -s SIGTERM 3 $1 redis-cli -h localhost -a ${p} -p ${PORT} ping)
if [ "$response" != "PONG" ] && [ "$response" != "LOADING Redis is loading the dataset in memory" ]; then
echo "$response"
exit 1
fi
`,
),
},
});

View file

@ -0,0 +1,21 @@
import * as k8s from '@pulumi/kubernetes';
import { Output } from '@pulumi/pulumi';
export class ServiceSecret<T extends Record<string, string | Output<string>>> {
public record: k8s.core.v1.Secret;
public raw: T;
constructor(
protected name: string,
protected data: T,
) {
this.raw = data;
this.record = new k8s.core.v1.Secret(this.name, {
metadata: {
name: this.name,
},
stringData: this.data,
type: 'Opaque',
});
}
}

View file

@ -3,13 +3,35 @@ 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: {
@ -47,6 +69,29 @@ export class ServiceDeployment {
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);
@ -64,6 +109,7 @@ export class ServiceDeployment {
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;
@ -183,7 +229,9 @@ export class ServiceDeployment {
},
},
},
].concat(additionalEnv),
]
.concat(additionalEnv)
.concat(secretsEnv),
name: this.name,
image: this.options.image,
resources: this.options?.autoScaling?.cpu.limit