in cdk/lib/telemetry-stack.ts [36:348]
constructor(scope: App, id: string, props: GuStackProps) {
super(scope, id, props);
const appName = 'user-telemetry';
/**
* Parameters
*/
const telemetryHostName = new CfnParameter(this, 'Hostname', {
type: 'String',
description: 'Hostname for telemetry endpoint',
});
const telemetryCert = new CfnParameter(this, 'Cert', {
type: 'String',
description: 'Certificate ARN for telemetry endpoint',
});
const bucketName = new CfnParameter(this, 'BucketName', {
type: 'String',
description: 'Name of the bucket to persist event data',
});
const kinesisStreamArn = new CfnParameter(this, 'KinesisArn', {
type: 'String',
description: 'ARN of the Kinesis stream to post event data',
});
const maxLogSize = new CfnParameter(this, 'MaxLogSize', {
type: 'String',
description:
'Maximum size (in bytes) of log data from an individual request',
});
const pandaSettingsKey = new CfnParameter(this, 'PandaSettingsKey', {
type: 'String',
description:
'The location of the pan-domain authentication settings file',
});
/**
* Secrets
*/
const hmacSecret = new Secret(this, 'EventApiHmacSecret', {
secretName: `/${this.stage}/${this.stack}/event-api-lambda/hmacSecret`,
description:
'The HMAC secret key used to authenticate machine clients with the event-api-lambda',
});
const allowOphanAccessToHmac = new PolicyStatement({
effect: Effect.ALLOW,
actions: ['secretsmanager:GetSecretValue'],
resources: [hmacSecret.secretArn],
});
const ophanRoleArn = new GuStringParameter(this, 'ophanRoleArn', {
default: `/${this.stage}/${this.stack}/event-api-lambda/ophanRoleArn`,
fromSSM: true,
description:
'ARN of Ophan dashboard role that assumes the hmacSecretAccessRoleForOphan',
}).valueAsString;
const hmacSecretRoleForOphan = new GuRole(
this,
'hmac-secret-access-role-for-ophan',
{
roleName: `hmacSecretAccessRoleForOphan-${this.stage}`,
assumedBy: new ArnPrincipal(ophanRoleArn),
},
);
hmacSecretRoleForOphan.addToPolicy(allowOphanAccessToHmac);
/**
* S3 bucket – where our telemetry data is persisted
*/
const telemetryDataBucket = Bucket.fromBucketName(
this,
'telemetry-bucket',
bucketName.valueAsString,
);
/**
* Lambda
*/
const deployBucket = Bucket.fromBucketName(
this,
'composer-dist',
'composer-dist',
);
const commonLambdaParams: Omit<FunctionProps, 'code'> = {
runtime: Runtime.NODEJS_20_X,
memorySize: 128,
timeout: Duration.seconds(10),
handler: 'index.handler',
environment: {
STAGE: this.stage,
STACK: this.stack,
APP: appName,
MAX_LOG_SIZE: maxLogSize.valueAsString,
LOG_ENDPOINT_ENABLED: 'true',
TELEMETRY_BUCKET_NAME: telemetryDataBucket.bucketName,
HMAC_SECRET_LOCATION: hmacSecret.secretName,
},
reservedConcurrentExecutions: this.stage === 'PROD' ? 25 : 5,
};
/**
* API Lambda
*/
const createTelemetryAPIFunction = () => {
const fn = new Function(this, `EventApiLambda`, {
...commonLambdaParams,
environment: {
...commonLambdaParams.environment,
PANDA_SETTINGS_KEY: pandaSettingsKey.valueAsString,
},
functionName: `event-api-lambda-${this.stage}`,
code: Code.fromBucket(
deployBucket,
`${this.stack}/${this.stage}/event-api-lambda/event-api-lambda.zip`,
),
});
const fnTags = Tags.of(fn);
fnTags.add('App', appName);
fnTags.add('Stage', this.stage);
fnTags.add('Stack', this.stack);
return fn;
};
const telemetryAPIFunction = createTelemetryAPIFunction();
const s3PutTelemetryBackendPolicyStatement = new PolicyStatement({
effect: Effect.ALLOW,
actions: ['s3:PutObject'],
resources: [
telemetryDataBucket.bucketArn,
`${telemetryDataBucket.bucketArn}/*`,
],
});
const hmacSecretReadTelemetryBackendPolicyStatement = new PolicyStatement({
effect: Effect.ALLOW,
actions: ['secretsmanager:GetSecretValue'],
resources: [hmacSecret.secretArn],
});
telemetryAPIFunction.addToRolePolicy(s3PutTelemetryBackendPolicyStatement);
telemetryAPIFunction.addToRolePolicy(
hmacSecretReadTelemetryBackendPolicyStatement,
);
/**
* S3 event handler lambda
*/
const kinesisStream = Stream.fromStreamArn(
this,
'user-telemetry-kinesis-stream',
kinesisStreamArn.valueAsString,
);
const createTelemetryS3Function = () => {
const fn = new Function(this, `EventS3Lambda`, {
...commonLambdaParams,
functionName: `event-s3-lambda-${this.stage}`,
code: Code.fromBucket(
deployBucket,
`${this.stack}/${this.stage}/event-s3-lambda/event-s3-lambda.zip`,
),
environment: {
...commonLambdaParams.environment,
TELEMETRY_STREAM_NAME: kinesisStream.streamName,
},
});
const fnTags = Tags.of(fn);
fnTags.add('App', appName);
fnTags.add('Stage', this.stage);
fnTags.add('Stack', this.stack);
// Notify our lambda when new objects are added to the telemetry bucket
telemetryDataBucket.addEventNotification(
EventType.OBJECT_CREATED,
new LambdaDestination(fn),
);
return fn;
};
const telemetryS3Function = createTelemetryS3Function();
const telemetryS3FunctionS3PolicyStatement = new PolicyStatement({
effect: Effect.ALLOW,
actions: ['s3:GetObject'],
resources: [
telemetryDataBucket.bucketArn,
`${telemetryDataBucket.bucketArn}/*`,
],
});
const telemetryS3FunctionKinesisPolicyStatement = new PolicyStatement({
effect: Effect.ALLOW,
actions: ['kinesis:PutRecords'],
resources: [kinesisStreamArn.valueAsString],
});
telemetryS3Function.addToRolePolicy(telemetryS3FunctionS3PolicyStatement);
telemetryS3Function.addToRolePolicy(
telemetryS3FunctionKinesisPolicyStatement,
);
/**
* API Gateway
*/
const telemetryApiPolicyStatement = new PolicyStatement({
effect: Effect.ALLOW,
actions: ['execute-api:Invoke'],
resources: ['*'],
});
telemetryApiPolicyStatement.addAnyPrincipal();
const telemetryApi = new LambdaRestApi(this, appName, {
handler: telemetryAPIFunction,
endpointTypes: [EndpointType.EDGE],
policy: new PolicyDocument({
statements: [telemetryApiPolicyStatement],
}),
defaultMethodOptions: {
apiKeyRequired: false,
},
});
const telemetryCertificate = Certificate.fromCertificateArn(
this,
`telemetry-cert-${this.stage}`,
telemetryCert.valueAsString,
);
const telemetryDomainName = new DomainName(
this,
`user-telemetry-domain-name-${this.stage}`,
{
domainName: telemetryHostName.valueAsString,
certificate: telemetryCertificate,
endpointType: EndpointType.EDGE,
},
);
telemetryDomainName.addBasePathMapping(telemetryApi, { basePath: '' });
new GuCname(this, `telemetry-dns-record-${this.stage}`, {
domainName: telemetryHostName.valueAsString,
app: 'user-telemetry-service',
resourceRecord: telemetryDomainName.domainNameAliasDomainName,
ttl: Duration.seconds(3600),
});
/**
* Re-drive lambda and step-function
*/
const reDriveFromS3Lambda = new GuLambdaFunction(
this,
'ReDriveFromS3Lambda',
{
app: `${appName}-redrive-from-s3-lambda`,
functionName: `${this.stack}-${this.stage}-${appName}-redrive-from-s3-lambda`,
...commonLambdaParams,
environment: {
...commonLambdaParams.environment,
TELEMETRY_STREAM_NAME: kinesisStream.streamName,
},
memorySize: 1024,
timeout: Duration.minutes(15), // maximum allowed by AWS
reservedConcurrentExecutions: 1,
fileName: 'redrive-from-S3-lambda.zip',
loggingFormat: LoggingFormat.TEXT,
},
);
telemetryDataBucket.grantRead(reDriveFromS3Lambda);
kinesisStream.grantWrite(reDriveFromS3Lambda);
kinesisStream.grant(
reDriveFromS3Lambda,
'kinesis:UpdateShardCount',
'kinesis:DescribeStream',
'kinesis:ListShards',
);
const lambdaInvokeStep = new LambdaInvoke(
this,
'ReDriveFromS3StepFunctionLambdaCall',
{
lambdaFunction: reDriveFromS3Lambda,
},
);
new StateMachine(this, 'ReDriveFromS3StepFunction', {
stateMachineName: `${this.stack}-${this.stage}-${appName}-redrive-from-s3-step-function`,
definitionBody: DefinitionBody.fromChainable(
lambdaInvokeStep.next(
new Choice(this, 'ReDriveFromS3StepFunctionIsDoneChoice')
.when(Condition.isNotNull('$.Payload'), lambdaInvokeStep)
.otherwise(new Pass(this, 'ReDriveFromS3StepFunctionIsDone')),
),
),
});
/**
* Stack Outputs
*/
new CfnOutput(this, 'EventApiHmacSecretArn', {
value: hmacSecret.secretArn,
});
}