Skip to content

Commit

Permalink
Added the AWS CDK code for the patterns - first version
Browse files Browse the repository at this point in the history
  • Loading branch information
Mohanna Shahrad committed Aug 16, 2022
1 parent 1e35703 commit 59e1e20
Show file tree
Hide file tree
Showing 48 changed files with 3,847 additions and 0 deletions.
9 changes: 9 additions & 0 deletions Cloud_Templates/AWS_CDK/IoTAnalyticsPattern/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
*.swp
__pycache__
.pytest_cache
.venv
*.egg-info

# CDK asset staging directory
.cdk.staging
cdk.out
105 changes: 105 additions & 0 deletions Cloud_Templates/AWS_CDK/IoTAnalyticsPattern/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@

# Welcome to your CDK project!
# IoT Data visulaization with AWS IoT Analytics

The `cdk.json` file tells the CDK Toolkit how to execute your app.

This project is set up like a standard Python project. The initialization
process also creates a virtualenv within this project, stored under the `.venv`
directory. To create the virtualenv it assumes that there is a `python3`
(or `python` for Windows) executable in your path with access to the `venv`
package. If for any reason the automatic creation of the virtualenv fails,
you can create the virtualenv manually.

To manually create a virtualenv on MacOS and Linux:

```
$ python3 -m venv .venv
```

After the init process completes and the virtualenv is created, you can use the following
step to activate your virtualenv.

```
$ source .venv/bin/activate
```

If you are a Windows platform, you would activate the virtualenv like this:

```
% .venv\Scripts\activate.bat
```

Once the virtualenv is activated, you can install the required dependencies.

```
$ pip install -r requirements.txt
```

At this point you can now synthesize the CloudFormation template for this code.

```
$ cdk synth
```

To add additional dependencies, for example other CDK libraries, just add
them to your `setup.py` file and rerun the `pip install -r requirements.txt`
command.

## Useful commands

* `cdk ls` list all stacks in the app
* `cdk synth` emits the synthesized CloudFormation template
* `cdk deploy` deploy this stack to your default AWS account/region
* `cdk diff` compare deployed stack with current state
* `cdk docs` open CDK documentation

## Context parameters
There are multiple context parameters that you need to set before synthesizing or delpoying this CDK stack. You can specify a context variable either as part of an AWS CDK CLI command, or in `cdk.json`.
To create a command line context variable, use the __--context (-c) option__, as shown in the following example.

```
$ cdk cdk synth -c bucket_name=mybucket
```

To specify the same context variable and value in the cdk.json file, use the following code.

```
{
"context": {
"bucket_name": "mybucket"
}
}
```

In this project, these are the following parameters to be set:

* `topic_sql`
<br>It is required for IoT Core rule creation to add a simplified SQL syntax to filter messages received on an MQTT topic and push the data elsewhere.
<br> __Format__: Enter an SQL statement using the following: ```SELECT <Attribute> FROM <Topic Filter> WHERE <Condition>```. For example: ```SELECT temperature FROM 'iot/topic' WHERE temperature > 50```. To learn more, see AWS IoT SQL Reference.

* `analytics_channel_name`&nbsp;&nbsp;&nbsp;&nbsp;`<Optional>`
<br> The name of the IoT Analytics channel that will get connected to the IoT Core to get your data.
<br> __Format__: Choose a unique name that you can easily identify. The channel name must contain 1-128 characters. Valid characters are a-z, A-Z, 0-9, and _ (underscore).

* `analytics_datastore_name`&nbsp;&nbsp;&nbsp;&nbsp;`<Optional>`
<br> The name of the IoT Analytics datastore that will get connected to the IoT Core to store your data.
<br> __Format__: A unique ID identifies your data store. You can't change this ID after you create it. Valid characters: a-z, A-Z, 0-9, and _ (underscore).

* `analytics_dataset_name`&nbsp;&nbsp;&nbsp;&nbsp;`<Optional>`
<br> The name of the IoT Analytics SQL dataset that will get connected to the IoT Core. A SQL dataset is a materialized view from a data store.
<br> __Format__: Choose a unique name that you can easily identify. The dataset name must contain 1-128 characters. Valid characters are a-z, A-Z, 0-9, and _ (underscore).

