継続は力なり

タイトル通り定期的な更新を心掛けるブログです。

AWS CDK の S3 バケットの振る舞いを理解して開発スピードをあげよう!

タダです.

CDK のS3 を作ってみてリソースを扱う時に注意すべきことがあると学んだのでその結果を記事にまとめます.

S3 バケットを作るコードで生成される CloudFormation テンプレートの確認

S3 だけを作る TypeScript のコードを書くとして以下のようなコードになります.

import * as cdk from '@aws-cdk/core';
import s3 = require('@aws-cdk/aws-s3');

export class Mks3Stack extends cdk.Stack {
  
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const cdkS3TestBucket = new s3.Bucket(this, 'CdkS3TestBucket');
  }
}

cdk synth で CloudFormation テンプレートで出してみるとわかりますが,UpdateReplacePolicyDeletionPolicy の値が Retain になっています.つまり,この2つのパラメーターが Retain であることはリソースのスタック更新時や削除する時にリソースが残り続ける状態というステータスです.開発中にバケットを作って消してを繰り返したい時はこの設定は手間が増えてしまいます.

Resources:
  CdkS3TestBucketD2F17ADE:
    Type: AWS::S3::Bucket
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
    Metadata:
      aws:cdk:path: Mks3Stack/CdkS3TestBucket/Resource
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Modules: aws-cdk=1.22.0,@aws-cdk/aws-events=1.21.1,@aws-cdk/aws-iam=1.21.1,@aws-cdk/aws-kms=1.21.1,@aws-cdk/aws-s3=1.21.1,@aws-cdk/core=1.21.1,@aws-cdk/cx-api=1.21.1,@aws-cdk/region-info=1.21.1,jsii-runtime=node.js/v13.2.0
    Condition: CDKMetadataAvailable
Conditions:
  CDKMetadataAvailable:
    Fn::Or:
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-northeast-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-northeast-2
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-southeast-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-southeast-2
          - Fn::Equals:
              - Ref: AWS::Region
              - ca-central-1
          - Fn::Equals:
              - Ref: AWS::Region
              - cn-north-1
          - Fn::Equals:
              - Ref: AWS::Region
              - cn-northwest-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-central-1
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-north-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-2
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-3
          - Fn::Equals:
              - Ref: AWS::Region
              - me-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - sa-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-east-2
          - Fn::Equals:
              - Ref: AWS::Region
              - us-west-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-west-2

関連 ドキュメント

スタック削除時の動作の確認

試しに cdk destroy を実行してみると CloudForamtion スタックは消えるが S3 バケットの削除がスキップされているのがコンソール画面で確認できます.

cdk destroy                             
Are you sure you want to delete: Mks3Stack (y/n)? y
Mks3Stack: destroying...

 ✅  Mks3Stack: destroyed

コンソール画面 f:id:sadayoshi_tada:20200128222326p:plain

スタック削除時にバケットを削除したい時

スタック削除時にバケットを削除したい時は removalPolicy で定義します.

import * as cdk from '@aws-cdk/core';
import s3 = require('@aws-cdk/aws-s3');

export class Mks3Stack extends cdk.Stack {
  
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here
    const cdkS3TestBucket = new s3.Bucket(this, 'CdkS3TestBucket',{
      removalPolicy: cdk.RemovalPolicy.DESTROY
    });
  }
}

cdk synth で CloudFormation テンプレートを出してみると,UpdateReplacePolicyDeletionPolicy の値が Delete に変更されました.

Resources:
  CdkS3TestBucketD2F17ADE:
    Type: AWS::S3::Bucket
    UpdateReplacePolicy: Delete <= 変化
    DeletionPolicy: Delete <= 変化
    Metadata:
      aws:cdk:path: Mks3Stack/CdkS3TestBucket/Resource
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Modules: aws-cdk=1.22.0,@aws-cdk/aws-events=1.21.1,@aws-cdk/aws-iam=1.21.1,@aws-cdk/aws-kms=1.21.1,@aws-cdk/aws-s3=1.21.1,@aws-cdk/core=1.21.1,@aws-cdk/cx-api=1.21.1,@aws-cdk/region-info=1.21.1,jsii-runtime=node.js/v13.2.0
    Condition: CDKMetadataAvailable
