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

2023.09.26

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

前回までは、AWS IoT TwinMaker でデジタルツインアプリケーションを構築する際に最上位のコンテナとなるリソースであるワークスペースと、実際にデジタルツインを編集して構成する場となる「シーン(Scene)」を AWS CDKで作成しました。

そして、シーン内で表したい物理的な機器や概念などのデジタル表現に、挙動や状態などのコンテキストを提供するリソースが、下記図の③に該当する エンティティ (Entity) です。 What is AWS IoT TwinMaker? - AWS IoT TwinMaker より引用

今回は、この AWS IoT TwinMaker のエンティティを AWS CDK で作成する方法を確認してみました。

最終的に動作した 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,
      }
    );
    const cdkDemoWorkspaceId = 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,
    });

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

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

    // 親エンティティ
    const sampleParentEntity = new aws_iottwinmaker.CfnEntity(
      this,
      'SampleParentEntity',
      {
        entityName: 'sampleParentEntity1',
        entityId: 'sampleParentEntity1',
        workspaceId: cdkDemoWorkspaceId,
      }
    );

    // ワークスペースと親エンティティの依存関係の設定
    sampleParentEntity.addDependency(cdkDemoWorkspace);

    // 子エンティティ1
    const sampleChildEntity1 = new aws_iottwinmaker.CfnEntity(
      this,
      'SampleChildEntity1',
      {
        entityName: 'sampleChildEntity1',
        workspaceId: cdkDemoWorkspaceId,
        parentEntityId: sampleParentEntity.entityId,
      }
    );

    // 子エンティティ2
    const sampleChildEntity2 = new aws_iottwinmaker.CfnEntity(
      this,
      'SampleChildEntity2',
      {
        entityName: 'sampleChildEntity2',
        workspaceId: cdkDemoWorkspaceId,
        parentEntityId: sampleParentEntity.entityId,
      }
    );

    // 親エンティティと子エンティティの依存関係の設定
    sampleChildEntity1.addDependency(sampleParentEntity);
    sampleChildEntity2.addDependency(sampleParentEntity);
  }
}

  • エンティティの作成では L1 Construct である CfnEntity を使用します。
  • エンティティはそれらの間で親子関係を持たせることができます。実際の 3D モデルも全体から子細への親子関係を持つことが多いため、そのマッピングのために親エンティティと子エンティティを作成すると便利です。
  • 注意点として、親エンティティでは entityId の指定が必須で、また addDependency による親子間の依存関係を明示する必要がありました。
  • さらに親エンティティはワークスペースとの依存関係の設定も必要です。未設定の場合はワークスペースおよび親エンティティの Construct 削除時に Workspace has entities. Delete all resources in the workspace.というエラーが発生する場合があります。
  • エンティティではコンポーネント(component)を設定することもできますが、こちらに関しては次回以降に別途ご紹介します。

CfnEntityProps 型定義

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

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

/**
 * Properties for defining a `CfnEntity`
 *
 * @struct
 * @stability external
 * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-entity.html
 */
export interface CfnEntityProps {
    /**
     * An object that maps strings to the components in the entity.
     *
     * Each string in the mapping must be unique to this object.
     *
     * For information on the component object see the [component](https://docs.aws.amazon.com//iot-twinmaker/latest/apireference/API_ComponentResponse.html) API reference.
     *
     * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-entity.html#cfn-iottwinmaker-entity-components
     */
    readonly components?: cdk.IResolvable | Record<string, CfnEntity.ComponentProperty | cdk.IResolvable>;
    /**
     * The description of the entity.
     *
     * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-entity.html#cfn-iottwinmaker-entity-description
     */
    readonly description?: string;
    /**
     * The entity ID.
     *
     * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-entity.html#cfn-iottwinmaker-entity-entityid
     */
    readonly entityId?: string;
    /**
     * The entity name.
     *
     * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-entity.html#cfn-iottwinmaker-entity-entityname
     */
    readonly entityName: string;
    /**
     * The ID of the parent entity.
     *
     * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-entity.html#cfn-iottwinmaker-entity-parententityid
     */
    readonly parentEntityId?: string;
    /**
     * Metadata that you can use to manage the entity.
     *
     * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-entity.html#cfn-iottwinmaker-entity-tags
     */
    readonly tags?: Record<string, string>;
    /**
     * The ID of the workspace.
     *
     * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iottwinmaker-entity.html#cfn-iottwinmaker-entity-workspaceid
     */
    readonly workspaceId: string;
}

トラブルシューティング

以降は、エンティティを CDK で作成する際にハマって行ったトラブルシューティングを書き残しておきます。

その1

事象

当初、次のように親および子エンティティを作成しました。一見すると良さそうですね。

$ git diff
diff --git a/lib/cdk-sample-stack.ts b/lib/cdk-sample-stack.ts
index 61b91c4..5b9c263 100644
--- a/lib/cdk-sample-stack.ts
+++ b/lib/cdk-sample-stack.ts
@@ -89,5 +89,29 @@ export class CdkSampleStack extends Stack {
       destinationKeyPrefix: 'SceneConfig/',
       retainOnDelete: false,
     });
