AWS IoT TwinMaker によるデジタルツインアプリケーションを AWS CDK で構築する 〜その2 シーン作成編〜

2023.09.25

こんにちは、CX 事業本部 Delivery 部の若槻です。

前回の下記エントリでは、AWS IoT TwinMaker でデジタルツインアプリケーションを構築する際に最上位のコンテナとなるリソースであるワークスペースを AWS CDK で作成しました。

そして、そのワークスペース内で実際にデジタルツインを編集して構成する場となるリソースが、下記図の⑤に該当するシーン (Scene) です。 What is AWS IoT TwinMaker? - AWS IoT TwinMaker より引用

今回は、AWS IoT TwinMaker によるデジタルツインアプリケーションを AWS CDK で作成する上で、このシーン作成する方法と、作成時に遭遇したいくつかのハマりポイントを合わせてご紹介します。

最終的に動作したもの

最終的には以下の CDK コードで、動作するシーンを構築することができました。

lib/cdk-sample-stack.ts

import {
  aws_iam,
  aws_s3,
  aws_iottwinmaker,
  aws_s3_deployment,
  Stack,
  RemovalPolicy,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSampleStack extends Stack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    const accounId = this.account;
    const region = this.region;
    const workspaceId = 'CdkDemoWorkspace';

    // ワークスペース用リソース保管バケット
    const workspaceResourceBucket = new aws_s3.Bucket(
      this,
      'WorkspaceResourceBucket',
      {
        removalPolicy: RemovalPolicy.DESTROY,
        autoDeleteObjects: true,
      }
    );

    // IoT TwinMaker ワークスペース実行ロール用プリンシパル
    const principal = new aws_iam.ServicePrincipal(
      'iottwinmaker.amazonaws.com'
    ).withConditions({
      StringEquals: {
        'aws:SourceAccount': accounId,
      },
      StringLike: {
        'aws:SourceArn': `arn:aws:iottwinmaker:${region}:${accounId}:workspace/${workspaceId}`,
      },
    });

    // ワークスペース実行ロール
    const workspaceExecutionRole = new aws_iam.Role(
      this,
      'WorkspaceExecutionRole',
      {
        assumedBy: principal,
        inlinePolicies: {
          readWorkspaceResourceBucket: aws_iam.PolicyDocument.fromJson({
            Version: '2012-10-17',
            Statement: [
              {
                Effect: 'Allow',
                Action: [
                  's3:GetBucket',
                  's3:GetObject',
                  's3:ListBucket',
                  's3:PutObject',
                  's3:ListObjects',
                  's3:ListObjectsV2',
                  's3:GetBucketLocation',
                ],
                Resource: [
                  workspaceResourceBucket.bucketArn,
                  workspaceResourceBucket.arnForObjects('*'),
                ],
              },
              {
                Effect: 'Allow',
                Action: ['s3:DeleteObject'],
                Resource: [
                  workspaceResourceBucket.arnForObjects(
                    `DO_NOT_DELETE_WORKSPACE_${workspaceId}`
                  ),
                ],
              },
            ],
          }),
        },
      }
    );

    // ワークスペース
    const cdkDemoWorkspace = new aws_iottwinmaker.CfnWorkspace(
      this,
      'CdkDemoWorkspace',
      {
        workspaceId,
        s3Location: workspaceResourceBucket.bucketArn,
        role: workspaceExecutionRole.roleArn,
      }
    );

    // シーン設定ファイルのアップロード
    new aws_s3_deployment.BucketDeployment(this, 'DeploySampleSceneConfig', {
      sources: [
        aws_s3_deployment.Source.asset('./src/iottwinmaker/cdk-demo-workspace'),
      ],
      destinationBucket: workspaceResourceBucket,
      destinationKeyPrefix: 'SceneConfig/',
    });

    // シーン
    const sampleScene = new aws_iottwinmaker.CfnScene(this, 'SampleScene', {
      sceneId: 'SampleScene',
      contentLocation: workspaceResourceBucket.s3UrlForObject(
        '/SceneConfig/SampleScene.json'
      ),
      workspaceId: cdkDemoWorkspace.workspaceId,
    });

    // ワークスペースとシーンの依存関係の設定
    sampleScene.addDependency(cdkDemoWorkspace);
  }
}

  • ワークスペースと同様に、シーンの作成でも L1 Construct の CfnScene を使用します。
    • contentLocationでは、シーン設定ファイルの S3 バケット内のパスを指定します。
    • L1 Construct 同士の依存となるので、addDependencycdkDemoWorkspace に対する依存関係を追加しています。
    • 依存関係を設定しない場合は、スタック削除時などにワークスペースの削除が先に試行されてしまい、Workspace has scenes. Delete all resources in the workspace. というエラーが発生します。
  • シーン設定ファイルを S3 バケットにアップロードするためにaws_s3_deployment.BucketDeploymentを使用しています。
  • 下記がシーン設定ファイルの内容です。マネジメントコンソールからシーン作成時にデフォルトで作成される設定ファイルを参考に、必須およびあると便利な項目を記述しています。

