AWSTemplateFormatVersion: "2010-09-09" Description: Creates a complete VPC, Node-RED EC2 instance, and public IP address. The Output tab has the specifics for directly accessing the instance or to SSH to the instance. Parameters: InstanceType: Description: 'EC2 instance type' Type: 'String' Default: 't2.micro' AllowedValues: - t2.nano - t2.micro - t2.small - t2.medium ConstraintDescription: 'Must be a valid EC2 instance type.' AllowedIPRange: Description: "The IP address range that can be used to SSH and connet to Node-RED to the EC2 instance" Type: "String" MinLength: "9" MaxLength: "18" Default: "0.0.0.0/0" AllowedPattern: "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})" ConstraintDescription: "must be a valid IP CIDR range of the form x.x.x.x/x." Mappings: AWSInstanceType2Arch: t2.nano: 'Arch': 'HVM64' t2.micro: 'Arch': 'HVM64' t2.small: 'Arch': 'HVM64' t2.medium: 'Arch': 'HVM64' Resources: VPC: Type: 'AWS::EC2::VPC' Properties: CidrBlock: 172.31.0.0/24 EnableDnsSupport: true EnableDnsHostnames: true InstanceTenancy: default Tags: - Key: Name Value: !Join [ '-', [ !Sub '${AWS::StackName}', 'Node-RED' ] ] InternetGateway: Type: 'AWS::EC2::InternetGateway' Properties: Tags: - Key: Name Value: !Join [ '-', [ !Sub '${AWS::StackName}', 'Node-RED' ] ] VPCGatewayAttachment: Type: 'AWS::EC2::VPCGatewayAttachment' Properties: VpcId: !Ref VPC InternetGatewayId: !Ref InternetGateway SubnetAPublic: Type: 'AWS::EC2::Subnet' Properties: CidrBlock: 172.31.0.0/24 MapPublicIpOnLaunch: true VpcId: !Ref VPC Tags: - Key: Name Value: !Join [ "-", [ !Sub '${AWS::StackName}', 'Node-RED-public' ] ] RouteTablePublic: Type: 'AWS::EC2::RouteTable' Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Join [ '-', [ !Sub '${AWS::StackName}', 'Node-RED-public' ] ] RouteTableAssociationAPublic: Type: 'AWS::EC2::SubnetRouteTableAssociation' Properties: SubnetId: !Ref SubnetAPublic RouteTableId: !Ref RouteTablePublic RouteTablePublicInternetRoute: # should be RouteTablePublicAInternetRoute, but logical id was not changed for backward compatibility Type: 'AWS::EC2::Route' DependsOn: VPCGatewayAttachment Properties: RouteTableId: !Ref RouteTablePublic DestinationCidrBlock: '0.0.0.0/0' GatewayId: !Ref InternetGateway EC2Instance: Type: "AWS::EC2::Instance" Properties: ImageId: !GetAtt AMIInfo.Id InstanceType: !Ref InstanceType NetworkInterfaces: - AssociatePublicIpAddress: "true" DeviceIndex: "0" GroupSet: - Ref: "SGSNodeRed" SubnetId: !Ref SubnetAPublic Tags: - Key: Name Value: !Join [ '-', [ !Sub '${AWS::StackName}', 'Node-RED' ] ] UserData: "Fn::Base64": "Fn::Sub": | #!/bin/bash yum upgrade -y yum install -y gcc-c++ make curl --silent --location https://rpm.nodesource.com/setup_6.x | bash - yum install -y nodejs mkdir /opt/.npm echo 'prefix = /opt/.npm' > ~/.npmrc echo 'export PATH=$PATH:/opt/.npm/bin' >> /home/ec2-user/.bashrc npm install -g --unsafe-perm node-red # Install supervisor pip install supervisor /usr/local/bin/echo_supervisord_conf > /etc/supervisord.conf # Append config for node-red cat << 'EOF' >> /etc/supervisord.conf [program:nodered] command=/opt/.npm/bin/node-red directory=/home/ec2-user autostart=true autorestart=true startretries=3 stderr_logfile=/home/ec2-user/nodered.err.log stdout_logfile=/home/ec2-user/nodered.out.log user=ec2-user environment=HOME="/home/ec2-user" EOF cat << 'EOF' >> supervisor #!/bin/bash # # supervisor Start/Stop the supervisor daemon. # # chkconfig: 345 90 10 # description: Supervisor is a client/server system that allows its users to # monitor and control a number of processes on UNIX-like operating # systems. . /etc/init.d/functions DAEMON=/usr/local/bin/supervisord PIDFILE=/var/run/supervisord.pid [ -x "$DAEMON" ] || exit 0 start() { echo -n "Starting supervisord: " if [ -f $PIDFILE ]; then PID=`cat $PIDFILE` echo supervisord already running: $PID exit 2; else daemon $DAEMON --pidfile=$PIDFILE -c /etc/supervisord.conf -u ec2-user -d /home/ec2-user RETVAL=$? echo [ $RETVAL -eq 0 ] && touch /var/lock/subsys/supervisord return $RETVAL fi } stop() { echo -n "Shutting down supervisord: " echo killproc -p $PIDFILE supervisord echo rm -f /var/lock/subsys/supervisord return 0 } case "$1" in start) start ;; stop) stop ;; status) status supervisord ;; restart) stop start ;; *) echo "Usage: {start|stop|status|restart}" exit 1 ;; esac exit $? EOF mv supervisor /etc/init.d chmod +x /etc/init.d/supervisor chkconfig --add supervisor chkconfig supervisor on /sbin/iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 1880 /etc/init.d/iptables save /bin/sed -i 's/HOSTNAME=localhost.localdomain/HOSTNAME=nodered/g' /etc/sysconfig/network /usr/bin/install -d -o ec2-user -g ec2-user 755 /home/ec2-user/.node-red /usr/bin/curl https://s3-us-west-2.amazonaws.com/sid402-artifacts/scripts/IoT_Security_Lab_Connect_Device.json > /home/ec2-user/.node-red/flows_nodered.json reboot SGSNodeRed: Type: "AWS::EC2::SecurityGroup" Properties: GroupDescription: Security controls for NodeRed instance SecurityGroupEgress: - # For yum updates IpProtocol: tcp FromPort: '80' ToPort: '80' CidrIp: 0.0.0.0/0 - # For yum updates IpProtocol: tcp FromPort: '443' ToPort: '443' CidrIp: 0.0.0.0/0 - # Connection to AWS IoT IpProtocol: tcp FromPort: '8883' ToPort: '8883' CidrIp: 0.0.0.0/0 SecurityGroupIngress: - IpProtocol: tcp FromPort: '80' ToPort: '80' CidrIp: !Ref AllowedIPRange VpcId: !Ref VPC AMIInfo: Type: Custom::AMIInfo Properties: ServiceToken: !GetAtt AMIInfoFunction.Arn Region: !Ref "AWS::Region" Architecture: Fn::FindInMap: - AWSInstanceType2Arch - !Ref InstanceType - Arch AMIInfoFunction: Properties: Description: Return AMI of Amazon Linux Handler: index.handler Runtime: python3.6 Role: !GetAtt LambdaExecutionRole.Arn Timeout: 60 Code: ZipFile: | import cfnresponse import boto3 from botocore.exceptions import ClientError from operator import itemgetter import json import time import logging logger = logging.getLogger() logger.setLevel(logging.INFO) archToAMINamePattern = { 'PV64': 'amzn-ami-pv*x86_64-ebs', 'HVM64': 'amzn-ami-hvm*x86_64-gp2', 'HVMG2': 'amzn-ami-graphics-hvm*x86_64-ebs*' } def handler(event, context): responseData = {} try: logger.info('Received event: {}'.format(json.dumps(event))) #Assume failed unless we find a match result = cfnresponse.FAILED if event['RequestType'] == 'Create' or event['RequestType'] == 'Update': ec2 = boto3.client('ec2', region_name=event['ResourceProperties']['Region']) start_time = time.time() result = ec2.describe_images( Filters=[ { 'Name': 'name', 'Values': [ archToAMINamePattern[event['ResourceProperties']['Architecture']] ] }, ], Owners=[ '679593333241' if event['ResourceProperties']['Architecture'] == "HVMG2" else 'amazon'] ) end_time = time.time() logger.info('Describe instances for {} region took {} seconds'.format(event['ResourceProperties']['Region'], end_time - start_time)) images = sorted(result['Images'], key=itemgetter('Description'), reverse=True) for x in images: if '.rc' in x['Description'].lower() or 'beta' in x['Description'].lower(): continue result = cfnresponse.SUCCESS responseData['Id'] = x['ImageId'] break elif event['RequestType'] == 'Delete': 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)) cfnresponse.send(event, context, result, responseData) Type: AWS::Lambda::Function LambdaExecutionRole: Type: AWS::IAM::Role Properties: Path: '/immersionday_iot/' 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: - ec2:DescribeImages Resource: "*" Outputs: HostIPAddress: Description: 'Node-RED Host IP Address' Value: !GetAtt EC2Instance.PublicIp DefaultUser: Description: 'Username for Node-RED Host' Value: 'ec2-user' NodeREDURL: Description: 'Link to Node-RED GUI' Value: !Join [ '', [ 'http://', !GetAtt EC2Instance.PublicIp] ]