* `analytics_pipeline_name`&nbsp;&nbsp;&nbsp;&nbsp;`<Optional>`
<br> The name of the IoT Analytics pipeline that will read messages from the channel and write processed data to the datastore.
<br> __Format__: Valid characters: a-z, A-Z, 0-9, and _ (underscore).

* `analytics_iot_rule_name`&nbsp;&nbsp;&nbsp;&nbsp;`<Optional>`
<br> The name of the IoT Core rule that is going to be created.
<br> __Format__: Should be an alphanumeric string that can also contain underscore (_) characters, but no spaces.

* `analytics_iot_role_name`&nbsp;&nbsp;&nbsp;&nbsp;`<Optional>`
<br> An IAM role should be created to grant AWS IoT access to your endpoint. This parameter is for setting the name of this role.
<br> __Format__: Enter a unique role name that contains alphanumeric characters, hyphens, and underscores. A role name can't contain any spaces.

Enjoy!
27 changes: 27 additions & 0 deletions Cloud_Templates/AWS_CDK/IoTAnalyticsPattern/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import os

import aws_cdk as cdk

from io_t_analytics_pattern.io_t_analytics_pattern_stack import IoTAnalyticsPatternStack


app = cdk.App()
IoTAnalyticsPatternStack(app, "IoTAnalyticsPatternStack",
# If you don't specify 'env', this stack will be environment-agnostic.
# Account/Region-dependent features and context lookups will not work,
# but a single synthesized template can be deployed anywhere.

# Uncomment the next line to specialize this stack for the AWS Account
# and Region that are implied by the current CLI configuration.

#env=cdk.Environment(account=os.getenv('CDK_DEFAULT_ACCOUNT'), region=os.getenv('CDK_DEFAULT_REGION')),

# Uncomment the next line if you know exactly what Account and Region you
# want to deploy the stack to. */

#env=cdk.Environment(account='123456789012', region='us-east-1'),

# For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html
)

app.synth()
45 changes: 45 additions & 0 deletions Cloud_Templates/AWS_CDK/IoTAnalyticsPattern/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"app": "python3 app.py",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"requirements*.txt",
"source.bat",
"**/__init__.py",
"python/__pycache__",
"tests"
]
},
"context": {
"@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
"@aws-cdk/core:stackRelativeExports": true,
"@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
"@aws-cdk/aws-lambda:recognizeVersionProps": true,
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true,
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/core:target-partitions": [
"aws",
"aws-cn"
],
"topic_sql": "SELECT * FROM 'IoT_Analytics_demo'",
"analytics_channel_name": "demo_iot_channel",
"analytics_datastore_name": "demo_iot_datastore",
"analytics_dataset_name": "demo_iot_dataset",
"analytics_pipeline_name": "demo_iot_pipeline",
"analytics_iot_role_name": "demo_iot_iotanalytics_role",
"analytics_iot_rule_name": "demo_to_iotanalytics_rule"
}
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
from aws_cdk import (
Stack,
aws_iam as iam,
aws_iot as iot,
aws_iotanalytics as iotanalytics,
aws_s3 as s3,
aws_logs as logs
)
from constructs import Construct
import aws_cdk as cdk
import re
import sys

sys.path.append('../')

from customExceptions import *

class IoTAnalyticsPatternStack(Stack):

# Defining the class variables
topic_sql = ""
analytics_channel_name = ""
analytics_datastore_name = ""
analytics_dataset_name = ""
analytics_pipeline_name = ""
analytics_iot_role_name = ""
analytics_iot_rule_name = ""

def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)

# Getting the context parameters

# Required parameters for users to set in the CLI command or cdk.json
self.topic_sql = self.node.try_get_context("topic_sql")

# Optional parameters for users to set in the CLI command or cdk.json
self.analytics_channel_name = self.node.try_get_context("analytics_channel_name")
self.analytics_datastore_name = self.node.try_get_context("analytics_datastore_name")
self.analytics_dataset_name = self.node.try_get_context("analytics_dataset_name")
self.analytics_pipeline_name = self.node.try_get_context("analytics_pipeline_name")
self.analytics_iot_role_name = self.node.try_get_context("analytics_iot_role_name")
self.analytics_iot_rule_name = self.node.try_get_context("analytics_iot_rule_name")

# Perform input validation
self.performInputValidation()

