AWSTemplateFormatVersion: "2010-09-09" Description: Create all of the required resources to deploy the LEGO™ Robot Demo Parameters: CoreName: Description: Greengrass Core name to be created. A "Thing" with be created with _Core appended to the name Type: String Default: lego_robot InstanceType: Description: Select your Cloud9 Development Instance Type Type: String Default: m4.large AllowedValues: - m4.large - t2.large - m4.xlarge - t2.xlarge - c4.xlarge - c4.2xlarge - m4.2xlarge - t2.2xlarge - c4.4xlarge - m4.4xlarge - c4.8xlarge - m4.10xlarge - m4.16xlarge # SecurityAccessCIDR: # Description: CIDR block to limit inbound access for only SSH # Type: String # Default: '0.0.0.0/0' # myKeyPair: # Description: Amazon EC2 Key Pair for accessing Greengrass Core instance # Type: "AWS::EC2::KeyPair::KeyName" # Mappings: # # If Greengrass becomes available in other regions, add the x86 AMI Id for the # # image description: "Ubuntu Server 16.04 LTS (HVM), SSD Volume Type" # RegionMap: # us-east-1: # "HVM": "ami-0565af6e282977273" # us-west-2: # "HVM": "ami-08692d171e3cf02d6" # eu-west-1: # "HVM": "ami-08660f1c6fb6b01e7" # eu-central-1: # "HVM": "ami-05af84768964d3dc0" # ap-northeast-1: # "HVM": "ami-06c43a7df16e8213c" # ap-southeast-2: # "HVM": "ami-0789a5fb42dcccc10" Resources: ############################################################################# # GREENGRASS RESOURCES SECTION # This section contains all the Greengrass related resources ############################################################################# GreengrassGroup: Type: AWS::Greengrass::Group Properties: Name: !Ref CoreName RoleArn: !GetAtt GreengrassResourceRole.Arn InitialVersion: CoreDefinitionVersionArn: !Ref GreengrassCoreDefinitionVersion FunctionDefinitionVersionArn: !GetAtt FunctionDefinition.LatestVersionArn SubscriptionDefinitionVersionArn: !GetAtt SubscriptionDefinition.LatestVersionArn # Other Greengrass resources that can be included in a group # not used in this example # # DeviceDefinitionVersionArn: !Ref ExampleDeviceDefinitionVersion # LoggerDefinitionVersionArn: !Ref ExampleLoggerDefinitionVersion # ResourceDefinitionVersionArn: !Ref ExampleResourceDefinitionVersion # ConnectorDefinitionVersionArn: !Ref ExampleConnectorDefinitionVersion GreengrassCoreDefinition: Type: AWS::Greengrass::CoreDefinition Properties: # use CoreName + "_Core" as "thingName" Name: !Join ["_", [!Ref CoreName, "Core"] ] GreengrassCoreDefinitionVersion: # Example of using GreengrassCoreDefinition referring to this # "Version" resource Type: AWS::Greengrass::CoreDefinitionVersion Properties: CoreDefinitionId: !Ref GreengrassCoreDefinition Cores: - Id: !Join ["_", [!Ref CoreName, "Core"] ] ThingArn: !Join - ":" - - "arn:aws:iot" - !Ref AWS::Region - !Ref AWS::AccountId - !Join - "/" - - "thing" - !Join ["_", [!Ref CoreName, "Core"] ] CertificateArn: !Join - ":" - - "arn:aws:iot" - !Ref AWS::Region - !Ref AWS::AccountId - !Join - "/" - - "cert" - !GetAtt IoTThing.certificateId SyncShadow: "false" # FunctionDefinition: # # Example of using "InitialVersion" to not have to reference a separate # # "Version" resource # Type: 'AWS::Greengrass::FunctionDefinition' # Properties: # Name: FunctionDefinition # InitialVersion: # DefaultConfig: # Execution: # IsolationMode: GreengrassContainer # Functions: # - Id: !Join ["_", [!Ref CoreName, "sample"] ] # FunctionArn: !Ref GGSampleFunctionVersion # FunctionConfiguration: # Pinned: 'true' # Executable: index.py # MemorySize: '65536' # Timeout: '300' # EncodingType: binary # Environment: # Variables: # CORE_NAME: !Ref CoreName # AccessSysfs: 'false' # Execution: # IsolationMode: GreengrassContainer # RunAs: # Uid: '1' # Gid: '10' # SubscriptionDefinition: # Type: 'AWS::Greengrass::SubscriptionDefinition' # Properties: # Name: SubscriptionDefinition # InitialVersion: # # Example of one-to-many subscriptions in single definition version # Subscriptions: # - Id: Subscription1 # Source: 'cloud' # Subject: !Join # - "/" # - - !Ref CoreName # - "in" # Target: !Ref GGSampleFunctionVersion # - Id: Subscription2 # Source: !Ref GGSampleFunctionVersion # Subject: !Join # - "/" # - - !Ref CoreName # - "out" # Target: 'cloud' # - Id: Subscription3 # Source: !Ref GGSampleFunctionVersion # Subject: !Join # - "/" # - - !Ref CoreName # - "telem" # Target: 'cloud' # GGSampleFunction: # # Lambda function deployed by Greengrass # Type: AWS::Lambda::Function # Properties: # FunctionName: !Join ["_", [!Ref CoreName, "sample"] ] # Description: Long running lambda that provides telemetry and pub/sub echo # Handler: index.function_handler # Runtime: python2.7 # # Role and Timeout not used when deployed to Lambda, but required for creation # Role: !GetAtt LambdaExecutionRole.Arn # Timeout: 60 # Code: # ZipFile: | # import os # from threading import Timer # import greengrasssdk # # # counter = 0 # client = greengrasssdk.client('iot-data') # # # def telemetry(): # '''Publish incrementing value to telemetry topic every 2 seconds''' # global counter # counter += 1 # client.publish( # topic='{}/telem'.format(os.environ['CORE_NAME']), # payload='Example telemetry counter, value: {}'.format(counter) # ) # Timer(5, telemetry).start() # # Call telemetry() to start telemetry publish # telemetry() # # # def function_handler(event, context): # '''Echo message on /in topic to /out topic''' # client.publish( # topic='{}/out'.format(os.environ['CORE_NAME']), # payload=event # ) # # # Functions need to be versioned for use in a Group config # GGSampleFunctionVersion: # # Example of using FunctionVersion # Type: AWS::Lambda::Version # Properties: # FunctionName : !GetAtt GGSampleFunction.Arn # IoTThing Secret Store IoTThingCertificatePEMSecret: Type: AWS::SecretsManager::Secret Properties: Name: !Sub ${CoreName}_Core_ThingCertificatePEM SecretString: !GetAtt IoTThing.certificatePem IoTThingPrivateKeySecret: Type: AWS::SecretsManager::Secret Properties: Name: !Sub ${CoreName}_Core_ThingPrivateKey SecretString: !GetAtt IoTThing.privateKey # # Networking Resources VPC: Type: 'AWS::EC2::VPC' Properties: CidrBlock: 172.31.0.0/24 EnableDnsSupport: true EnableDnsHostnames: true InstanceTenancy: default Tags: - Key: Name Value: !Ref CoreName InternetGateway: Type: 'AWS::EC2::InternetGateway' VPCGatewayAttachment: Type: 'AWS::EC2::VPCGatewayAttachment' Properties: VpcId: !Ref VPC InternetGatewayId: !Ref InternetGateway SubnetAPublic: Type: 'AWS::EC2::Subnet' Properties: # Use returned AZ to ensure instance type available AvailabilityZone: !GetAtt InstanceAZ.AvailabilityZone CidrBlock: 172.31.0.0/24 MapPublicIpOnLaunch: true VpcId: !Ref VPC RouteTablePublic: Type: 'AWS::EC2::RouteTable' Properties: VpcId: !Ref VPC RouteTableAssociationAPublic: Type: 'AWS::EC2::SubnetRouteTableAssociation' Properties: SubnetId: !Ref SubnetAPublic RouteTableId: !Ref RouteTablePublic RouteTablePublicInternetRoute: Type: 'AWS::EC2::Route' DependsOn: VPCGatewayAttachment Properties: RouteTableId: !Ref RouteTablePublic DestinationCidrBlock: '0.0.0.0/0' GatewayId: !Ref InternetGateway IoTThing: # Resource creates thing, certificate key pair, and IoT policy Type: Custom::IoTThing Properties: ServiceToken: !GetAtt CreateThingFunction.Arn ThingName: !Join ["_", [!Ref CoreName, "Core"] ] CreateThingFunction: Type: AWS::Lambda::Function Properties: Description: Create thing, certificate, and policy, return cert and private key Handler: index.handler Runtime: python3.6 Role: !GetAtt LambdaExecutionRole.Arn Timeout: 60 Code: ZipFile: | import sys import cfnresponse import boto3 from botocore.exceptions import ClientError import json import logging logger = logging.getLogger() logger.setLevel(logging.INFO) policyDocument = { 'Version': '2012-10-17', 'Statement': [ { 'Effect': 'Allow', 'Action': 'iot:*', 'Resource': '*' }, { 'Effect': 'Allow', 'Action': 'greengrass:*', 'Resource': '*' } ] } def handler(event, context): responseData = {} try: logger.info('Received event: {}'.format(json.dumps(event))) result = cfnresponse.FAILED client = boto3.client('iot') thingName=event['ResourceProperties']['ThingName'] if event['RequestType'] == 'Create': thing = client.create_thing( thingName=thingName ) response = client.create_keys_and_certificate( setAsActive=True ) certId = response['certificateId'] certArn = response['certificateArn'] certPem = response['certificatePem'] privateKey = response['keyPair']['PrivateKey'] client.create_policy( policyName='{}-full-access'.format(thingName), policyDocument=json.dumps(policyDocument) ) response = client.attach_policy( policyName='{}-full-access'.format(thingName), target=certArn ) response = client.attach_thing_principal( thingName=thingName, principal=certArn, ) logger.info('Created thing: %s, cert: %s and policy: %s' % (thingName, certId, '{}-full-access'.format(thingName))) result = cfnresponse.SUCCESS responseData['certificateId'] = certId responseData['certificatePem'] = certPem responseData['privateKey'] = privateKey responseData['iotEndpoint'] = client.describe_endpoint(endpointType='iot:Data-ATS')['endpointAddress'] elif event['RequestType'] == 'Update': logger.info('Updating thing: %s' % thingName) result = cfnresponse.SUCCESS elif event['RequestType'] == 'Delete': logger.info('Deleting thing: %s and cert/policy' % thingName) response = client.list_thing_principals( thingName=thingName ) for i in response['principals']: response = client.detach_thing_principal( thingName=thingName, principal=i ) response = client.detach_policy( policyName='{}-full-access'.format(thingName), target=i ) response = client.update_certificate( certificateId=i.split('/')[-1], newStatus='INACTIVE' ) response = client.delete_certificate( certificateId=i.split('/')[-1], forceDelete=True ) response = client.delete_policy( policyName='{}-full-access'.format(thingName), ) response = client.delete_thing( thingName=thingName ) result = cfnresponse.SUCCESS except ClientError as e: logger.error('Error: {}'.format(e)) result = cfnresponse.FAILED logger.info('Returning response of: {}, with result of: {}'.format(result, responseData)) sys.stdout.flush() cfnresponse.send(event, context, result, responseData) GroupDeploymentReset: # Allows for deletion of Greengrass group if the deployment status is not # reset manually via the console or API Type: Custom::GroupDeploymentReset DependsOn: GreengrassGroup Properties: ServiceToken: !GetAtt GroupDeploymentResetFunction.Arn Region: !Ref "AWS::Region" ThingName: !Join ["_", [!Ref CoreName, "Core"] ] GroupDeploymentResetFunction: Type: AWS::Lambda::Function Properties: Description: Resets any deployments during stack delete and manages Greengrass service role needs Handler: index.handler Runtime: python3.6 Role: !GetAtt LambdaExecutionRole.Arn Timeout: 60 Environment: Variables: STACK_NAME: !Ref "AWS::StackName" Code: ZipFile: | import os import sys import json import logging import cfnresponse import boto3 from botocore.exceptions import ClientError logger = logging.getLogger() logger.setLevel(logging.INFO) c = boto3.client('greengrass') iam = boto3.client('iam') role_name = 'greengrass_cfn_{}_ServiceRole'.format(os.environ['STACK_NAME']) def find_group(thingName): response_auth = '' response = c.list_groups() for group in response['Groups']: thingfound = False group_version = c.get_group_version( GroupId=group['Id'], GroupVersionId=group['LatestVersion'] ) core_arn = group_version['Definition'].get('CoreDefinitionVersionArn', '') if core_arn: core_id = core_arn[core_arn.index('/cores/')+7:core_arn.index('/versions/')] core_version_id = core_arn[core_arn.index('/versions/')+10:len(core_arn)] thingfound = False response_core_version = c.get_core_definition_version( CoreDefinitionId=core_id, CoreDefinitionVersionId=core_version_id ) if 'Cores' in response_core_version['Definition']: for thing_arn in response_core_version['Definition']['Cores']: if thingName == thing_arn['ThingArn'].split('/')[1]: thingfound = True break if(thingfound): logger.info('found thing: %s, group id is: %s' % (thingName, group['Id'])) response_auth = group['Id'] return(response_auth) def manage_greengrass_role(cmd): if cmd == 'CREATE': r = iam.create_role( RoleName=role_name, AssumeRolePolicyDocument='{"Version": "2012-10-17","Statement": [{"Effect": "Allow","Principal": {"Service": "greengrass.amazonaws.com"},"Action": "sts:AssumeRole"}]}', Description='Role for CloudFormation blog post', ) role_arn = r['Role']['Arn'] iam.attach_role_policy( RoleName=role_name, PolicyArn='arn:aws:iam::aws:policy/service-role/AWSGreengrassResourceAccessRolePolicy' ) c.associate_service_role_to_account(RoleArn=role_arn) logger.info('Created and associated role {}'.format(role_name)) else: try: r = iam.get_role(RoleName=role_name) role_arn = r['Role']['Arn'] c.disassociate_service_role_from_account() iam.delete_role(RoleName=role_name) logger.info('Disassociated and deleted role {}'.format(role_name)) except ClientError: return def handler(event, context): responseData = {} try: logger.info('Received event: {}'.format(json.dumps(event))) result = cfnresponse.FAILED thingName=event['ResourceProperties']['ThingName'] if event['RequestType'] == 'Create': try: c.get_service_role_for_account() result = cfnresponse.SUCCESS except ClientError as e: manage_greengrass_role('CREATE') logger.info('Greengrass service role created') result = cfnresponse.SUCCESS elif event['RequestType'] == 'Delete': group_id = find_group(thingName) logger.info('Group id to delete: %s' % group_id) if group_id: c.reset_deployments( Force=True, GroupId=group_id ) result = cfnresponse.SUCCESS logger.info('Forced reset of Greengrass deployment') manage_greengrass_role('DELETE') else: logger.error('No group Id for thing: %s found' % thingName) except ClientError as e: logger.error('Error: %s' % e) result = cfnresponse.FAILED logger.info('Returning response of: %s, with result of: %s' % (result, responseData)) sys.stdout.flush() cfnresponse.send(event, context, result, responseData) InstanceAZ: # Returns an AZ that supports the t3.micro instance type Type: Custom::InstanceAZ Properties: ServiceToken: !GetAtt InstanceAZFunction.Arn Region: !Ref "AWS::Region" InstanceAZFunction: Type: AWS::Lambda::Function Properties: Description: Queries account and region for supported AZ Handler: index.handler Runtime: python3.6 Role: !GetAtt LambdaExecutionRole.Arn Timeout: 60 Code: ZipFile: | import sys import cfnresponse import boto3 from botocore.exceptions import ClientError import json import logging logger = logging.getLogger() logger.setLevel(logging.INFO) c = boto3.client('ec2') def handler(event, context): responseData = {} try: logger.info('Received event: {}'.format(json.dumps(event))) result = cfnresponse.FAILED if event['RequestType'] == 'Create': r = c.describe_reserved_instances_offerings( Filters=[ { 'Name': 'scope', 'Values': [ 'Availability Zone', ] }, ], IncludeMarketplace=False, InstanceType='t3.micro', ) x = r['ReservedInstancesOfferings'] while 'NextToken' in r: r = c.describe_reserved_instances_offerings( Filters=[ { 'Name': 'scope', 'Values': [ 'Availability Zone', ] }, ], IncludeMarketplace=False, InstanceType='t3.micro', NextToken=r['NextToken'] ) x.extend(r['ReservedInstancesOfferings']) responseData['AvailabilityZone'] = set(d['AvailabilityZone'] for d in x).pop() result = cfnresponse.SUCCESS else: result = cfnresponse.SUCCESS except ClientError as e: logger.error('Error: {}'.format(e)) result = cfnresponse.FAILED logger.info('Returning response of: %s, with result of: %s' % (result, responseData)) sys.stdout.flush() cfnresponse.send(event, context, result, responseData) # Roles LambdaExecutionRole: # Role used by CloudFormation created Lambda functions, used by the custom # resource functions to perform their objectives. # Overly permissive for iot:* and greengrass:* to reduce Statement complexity Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: arn:aws:logs:*:*:* - Effect: Allow Action: - iot:* Resource: "*" - Effect: Allow Action: - greengrass:* Resource: "*" - Effect: Allow Action: - ec2:DescribeReservedInstancesOfferings Resource: "*" - Effect: Allow Action: - iam:CreateRole - iam:AttachRolePolicy - iam:GetRole - iam:DeleteRole - iam:PassRole Resource: !Join ["", ["arn:aws:iam::", !Ref "AWS::AccountId", ":role/greengrass_cfn_", !Ref "AWS::StackName", "_ServiceRole"] ] GreengrassResourceRole: # Role for deployed Lambda functions to a Greengrass core to call other # AWS services directly Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: greengrass.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: arn:aws:logs:*:*:* - Effect: Allow Action: - iot:* Resource: "*" - Effect: Allow Action: - robomaker:UpdateRobotDeployment Resource: "*" - Effect: Allow Action: - s3:Get* - s3:List* Resource: !Sub ${BundlesBucket.Arn}/* RoboMakerRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: robomaker.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - logs:DescribeLogStreams Resource: arn:aws:logs:*:*:* - Effect: Allow Action: - robomaker:* Resource: "*" - Effect: Allow Action: - s3:Get* - s3:List* Resource: - !GetAtt BundlesBucket.Arn - !Sub ${BundlesBucket.Arn}/* # RoboMaker Resources Robot: Type: AWS::RoboMaker::Robot Properties: Architecture: ARMHF Fleet: !Ref RobotFleet GreengrassGroupId: !Ref GreengrassGroup # Name: !Sub ${CoreName} RobotFleet: Type: AWS::RoboMaker::Fleet # Properties: # Name: !Sub ${CoreName} RobotApp: Type: AWS::RoboMaker::RobotApplication Properties: # CurrentRevisionId: String # Name: !Sub ${CoreName} RobotSoftwareSuite: Name: ROS Version: Melodic Sources: - Architecture: ARMHF S3Bucket: !Ref BundlesBucket S3Key: output.tar RobotAppVersion: Type: AWS::RoboMaker::RobotApplicationVersion Properties: Application: !Ref RobotApp # CurrentRevisionId: 2 BundlesBucket: Type: AWS::S3::Bucket DeletionPolicy: Retain Outputs: ThingArn: Description: "The IoT Thing Arn" Value: !Sub arn:aws:iot:${AWS::Region}:${AWS::AccountId}:thing/${CoreName}_Core IoTEndpoint: Description: "The IoT Endpoint Hostname" Value: !GetAtt IoTThing.iotEndpoint VPC: Description: "The development environment VPC ID" Value: !Ref VPC Subnet: Description: "The development environment Subnet ID" Value: !Ref SubnetAPublic RoboMakerRole: Description: "The RoboMaker Role Arn" Value: !GetAtt RoboMakerRole.Arn BundlesBucket: Description: "The S3 bucket containing all of our Robomaker assets." Value: !Ref BundlesBucket # # Emit values needed for deployment status (e.g., where to SSH to) # EC2IPAddress: # Description: "EC2 Instance Public IP Address" # Value: !GetAtt GreengrassInstance.PublicIp