AWS CDKでもリソース生成の順番が保証されないことがある件

こんにちは。 エキサイト株式会社で内定者アルバイトをしています。

今回は、AWS CDKの勉強をしていてハマった、リソース生成の順番が保証されないことがある件についてまとめます。

発生したエラー

コード

今回は複数のアベイラビリティゾーンに一つずつDBインスタンスをおくようなAWS CDKアプリケーションを作成していました。

// importは省略

public class SampleDbStack extends Stack {
    public SampleDbStack(final Construct scope, final String id) {
        this(scope, id, null);
    }

    public SampleDbStack(final Construct scope, final String id, final StackProps props) {
        super(scope, id, props);
        
        // VPCやパブリックサブネットの定義は省略
        
        // Subnet
        Subnet privateSubnetA = PrivateSubnet.Builder.create(this, "PrivateSubnetA")
                .availabilityZone("ap-northeast-1a")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidrBlcok")
                .build();

        // Subnet
        Subnet privateSubnetB = PrivateSubnet.Builder.create(this, "PrivateSubnetB")
                .availabilityZone("ap-northeast-1c")
                .vpcId(vpc.getVpcId())
                .cidrBlock("cidrBlcok")
                .build();

        // RDS
        // DBインスタンス用のサブネットグループ
        CfnDBSubnetGroup dbSubnetGroup = CfnDBSubnetGroup.Builder.create(this, "DBSubnetGroup")
                .dbSubnetGroupDescription("subnet-group-for-rds")
                .subnetIds(List.of(privateSubnetA.getSubnetId(), privateSubnetB.getSubnetId()))
                .dbSubnetGroupName("sample-db-subnet-group")
                .build();

        CfnDBCluster dbCluster = CfnDBCluster.Builder.create(this, "DBCluster")
                .engine("aurora-mysql")
                .engineMode("provisioned")
                .databaseName("sampledb")
                .dbClusterIdentifier("sample-db-cluster")
                .engineVersion("5.7.mysql_aurora.2.10.2")
                .masterUsername("iammaster")
                .masterUserPassword("idontknow")
                .port(3306)
                .preferredMaintenanceWindow("wed:20:15-wed:20:45")
                .preferredBackupWindow("19:30-20:00")
                .storageEncrypted(true)
                .deletionProtection(true)
                .enableIamDatabaseAuthentication(false)
                .vpcSecurityGroupIds(List.of(vpcSecurityGroup.getSecurityGroupId()))
                .dbSubnetGroupName(dbSubnetGroup.getDbSubnetGroupName())
                .build();

        // Primary DB instance
        CfnDBInstance dbInstancePrimary = CfnDBInstance.Builder.create(this, "DBInstancePrimary")
                .dbInstanceClass("db.r5.large")
                .autoMinorVersionUpgrade(true)
                .availabilityZone("ap-northeast-1a")
                .dbClusterIdentifier(dbCluster.getDbClusterIdentifier())
                .dbInstanceIdentifier("sample-db-01")
                .dbSubnetGroupName(dbSubnetGroup.getDbSubnetGroupName())
                .engine("aurora-mysql")
                .preferredMaintenanceWindow("wed:19:35-wed:20:05")
                .build();
        
        // Replica DB instance
        CfnDBInstance dbInstanceReplica = CfnDBInstance.Builder.create(this, "DBInstanceReplica")
                .dbInstanceClass("db.r5.xlarge")
                .autoMinorVersionUpgrade(true)
                .availabilityZone("ap-northeast-1c")
                .sourceDbInstanceIdentifier(dbInstancePrimary.getSourceDbInstanceIdentifier())
                .dbClusterIdentifier(dbCluster.getDbClusterIdentifier())
                .dbInstanceIdentifier("sample-db-02")
                .dbSubnetGroupName(dbSubnetGroup.getDbSubnetGroupName())
                .engine("aurora-mysql")
                .preferredMaintenanceWindow("wed:19:35-wed:20:05")
                .build();
    }
}

エラー内容

このスタックをアプリケーションで生成し、AWSにデプロイするために

cdk synth
cdk deploy

を実行すると、以下のようなエラーが発生しました。