# Creating an IoT Analytics Channel
analytics_channel = iotanalytics.CfnChannel(self, self.analytics_channel_name, channel_name=self.analytics_channel_name)
analytics_channel.apply_removal_policy(policy=cdk.RemovalPolicy.DESTROY)

# Creating an IoT Analytics Datastore
analytics_datastore = iotanalytics.CfnDatastore(self, self.analytics_datastore_name, datastore_name=self.analytics_datastore_name,
datastore_storage=iotanalytics.CfnDatastore.DatastoreStorageProperty(
service_managed_s3={}
),
retention_period=iotanalytics.CfnDatastore.RetentionPeriodProperty(
number_of_days=30,
unlimited=False
))
analytics_datastore.apply_removal_policy(policy=cdk.RemovalPolicy.DESTROY)

# Creating an IoT Analytics Dataset
analytics_dataset = iotanalytics.CfnDataset(self, self.analytics_dataset_name, actions=[iotanalytics.CfnDataset.ActionProperty(
action_name="QueryDatastoreCDK",
query_action=iotanalytics.CfnDataset.QueryActionProperty(
sql_query= f'''SELECT * FROM {analytics_datastore.datastore_name}'''
)
)])
analytics_dataset.node.add_dependency(analytics_datastore)
analytics_dataset.apply_removal_policy(policy=cdk.RemovalPolicy.DESTROY)

# Creating an Iot Analytics Pipeline
analytics_pipeline = iotanalytics.CfnPipeline(self, self.analytics_pipeline_name, pipeline_name=self.analytics_pipeline_name, pipeline_activities=[
iotanalytics.CfnPipeline.ActivityProperty(
channel=iotanalytics.CfnPipeline.ChannelProperty(
channel_name=analytics_channel.channel_name,
name=analytics_channel.channel_name,
next=analytics_datastore.datastore_name
),
datastore=iotanalytics.CfnPipeline.DatastoreProperty(
datastore_name=analytics_datastore.datastore_name,
name=analytics_datastore.datastore_name
)
)])
analytics_pipeline.node.add_dependency(analytics_datastore)
analytics_pipeline.node.add_dependency(analytics_channel)
analytics_pipeline.apply_removal_policy(policy=cdk.RemovalPolicy.DESTROY)


# Creating the role for the IoT-Analytics rule
channel_arn = f"arn:aws:iotanalytics:{self.region}:{self.account}:channel/{analytics_channel.channel_name}"
iot_analytics_role = iam.Role(self, self.analytics_iot_role_name, assumed_by=iam.ServicePrincipal("iot.amazonaws.com"))
iot_analytics_role.add_to_policy(iam.PolicyStatement(effect=iam.Effect.ALLOW, resources=[channel_arn], actions=["iotanalytics:BatchPutMessage"]))
iot_analytics_role.node.add_dependency(analytics_channel)
iot_analytics_role.apply_removal_policy(policy=cdk.RemovalPolicy.DESTROY)

# Creating a cloudwatch log group for topic rule's error action
log_group = logs.LogGroup(self, "iot_to_analytics_log_group" , log_group_name="iot_to_analytics_log_group", removal_policy=cdk.RemovalPolicy.DESTROY)