Conditions:
  CDKMetadataAvailable:
    Fn::Or:
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-northeast-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-northeast-2
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-southeast-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-southeast-2
          - Fn::Equals:
              - Ref: AWS::Region
              - ca-central-1
          - Fn::Equals:
              - Ref: AWS::Region
              - cn-north-1
          - Fn::Equals:
              - Ref: AWS::Region
              - cn-northwest-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-central-1
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-north-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-2
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-3
          - Fn::Equals:
              - Ref: AWS::Region
              - me-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - sa-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-east-2
          - Fn::Equals:
              - Ref: AWS::Region
              - us-west-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-west-2

関連ドキュメント

docs.aws.amazon.com

docs.aws.amazon.com

スタック削除時の動作の確認

cdk destroy を実行してみると今度は CloudForamtion スタックも S3 バケットも削除されたのをコンソールで確認できました.

cdk destroy
Are you sure you want to delete: Mks3Stack (y/n)? y
Mks3Stack: destroying...
   0 | 22:27:27 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack | Mks3Stack User Initiated
   0 | 22:27:30 | DELETE_IN_PROGRESS   | AWS::S3::Bucket    | CdkS3TestBucket (CdkS3TestBucketD2F17ADE) 
   0 | 22:27:30 | DELETE_IN_PROGRESS   | AWS::CDK::Metadata | CDKMetadata 
   1 | 22:27:31 | DELETE_COMPLETE      | AWS::S3::Bucket    | CdkS3TestBucket (CdkS3TestBucketD2F17ADE) 
   2 | 22:27:32 | DELETE_COMPLETE      | AWS::CDK::Metadata | CDKMetadata 

 ✅  Mks3Stack: destroyed

コンソール画面 f:id:sadayoshi_tada:20200128222858p:plain

まとめ

CDK で S3 の振る舞いについて書きました.ツールの振る舞いを理解して開発スピードをあげることに繋げられるようにしていきたいですね.この他にも CDK での理解したことはブログに書いていきます!

可変になっている ALB の IP を AWS CLI で確認する方法

タダです.

ALB の IP アドレスは固定ではなく、可変になっています.そのため,現在の ALB の IP アドレスを知りたい場合は都度確認する必要があります.業務で ALB の IP アドレスを確認する機会があったので確認方法をまとめておきます.

ALB の IP アドレスの仕組み

ALB の IP アドレスは ENI(仮想 NIC)で付与されます.ALB の NIC 情報は ENI の説明タブ Attachment owner の欄にamazon-elbとなっているのがの特徴です.ENI の画面から確認したい対象の ALB 名を検索窓に入れれば IP アドレスを確認できますが,GUI ではなくすぐ手元で確認したい!なんてこともあると思います.そんな時向けの AWS CLI コマンド例を紹介します.

f:id:sadayoshi_tada:20200124232837p:plain

AWS CLI での ENI および IP アドレス情報の確認

今回使うのが describe-network-interfaces コマンドです.これは ENI 情報を参照することが可能なコマンドになっています.

docs.aws.amazon.com

--filters で ALB の NIC のみに絞り込んだ出力結果が以下になります.IP アドレス以外にもたくさんの情報が含まれているため IP アドレス情報のみに絞り込んでみます.