+
+    // 親エンティティ
+    const sampleParentEntity = new aws_iottwinmaker.CfnEntity(
+      this,
+      'SampleParentEntity',
+      {
+        entityName: 'sampleParentEntity1',
+        workspaceId: cdkDemoWorkspace.workspaceId,
+      }
+    );
+
+    // 子エンティティ1
+    new aws_iottwinmaker.CfnEntity(this, 'sampleChildEntity1', {
+      entityName: 'sampleChildEntity1',
+      workspaceId: cdkDemoWorkspace.workspaceId,
+      parentEntityId: sampleParentEntity.entityId,
+    });
+
+    // 子エンティティ2
+    new aws_iottwinmaker.CfnEntity(this, 'sampleChildEntity2', {
+      entityName: 'sampleChildEntity2',
+      workspaceId: cdkDemoWorkspace.workspaceId,
+      parentEntityId: sampleParentEntity.entityId,
+    });
   }
 }

しかし CDK デプロイすると次のようにエンティティが親子関係となりません。フラットな構造になってしまいます。

調査

CLI で確認すると、子としたかったエンティティの parentEntityId が $ROOT となってしまっています。ここに親エンティティの ID が入ってくるのが正しそうです。

$ aws iottwinmaker list-entities --workspace-id CdkDemoWorkspace --region us-east-1
{
    "entitySummaries": [
        {
            "entityId": "29abd18f-b04c-421c-92a7-710d1eb205f8",
            "entityName": "sampleChildEntity2",
            "arn": "arn:aws:iottwinmaker:us-east-1:XXXXXXXXXXXX:workspace/CdkDemoWorkspace/entity/29abd18f-b04c-421c-92a7-710d1eb205f8",
            "parentEntityId": "$ROOT",
            "status": {
                "state": "ACTIVE",
                "error": {}
            },
            "description": "",
            "hasChildEntities": false,
            "creationDateTime": 1695656935.714,
            "updateDateTime": 1695656935.714
        },
        {
            "entityId": "7a2c5248-d054-47f0-bbe4-9c267825c20f",
            "entityName": "sampleChildEntity1",
            "arn": "arn:aws:iottwinmaker:us-east-1:XXXXXXXXXXXX:workspace/CdkDemoWorkspace/entity/7a2c5248-d054-47f0-bbe4-9c267825c20f",
            "parentEntityId": "$ROOT",
            "status": {
                "state": "ACTIVE",
                "error": {}
            },
            "description": "",
            "hasChildEntities": false,
            "creationDateTime": 1695656935.609,
            "updateDateTime": 1695656935.609
        },
        {
            "entityId": "60b8e75b-b164-43f0-a63f-b6292bb25f97",
            "entityName": "sampleParentEntity1",
            "arn": "arn:aws:iottwinmaker:us-east-1:XXXXXXXXXXXX:workspace/CdkDemoWorkspace/entity/60b8e75b-b164-43f0-a63f-b6292bb25f97",
            "parentEntityId": "$ROOT",
            "status": {
                "state": "ACTIVE",
                "error": {}
            },
            "description": "",
            "hasChildEntities": false,
            "creationDateTime": 1695656938.209,
            "updateDateTime": 1695656938.209
        }
    ]
}

そこで親エンティティに明示的に entityId を指定してみました。

$ git diff
diff --git a/lib/cdk-sample-stack.ts b/lib/cdk-sample-stack.ts
index 5b9c263..0e6ce2e 100644
--- a/lib/cdk-sample-stack.ts
+++ b/lib/cdk-sample-stack.ts
@@ -96,6 +96,7 @@ export class CdkSampleStack extends Stack {
       'SampleParentEntity1',
       {
         entityName: 'sampleParentEntity1',
+        entityId: 'sampleParentEntity1',
         workspaceId: cdkDemoWorkspace.workspaceId,
       }
     );

再度 CDK デプロイすると、親エンティティの entityID は指定されましたが、子としたかったエンティティの parentEntityId が、一つが $ROOT となり、もう一つにはちゃんと親エンティティの ID が設定されました。

$ aws iottwinmaker list-entities --workspace-id CdkDemoWorkspace --region us-east-1
{
    "entitySummaries": [
        {
            "entityId": "85ecbdeb-870d-4e06-ae04-297602b63166",
            "entityName": "sampleChildEntity2",
            "arn": "arn:aws:iottwinmaker:us-east-1:XXXXXXXXXXXX:workspace/CdkDemoWorkspace/entity/85ecbdeb-870d-4e06-ae04-297602b63166",
            "parentEntityId": "sampleParentEntity1",
            "status": {
                "state": "ACTIVE",
                "error": {}
            },
            "description": "",
            "hasChildEntities": false,
            "creationDateTime": 1695748643.963,
            "updateDateTime": 1695748643.963
        },
        {
            "entityId": "sampleParentEntity1",
            "entityName": "sampleParentEntity1",
            "arn": "arn:aws:iottwinmaker:us-east-1:XXXXXXXXXXXX:workspace/CdkDemoWorkspace/entity/sampleParentEntity1",
            "parentEntityId": "$ROOT",
            "status": {
                "state": "ACTIVE",
                "error": {}
            },
            "description": "",
            "hasChildEntities": true,
            "creationDateTime": 1695748639.265,
            "updateDateTime": 1695748639.265
        },
        {
            "entityId": "23d5012e-dfdd-4235-98f9-1abf3348f11d",
            "entityName": "sampleChildEntity1",
            "arn": "arn:aws:iottwinmaker:us-east-1:XXXXXXXXXXXX:workspace/CdkDemoWorkspace/entity/23d5012e-dfdd-4235-98f9-1abf3348f11d",
            "parentEntityId": "sampleParentEntity1",
            "status": {
                "state": "ACTIVE",
                "error": {}
            },
            "description": "",
            "hasChildEntities": false,
            "creationDateTime": 1695748643.96,
            "updateDateTime": 1695748643.96
        }
    ]
}