iot_to_cloudwatch_logs_role = iam.Role(self, "iot_to_analytics_log_group_role", assumed_by=iam.ServicePrincipal("iot.amazonaws.com"))
iot_to_cloudwatch_logs_role.add_to_policy(iam.PolicyStatement(
effect=iam.Effect.ALLOW, resources=[log_group.log_group_arn],
actions=["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "logs:PutMetricFilter", "logs:PutRetentionPolicy"]))
iot_to_cloudwatch_logs_role.node.add_dependency(log_group)
iot_to_cloudwatch_logs_role.apply_removal_policy(policy=cdk.RemovalPolicy.DESTROY)


# Creating the IoT Core Rule
topic_rule = iot.CfnTopicRule(self, self.analytics_iot_rule_name, topic_rule_payload=iot.CfnTopicRule.TopicRulePayloadProperty(
actions=[iot.CfnTopicRule.ActionProperty( iot_analytics=iot.CfnTopicRule.IotAnalyticsActionProperty(
channel_name=analytics_channel.channel_name,
role_arn=iot_analytics_role.role_arn,
)
)],
sql=self.topic_sql,
error_action= iot.CfnTopicRule.ActionProperty(
cloudwatch_logs=iot.CfnTopicRule.CloudwatchLogsActionProperty(
log_group_name=log_group.log_group_name,
role_arn=iot_to_cloudwatch_logs_role.role_arn
)
)))

topic_rule.node.add_dependency(analytics_channel)
topic_rule.node.add_dependency(iot_analytics_role)
topic_rule.apply_removal_policy(policy=cdk.RemovalPolicy.DESTROY)


def performInputValidation(self):
self.validateTopicSQL(self.topic_sql)
self.validateAnalyticsChannelName(self.analytics_channel_name)
self.validateAnalyticsDatasetName(self.analytics_dataset_name)
self.validateAnalyticsDatastoreName(self.analytics_datastore_name)
self.validateAnalyticsPipelineName(self.analytics_pipeline_name)
self.validateRoleName(self.analytics_iot_role_name)
self.validateIoTRuleName(self.analytics_iot_rule_name)

def validateTopicSQL(self, sqlStatement):
if not sqlStatement:
raise NoSQL
elif type(sqlStatement) != str:
raise WrongFormattedInput("The input sql statement does not have a right format. Please refer to README.md for more information.")
return

def validateAnalyticsChannelName(self, channelName):
if not channelName:
self.analytics_channel_name = "demo_iot_channel"
else:
if len(channelName) < 1 or len(channelName) > 128:
raise WrongLengthForInput("Not a valid input for channel name: The channel name must contain 1-128 characters.")
elif not re.match(r'^[a-zA-Z0-9_]+$', channelName):
raise WrongFormattedInput("String format error for channel name: Valid characters are a-z, A-Z, 0-9, and _ (underscore)")
return

def validateAnalyticsDatasetName(self, datasetName):
if not datasetName:
self.analytics_dataset_name = "demo_iot_dataset"
else:
if len(datasetName) < 1 or len(datasetName) > 128:
raise WrongLengthForInput("Not a valid input for dataset name: The dataset name must contain 1-128 characters.")
elif not re.match(r'^[a-zA-Z0-9_]+$', datasetName):
raise WrongFormattedInput("String format error for dataset name: Valid characters are a-z, A-Z, 0-9, and _ (underscore)")
return

def validateAnalyticsDatastoreName(self, datastoreName):
if not datastoreName:
self.analytics_datastore_name = "demo_iot_datastore"
else:
if not re.match(r'^[a-zA-Z0-9_]+$', datastoreName):
raise WrongFormattedInput("String format error for datastore name: Valid characters are a-z, A-Z, 0-9, and _ (underscore)")
return

def validateAnalyticsPipelineName(self, pipelineName):
if not pipelineName:
self.analytics_pipeline_name = "demo_iot_pipeline"
else:
if not re.match(r'^[a-zA-Z0-9_]+$', pipelineName):
raise WrongFormattedInput("String format error for pipeline name: Valid characters are a-z, A-Z, 0-9, and _ (underscore)")
return

def validateRoleName(self, roleName):
if not roleName:
self.analytics_iot_role_name = "demo_iot_iotanalytics_role"
elif type(roleName) != str:
raise WrongFormattedInput("The provided input for the IAM role name is not of type string")
elif len(roleName) > 64:
raise WrongLengthForInput("The length of the IAM role name string should not exceed 64 characters.")
elif not re.match(r'^[a-zA-Z0-9+=,@-_\.]+$', roleName):
raise WrongFormattedInput("String format error: The IAM role name should be an alphanumeric string that can also contain '+=,.@-_' characters.")
else:
return

def validateIoTRuleName(self, ruleName):
if not ruleName:
self.analytics_iot_rule_name = "demo_to_iotanalytics_rule"
elif type(ruleName) != str:
raise WrongFormattedInput("The provided input for topic rule name is not of type string")
elif not re.match(r'^[a-zA-Z0-9_]+$', ruleName):
raise WrongFormattedInput("String format error: The topic rule name should be an alphanumeric string that can also contain underscore (_) characters, but no spaces.")
else:
return
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytest==6.2.5
2 changes: 2 additions & 0 deletions Cloud_Templates/AWS_CDK/IoTAnalyticsPattern/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
aws-cdk-lib==2.37.1
constructs>=10.0.0,<11.0.0
Loading

0 comments on commit 59e1e20

Please sign in to comment.