aws ec2 describe-network-interfaces --filters Name=attachment.instance-owner-id,Values=amazon-elb
{
    "NetworkInterfaces": [
        {
            "Status": "in-use",
            "MacAddress": "0a:9f:c3:f3:55:f4",
            "SourceDestCheck": true,
            "VpcId": "vpc-XXXX",
            "Description": "ELB app/test/1e507bac7c2472cb",
            "Association": {
                "PublicIp": "52.197.170.100",
                "PublicDnsName": "ec2-52-197-170-100.ap-northeast-1.compute.amazonaws.com",
                "IpOwnerId": "amazon-elb"
            },
            "NetworkInterfaceId": "eni-XXXX",
            "PrivateIpAddresses": [
                {
                    "PrivateDnsName": "ip-172-31-15-188.ap-northeast-1.compute.internal",
                    "Association": {
                        "PublicIp": "52.197.170.100",
                        "PublicDnsName": "ec2-52-197-170-100.ap-northeast-1.compute.amazonaws.com",
                        "IpOwnerId": "amazon-elb"
                    },
                    "Primary": true,
                    "PrivateIpAddress": "172.31.15.188"
                }
            ],
            "RequesterManaged": true,
            "Groups": [
                {
                    "GroupName": "XXXX",
                    "GroupId": "sg-XXXX"
                }
            ],
            "PrivateDnsName": "ip-172-31-15-188.ap-northeast-1.compute.internal",
            "AvailabilityZone": "ap-northeast-1c",
            "InterfaceType": "interface",
            "Attachment": {
                "Status": "attached",
                "DeviceIndex": 1,
                "AttachTime": "2020-01-23T18:39:10.000Z",
                "DeleteOnTermination": false,
                "AttachmentId": "eni-attach-XXXX",
                "InstanceOwnerId": "amazon-elb"
            },
            "RequesterId": "amazon-elb",
            "Ipv6Addresses": [],
            "SubnetId": "subnet-XXXX",
            "OwnerId": "XXXXXXXXXXXXXXXX,
            "TagSet": [],
            "PrivateIpAddress": "172.31.15.188"
        }
    ]
}

取得したい IP アドレス別のコマンド実行例

パブリック IP アドレスとプライベート IP アドレスそれぞれ確認するコマンド例は以下の通りです.

パブリックIPアドレスを特定

aws ec2 describe-network-interfaces --filters Name=description,Values="ELB <ロードバランサー ID>" --query 'NetworkInterfaces[*].Association.PublicIp' --output text

52.197.170.100

プライベートIPアドレスを特定

aws ec2 describe-network-interfaces --filters Name=description,Values="ELB <ロードバランサー ID>" --query 'NetworkInterfaces[*].PrivateIpAddresses[*].PrivateIpAddress' --output text

172.31.15.188

なお,ロードバランサー ID は describe-load-balancers コマンドで確認することが可能です.ALB の場合は app/ロードバランサー名/乱数 になっているので,awk コマンドと組み合わせてロードバランサー ID のみ抽出したのが以下になります.

aws elbv2 describe-load-balancers --names <ロードバランサー名> --query 'LoadBalancers[*].LoadBalancerArn' --output text | awk '{print substr($0,index($0, "app"))}'

app/test/1e507bac7c2472cb

何度も確認することがありそうならシェルスクリプト化してしまうと良いでしょう.ALB 名を引数に与えればプライベート IP アドレスを出力するスクリプトも最後に書いてみました.

#!/bin/sh

ALB_NAME=$1
ELB_ID=`aws elbv2 describe-load-balancers --names $ELB_NAME --query 'LoadBalancers[*].LoadBalancerArn' --output text | awk '{print substr($0,index($0, "app"))}'`
aws ec2 describe-network-interfaces --filters Name=description,Values="ELB ${ELB_ID}" --query 'NetworkInterfaces[*].PrivateIpAddresses[*].PrivateIpAddress' --output text

スクリプトを実行してみると,以下のような結果が返ってきます.

./スクリプト名.sh <ロードバランサー名>

172.31.15.188

まとめ

AWS CLI による ALB の IP アドレスを確認する手法を書きました.dig コマンドでも IP アドレスを確認できますが,パブリックのアドレスのみでプライベートのアドレスは返してくれないので AWS CLI であれば素早く情報を得られます.運用において AWS CLI の出番が多く助けられます.

RDS の SSL/TLS 証明書を AWS CLI でローテーションする

タダです.

RDS および Aurora で使用している SSL/TLS 証明書が rds-ca-2015 を使っている場合は 3/5までにrds-ca-2019 に切り替える必要があります.今回は AWS CLI で RDS の証明書を切り替える検証を行なったので同様の対応を検討している方の参考になれば嬉しいです. sadayoshi-tada.hatenablog.com

証明書の切り替えで使うコマンドについて