src/iottwinmaker/cdk-demo-workspace/SampleScene.json

{
  "specVersion": "1.0",
  "version": "1",
  "unit": "meters",
  "nodes": [],
  "rootNodeIndexes": [],
  "cameras": [],
  "properties": {
    "environmentPreset": "neutral"
  }
}

上記を CDK デプロイ後に、AWS CLI で ID SampleScene のシーンを取得すると、シーンが作成されていることが確認できました。

$ aws iottwinmaker get-scene --workspace-id CdkDemoWorkspace --scene-id SampleScene --region us-east-1
{
    "workspaceId": "CdkDemoWorkspace",
    "sceneId": "SampleScene",
    "contentLocation": "s3://cdksamplestack-workspaceresourcebucketeecc84e2-xxxxxxxxxx/SceneConfig/SampleScene.json",
    "arn": "arn:aws:iottwinmaker:us-east-1:xxxxxxxxxxxx:workspace/CdkDemoWorkspace/scene/SampleScene",
    "creationDateTime": 1695566570.944,
    "updateDateTime": 1695571709.3,
    "capabilities": [],
    "sceneMetadata": {},
    "generatedSceneMetadata": {}
}

マネジメントコンソールからワークスペースにアクセスすると、こちらでもシーンが作成されていることが確認できます。

シーンをクリックして開くと、シーンコンポーザーにも問題なくアクセスすることができるようになっています。

CfnSceneProps 型定義

参考までに、CfnScene のプロパティの型定義一覧は次のようになります。

node_modules/aws-cdk-lib/aws-iottwinmaker/lib/iottwinmaker.generated.d.ts

/**
 * Properties for defining a `CfnScene`
 *
 * @struct
 * @stability external
 * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-scene.html
 */
export interface CfnSceneProps {
    /**
     * A list of capabilities that the scene uses to render.
     *
     * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-scene.html#cfn-iottwinmaker-scene-capabilities
     */
    readonly capabilities?: Array<string>;
    /**
     * The relative path that specifies the location of the content definition file.
     *
     * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-scene.html#cfn-iottwinmaker-scene-contentlocation
     */
    readonly contentLocation: string;
    /**
     * The description of this scene.
     *
     * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-scene.html#cfn-iottwinmaker-scene-description
     */
    readonly description?: string;
    /**
     * The scene ID.
     *
     * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-scene.html#cfn-iottwinmaker-scene-sceneid
     */
    readonly sceneId: string;
    /**
     * The scene metadata.
     *
     * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-scene.html#cfn-iottwinmaker-scene-scenemetadata
     */
    readonly sceneMetadata?: cdk.IResolvable | Record<string, string>;
    /**
     * The ComponentType tags.
     *
     * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-scene.html#cfn-iottwinmaker-scene-tags
     */
    readonly tags?: Record<string, string>;
    /**
     * The ID of the workspace.
     *
     * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-scene.html#cfn-iottwinmaker-scene-workspaceid
     */
    readonly workspaceId: string;
}