親エンティティと子エンティティのリソースの依存関係が無いことによる事象のようです。これは L1 Construct あるあるですね。

解決

次のように、親エンティティの ID を指定し、かつ子エンティティのリソースの依存関係を明示することで、親子関係を持つエンティティを作成することができました。

lib/cdk-sample-stack.ts

    // 親エンティティ
    const sampleParentEntity = new aws_iottwinmaker.CfnEntity(
      this,
      'SampleParentEntity',
      {
        entityName: 'sampleParentEntity1',
        entityId: 'sampleParentEntity1',
        workspaceId: cdkDemoWorkspace.workspaceId,
      }
    );

    // 子エンティティ1
    const sampleChildEntity1 = new aws_iottwinmaker.CfnEntity(
      this,
      'sampleChildEntity1',
      {
        entityName: 'sampleChildEntity1',
        workspaceId: cdkDemoWorkspace.workspaceId,
        parentEntityId: sampleParentEntity.entityId,
      }
    );

    // 子エンティティ2
    const sampleChildEntity2 = new aws_iottwinmaker.CfnEntity(
      this,
      'sampleChildEntity2',
      {
        entityName: 'sampleChildEntity2',
        workspaceId: cdkDemoWorkspace.workspaceId,
        parentEntityId: sampleParentEntity.entityId,
      }
    );

    sampleChildEntity1.addDependency(sampleParentEntity);
    sampleChildEntity2.addDependency(sampleParentEntity);

CDK デプロイすると、次のように子としたかったエンティティの parentEntityId に親エンティティの ID が設定されました。

$ aws iottwinmaker list-entities --workspace-id CdkDemoWorkspace --region us-east-1
{
    "entitySummaries": [
        {
            "entityId": "sampleParentEntity1",
            "entityName": "sampleParentEntity1",
            "arn": "arn:aws:iottwinmaker:us-east-1:XXXXXXXXXXXX:workspace/CdkDemoWorkspace/entity/sampleParentEntity1",
            "parentEntityId": "$ROOT",
            "status": {
                "state": "ACTIVE",
                "error": {}
            },
            "description": "",
            "hasChildEntities": true,
            "creationDateTime": 1695657553.246,
            "updateDateTime": 1695657553.246
        },
        {
            "entityId": "6042e94d-1704-495d-b01e-3accb88ab9bd",
            "entityName": "sampleChildEntity2",
            "arn": "arn:aws:iottwinmaker:us-east-1:XXXXXXXXXXXX:workspace/CdkDemoWorkspace/entity/6042e94d-1704-495d-b01e-3accb88ab9bd",
            "parentEntityId": "sampleParentEntity1",
            "status": {
                "state": "ACTIVE",
                "error": {}
            },
            "description": "",
            "hasChildEntities": false,
            "creationDateTime": 1695657555.163,
            "updateDateTime": 1695657555.163
        },
        {
            "entityId": "296a46e9-7d4a-4825-92cc-561ae0325c5a",
            "entityName": "sampleChildEntity1",
            "arn": "arn:aws:iottwinmaker:us-east-1:XXXXXXXXXXXX:workspace/CdkDemoWorkspace/entity/296a46e9-7d4a-4825-92cc-561ae0325c5a",
            "parentEntityId": "sampleParentEntity1",
            "status": {
                "state": "ACTIVE",
                "error": {}
            },
            "description": "",
            "hasChildEntities": false,
            "creationDateTime": 1695657555.222,
            "updateDateTime": 1695657555.222
        }
    ]
}

コンソールからも親子関係となっていることが確認できます。

結論としてここまで試した設定は両方とも行う必要がありました。依存関係の追加のみの場合は、parentEntityId が $ROOT となります。逆に entityId の明示的な指定のみの場合は CDK デプロイで子エンティティが先に作成される動作となり Unable to find parent entity id sampleParentEntity1 in workspace CdkDemoWorkspace.というエラーとなります。

おわりに

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

ちなみに今回ハマった事象は L1 Construct であればどれでも起きうるため、ワークスペースおよびシーンでも発生する可能性はあります。発生時は同様の対処を行うか、予防的に依存関係を設定しておくと良いかと思います。依存関係を自動的に解決してよろしくやってくれる L2 Construct が恋しくなりますね。

以上