in source/resources/lib/cl-primary-stack.ts [110:1258]
constructor(scope: App, id: string) {
super(scope, id);
const stack = Stack.of(this);
this.account = stack.account; // Returns the AWS::AccountId for this stack (or the literal value if known)
this.region = stack.region; // Returns the AWS::Region for this stack (or the literal value if known)
this.partn = stack.partition; // Returns the AWS::Partition for this stack
//=========================================================================
// Parameter
//=========================================================================
/**
* @description ES domain name
* @type {CfnParameter}
*/
const esDomain: CfnParameter = new CfnParameter(this, "DomainName", {
type: "String",
default: "centralizedlogging",
});
/**
* @description email address for Cognito admin
* @type {CfnParameter}
*/
const adminEmail: CfnParameter = new CfnParameter(this, "AdminEmail", {
type: "String",
allowedPattern: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$",
});
/**
* @description ES cluster size
* @type {CfnParameter}
*/
const clusterSize: CfnParameter = new CfnParameter(this, "ClusterSize", {
description:
"Elasticsearch cluster size; small (4 data nodes), medium (6 data nodes), large (6 data nodes)",
type: "String",
default: "Small",
allowedValues: ["Small", "Medium", "Large"],
});
/**
* @description Option to deploy demo template
* @type {CfnParameter}
*/
const demoTemplate: CfnParameter = new CfnParameter(this, "DemoTemplate", {
description: "Deploy demo template for sample data and logs?",
type: "String",
default: "No",
allowedValues: ["No", "Yes"],
});
/**
* @description List of spoke account ids
* @type {CfnParameter}
*/
const spokeAccts: CfnParameter = new CfnParameter(this, "SpokeAccounts", {
description:
"Account IDs which you want to allow for centralized logging (comma separated list eg. 11111111,22222222)",
type: "CommaDelimitedList",
});
/**
* @regions List of regions for CW Logs Destination
* @type {CfnParameter}
*/
const spokeRegions: CfnParameter = new CfnParameter(this, "SpokeRegions", {
description:
"Regions which you want to allow for centralized logging (comma separated list eg. us-east-1,us-west-2)",
type: "CommaDelimitedList",
default: "All",
});
/**
* @description deploy jumbox
* @type {CfnParameter}
*/
const jumpboxDeploy: CfnParameter = new CfnParameter(
this,
"JumpboxDeploy",
{
description: "Do you want to deploy jumbox?",
type: "String",
default: "No",
allowedValues: ["No", "Yes"],
}
);
/**
* @description key pair for jump box
* @type {CfnParameter}
*/
const jumpboxKey: CfnParameter = new CfnParameter(this, "JumpboxKey", {
description:
"Key pair name for jumpbox (You may leave this empty if you chose 'No' above)",
type: "String",
});
//=============================================================================================
// Metadata
//=============================================================================================
this.templateOptions.metadata = {
"AWS::CloudFormation::Interface": {
ParameterGroups: [
{
Label: {
default: "Elasticsearch Configuration",
},
Parameters: [
esDomain.logicalId,
clusterSize.logicalId,
adminEmail.logicalId,
],
},
{
Label: {
default: "Spoke Configuration",
},
Parameters: [spokeAccts.logicalId, spokeRegions.logicalId],
},
{
Label: {
default: "Do you want to deploy sample log sources?",
},
Parameters: [demoTemplate.logicalId],
},
{
Label: {
default: "Jumpbox Configuration",
},
Parameters: [jumpboxDeploy.logicalId, jumpboxKey.logicalId],
},
],
ParameterLabels: {
[adminEmail.logicalId]: {
default: "Admin Email Address",
},
[esDomain.logicalId]: {
default: "Elasticsearch Domain Name",
},
[jumpboxKey.logicalId]: {
default: "Key pair for jumpbox",
},
[jumpboxDeploy.logicalId]: {
default: "Deployment",
},
[clusterSize.logicalId]: {
default: "Cluster Size",
},
[demoTemplate.logicalId]: {
default: "Sample Logs",
},
[spokeAccts.logicalId]: {
default: "Spoke Accounts",
},
[spokeRegions.logicalId]: {
default: "Spoke Regions",
},
},
},
};
this.templateOptions.description = `(${manifest.solutionId}) - The AWS CloudFormation template for deployment of the ${manifest.solutionName}. Version ${manifest.solutionVersion}`;
this.templateOptions.templateFormatVersion = manifest.templateVersion;
//=========================================================================
// Mapping
//=========================================================================
const metricsMap = new CfnMapping(this, "CLMap", {
mapping: {
Metric: {
SendAnonymousMetric: manifest.sendMetric,
MetricsEndpoint: manifest.metricsEndpoint, // aws-solutions metrics endpoint
},
},
});
const esMap = new CfnMapping(this, "ESMap", {
mapping: {
NodeCount: {
Small: 4,
Medium: 6,
Large: 6,
},
MasterSize: {
Small: "c5.large.elasticsearch",
Medium: "c5.large.elasticsearch",
Large: "c5.large.elasticsearch",
},
InstanceSize: {
Small: "r5.large.elasticsearch",
Medium: "r5.2xlarge.elasticsearch",
Large: "r5.4xlarge.elasticsearch",
},
},
});
//=============================================================================================
// Condition
//=============================================================================================
const demoDeploymentCheck = new CfnCondition(this, "demoDeploymentCheck", {
expression: Fn.conditionEquals(demoTemplate.valueAsString, "Yes"),
});
const jumpboxDeploymentCheck = new CfnCondition(
this,
"JumpboxDeploymentCheck",
{
expression: Fn.conditionEquals(jumpboxDeploy.valueAsString, "Yes"),
}
);
//=============================================================================================
// Resource
//=============================================================================================
/**
* @description helper lambda role
* @type {Role}
*/
const helperRole: Role = new Role(this, "HelperRole", {
assumedBy: new ServicePrincipal("lambda.amazonaws.com"),
});
const helperPolicy1 = new Policy(this, "HelperRolePolicy1", {
roles: [helperRole],
statements: [
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:CreateLogGroup",
],
resources: [
`arn:${this.partn}:logs:${this.region}:${this.account}:log-group:*`,
`arn:${this.partn}:logs:${this.region}:${this.account}:log-group:*:log-stream:*`,
],
}),
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
"ec2:DescribeRegions",
"logs:PutDestination",
"logs:DeleteDestination",
"logs:PutDestinationPolicy",
],
resources: ["*"],
}),
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["iam:CreateServiceLinkedRole"],
resources: [
`arn:${this.partn}:iam::*:role/aws-service-role/es.amazonaws.com/AWSServiceRoleForAmazonElasticsearchService*`,
],
conditions: {
["StringLike"]: {
"iam:AWSServiceName": "es.amazonaws.com",
},
},
}),
],
});
/**
* @description helper lambda
* @type {Function}
*/
// eslint-disable-next-line @typescript-eslint/ban-types
const helperFunc: Function = new Function(this, "HelperLambda", {
description: manifest.solutionName + " - solution helper functions",
environment: {
LOG_LEVEL: LogLevel.INFO, //change as needed
METRICS_ENDPOINT: metricsMap.findInMap("Metric", "MetricsEndpoint"),
SEND_METRIC: metricsMap.findInMap("Metric", "SendAnonymousMetric"),
CUSTOM_SDK_USER_AGENT: `AwsSolution/${manifest.solutionId}/${manifest.solutionVersion}`,
},
handler: "index.handler",
code: Code.fromAsset(
`${path.dirname(__dirname)}/../services/helper/dist/cl-helper.zip`
),
runtime: Runtime.NODEJS_14_X,
timeout: Duration.seconds(300),
role: helperRole,
});
applyDependsOn(helperFunc, helperPolicy1);
/**
* @description custom resource for helper functions
* @type {Provider}
*/
const helperProvider: Provider = new Provider(this, "HelperProvider", {
onEventHandler: helperFunc,
});
/**
* Get UUID for deployment
*/
const createUniqueId = new CustomResource(this, "CreateUUID", {
resourceType: "Custom::CreateUUID",
serviceToken: helperProvider.serviceToken,
});
/**
* Create service linked role for ES
*/
new CustomResource(this, "CreateESServiceRole", {
resourceType: "Custom::CreateESServiceRole",
serviceToken: helperProvider.serviceToken,
});
/**
* Send launch data to aws-solutions
*/
new CustomResource(this, "LaunchData", {
resourceType: "Custom::LaunchData",
serviceToken: helperProvider.serviceToken,
properties: {
SolutionId: manifest.solutionId,
SolutionVersion: manifest.solutionVersion,
SolutionUuid: createUniqueId.getAttString("UUID"),
Stack: "PrimaryStack",
},
});
/**
* @description cognito user pool
* @type {UserPool}
*/
const esUserPool: UserPool = new UserPool(this, "ESUserPool", {
standardAttributes: {
email: {
mutable: true,
required: true,
},
},
passwordPolicy: {
minLength: 8,
requireLowercase: true,
requireUppercase: true,
requireDigits: true,
requireSymbols: true,
tempPasswordValidity: Duration.days(3),
},
signInAliases: { email: true },
accountRecovery: AccountRecovery.EMAIL_ONLY,
selfSignUpEnabled: false,
});
// enforce advanced security mode
(esUserPool.node.defaultChild as CfnUserPool).addPropertyOverride(
"UserPoolAddOns",
{
AdvancedSecurityMode: "ENFORCED",
}
);
// add domain to user pool
const upDomain = esUserPool.addDomain("ESCognitoDomain", {
cognitoDomain: {
domainPrefix: `${esDomain.valueAsString}-${createUniqueId.getAttString(
"UUID"
)}`,
},
});
/**
* @description adding admin to user pool
* @type {CfnUserPoolUser}
*/
new CfnUserPoolUser(this, "AdminUser", {
userPoolId: esUserPool.userPoolId,
userAttributes: [{ name: "email", value: adminEmail.valueAsString }],
username: adminEmail.valueAsString,
});
/**
* @description cognito user pool
* @type {CfnIdentityPool}
* @remarks higher level constructs for Identity pools are yet not developed
* @see https://docs.aws.amazon.com/cdk/api/latest/docs/aws-cognito-readme.html
*/
const identityPool: CfnIdentityPool = new CfnIdentityPool(
this,
"ESIdentityPool",
{
allowUnauthenticatedIdentities: false,
}
);
/**
* @description cognito authenticated role
* @type {Role}
*/
const idpAuthRole: Role = new Role(this, "CognitoAuthRole", {
assumedBy: new FederatedPrincipal(
"cognito-identity.amazonaws.com",
{
["StringEquals"]: {
"cognito-identity.amazonaws.com:aud": identityPool.ref,
},
["ForAnyValue:StringLike"]: {
"cognito-identity.amazonaws.com:amr": "authenticated",
},
},
"sts:AssumeRoleWithWebIdentity"
),
});
// identity pool authorized role
new CfnIdentityPoolRoleAttachment(this, "IdentityPoolRoleAttachment", {
identityPoolId: identityPool.ref,
roles: { authenticated: idpAuthRole.roleArn },
});
/**
* @description es role for cognito access
* @type {Role}
* @remark same policy as arn:aws:iam::aws:policy/AmazonESCognitoAccess
*/
const esCognitoRole: Role = new Role(this, "ESCognitoRole", {
assumedBy: new ServicePrincipal("es.amazonaws.com"),
inlinePolicies: {
["ESCognitoAccess"]: PolicyDocument.fromJson({
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: [
"cognito-idp:DescribeUserPool",
"cognito-idp:CreateUserPoolClient",
"cognito-idp:DeleteUserPoolClient",
"cognito-idp:DescribeUserPoolClient",
"cognito-idp:AdminInitiateAuth",
"cognito-idp:AdminUserGlobalSignOut",
"cognito-idp:ListUserPoolClients",
"cognito-identity:DescribeIdentityPool",
"cognito-identity:UpdateIdentityPool",
"cognito-identity:SetIdentityPoolRoles",
"cognito-identity:GetIdentityPoolRoles",
],
Resource: "*",
},
],
}),
},
});
esCognitoRole.addToPolicy(
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["iam:PassRole"],
resources: [esCognitoRole.roleArn],
conditions: {
["StringLike"]: {
"iam:PassedToService": "cognito-identity.amazonaws.com",
},
},
})
);
/**
* @description IAM role for kinesis firehose
* @type {Role}
*/
const firehoseRole: Role = new Role(this, "FirehoseRole", {
assumedBy: new ServicePrincipal("firehose.amazonaws.com"),
});
/**
* @description log group for VPC flow logs
* @type {LogGroup}
*/
const flowLg: LogGroup = new LogGroup(this, "VPCFlowLogGroup", {
removalPolicy: RemovalPolicy.RETAIN,
});
/**
* @description iam role for flow logs
* @type {Role}
*/
const flowRole: Role = new Role(this, "flowRole", {
assumedBy: new ServicePrincipal("vpc-flow-logs.amazonaws.com"),
});
/**
* @description es vpc with 2 isolated subnets
* @type {Vpc}
*/
const VPC: Vpc = new Vpc(this, "ESVPC", {
cidr: manifest.esdomain.vpcCIDR,
vpnGateway: false,
flowLogs: {
["ESVpcFlow"]: {
destination: FlowLogDestination.toCloudWatchLogs(flowLg, flowRole),
trafficType: FlowLogTrafficType.ALL,
},
},
subnetConfiguration: [
{
cidrMask: 24,
subnetType: SubnetType.PRIVATE_ISOLATED,
name: "ESIsolatedSubnet",
},
{
cidrMask: 24,
subnetType: SubnetType.PUBLIC,
name: "ESPublicSubnet",
},
],
});
Aspects.of(VPC).add(new ResourceRetentionAspect());
/**
* @description security group for es domain
* @type {SecurityGroup}
*/
const esSg: SecurityGroup = new SecurityGroup(this, "ESSG", {
vpc: VPC,
allowAllOutbound: false,
});
esSg.addIngressRule(
Peer.ipv4(VPC.vpcCidrBlock),
Port.tcp(443),
"allow inbound https traffic"
);
esSg.addEgressRule(
Peer.ipv4(VPC.vpcCidrBlock),
Port.tcp(443),
"allow outbound https"
);
Aspects.of(esSg).add(new ResourceRetentionAspect());
/**
* @description es domain
* @type {Domain}
*/
const domain: Domain = new Domain(this, "ESDomain", {
version: ElasticsearchVersion.V7_7,
domainName: esDomain.valueAsString,
enforceHttps: true,
vpc: VPC,
vpcSubnets: [{ subnets: VPC.isolatedSubnets }],
securityGroups: [esSg],
encryptionAtRest: {
enabled: true,
},
zoneAwareness: {
availabilityZoneCount: 2,
},
nodeToNodeEncryption: true,
automatedSnapshotStartHour: 0,
cognitoKibanaAuth: {
identityPoolId: identityPool.ref,
role: esCognitoRole,
userPoolId: esUserPool.userPoolId,
},
});
// attach policy to idp auth role
idpAuthRole.attachInlinePolicy(
new Policy(this, "authRolePolicy", {
statements: [
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
"es:ESHttpGet",
"es:ESHttpDelete",
"es:ESHttpPut",
"es:ESHttpPost",
"es:ESHttpHead",
"es:ESHttpPatch",
],
resources: [domain.domainArn],
}),
],
})
);
/**
* @description cluster configurations for es domain
* @remark property is not supported on higher level construct
*/
const clusterConfig = {
DedicatedMasterEnabled: true,
InstanceCount: esMap.findInMap("NodeCount", clusterSize.valueAsString),
ZoneAwarenessEnabled: true,
InstanceType: esMap.findInMap("InstanceSize", clusterSize.valueAsString),
DedicatedMasterType: esMap.findInMap(
"MasterSize",
clusterSize.valueAsString
),
DedicatedMasterCount: 3,
};
// adding cluster config
applyDependsOn(domain, upDomain);
const cfnDomain = domain.node.defaultChild as CfnDomain;
cfnDomain.addPropertyOverride("ElasticsearchClusterConfig", clusterConfig);
/**
* @description es domain access policy
* @remark domain construct adds access policy using lambda function
*/
const accessPolicies = {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: [
"es:ESHttpGet",
"es:ESHttpDelete",
"es:ESHttpPut",
"es:ESHttpPost",
"es:ESHttpHead",
"es:ESHttpPatch",
],
Principal: { AWS: idpAuthRole.roleArn },
Resource: `arn:${this.partn}:es:${this.region}:${this.account}:domain/${esDomain.valueAsString}/*`,
},
{
Effect: "Allow",
Action: [
"es:DescribeElasticsearchDomain",
"es:DescribeElasticsearchDomains",
"es:DescribeElasticsearchDomainConfig",
"es:ESHttpPost",
"es:ESHttpPut",
"es:HttpGet",
],
Principal: { AWS: firehoseRole.roleArn },
Resource: `arn:${this.partn}:es:${this.region}:${this.account}:domain/${esDomain.valueAsString}/*`,
},
],
};
// adding access policy
cfnDomain.addPropertyOverride("AccessPolicies", accessPolicies);
/**
* @description dead letter queue for lambda
* @type {Queue}
*/
const dlq: Queue = new Queue(this, `dlq`, {
encryption: QueueEncryption.KMS_MANAGED,
});
/**
* @description Lambda transformer for log events
* @type {Function}
*/
// eslint-disable-next-line @typescript-eslint/ban-types
const logTransformer: Function = new Function(this, "CLTransformer", {
description: `${manifest.solutionName} - Lambda function to transform log events and send to kinesis firehose`,
environment: {
LOG_LEVEL: LogLevel.INFO, //change as needed
SOLUTION_ID: manifest.solutionId,
SOLUTION_VERSION: manifest.solutionVersion,
UUID: createUniqueId.getAttString("UUID"),
CLUSTER_SIZE: clusterSize.valueAsString,
DELIVERY_STREAM: manifest.firehoseName,
METRICS_ENDPOINT: metricsMap.findInMap("Metric", "MetricsEndpoint"),
SEND_METRIC: metricsMap.findInMap("Metric", "SendAnonymousMetric"),
CUSTOM_SDK_USER_AGENT: `AwsSolution/${manifest.solutionId}/${manifest.solutionVersion}`,
},
handler: "index.handler",
code: Code.fromAsset(
`${path.dirname(
__dirname
)}/../services/transformer/dist/cl-transformer.zip`
),
runtime: Runtime.NODEJS_14_X,
timeout: Duration.seconds(300),
deadLetterQueue: dlq,
deadLetterQueueEnabled: true,
});
/**
* @description Kms key for SNS topic
* @type {IAlias}
*/
const snsKeyAlias: IAlias = Alias.fromAliasName(
this,
"snsKey",
"alias/aws/sns"
);
/**
* @description sns topic for alarms
* @type {Topic}
*/
const topic: Topic = new Topic(this, "Topic", {
displayName: "CL-Lambda-Error",
masterKey: snsKeyAlias,
});
// add email subscription for admin
topic.addSubscription(new EmailSubscription(adminEmail.valueAsString));
// adding cw alarm for lambda error rate
const alarm = logTransformer
.metricErrors()
.createAlarm(this, "CL-LambdaError-Alarm", {
threshold: 0.05,
evaluationPeriods: 1,
});
alarm.addAlarmAction(new SnsAction(topic));
/**
* @description kinesis data stream for centralized logging
* @type {Stream}
*/
const clDataStream: Stream = new Stream(this, "CLDataStream", {
shardCount: manifest.kinesisDataStream.shard,
retentionPeriod: Duration.hours(
manifest.kinesisDataStream.retentionInHrs
),
encryption: StreamEncryption.MANAGED,
});
// add event source for kinesis data stream
logTransformer.addEventSource(
new KinesisEventSource(clDataStream, {
batchSize: 100, // default
startingPosition: StartingPosition.TRIM_HORIZON,
})
);
/**
* @description S3 bucket for access logs
* @type {Bucket}
*/
const accessLogsBucket: Bucket = new Bucket(this, "AccessLogsBucket", {
encryption: BucketEncryption.S3_MANAGED,
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
accessControl: BucketAccessControl.LOG_DELIVERY_WRITE,
});
/**
* @description S3 bucket for Firehose
* @type {Bucket}
*/
const firehoseBucket: Bucket = new Bucket(this, "CLBucket", {
encryption: BucketEncryption.S3_MANAGED,
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
serverAccessLogsBucket: accessLogsBucket,
serverAccessLogsPrefix: "cl-access-logs",
});
// adding bucket policy
firehoseBucket.addToResourcePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
principals: [new ArnPrincipal(firehoseRole.roleArn)],
actions: ["s3:Put*", "s3:Get*"],
resources: [firehoseBucket.bucketArn, `${firehoseBucket.bucketArn}/*`],
})
);
// apply retention policy
Aspects.of(firehoseBucket).add(new ResourceRetentionAspect());
/**
* @description log group for firehose error events
* @type {LogGroup}
*/
const firehoseLG: LogGroup = new LogGroup(this, "FirehoseLogGroup", {
removalPolicy: RemovalPolicy.RETAIN,
logGroupName: `/aws/kinesisfirehose/${manifest.firehoseName}`,
});
/**
* @description log stream for elasticsearch delivery logs
* @type {LogStream}
*/
const firehoseLS: LogStream = new LogStream(this, "FirehoseESLogStream", {
logGroup: firehoseLG,
logStreamName: "ElasticsearchDelivery",
});
/**
* @description log stream for s3 delivery logs
* @type {LogStream}
*/
const firehoseLSS3: LogStream = new LogStream(this, "FirehoseS3LogStream", {
logGroup: firehoseLG,
logStreamName: "S3Delivery",
});
/**
* @description iam policy for firehose role
* @type {Policy}
*/
const firehosePolicy: Policy = new Policy(this, "FirehosePolicy", {
policyName: manifest.firehosePolicy,
roles: [firehoseRole],
statements: [
// policy to access S3 bucket
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
"s3:AbortMultipartUpload",
"s3:GetBucketLocation",
"s3:GetObject",
"s3:ListBucket",
"s3:ListBucketMultipartUploads",
"s3:PutObject",
],
resources: [
`arn:${this.partn}:s3:::${firehoseBucket.bucketName}`,
`arn:${this.partn}:s3:::${firehoseBucket.bucketName}/*`,
],
}),
// policy for kms key
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["kms:GenerateDataKey", "kms:Decrypt"],
resources: [
`arn:${this.partn}:kms:${this.region}:${this.account}:key/*`,
],
conditions: {
["StringEquals"]: {
"kms:ViaService": `s3.${this.region}.amazonaws.com`,
},
["StringLike"]: {
"kms:EncryptionContext:aws:s3:arn": [
`arn:${this.partn}:s3:::${firehoseBucket.bucketName}/*`,
],
},
},
}),
// policy for es vpc
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
"ec2:DescribeVpcs",
"ec2:DescribeVpcAttribute",
"ec2:DescribeSubnets",
"ec2:DescribeSecurityGroups",
"ec2:DescribeNetworkInterfaces",
"ec2:CreateNetworkInterface",
"ec2:CreateNetworkInterfacePermission",
"ec2:DeleteNetworkInterface",
],
resources: ["*"],
}),
// policy for es
new PolicyStatement({
effect: Effect.ALLOW,
actions: [
"es:DescribeElasticsearchDomain",
"es:DescribeElasticsearchDomains",
"es:DescribeElasticsearchDomainConfig",
"es:ESHttpPost",
"es:ESHttpPut",
],
resources: [
`arn:${this.partn}:es:${this.region}:${this.account}:domain/${domain.domainName}`,
`arn:${this.partn}:es:${this.region}:${this.account}:domain/${domain.domainName}/*`,
],
}),
// policy for HTTP Get
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["es:ESHttpGet"],
resources: [
`arn:${this.partn}:es:${this.region}:${this.account}:domain/${domain.domainName}/_all/_settings`,
`arn:${this.partn}:es:${this.region}:${this.account}:domain/${domain.domainName}/_cluster/stats`,
`arn:${this.partn}:es:${this.region}:${this.account}:domain/${domain.domainName}/cwl-kinesis/_mapping/kinesis`,
`arn:${this.partn}:es:${this.region}:${this.account}:domain/${domain.domainName}/_nodes`,
`arn:${this.partn}:es:${this.region}:${this.account}:domain/${domain.domainName}/_nodes/*/stats`,
`arn:${this.partn}:es:${this.region}:${this.account}:domain/${domain.domainName}/_stats`,
`arn:${this.partn}:es:${this.region}:${this.account}:domain/${domain.domainName}/cwl-kinesis/_stats`,
],
}),
// policy for CW logs
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["logs:PutLogEvents", "logs:CreateLogStream"],
resources: [`${firehoseLG.logGroupArn}`],
}),
// policy for kms decryption
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["kms:Decrypt"],
resources: [
`arn:${this.partn}:kms:${this.region}:${this.account}:key/*`,
],
conditions: {
["StringEquals"]: {
"kms:ViaService": `kinesis.${this.region}.amazonaws.com`,
},
["StringLike"]: {
"kms:EncryptionContext:aws:kinesis:arn": `${clDataStream.streamArn}`,
},
},
}),
],
});
/**
* @description CL Firehose
* @type {CfnDeliveryStream}
*/
const clFirehose: CfnDeliveryStream = new CfnDeliveryStream(
this,
"CLFirehose",
{
elasticsearchDestinationConfiguration: {
indexName: "cwl",
domainArn: domain.domainArn,
roleArn: firehoseRole.roleArn,
indexRotationPeriod: "OneDay",
s3Configuration: {
bucketArn: firehoseBucket.bucketArn,
roleArn: firehoseRole.roleArn,
cloudWatchLoggingOptions: {
enabled: true,
logGroupName: `/aws/kinesisfirehose/${manifest.firehoseName}`,
logStreamName: firehoseLSS3.logStreamName,
},
},
s3BackupMode: "AllDocuments",
vpcConfiguration: {
roleArn: firehoseRole.roleArn,
subnetIds: VPC.isolatedSubnets.map((subnet) => subnet.subnetId),
securityGroupIds: [esSg.securityGroupId],
},
cloudWatchLoggingOptions: {
enabled: true,
logGroupName: `/aws/kinesisfirehose/${manifest.firehoseName}`,
logStreamName: firehoseLS.logStreamName,
},
},
deliveryStreamType: "DirectPut",
deliveryStreamName: manifest.firehoseName,
deliveryStreamEncryptionConfigurationInput: {
keyType: "AWS_OWNED_CMK",
},
}
);
applyDependsOn(clFirehose, firehosePolicy);
// allow lambda to put records on firehose
logTransformer.addToRolePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["firehose:PutRecordBatch"],
resources: [clFirehose.attrArn],
})
);
/**
* @description IAM role for cw logs destination
* @type {Role}
*/
const cwDestinationRole: Role = new Role(this, "CWDestinationRole", {
assumedBy: new ServicePrincipal("logs.amazonaws.com"),
});
const assumeBy = {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Principal: {
Service: "logs.amazonaws.com",
},
Action: "sts:AssumeRole",
},
],
};
(cwDestinationRole.node.defaultChild as CfnRole).addOverride(
"Properties.AssumeRolePolicyDocument",
assumeBy
);
/**
* @description iam permissions for putting record on kinesis data stream
* @type {Policy}
*/
const cwDestPolicy: Policy = new Policy(this, "CWDestPolicy", {
roles: [cwDestinationRole],
statements: [
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["kinesis:PutRecord"],
resources: [`${clDataStream.streamArn}`],
}),
],
});
/**
* @description iam permission to pass role for creating cw destinations
* @type {Policy}
*/
const helperPolicy2: Policy = new Policy(this, "HelperRolePolicy2", {
roles: [helperRole],
statements: [
new PolicyStatement({
effect: Effect.ALLOW,
actions: ["iam:PassRole"],
resources: [cwDestinationRole.roleArn],
}),
],
});
applyDependsOn(helperPolicy2, cwDestPolicy);
/**
* @description create CW Logs Destination
* @type {CustomResource}
*/
const cwDestination: CustomResource = new CustomResource(
this,
"CWDestination",
{
resourceType: "Custom::CWDestination",
serviceToken: helperProvider.serviceToken,
properties: {
Regions: spokeRegions.valueAsList,
DestinationName: `${
manifest.cwDestinationName
}-${createUniqueId.getAttString("UUID")}`, // adding uuid for unique deployments
Role: cwDestinationRole.roleArn,
DataStream: clDataStream.streamArn,
SpokeAccounts: spokeAccts.valueAsList,
},
}
);
applyDependsOn(cwDestination, helperPolicy2);
/**
* @description Jumpbox resources
* @type {Construct}
*/
new Jumpbox(this, "CL-Jumpbox", {
vpc: VPC,
subnets: VPC.publicSubnets,
keyname: jumpboxKey.valueAsString,
deploy: jumpboxDeploymentCheck,
});
/**
* @description Demo stack
* @type {NestedStack}
*/
const demo: NestedStack = new CLDemo(this, "CL-DemoStack", {
parameters: {
["CWDestinationParm"]: `arn:${this.partn}:logs:${this.region}:${
this.account
}:destination:${
manifest.cwDestinationName
}-${createUniqueId.getAttString("UUID")}`,
},
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
demo.nestedStackResource!.cfnOptions.condition = demoDeploymentCheck;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
applyDependsOn(demo.nestedStackResource as CfnResource, domain);
//=============================================================================================
// cfn_nag suppress rules
//=============================================================================================
applyCfnNagSuppressRules(helperPolicy1.node.defaultChild as CfnResource, [
cfn_suppress_rules.W12,
]);
applyCfnNagSuppressRules(helperFunc.node.defaultChild as CfnResource, [
cfn_suppress_rules.W58,
cfn_suppress_rules.W89,
cfn_suppress_rules.W92,
]);
applyCfnNagSuppressRules(
helperProvider.node.children[0].node.findChild("Resource") as CfnResource,
[cfn_suppress_rules.W58, cfn_suppress_rules.W89, cfn_suppress_rules.W92]
);
applyCfnNagSuppressRules(esCognitoRole.node.defaultChild as CfnResource, [
cfn_suppress_rules.W11,
]);
VPC.publicSubnets.forEach((subnet) => {
applyCfnNagSuppressRules(subnet.node.defaultChild as CfnResource, [
cfn_suppress_rules.W33,
]);
});
applyCfnNagSuppressRules(domain.node.defaultChild as CfnResource, [
cfn_suppress_rules.W28,
]);
applyCfnNagSuppressRules(logTransformer.node.defaultChild as CfnResource, [
cfn_suppress_rules.W58,
cfn_suppress_rules.W89,
cfn_suppress_rules.W92,
]);
applyCfnNagSuppressRules(
accessLogsBucket.node.defaultChild as CfnResource,
[cfn_suppress_rules.W35, cfn_suppress_rules.W51]
);
applyCfnNagSuppressRules(
firehoseLG.node.findChild("Resource") as CfnResource,
[cfn_suppress_rules.W84]
);
applyCfnNagSuppressRules(firehosePolicy.node.defaultChild as CfnResource, [
cfn_suppress_rules.W12,
cfn_suppress_rules.W76,
]);
applyCfnNagSuppressRules(flowLg.node.findChild("Resource") as CfnResource, [
cfn_suppress_rules.W84,
]);
//=============================================================================================
// Output
//=============================================================================================
new CfnOutput(this, "Destination Subscription Command", {
description: "Command to run in spoke accounts/regions",
value: `aws logs put-subscription-filter \
--destination-arn arn:${this.partn}:logs:<region>:${
this.account
}:destination:${manifest.cwDestinationName}-${createUniqueId.getAttString(
"UUID"
)} \
--log-group-name <MyLogGroup> \
--filter-name <MyFilterName> \
--filter-pattern <MyFilterPattern> \
--profile <MyAWSProfile> `,
});
new CfnOutput(this, "Unique ID", {
description: "UUID for Centralized Logging Stack",
value: createUniqueId.getAttString("UUID"),
});
new CfnOutput(this, "Admin Email", {
description: "Admin Email address",
value: adminEmail.valueAsString,
});
new CfnOutput(this, "Domain Name", {
description: "ES Domain Name",
value: esDomain.valueAsString,
});
new CfnOutput(this, "Kibana URL", {
description: "Kibana URL",
value: `https://${domain.domainEndpoint}/_plugin/kibana/`,
});
new CfnOutput(this, "Cluster Size", {
description: "ES Cluster Size",
value: clusterSize.valueAsString,
});
new CfnOutput(this, "Demo Deployment", {
description: "Demo data deployed?",
value: demoTemplate.valueAsString,
});
}