証明書を切り替えるために使ったコマンドは以下のコマンドです.

  • describe-db-instances
  • describe-pending-maintenance-actions
  • modify-db-instance

コマンドの利用詳細

それぞれのコマンドの利用の詳細を順番で書きます.

1, describe-db-instancesrds-ca-2015インスタンスを確認します.

aws rds describe-db-instances --query 'DBInstances[?CACertificateIdentifier==`rds-ca-2015`].{Name:DBInstanceIdentifier}' --output table
---------------------
|DescribeDBInstances|
+-------------------+
|       Name        |
+-------------------+
|  database-1       |
+-------------------+

2, describe-pending-maintenance-actions で保留中のメンテナンスの有無を確認します.

aws rds describe-pending-maintenance-actions                    
{
    "PendingMaintenanceActions": [
        {
            "ResourceIdentifier": "arn:aws:rds:ap-northeast-1:XXXXXXXX:db:database-1",
            "PendingMaintenanceActionDetails": [
                {
                    "Action": "ca-certificate-rotation",
                    "AutoAppliedAfterDate": "2020-02-04T22:03:06Z",
                    "CurrentApplyDate": "2020-02-04T22:03:06Z",
                    "Description": "Rotation of CA certificate"
                }
            ]
        }
    ]
}

AWS のブログによると 2/5~3/5 に証明書が切り替わって再起動すれば証明書が有効な状態になります.上記の保留中のメンテナンスはこの証明書切り替えのメンテンスとなります.このメンテナンスが表示されているうちは証明書切り替えが完了していないことになります.

RDS、Aurora、DocumentDB 用の SSL/TLS 証明書が失効し、メンテナンスおよびセキュリティ管理に関する当社の標準規律に則った 5 年に 1 度の置き換えが行われます。それに関して、重要なスケジュールを次に明記しておきます。

2019 年 9 月 19 日 ー CA-2019 証明書が利用可能になりました。

2020 年 1 月 14 日 ー この日以降、作成されるインスタンスは、新しい証明書 (CA-2019) を使用するようになります。必要性がある場合は、一時的に古い証明書に戻すこともできます。

2020 年 2 月 5 日~3 月 5 日 ー RDS において、既存インスタンスに対する新しい証明書の (インストールのみで有効化はしない) ステージングが行われます。インスタンスを再起動することで、この証明書が有効化されます。

2020 年 3 月 5 日 ー CA-2015 が失効します。証明書の検証を使用しているアプリケーションで更新を行っていないものは、接続ができなくなります。

aws.amazon.com

3, modify-db-instance で証明書を切り替える

いよいよ証明書を切り替えます.SSL 接続を行う場合はインスタンスクラスターの再起動が発生しますが,SSL 接続を行なっていない場合は再起動不要な AWS CLI オプション --no-certificate-rotation-restart もあるので,AWS のブログで紹介されたコマンドを試してみました.コマンドを実行後に describe-pending-maintenance-actions で確認したらまだ保留中のメンテナンスが残っています.つまり,rds-ca-2019 への変更が保留中となっている状態になっていてまだ証明書の切り替えは完了していない状態と言えます.

 aws rds modify-db-instance --db-instance-identifier database-1 --ca-certificate-identifier rds-ca-2019 --no-certificate-rotation-restart
{
    "DBInstance": {
        "DBInstanceIdentifier": "database-1",
        "DBInstanceClass": "db.t2.micro",
        "Engine": "mysql",
        "DBInstanceStatus": "available",
        ~中略~
        "PreferredMaintenanceWindow": "thu:15:50-thu:16:20",
        "PendingModifiedValues": {
            "CACertificateIdentifier": "rds-ca-2019"
        },
        ~中略~
        "CACertificateIdentifier": "rds-ca-2015",
        ~中略~
    }
}

aws rds describe-pending-maintenance-actions               
{
    "PendingMaintenanceActions": [
        {
            "ResourceIdentifier": "arn:aws:rds:ap-northeast-1:XXXXXXXX:db:database-1",
            "PendingMaintenanceActionDetails": [
                {
                    "Action": "ca-certificate-rotation",
                    "AutoAppliedAfterDate": "2020-02-04T22:03:06Z",
                    "CurrentApplyDate": "2020-02-04T22:03:06Z",
                    "Description": "Rotation of CA certificate"
                }
            ]
        }
    ]
}

