constructor()

in packages/cdk/lib/service-catalogue.ts [124:397]


	constructor(scope: App, id: string, props: ServiceCatalogueProps) {
		super(scope, id, props);

		const { account, stage, stack } = this;
		const app = props.app ?? 'service-catalogue';

		const {
			gitHubOrg = 'guardian',
			securityAlertSchedule,
			enableCloudquerySchedules,
			databaseDeletionProtection,
			databaseMultiAz,
			databaseInstanceType,
			databaseEbsByteBalanceAlarm,
		} = props;

		const alertTopicName = 'devx-alerts';

		const alertTopic = Topic.fromTopicArn(
			this,
			'AlertTopic',
			`arn:aws:sns:eu-west-1:${account}:${alertTopicName}`,
		);

		const privateSubnets = GuVpc.subnetsFromParameter(this, {
			type: SubnetType.PRIVATE,
		});

		const vpc = GuVpc.fromIdParameter(this, 'vpc', {
			/*
			CDK wants privateSubnetIds to be a multiple of availabilityZones.
			We're pulling the subnets from a parameter at runtime.
			We know they evaluate to 3 subnets, but at compile time CDK doesn't.
			Set the number of AZs to 1 to avoid the error:
			  `Error: Number of privateSubnetIds (1) must be a multiple of availability zones (2).`
			 */
			availabilityZones: ['ignored'],
			privateSubnetIds: privateSubnets.map((subnet) => subnet.subnetId),
		});

		const port = 5432;

		const dbSecurityGroup = new GuSecurityGroup(this, 'PostgresSecurityGroup', {
			app,
			vpc,
		});

		const dbProps: DatabaseInstanceProps = {
			engine: DatabaseInstanceEngine.POSTGRES,
			port,
			vpc,
			instanceType: databaseInstanceType,
			vpcSubnets: { subnets: privateSubnets },
			iamAuthentication: true, // We're not using IAM auth for ECS tasks, however we do use IAM auth when connecting to RDS locally.
			storageEncrypted: true,
			securityGroups: [dbSecurityGroup],
			deletionProtection: databaseDeletionProtection,
			multiAz: databaseMultiAz,
			/*
			This certificate supports automatic rotation.
			See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.RegionCertificateAuthorities
			 */
			caCertificate: CaCertificate.RDS_CA_RSA2048_G1,
			storageType: StorageType.GP3,
			enablePerformanceInsights: true,
			monitoringInterval: Duration.seconds(10),
		};

		const db = new DatabaseInstance(this, 'PostgresInstance1', dbProps);

		Tags.of(db).add('devx-backup-enabled', 'true');

		if (databaseEbsByteBalanceAlarm) {
			const metric = db.metric('EBSByteBalance%');
			const metricStat = metric.toMetricConfig().metricStat;

			if (!metricStat) {
				throw new Error("MetricStat is undefined. This shouldn't happen.");
			}

			new CfnAnomalyDetector(this, `${metric.metricName}AnomalyDetector`, {
				metricName: metric.metricName,
				namespace: metric.namespace,
				stat: metricStat.statistic,
				dimensions: metricStat.dimensions,
			});

			new CfnAlarm(this, `${metric.metricName}Alarm`, {
				comparisonOperator:
					ComparisonOperator.LESS_THAN_LOWER_OR_GREATER_THAN_UPPER_THRESHOLD,
				treatMissingData: TreatMissingData.BREACHING,
				datapointsToAlarm: 1,
				evaluationPeriods: 1,

				/*
				This alarms is using anomaly detection which we're not too familiar with yet.
				A disabled alarm will provide an activity history we can use to fine-tune the alarm.
				TODO Enable this once we're confident the alarm is working as we'd like.
				 */
				actionsEnabled: false,

				alarmActions: [alertTopic.topicArn],
				okActions: [alertTopic.topicArn],
				thresholdMetricId: 'ad1',
				metrics: [
					{
						id: 'ad1',
						returnData: true,
						expression: 'ANOMALY_DETECTION_BAND(m1, 2)',
					},
					{
						id: 'm1',
						returnData: true,
						metricStat: {
							metric: {
								namespace: metric.namespace,
								metricName: metric.metricName,
								dimensions: metricStat.dimensions,
							},
							period: Duration.minutes(5).toSeconds(),
							stat: metricStat.statistic,
						},
					},
				],
			});
		}

		const applicationToPostgresSecurityGroup = new GuSecurityGroup(
			this,
			'PostgresAccessSecurityGroup',
			{ app, vpc },
		);

		// TODO use a bastion host here instead? https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.BastionHostLinux.html
		dbSecurityGroup.addIngressRule(
			Peer.ipv4(GuardianPrivateNetworks.Engineering),
			Port.tcp(port),
			'Allow connection to Postgres from the office network.',
		);

		dbSecurityGroup.connections.allowFrom(
			applicationToPostgresSecurityGroup,
			Port.tcp(port),
		);

		// Used by downstream services that read ServiceCatalogue data, namely Grafana.
		new StringParameter(this, 'PostgresAccessSecurityGroupParam', {
			parameterName: `/${stage}/${stack}/${app}/postgres-access-security-group`,
			simpleName: false,
			stringValue: applicationToPostgresSecurityGroup.securityGroupId,
			tier: ParameterTier.STANDARD,
			dataType: ParameterDataType.TEXT,
		});
		new StringParameter(this, 'PostgresInstanceEndpointAddress', {
			parameterName: `/${stage}/${stack}/${app}/postgres-instance-endpoint-address`,
			simpleName: false,
			stringValue: db.dbInstanceEndpointAddress,
			tier: ParameterTier.STANDARD,
			dataType: ParameterDataType.TEXT,
		});

		const loggingStreamName =
			GuLoggingStreamNameParameter.getInstance(this).valueAsString;

		const loggingStreamArn = this.formatArn({
			service: 'kinesis',
			resource: 'stream',
			resourceName: loggingStreamName,
		});

		const logShippingPolicy = new PolicyStatement({
			actions: ['kinesis:Describe*', 'kinesis:Put*'],
			effect: Effect.ALLOW,
			resources: [loggingStreamArn],
		});

		const cloudqueryApiKey = new Secret(this, 'cloudquery-api-key', {
			secretName: `/${stage}/${stack}/${app}/cloudquery-api-key`,
		});

		const cloudqueryCluster = addCloudqueryEcsCluster(this, {
			enableCloudquerySchedules,
			db,
			vpc,
			dbAccess: applicationToPostgresSecurityGroup,
			loggingStreamName,
			logShippingPolicy,
			gitHubOrg,
			cloudqueryApiKey,
		});

		addCloudqueryUsageLambda(this, {
			vpc,
			db,
			dbAccess: applicationToPostgresSecurityGroup,
			cloudqueryApiKey,
		});

		const anghammaradTopicParameter =
			GuAnghammaradTopicParameter.getInstance(this);

		const interactiveMonitor = new InteractiveMonitor(this, gitHubOrg);

		const anghammaradTopic = Topic.fromTopicArn(
			this,
			'anghammarad-arn',
			anghammaradTopicParameter.valueAsString,
		);

		const repocopGithubCredentials = new Secret(
			this,
			`repocop-github-app-auth`,
			{
				secretName: `/${stage}/${stack}/service-catalogue/repocop-github-app-secret`,
			},
		);

		new Repocop(
			this,
			securityAlertSchedule,
			anghammaradTopic,
			db,
			createLambdaMonitoringConfiguration(stage, 'repocop'),
			vpc,
			interactiveMonitor.topic,
			applicationToPostgresSecurityGroup,
			repocopGithubCredentials,
			gitHubOrg,
		);

		addDataAuditLambda(this, {
			vpc,
			db,
			dbAccess: applicationToPostgresSecurityGroup,
		});

		addGithubActionsUsageLambda(this, {
			vpc,
			db,
			dbAccess: applicationToPostgresSecurityGroup,
		});

		addPrismaMigrateTask(this, {
			loggingStreamName,
			logShippingPolicy,
			db,
			dbAccess: applicationToPostgresSecurityGroup,
			cluster: cloudqueryCluster,
		});

		addRefreshMaterializedViewLambda(this, {
			vpc,
			db,
			dbAccess: applicationToPostgresSecurityGroup,
		});

		new Obligatron(this, {
			vpc,
			db,
			dbAccess: applicationToPostgresSecurityGroup,
		});

		new CloudBuster(this, {
			vpc,
			db,
			dbAccess: applicationToPostgresSecurityGroup,
			anghammaradTopic,
			monitoringConfiguration: createLambdaMonitoringConfiguration(
				stage,
				'cloudbuster',
			),
			schedule: securityAlertSchedule,
		});
	}