試行錯誤

以降は、シーンを CDK で作成する際にハマって試行錯誤したポイントを書き残しておきます。

その1

事象

以前のエントリでのワークスペース作成時のコードを以下のように変更してシーンを作成しようとしました。

$ git diff
diff --git a/lib/cdk-sample-stack.ts b/lib/cdk-sample-stack.ts
index bc5371c..5a17cf9 100644
--- a/lib/cdk-sample-stack.ts
+++ b/lib/cdk-sample-stack.ts
@@ -62,10 +62,21 @@ export class CdkSampleStack extends Stack {
     );
 
     // ワークスペース
-    new aws_iottwinmaker.CfnWorkspace(this, 'IotTwinmakerWorkspace', {
-      workspaceId: 'CdkDemoWorkspace',
-      s3Location: workspaceResourceBucket.bucketArn,
-      role: workspaceExecutionRole.roleArn,
+    const cdkDemoWorkspace = new aws_iottwinmaker.CfnWorkspace(
+      this,
+      'CdkDemoWorkspace',
+      {
+        workspaceId: 'CdkDemoWorkspace',
+        s3Location: workspaceResourceBucket.bucketArn,
+        role: workspaceExecutionRole.roleArn,
+      }
+    );
+
+    // シーン
+    new aws_iottwinmaker.CfnScene(this, 'SampleScene', {
+      sceneId: 'SampleScene',
+      contentLocation: `${workspaceResourceBucket.bucketArn}/SampleScene.json`,
+      workspaceId: cdkDemoWorkspace.workspaceId,
     });
   }
 }

CDK デプロイすると次のようなエラーが発生しました。

$ cdk deploy                                                                

✨  Synthesis time: 3.16s

CdkSampleStack:  start: Building d772b71253b31ce822e7c9a83209cc1a3dba29d2bc2dd65e58ba58a760cd22b4:current_account-us-east-1
CdkSampleStack:  success: Built d772b71253b31ce822e7c9a83209cc1a3dba29d2bc2dd65e58ba58a760cd22b4:current_account-us-east-1
CdkSampleStack:  start: Publishing d772b71253b31ce822e7c9a83209cc1a3dba29d2bc2dd65e58ba58a760cd22b4:current_account-us-east-1
CdkSampleStack:  success: Published d772b71253b31ce822e7c9a83209cc1a3dba29d2bc2dd65e58ba58a760cd22b4:current_account-us-east-1
CdkSampleStack: deploying... [1/1]
CdkSampleStack: creating CloudFormation changeset...
11:17:28 PM | CREATE_FAILED        | AWS::IoTTwinMaker::Scene     | SampleScene
Properties validation failed for resource SampleScene with message:
#/ContentLocation: failed validation constraint for keyword [pattern]


 ❌  CdkSampleStack failed: Error: The stack named CdkSampleStack failed to deploy: UPDATE_ROLLBACK_COMPLETE: Properties validation failed for resource SampleScene with message:
#/ContentLocation: failed validation constraint for keyword [pattern]
    at FullCloudFormationDeployment.monitorDeployment (/Users/wakatsuki.ryuta/.nvm/versions/node/v18.17.0/lib/node_modules/aws-cdk/lib/index.js:454:10232)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async Object.deployStack2 [as deployStack] (/Users/wakatsuki.ryuta/.nvm/versions/node/v18.17.0/lib/node_modules/aws-cdk/lib/index.js:457:179910)
    at async /Users/wakatsuki.ryuta/.nvm/versions/node/v18.17.0/lib/node_modules/aws-cdk/lib/index.js:457:163158

 ❌ Deployment failed: Error: The stack named CdkSampleStack failed to deploy: UPDATE_ROLLBACK_COMPLETE: Properties validation failed for resource SampleScene with message:
#/ContentLocation: failed validation constraint for keyword [pattern]
    at FullCloudFormationDeployment.monitorDeployment (/Users/wakatsuki.ryuta/.nvm/versions/node/v18.17.0/lib/node_modules/aws-cdk/lib/index.js:454:10232)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async Object.deployStack2 [as deployStack] (/Users/wakatsuki.ryuta/.nvm/versions/node/v18.17.0/lib/node_modules/aws-cdk/lib/index.js:457:179910)
    at async /Users/wakatsuki.ryuta/.nvm/versions/node/v18.17.0/lib/node_modules/aws-cdk/lib/index.js:457:163158

The stack named CdkSampleStack failed to deploy: UPDATE_ROLLBACK_COMPLETE: Properties validation failed for resource SampleScene with message:
#/ContentLocation: failed validation constraint for keyword [pattern]

解決

CfnSceneでのcontentLocationの指定方法が間違っていました。値として Construct を指定すれば良い L2 Construct とは異なり、L1 Construct は生の文字列の指定となるので、こういう所に気をつけないといけないですね。

$ git diff
diff --git a/lib/cdk-sample-stack.ts b/lib/cdk-sample-stack.ts
index 5a17cf9..cfd5519 100644
--- a/lib/cdk-sample-stack.ts
+++ b/lib/cdk-sample-stack.ts
@@ -75,7 +75,7 @@ export class CdkSampleStack extends Stack {
     // シーン
     new aws_iottwinmaker.CfnScene(this, 'SampleScene', {
       sceneId: 'SampleScene',
-      contentLocation: `${workspaceResourceBucket.bucketArn}/SampleScene.json`,
+      contentLocation: `s3://${workspaceResourceBucket.bucketName}/SampleScene.json`,
       workspaceId: cdkDemoWorkspace.workspaceId,
     });
   }

その2

事象

その 1 で設定ファイルをデプロイした後にマネジメントコンソールからシーンにアクセスしてみました。

すると次のようなエラーが発生し、シーンコンポーザーを操作することができませんでした。

調査

コンソールから手動でワークスペースとシーンを作成してみます。

この場合はシーンコンポーザーを正常に開くことができます。

そして S3 バケットの内容を見ると、次のように <シーン名>.json という JSON ファイルが作成されていました。

$ aws s3 ls s3://twinmaker-workspace-test-<accountId>-iad
2023-09-25 00:22:05          0 DO_NOT_DELETE_WORKSPACE_test
2023-09-25 00:22:42       1103 test_scene.json

JSON ファイルの内容は次のようになっています。シーンのデフォルトの設定や、サンプルルールが記述がコンフィグとして記述されています。

test_scene.json

{
  "specVersion": "1.0",
  "version": "1",
  "unit": "meters",
  "nodes": [],
  "rootNodeIndexes": [],
  "cameras": [],
  "rules": {
    "sampleAlarmIconRule": {
      "statements": [
        {
          "expression": "alarm_status == 'ACTIVE'",
          "target": "iottwinmaker.common.icon:Error"
        },
        {
          "expression": "alarm_status == 'ACKNOWLEDGED'",
          "target": "iottwinmaker.common.icon:Warning"
        },
        {
          "expression": "alarm_status == 'SNOOZE_DISABLED'",
          "target": "iottwinmaker.common.icon:Warning"
        },
        {
          "expression": "alarm_status == 'NORMAL'",
          "target": "iottwinmaker.common.icon:Info"
        }
      ]
    },
    "sampleTimeSeriesIconRule": {
      "statements": [
        {
          "expression": "temperature >= 40",
          "target": "iottwinmaker.common.icon:Error"
        },
        {
          "expression": "temperature >= 20",
          "target": "iottwinmaker.common.icon:Warning"
        },
        {
          "expression": "temperature < 20",
          "target": "iottwinmaker.common.icon:Info"
        }
      ]
    },
    "sampleTimeSeriesColorRule": {
      "statements": [
        {
          "expression": "temperature >= 40",
          "target": "iottwinmaker.common.color:#FF0000"
        },
        {
          "expression": "temperature >= 20",
          "target": "iottwinmaker.common.color:#FFFF00"
        },
        {
          "expression": "temperature < 20",
          "target": "iottwinmaker.common.color:#00FF00"
        }
      ]
    }
  },
  "properties": {
    "environmentPreset": "neutral"
  }
}