SampleDbStack: creating CloudFormation changeset...
14:51:43 | CREATE_FAILED        | AWS::RDS::DBInstance                  | DBInstancePrimary
Resource handler returned message: "DBSubnetGroup 'sample-db-subnet-group' not found. (Service: Rds, Status Code: 404, Request ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)" (RequestToken: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, HandlerErrorCode: NotFound)

14:51:44 | CREATE_FAILED        | AWS::RDS::DBInstance                  | DBInstanceReplica
Resource handler returned message: "DBSubnetGroup 'sample-db-subnet-group' not found. (Service: Rds, Status Code: 404, Request ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)" (RequestToken: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, HandlerErrorCode: NotFound)

エラーメッセージを解読すると、DBInstancePrimaryDBInstanceReplicaにおいて .dbSubnetGroupName(dbSubnetGroup.getDbSubnetGroupName())で参照しているサブネットグループ sample-db-subnet-groupが見つからないとのこと。

どちらもsample-db-subnet-groupよりも後に記述しているのに...。

参照の方法が違うのか?名前に使ってはならない文字が含まれているのか?といろいろ考えましたが、どれも原因ではないようで...。

解決方法

結論から申し上げますと、Sさんのご指摘もあり、どうやらAWS CDKであっても明示的に指定しないと実行順序が担保されないことがあるということがわかりました。

今回の例だと、DBクラスタやDBインスタンスの作成に必要なサブネットグループが作成されるよりも前に、DBクラスタを作成しようとしたことで、サブネットグループを参照できないことが原因でした。

以下の公式ドキュメントによるとa.addDependsOn(b)abに依存していることを明示的に指定できるようです。

aws-cdk-lib module · AWS CDK

そこで、以下のようなコードを加えました。

        dbCluster.addDependsOn(dbSubnetGroup);

        dbInstancePrimary.addDependsOn(dbCluster);

        dbInstanceReplica.addDependsOn(dbCluster);
        dbInstanceReplica.addDependsOn(dbInstancePrimary);

DBクラスタを作成する前にサブネットグループが作成されていなければならないという依存関係、DBインスタンスが作成される前にDBクラスタが作成されていなければならないという依存関係、レプリカのDBインスタンスが作成される前にプライマリのDBインスタンスが作成されていなければならないという依存関係を追加しました。これらはそれぞれ、リソース作成の際に依存元のリソースを参照しているためです。

これで、再び

cdk synth
cdk deploy

を実行すると...

無事、AWSにデプロイできました!!

念のため、AWSのmanagement consoleを確認してみると...

データベースが作成されていました。

Sさんたまんねえ!

補足1

AWS CloudFormationをご存知の方は、cdk synthを実行することで表示されるテンプレートを確認すると、

  DBInstanceReplica:
    ~~~省略~~~
    DependsOn:
      - DBCluster
      - DBInstancePrimary
    Metadata:
      aws:cdk:path: SampleDbStack/DBInstanceReplica

とあり、テンプレートのDependsOnaddDependsOn()が対応していることがわかるかと思います。

補足2

addDependsOn()メソッドはL1 construct(最も低レベルなコンストラクトで先頭にCfnがついているもの)にしかなく、L2 construct(L1 constructをカプセル化し、適切なデフォルト値やセキュリティポリシーを提供してくれる)やL3 construct(特定のユースケースのために複数のリソースを宣言できる)にはありません。また、addDependsOn()メソッドの引数に指定できる依存先もL1 constructのみです。

これはあくまで推測ですが、L1 constructは「AWS CloudFormationによって定義されたリソースに直接対応している」そうなので、テンプレートと同じ設定が必要になるからかと思います。

ちなみに、DBクラスタやRDSを作成するだけならL2 constructを使用することができ、多くの場合はこちらの方が容易ですが、詳細な設定が必要な場合にはL1 constructを使う方がよさそうです。

終わりに

今回は、AWS CDKであっても(L1 constructでは)明示的に依存関係を指定しないとデプロイがうまくいかないことがあるという内容の記事でした。

まだまだAWS CDKも勉強を始めたばかりでハマりポイントも多くあると思いますが、引き続き勉強を頑張っていきたいと思います。

では、また次回。

参考文献