constructor()

in lib/aws-iot-rpi-fleet-provisioning-stack.ts [53:334]


  constructor(scope: cdk.Construct, id: string, props: AwsIotRpiFleetProvisioningStackProps) {
    super(scope, id, props);

    // Policy attached to IoT things generated by this stack
    const thingsPolicy = new iot.CfnPolicy(this, 'thingsPolicy', {
      policyDocument: {
        'Version': '2012-10-17',
        'Statement': [
          {
            "Effect": "Allow",
            "Action": [
              "iot:Connect"
            ],
            "Resource": [
              `arn:aws:iot:${this.region}:${this.account}:client/\${iot:Connection.Thing.ThingName}`,
            ]
          },
          {
              "Effect": "Allow",
              "Action": [
                  "iot:Subscribe",
              ],
              "Resource": [
                `arn:aws:iot:${this.region}:${this.account}:topicfilter/\${iot:Connection.Thing.ThingName}/*`,
                `arn:aws:iot:${this.region}:${this.account}:topicfilter/openworld`,
              ]
          },
          {
              "Effect": "Allow",
              "Action": [
                  "iot:Publish",
                  "iot:Receive",
              ],
              "Resource": [
                  `arn:aws:iot:${this.region}:${this.account}:topic/\${iot:Connection.Thing.ThingName}/*`,
                  `arn:aws:iot:${this.region}:${this.account}:topic/openworld`,
              ]
          },
        ]
      }
    });

    // Give the AWS IoT service permission to create or update IoT resources such as things and certificates in your account when provisioning devices
    const provisioningRole = new iam.Role(this, 'ProvisioningRoleArn', {
      assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'),
    });
    provisioningRole.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSIoTThingsRegistration'));

    // The provisioning template used to create IoT things
    // https://docs.aws.amazon.com/iot/latest/developerguide/provision-template.html
    const provisioningTemplate = new iot.CfnProvisioningTemplate(this, 'ProvisioningTemplate', {
      provisioningRoleArn: provisioningRole.roleArn,
      enabled: true,
      templateBody: `{
        "Parameters": {
          "SerialNumber": {
            "Type": "String"
          },
          "AWS::IoT::Certificate::Id": {
            "Type": "String"
          }
        },
        "Resources": {
          "certificate": {
            "Properties": {
              "CertificateId": {
                "Ref": "AWS::IoT::Certificate::Id"
              },
              "Status": "Active"
            },
            "Type": "AWS::IoT::Certificate"
          },
          "policy": {
            "Properties": {
              "PolicyName": "${thingsPolicy.ref}"
            },
            "Type": "AWS::IoT::Policy"
          },
          "thing": {
            "OverrideSettings": {
              "AttributePayload": "MERGE",
              "ThingGroups": "DO_NOTHING",
              "ThingTypeName": "REPLACE"
            },
            "Properties": {
              "ThingGroups": [],
              "ThingName": {
                "Ref": "SerialNumber"
              }
            },
            "Type": "AWS::IoT::Thing"
          }
        },
        "DeviceConfiguration": {
        }
      }`
    });

    // AWS IoT fleet provisioning uses claim certificates to generate things certificates
    // This policy restricts the use of claim certificates to device provisioning
    const fleetProvisioningPolicy = new iot.CfnPolicy(this, 'FleetProvisioningPolicy', {
      policyDocument: {
        "Version": "2012-10-17",
        "Statement": [
          {
            "Effect": "Allow",
            "Action": ["iot:Connect"],
            "Resource": ["*"]
          },
          {
            "Effect": "Allow",
            "Action": ["iot:Publish", "iot:Receive"],
            "Resource": [
              `arn:aws:iot:${this.region}:${this.account}:topic/$aws/certificates/create/*`,
              `arn:aws:iot:${this.region}:${this.account}:topic/$aws/provisioning-templates/${provisioningTemplate.ref}/provision/*`
            ]
          },
          {
            "Effect": "Allow",
            "Action": ["iot:Subscribe"],
            "Resource": [
              `arn:aws:iot:${this.region}:${this.account}:topicfilter/$aws/certificates/create/*`,
              `arn:aws:iot:${this.region}:${this.account}:topicfilter/$aws/provisioning-templates/${provisioningTemplate.ref}/provision/*`
            ]
          }
        ]
      }
    });

    // The bucket where the configured rpi-image-builder used as a source of the pipeline is stored
    const rpiImageBuilderSourceBucket = new s3.Bucket(this, 'rpiImageBuilderSourceBucket', {
      encryption: s3.BucketEncryption.S3_MANAGED,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      versioned: true, // CodePipeline requires a versioned source for source stages
    });

    // The bucket where the custom raspbian image created by the pipeline is stored
    const rpiImageOutputBucket = new s3.Bucket(this, 'RpiImageOutputBucket', {
      encryption: s3.BucketEncryption.S3_MANAGED,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
    });

    // Lambda that configures the rpi-image-builder and stores it in the pipeline source bucket
    const configureRpiImageBuilderFunction = new lambda.Function(this, 'ConfigureRpiImageBuilderFunction', {
      runtime: lambda.Runtime.PYTHON_3_7,
      handler: 'app.on_event',
      code: lambda.Code.fromAsset(path.join(__dirname, './lambda/configure_rpi_image_builder')),
      timeout: cdk.Duration.seconds(60),
      initialPolicy: [
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: [
            'iot:CreateKeysAndCertificate',
            'iot:AttachPolicy',
            'iot:DescribeEndpoint',
          ],
          resources: ['*'],
        }),
      ],
    });

    // Store the rpi-image-builder source code in S3
    const rpiImageBuilderAsset = new s3Assets.Asset(this, 'RpiImageBuilderAsset', {
      path: path.join(__dirname, '../rpi-image-builder'),
    });

    rpiImageBuilderSourceBucket.grantWrite(configureRpiImageBuilderFunction);
    rpiImageBuilderAsset.grantRead(configureRpiImageBuilderFunction);

    // Custom resource that calls the Lambda that will configure the rpi-image-builder client
    new cdk.CustomResource(this, 'ConfigureRpiImageBuilderCR', {
      serviceToken: new cr.Provider(this, 'ConfigureRpiImageBuilderProvider', {
        onEventHandler: configureRpiImageBuilderFunction,
      }).serviceToken,
      properties: {
        'FLEET_PROVISIONING_POLICY_NAME': fleetProvisioningPolicy.ref,
        'PROVISIONING_TEMPLATE_NAME': provisioningTemplate.ref,
        'RPI_IMAGE_BUILDER_BUCKET_NAME': rpiImageBuilderAsset.s3BucketName,
        'RPI_IMAGE_BUILDER_OBJECT_KEY': rpiImageBuilderAsset.s3ObjectKey,
        'CONFIGURED_RPI_IMAGE_BUILDER_BUCKET_NAME': rpiImageBuilderSourceBucket.bucketName,
        'CONFIGURED_RPI_IMAGE_BUILDER_OBJECT_KEY': this.rpiImageBuilderArchiveName,
      },
    });

    // Codebuild project that generates the custom raspbian image
    const buildRpiImageProject = new codebuild.PipelineProject(this, 'BuildRpiImageProject', {
      environment: {
        buildImage: codebuild.LinuxBuildImage.STANDARD_4_0,
        privileged: true,
      },
      buildSpec: codebuild.BuildSpec.fromObject({
        version: '0.2',
        env: {
          variables: {
            // Set environment variables expected by the build-rpi-image.bash script
            'SSH_PUBLIC_KEY': props.sshPublicKey,
            'WIFI_SSID': props.wifiSsid,
            'WIFI_COUNTRY': props.wifiCountry,
            'ARTIFACT_IMAGE_NAME': 'aws-raspbian.img',
          },
          'secrets-manager': {
            'WIFI_PASSWORD': `${props.wifiPasswordSecretName}`,
          },
        },
        phases: {
          install: {
            commands: [
              // Install dependencies required by the build-rpi-image.bash script
              'apt-get update',
              'apt-get -y install p7zip-full wget libxml2-utils kpartx',
            ],
          },
          build: {
            commands: [
              './build-rpi-image.bash'
            ],
          },
        },
        artifacts: {
          files: [
            '$ARTIFACT_IMAGE_NAME',
          ],
        },
      }),
    });

    // Give access to the secret containing the wifi password to the codebuild project
    if (buildRpiImageProject.role) {
      const rpiSecret = secrets.Secret.fromSecretNameV2(this, 'RPISecrets', props.wifiPasswordSecretName);
      rpiSecret.grantRead(buildRpiImageProject.role);
    }

    const pipelineSourceArtifact = new codepipeline.Artifact();
    const buildOutputArtifact = new codepipeline.Artifact();

    new codepipeline.Pipeline(this, 'BuildRpiImagePipeline', {
      crossAccountKeys: false,
      restartExecutionOnUpdate: true,
      stages: [
        {
          stageName: 'Source',
          actions: [
            new codepipelineActions.S3SourceAction({
              actionName: 'Source',
              bucket: rpiImageBuilderSourceBucket,
              bucketKey: this.rpiImageBuilderArchiveName,
              output: pipelineSourceArtifact,
            }),
          ],
        },
        {
          stageName: 'BuildRpiImage',
          actions: [
            new codepipelineActions.CodeBuildAction({
              actionName: 'BuildRpiImage',
              input: pipelineSourceArtifact,
              project: buildRpiImageProject,
              outputs: [buildOutputArtifact],
            }),
          ],
        },
        {
          stageName: 'StoreRpiImage',
          actions: [
            new codepipelineActions.S3DeployAction({
              actionName: 'StoreRpiImage',
              input: buildOutputArtifact,
              bucket: rpiImageOutputBucket,
              extract: false,
              objectKey: this.customImageArchiveName,
            }),
          ],
        },
      ],
    });

    new cdk.CfnOutput(this, 'RpiImageBucketName', {
      description: 'Download the raspbian image from this S3 bucket',
      value: rpiImageOutputBucket.bucketName,
    });

  }