この JSON ファイルを CDK デプロイ時にも作成すると良さそうです。

解決

まず、試しに空の JSON ファイルを使用してみます。

src/iottwinmaker/cdk-demo-workspace/SampleScene.json

{}

aws_s3_deployment.BucketDeploymentを使用して、S3 バケットに JSON ファイルをアップロードするようにします。この時、プレフィクスSceneConfigを指定することにより、他のパスのオブジェクトが意図せず削除されないようにします。

$ git diff
diff --git a/lib/cdk-sample-stack.ts b/lib/cdk-sample-stack.ts
index 5a17cf9..f436f45 100644
--- a/lib/cdk-sample-stack.ts
+++ b/lib/cdk-sample-stack.ts
@@ -2,6 +2,7 @@ import {
   aws_iam,
   aws_s3,
   aws_iottwinmaker,
+  aws_s3_deployment,
   Stack,
   StackProps,
   RemovalPolicy,
@@ -75,8 +76,17 @@ export class CdkSampleStack extends Stack {
     // シーン
     new aws_iottwinmaker.CfnScene(this, 'SampleScene', {
       sceneId: 'SampleScene',
-      contentLocation: `${workspaceResourceBucket.bucketArn}/SampleScene.json`,
+      contentLocation: `s3://${workspaceResourceBucket.bucketName}/SceneConfig/SampleScene.json`,
       workspaceId: cdkDemoWorkspace.workspaceId,
     });
+
+    // シーン設定ファイルのアップロード
+    new aws_s3_deployment.BucketDeployment(this, 'DeploySampleSceneConfig', {
+      sources: [
+        aws_s3_deployment.Source.asset('./src/iottwinmaker/cdk-demo-workspace'),
+      ],
+      destinationBucket: workspaceResourceBucket,
+      destinationKeyPrefix: 'SceneConfig/',
+      retainOnDelete: false,
+    });
   }
 }

上記をデプロイすると、下記の通り S3 バケットに JSON ファイルが作成されました。

$ aws s3 ls s3://${workspaceResourceBucket} --recursive
2023-09-25 00:53:49          0 DO_NOT_DELETE_WORKSPACE_CdkDemoWorkspace
2023-09-25 01:14:24          3 SceneConfig/SampleScene.json

再度シーンコンポーザーにアクセスすると、先ほどとエラーが変わりました。

Error - unable to render the scene
Invalid scene document. Cause - unable to find specVersion

specVersionなど必要なプロパティを追加すれば良さそうです。

src/iottwinmaker/cdk-demo-workspace/SampleScene.json

{
  "specVersion": "1.0",
  "version": "1",
  "unit": "meters",
  "nodes": [],
  "rootNodeIndexes": [],
  "cameras": [],
  "properties": {
    "environmentPreset": "neutral"
  }
}

再度デプロイした後にシーンコンポーザーを開くと、今度は正常にアクセスすることができました。

おわりに

AWS IoT TwinMaker によるデジタルツインアプリケーションを AWS CDK で作成する上で、シーン作成する方法と、作成時に遭遇したいくつかのハマりポイントを合わせてご紹介しました。

次回はエンティティを作成し、3D モデルのアップロードおよびバインディングまで挑戦してみたいと思います。

以上