cdk/lib/plant_reference-stack.ts (284 lines of code) (raw):

import { Stack, StackProps, RemovalPolicy, Duration } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { Dashboard as CloudWatchDashboard, PeriodOverride, GraphWidget, Alarm, ComparisonOperator, AlarmWidget } from 'aws-cdk-lib/aws-cloudwatch'; import { SnsAction } from 'aws-cdk-lib/aws-cloudwatch-actions'; import { Role, ServicePrincipal, PolicyStatement, Effect, CfnRole, Policy } from 'aws-cdk-lib/aws-iam' import { LogGroup, MetricFilter, FilterPattern } from "aws-cdk-lib/aws-logs"; import { CfnTopicRule } from "aws-cdk-lib/aws-iot"; import { Topic } from "aws-cdk-lib/aws-sns"; import { EmailSubscription } from "aws-cdk-lib/aws-sns-subscriptions"; import { ConfigParams } from '../config-params'; import { CfnWorkspace } from 'aws-cdk-lib/aws-grafana'; import { Runtime, Code, LayerVersion } from 'aws-cdk-lib/aws-lambda'; import { TriggerFunction } from 'aws-cdk-lib/triggers'; import { EventbridgeToLambda } from '@aws-solutions-constructs/aws-eventbridge-lambda'; import { Schedule } from 'aws-cdk-lib/aws-events'; export class PlantReferenceStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); const IoTLogGroup = new LogGroup(this, ConfigParams.logGroupName, { logGroupName: ConfigParams.logGroupName, removalPolicy: RemovalPolicy.DESTROY }) const loggingRole = new Role( this, 'Logging', { roleName: 'RoleForIoTCoreLogging', assumedBy: new ServicePrincipal('iot.amazonaws.com') }); // policy to allow logging in CloudWatch loggingRole.addToPolicy(new PolicyStatement( { effect: Effect.ALLOW, actions: [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "logs:PutMetricFilter", "logs:PutRetentionPolicy", "logs:GetLoggingOptions", "logs:SetLoggingOptions", "logs:SetV2LoggingOptions", "logs:GetV2LoggingOptions", "logs:SetV2LoggingLevels", "logs:ListV2LoggingLevels", "logs:DeleteV2LoggingLevels" ], resources: [IoTLogGroup.logGroupArn] })); loggingRole.node.addDependency(IoTLogGroup); loggingRole.applyRemovalPolicy(RemovalPolicy.DESTROY); // IAM role to enable Lambda functions, Grafana and allow CloudWatch datasource to Grafana const grafanaRole = new CfnRole(this, `${ConfigParams.appName}--Grafana-Role`, { assumeRolePolicyDocument: { Version: "2012-10-17", Statement: [ { Action: "sts:AssumeRole", Effect: "Allow", Sid: "", Principal: { Service: ["grafana.amazonaws.com", "lambda.amazonaws.com"] } } ] }, policies: [{ policyName: "GrafanaRole", policyDocument: { Version: "2012-10-17", Statement: [{ Sid: "AllowReadingMetricsFromCloudWatch", Effect: "Allow", Action: [ "cloudwatch:DescribeAlarmsForMetric", "cloudwatch:DescribeAlarmHistory", "cloudwatch:DescribeAlarms", "cloudwatch:ListMetrics", "cloudwatch:GetMetricStatistics", "cloudwatch:GetMetricData" ], Resource: "*" }, { Sid: "AllowReadingLogsFromCloudWatch", Effect: "Allow", Action: [ "logs:DescribeLogGroups", "logs:GetLogGroupFields", "logs:StartQuery", "logs:StopQuery", "logs:GetQueryResults", "logs:GetLogEvents" ], Resource: "*" }, { Sid: "AllowReadingTagsInstancesRegionsFromEC2", Effect: "Allow", Action: [ "ec2:DescribeTags", "ec2:DescribeInstances", "ec2:DescribeRegions" ], Resource: "*" }, { Sid: "AllowReadingResourcesForTags", Effect: "Allow", Action: "tag:GetResources", Resource: "*" }, { Sid: "AllowLoggingForLambdaFunctions", Effect: "Allow", Action: [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ], Resource: "*" }, { Sid: "AWSGrafanaOrganizationAdmin", Effect: "Allow", Action: ["iam:ListRoles"], Resource: "*" }, { Sid: "GrafanaIAMGetRolePermission", Effect: "Allow", Action: "iam:GetRole", Resource: "arn:aws:iam::*:role/*" }, { Sid: "AWSGrafanaPermissions", Effect: "Allow", Action: [ "grafana:CreateWorkspace", "grafana:CreateWorkspaceApiKey", "grafana:DeleteWorkspace", "grafana:DeleteWorkspaceApiKey", "grafana:UpdateWorkspace" ], Resource: "*" }, { Sid: "GrafanaIAMPassRolePermission", Effect: "Allow", Action: "iam:PassRole", Resource: "arn:aws:iam::*:role/*", Condition: { StringLike: { "iam:PassedToService": "grafana.amazonaws.com" } } }] } }] }); grafanaRole.applyRemovalPolicy(RemovalPolicy.DESTROY); // filter 'PlantData' MQTT messages to Cloudwatch logs const topic_rule = new CfnTopicRule(this, "iot_to_cloudwatch_topic_rule", { topicRulePayload: { actions: [{ cloudwatchLogs: { logGroupName: IoTLogGroup.logGroupName, roleArn: loggingRole.roleArn, batchMode: false, } }], sql: "SELECT * FROM 'PlantData'" } }); topic_rule.node.addDependency(loggingRole); topic_rule.node.addDependency(IoTLogGroup); topic_rule.applyRemovalPolicy(RemovalPolicy.DESTROY); const dashboard = new CloudWatchDashboard(this, `${ConfigParams.appName}--Dashboard`, { dashboardName: `${ConfigParams.appName}--Dashboard`, periodOverride: PeriodOverride.AUTO, widgets: [[]], }); const snsTopic = new Topic(this, `${ConfigParams.appName}--SnsTopic`, { displayName: `${ConfigParams.appName}--SnsTopic`, fifo: false }); snsTopic.addSubscription(new EmailSubscription(ConfigParams.email)); for (var sensor of ConfigParams.sensors) { const metricFilter = new MetricFilter(this, `${ConfigParams.appName}--${sensor}MetricFilter`, { logGroup: IoTLogGroup, metricNamespace: `${ConfigParams.appName}--Metrics`, metricName: `${sensor}Metric`, filterPattern: FilterPattern.exists(`$.${sensor}`), metricValue: `$.${sensor}`, }); const metric = metricFilter.metric({ statistic: "minimum", period: Duration.minutes(5) }); const metricWidget = new GraphWidget({ height: ConfigParams.dashboard.widgetHeight, width: ConfigParams.dashboard.widgetWidth, }); metricWidget.addLeftMetric(metric); dashboard.addWidgets(metricWidget); if (sensor == "waterLevel") { const waterLevelAlarm = new Alarm(this, `${ConfigParams.appName}--WaterLevelAlarm`, { metric, comparisonOperator: ComparisonOperator.LESS_THAN_THRESHOLD, threshold: ConfigParams.waterAlarm.threshold, datapointsToAlarm: ConfigParams.waterAlarm.datapointsToAlarm, evaluationPeriods: ConfigParams.waterAlarm.evaluationPeriods }); waterLevelAlarm.addAlarmAction(new SnsAction(snsTopic)); const alarmWidget = new AlarmWidget({ height: ConfigParams.dashboard.widgetHeight, width: ConfigParams.dashboard.widgetWidth, alarm: waterLevelAlarm, title: "waterLevelAlarm" }); dashboard.addWidgets(alarmWidget); } } const grafanaWorkspace = new CfnWorkspace(this, `${ConfigParams.appName}--GrafanaWorkspace`, { accountAccessType: 'CURRENT_ACCOUNT', authenticationProviders: ['AWS_SSO'], dataSources: ['CLOUDWATCH'], description: 'Amazon Grafana Workspace for Plant Environment', name: `${ConfigParams.appName}--GrafanaWorkspace`, permissionType: 'SERVICE_MANAGED', roleArn: grafanaRole.attrArn, }); grafanaWorkspace.applyRemovalPolicy(RemovalPolicy.DESTROY); // lambda function triggered on deployment for grafana API calls const grafanaLambda = new TriggerFunction(this, `${ConfigParams.appName}--GrafanaLambda`, { runtime: Runtime.NODEJS_18_X, code: Code.fromAsset('lib'), handler: 'grafana.handler', environment: { 'workspaceId': grafanaWorkspace.attrId, 'url': `https://${grafanaWorkspace.attrId}.grafana-workspace.${ConfigParams.env.region}.amazonaws.com`, 'region': `${ConfigParams.env.region}` }, role: Role.fromRoleArn(this, "GrafanaLambdaRole", grafanaRole.attrArn), timeout: Duration.seconds(30) }); grafanaLambda.applyRemovalPolicy(RemovalPolicy.DESTROY); // add node-fetch as lambda layer grafanaLambda.addLayers(new LayerVersion(this, `${ConfigParams.appName}--GrafanaLambdaLayer`, { code: Code.fromAsset("lib/node-fetch.zip"), compatibleRuntimes: [Runtime.NODEJS_18_X] })); const wateringScheduler = new EventbridgeToLambda(this, `${ConfigParams.appName}--ShadowLambda`, { lambdaFunctionProps: { runtime: Runtime.NODEJS_18_X, code: Code.fromAsset('lib'), handler: 'watering.handler', timeout: Duration.seconds(30), environment: { 'thingName': `${ConfigParams.env.thingName}`, 'endpoint': `${ConfigParams.env.endpoint}` }, }, eventRuleProps: { schedule: Schedule.rate(Duration.days(ConfigParams.wateringFrequency)) } } ); wateringScheduler.lambdaFunction.addToRolePolicy( new PolicyStatement({ effect: Effect.ALLOW, actions: [ "iot:Connect", "iot:Publish", "iot:Subscribe", "iot:Receive", "iot:DeleteThingShadow", "iot:GetThingShadow", "iot:UpdateThingShadow" ], resources: ["*"] }) ) wateringScheduler.lambdaFunction.applyRemovalPolicy(RemovalPolicy.DESTROY); wateringScheduler.eventsRule.applyRemovalPolicy(RemovalPolicy.DESTROY); } }