GUI で証明書を切り替えを行う場合もこの変更をすぐに適用するか,メンテナンスウィンドウで適用かを指定しています.そのため, 変更をすぐに適用する --apply-immediately オプションを指定して再度実行しました.describe-pending-maintenance-actions で再度確認したら保留中のメンテナンスの表示が消えたので証明書切り替え完了です.

aws rds modify-db-instance --db-instance-identifier database-1 \
  --ca-certificate-identifier rds-ca-2019 --no-certificate-rotation-restart --apply-immediately
{
    "DBInstance": {
        "DBInstanceIdentifier": "database-1",
        "DBInstanceClass": "db.t2.micro",
        "Engine": "mysql",
        "DBInstanceStatus": "available",
        ~中略~
        "PreferredMaintenanceWindow": "thu:15:50-thu:16:20",
        "PendingModifiedValues": {
            "CACertificateIdentifier": "rds-ca-2019"
        },
        ~中略~
        "CACertificateIdentifier": "rds-ca-2015",
        ~中略~
    }
}

aws rds describe-pending-maintenance-actions
{
    "PendingMaintenanceActions": []
}

念のため GUI でも確認しても証明書が rds-ca-2019 に切り替わっていることが確認できました. f:id:sadayoshi_tada:20200117214821p:plain

まとめ

CA 証明書切り替えを AWS CLI で動作を確認したのでその内容をまとめました.--no-certificate-rotation-restartオプションを使うまでこのオプションを使えば証明書が切り替わっていると勘違いしていたので確認ができてよかったです.改めて検証の大切さを感じました.

『PowerUserAccess』を使わず EC2 や Lambda の IAM を制御するポリシーサンプル

タダです.

開発期間中に開発者の権限としてAWS 管理ポリシー「PowerUserAccess」を設定することがあると思います.ただ,「PowerUserAccess」の詳細を確認すると IAM の操作がNotActionで不許可にされています.

PowerUserAccess」の詳細

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "NotAction": [
                "iam:*",
                "organizations:*",
                "account:*"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:CreateServiceLinkedRole",
                "iam:DeleteServiceLinkedRole",
                "iam:ListRoles",
                "organizations:DescribeOrganization",
                "account:ListRegions"
            ],
            "Resource": "*"
        }
    ]
}

EC2 や Lambda などのサービスでは IAM ロールを作り他のサービスと連携しますが,「PowerUserAccess」を設定すると IAM ロールや ポリシーの作成や付与ができません.AWS の管理者としては開発スピードを落とさず,IAM は必要最低限の権限を付与させたいです.今回は,EC2 と Lambda の IAM ロール・ポリシーを作成,付与する場合の IAM ポリシーを検証したので設定例を紹介します.

EC2 の IAM ポリシー例

EC2 の IAM ロール・ポリシー作成,付与のための IAM ポリシー例が以下になります.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "iam:CreateRole",
                "iam:CreatePolicy",
                "iam:PutRolePolicy",
                "iam:AttachRolePolicy",
                "iam:PassRole",
                "iam:List*",
                "iam:Get*",
                "iam:CreateInstanceProfile",
                "iam:AddRoleToInstanceProfile"
            ],
            "Resource": "*"
        }
    ]
}

肝となるのが iam:CreateInstanceProfileiam:AddRoleToInstanceProfile になります.EC2 の IAM ロールには インスタンスプロファイルを使用しますので,インスタンスプロファイルを作り(iam:CreateInstanceProfile)IAM ロールにインスタンスプロファイルを追加する( iam:AddRoleToInstanceProfile ) が必要です.

Lambda の IAM ポリシー例

次に, Lambda の IAM ロール・ポリシー作成,付与のための IAM ポリシー例が以下になります.カスタムポリシー,AWS 管理ポリシーどちらも Lambda の IAM ロールに適用可能なポリシーとなっています.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "iam:CreateRole",
                "iam:CreatePolicy",
                "iam:PutRolePolicy",
                "iam:AttachRolePolicy",
                "iam:PassRole",
                "iam:List*",
                "iam:Get*"
            ],
            "Resource": "*"
        }
    ]
}

Lambda 関数に綿密なアクセス権の解説ドキュメント

なお,Lambda の IAM ポリシーはドキュメントでも詳しく解説されている部分が少なかったのですが,下記の記事でケースごとにポリシー例が紹介されているので細かな制御を検討する時に参考にするとよいでしょう. aws.amazon.com

まとめ

EC2 と Lambda の IAM ロールやポリシーを作成・付与するための IAM ポリシー例を紹介しました.ユーザーの権限制御が AWS をセキュアに使うために必要な要素の1つです.IAM ポリシーの作成にあたっては実際に操作してみないと意図通りに設定されてるかがわかりにくいと思いますので,こう言ったナレッジは記事化して同じ悩みを持つ方の参考になれば嬉しいです!

『Jest』での『Fine-grained assertions test』と『Validation tests』の実践

タダです.

AWS CDK のテスト手法で前回は「Snapshot tests」を学んだので,今回は「Fine-grained assertions test」と「Validation tests」を学んでいきます.

「Fine-grained assertions test」と「Validation tests」の意義

  • Fine-grained assertions test」とは,CDKで作成したテンプレートの一部をチェックし意図した設定になっているかをテストするための手法です.
    • 例えば,SQS の場合は expect(stack).toHaveResource('AWS::SQS::Queue', {プロパティ}) の記述で検証します.
  • Validation tests」とは,特定のリソースに対する適切な Construct が定義されているかを検証する手法です.一般的なユニットテストにあたります.

上記の2つのテストを前回の SQS リソースを使って実践します.

sadayoshi-tada.hatenablog.com

「Fine-grained assertions test」 の実践

インフラソースコードの準備

以下のSQS デッドレターキューのソースコードに対して「Fine-grained assertions test」を行います.//でコメントしている箇所のみを変更したとします.

import cloudwatch = require('@aws-cdk/aws-cloudwatch');
import sqs = require('@aws-cdk/aws-sqs');
import { Construct, Duration } from '@aws-cdk/core';

export class DeadLetterQueue extends sqs.Queue {
    public readonly messagesInQueueAlarm: cloudwatch.IAlarm;

    // デッドレターキューの保存期間が14日間の定義
    constructor(scope: Construct, id: string) {
        super(scope,id , {
            retentionPeriod: Duration.days(14)
        });
    // SQS の CloudWatch メトリクスで ApproximateNumberOfMessagesVisible を定義
        this.messagesInQueueAlarm = new cloudwatch.Alarm(this, 'Alarm', {
            alarmDescription: 'There are messages in the Dead Letter Queue',
            evaluationPeriods: 1,
            threshold: 1,
            metric: this.metricApproximateNumberOfMessagesVisible(),
            period: Duration.minutes(1),
        });
    }
}

テストコードの準備

2つのテストコードを用意します.1つ目はデッドレターキューの CloudWatch メトリクス ApproximateNumberOfMessagesVisible が設定されているかのテストで,2つ目はデッドレターキューの保存期間が14日間かのテストになります.

import { Stack } from '@aws-cdk/core';
import '@aws-cdk/assert/jest';

import dlq = require('../lib/dead-letter-queue');

test('dlq creates an alarm', () => {
    const stack = new Stack();

    new dlq.DeadLetterQueue(stack, 'DLQ');

    expect(stack).toHaveResource('AWS::CloudWatch::Alarm', {
        MetricName: "ApproximateNumberOfMessagesVisible",
        Namespace: "AWS/SQS",
        Dimensions: [
        {
            Name: "QueueName",
            Value: { "Fn::GetAtt": [ "DLQ581697C4", "QueueName" ] }
        }
        ],
    });
});

test('dlq has maximum retention period', () => {
    const stack = new Stack();

    new dlq.DeadLetterQueue(stack, 'DLQ');

    expect(stack).toHaveResource('AWS::SQS::Queue', {
        MessageRetentionPeriod: 1209600
    });
});

テストコードの実行

テストコードを実行してみると2つとも意図した設定の通りであったためテストが通りました.変更を加えた箇所のみをテストをしたい場合に「Fine-grained assertions test」が有効でしょう.

>> npm run build && npm test

> jest-handson@0.1.0 build /XXX/XXX/awscdk-handson/jest-handson
> tsc


> jest-handson@0.1.0 test /XXX/XXX/awscdk-handson/jest-handson
> jest

 PASS  test/dead-letter-queue.test.ts
  ✓ dlq creates an alarm (69ms)
  ✓ dlq has maximum retention period (30ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.532s
Ran all test suites.

「Validation tests」の実践

インフラソースコードの準備

次に以下のコードに対して「Validation tests」を実践します.SQS のデッドレターキューの保存期間を Construct の Props に渡された値で設定可能なのが最長14日間であるよう定義しています.

import { IAlarm, Alarm } from '@aws-cdk/aws-cloudwatch'
import { Queue } from '@aws-cdk/aws-sqs'
import { Construct, Duration } from '@aws-cdk/core'

export interface DeadLetterQueueProps {
    /**
     * The amount of days messages will live in the dead letter queue
     *
     * Cannot exceed 14 days.
     *
     * @default 14
     */
    retentionDays?: number;
}

export class DeadLetterQueue extends sqs.Queue {
  public readonly messagesInQueueAlarm: cloudwatch.IAlarm;

  constructor(scope: Construct, id: string, props: DeadLetterQueueProps = {}) {
    if (props.retentionDays !== undefined && props.retentionDays > 14) {
      throw new Error('retentionDays may not exceed 14 days');
    }

    super(scope, id, {
        // Given retention period or maximum
        retentionPeriod: Duration.days(props.retentionDays || 14)
    })

    this.messagesInQueueAlarm = new Alarm(this, 'Alarm', {
        alarmDescription: 'There are messages in the Dead Letter Queue',
        evaluationPeriods: 1,
        threshold: 1,
        metric: this.metricApproximateNumberOfMessagesVisible(),
    })
    }
}

テストコードの準備

このテストは CloudFormation テンプレートでリソースにデッドレターキューの保存期間の値を保持しているか,デッドレターキューの保存期間が14日間を超えた場合にエラーが出るかをテストします.

import { Stack } from '@aws-cdk/core';
import '@aws-cdk/assert/jest';

import dlq = require('../lib/dead-letter-queue');

test('retention period can be configured', () => {
    const stack = new Stack();

    new dlq.DeadLetterQueue(stack, 'DLQ', {
        retentionDays: 7
    })
    expect(stack).toHaveResource('AWS::SQS::Queue', {
        MessageRetentionPeriod: 604800,
    })
})

test('configurable retention period cannot exceed 14 days', () => {
    const stack = new Stack()
    expect(() => {
        new dlq.DeadLetterQueue(stack, 'DLQ', {retentionDays: 15})
    }).toThrowError(/retentionDays may not exceed 14 days/)
})

テストコードの実行

テストコードを実行してみると以下のように2つのテストとも通ったことを確認しました.

>> npm run build && npm test

> jest-handson@0.1.0 build /XXX/XXX/awscdk-handson/jest-handson
> tsc


> jest-handson@0.1.0 test /XXX/XXX/awscdk-handson/jest-handson
> jest

 PASS  test/dead-letter-queue.test.ts
  ✓ retention period can be configured (87ms)
  ✓ configurable retention period cannot exceed 14 days (2ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        4.753s, estimated 5s
Ran all test suites.

まとめ

Fine-grained assertions test」と「Validation tests」の意義とテストコードを実践しました.目的に応じて3種類のテストを使い分けて,質の高いインフラのコード開発とデプロイを実践していきたいですね.

関連記事

AWS CDK の他の記事もぜひ!

sadayoshi-tada.hatenablog.com

sadayoshi-tada.hatenablog.com

sadayoshi-tada.hatenablog.com

sadayoshi-tada.hatenablog.com