[アップデート]テンプレートの再利用が簡単に!!CloudFormationでモジュールが利用可能になりました

CloudFormationのテンプレートを再利用するために「モジュール」という選択肢が増えました!!
2020.11.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

CX事業本部@大阪の岩田です。少し間が空いてしまいましたが、11/24付のアップデートによりCloudFormationでモジュールが利用可能になりました。このブログではモジュール機能について簡単に紹介させて頂きます。

概要

モジュールはCloudFormationのテンプレートを再利用するための機能です。CloudFormationのテンプレートをモジュールとしてCloudFormationレジストリに登録しておくことで、別のテンプレートから参照して取り込むことが可能です。

例えば

  • EC2インスタンス
  • セキュリティグループ

を作成するテンプレートAがあるとします。このテンプレートAをモジュールとして登録しておき、テンプレートBから参照することで、テンプレートBにもEC2インスタンスとセキュリティグループの定義が展開されます。テンプレートB側ではモジュールを参照するだけで、EC2インスタンスとセキュリティグループがデプロイできます。このようにモジュールを利用するとリソース構成をモジュール内にカプセル化できるので、モジュール利用側はリソースの詳細なプロパティやベストプラクティスについて理解する必要はありません。そのため

  • ネットワークの専門家がVPC等のリソースを構築するモジュールを作成しておく
  • 開発者は登録済みのモジュールを利用してネットワーク関連のリソースを作成する

といった運用が可能です。

また、モジュール利用側のテンプレートとモジュールの間では

  • Fn::RefFn::Subを利用してリソースの情報を参照する(※利用側のテンプレートからモジュールを参照することも、その逆も両方可能)
  • 利用側のテンプレートからモジュール側のテンプレートにParametersを渡す

といったことも可能で、モジュールを利用するテンプレートを柔軟に構築できます。

公式ドキュメントではモジュールのメリットとして

  • 予測可能性
  • 再利用性
  • トレーサビリティ
  • 管理性

といったメリットが紹介されています。

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/modules.html

やってみる

それでは早速モジュールを試してみましょう。以下AWSブログの手順に沿って進めていきます。

Introducing AWS CloudFormation modules

モジュールの作成と登録にはCloudFormation CLIを利用するので、pip等を利用して事前にインストールしておきましょう。私の環境では以下のコマンドを利用しました。

$pip install cloudformation-cli-python-plugin

モジュールの作成とCloudFormationレジストリへの登録

まずCloudFormation CLIを利用してプロジェクトの雛形を作成します、

$cfn init

コマンドを実行するとTUIでいくつかの入力が要求されます。まず開発対象(リソース/モジュール)の選択が要求されるので、モジュールを意味するmを入力します。続いてモジュール名を聞かれるので、適当なモジュール名を入力します。今回はClassmethod::S3::Bucket::MODULEというモジュール名にしました。モジュール名は::MODULEで終わる必要があるので、ここは注意が必要です。

Initializing new project
Do you want to develop a new resource(r) or a module(m)?.
>> m
What's the name of your module type?
(<Organization>::<Service>::<Name>::MODULE)
>> Classmethod::S3::Bucket::MODULE
Directory  /Users/xxx/yyy/zzz/fragments  Created
Initialized a new project in /Users/xxx/yyy/zzz/

初期化が完了すると、以下のような構造でファイルとディレクトリが作成されます。

.
├── .rpdk-config
├── fragments
│   └── sample.json
└── rpdk.log
  • fragmentsディレクトリはモジュールとして利用するためのCloudFormationテンプレートを配置するディレクトリです。初期化したさいにサンプルのsample.jsonというファイルが生成されています。
  • .rpdk-configというファイルはモジュール名前などの情報を保持するファイルです。
  • rpdk.logというファイルはCloudFormation CLIの出力が記録されるログ・ファイルです。

fragmentsディレクトリ配下のsample.jsonをs3.jsonにリネームし、以下の内容に置き換えます。※現状モジュールとして利用するCloudFormationテンプレートはJSON形式のみ対応しています。YAMLには対応していません

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "Create a S3 bucket that follows MyCompany's standards",
    "Parameters": {
        "KMSKeyAlias": {
            "Description": "The alias for your KMS key. If you will leave this field empty KMS key alias won't be created along the key.",
            "Type": "String",
            "AllowedPattern": "^(?!aws)([a-zA-Z0-9\\-\\_\\/]){1,32}$|^$",
            "MinLength": 0,
            "MaxLength": 32,
            "ConstraintDescription": "KMS key alias must be at least 1 and no more than 32 characters long, can contain lowercase and uppercase letters, numbers, hyphens, underscores and forward slashes and cannot begin with aws."
        },
        "ReadOnlyArn": {
            "Description": "Provide ARN of an existing Principal (role) that will be granted with read only access to the S3 bucket (e.g. 'arn:aws:iam::123456789xxx:role/myS3ROrole'). If not specified, access will be granted to current AWS account:root only. CF deployment will fail and rollback for non-existing ARN.",
            "Type": "String",
            "Default": "",
            "AllowedPattern": "^(arn:aws:iam::\\d{12}:role(\\/|\\/[\\w\\!\\\"\\#\\$\\%\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\`\\{\\|\\}\\~]{1,510}\\/)[\\w\\+\\=\\,\\.\\@\\-]{1,64})$|^$",
            "ConstraintDescription": "IAM role ARN must start with arn:aws:iam::<12-digit AWS account number>:role/[<path>/]<name of role>. The name of role must be at least 1 and no more than 64 characters long, can contain lowercase letters, uppercase letters, numbers, plus (+), equal (=), comma (,), period (.), at (@), underscore (_), and hyphen (-). Path is optional and must not exceed 510 characters."
        },
        "ReadWriteArn": {
            "Description": "Provide ARN of an existing Principal (role) that will be granted with read and write access to the S3 bucket (e.g. 'arn:aws:iam::123456789xxx:role/myS3RWrole'). If not specified, access will be granted to current AWS account:root only. CF deployment will fail and rollback for non-existing ARN.",
            "Type": "String",
            "Default": "",
            "AllowedPattern": "^(arn:aws:iam::\\d{12}:role(\\/|\\/[\\w\\!\\\"\\#\\$\\%\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\`\\{\\|\\}\\~]{1,510}\\/)[\\w\\+\\=\\,\\.\\@\\-]{1,64})$|^$",
            "ConstraintDescription": "IAM role ARN must start with arn:aws:iam::<12-digit AWS account number>:role/[<path>/]<name of role>. The name of role must be at least 1 and no more than 64 characters long, can contain lowercase letters, uppercase letters, numbers, plus (+), equal (=), comma (,), period (.), at (@), underscore (_), and hyphen (-). Path is optional and must not exceed 510 characters."
        }
    },
    "Resources": {
        "KmsKey": {
            "Type": "AWS::KMS::Key",
            "DeletionPolicy": "Retain",
            "Properties": {
                "Enabled": true,
                "EnableKeyRotation": true,
                "KeyPolicy": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Sid": "Give AWS account:root full control over the KMS key",
                            "Effect": "Allow",
                            "Principal": {
                                "AWS": {
                                    "Fn::Sub": "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
                                }
                            },
                            "Action": [
                                "kms:*"
                            ],
                            "Resource": "*"
                        },
                        {
                            "Sid": "Give ReadOnlyRole access to use KMS key for decryption",
                            "Effect": "Allow",
                            "Principal": {
                                "AWS": {
                                    "Ref": "ReadOnlyArn"
                                }
                            },
                            "Action": [
                                "kms:Decrypt",
                                "kms:DescribeKey"
                            ],
                            "Resource": "*"
                        },
                        {
                            "Sid": "Give the ReadWriteRole access to use KMS key for encryption and decryption",
                            "Effect": "Allow",
                            "Principal": {
                                "AWS": {
                                    "Ref": "ReadWriteArn"
                                }
                            },
                            "Action": [
                                "kms:Encrypt",
                                "kms:Decrypt",
                                "kms:ReEncrypt",
                                "kms:GenerateDataKey*",
                                "kms:DescribeKey"
                            ],
                            "Resource": "*"
                        }
                    ]
                }
            }
        },
        "KmsKeyAlias": {
            "Type": "AWS::KMS::Alias",
            "Properties": {
                "AliasName": {
                    "Fn::Sub": "alias/${KMSKeyAlias}"
                },
                "TargetKeyId": {
                    "Ref": "KmsKey"
                }
            }
        },
        "Bucket": {
            "Type": "AWS::S3::Bucket",
            "Properties": {
                "AccessControl": "BucketOwnerFullControl",
                "BucketEncryption": {
                    "ServerSideEncryptionConfiguration": [
                        {
                            "ServerSideEncryptionByDefault": {
                                "KMSMasterKeyID": {
                                    "Ref": "KmsKey"
                                },
                                "SSEAlgorithm": "aws:kms"
                            }
                        }
                    ]
                },
                "BucketName": {
                    "Fn::Sub": "${AWS::StackName}-${AWS::AccountId}-${AWS::Region}"
                },
                "PublicAccessBlockConfiguration": {
                    "BlockPublicAcls": true,
                    "IgnorePublicAcls": true,
                    "BlockPublicPolicy": true,
                    "RestrictPublicBuckets": true
                }
            }
        },
        "BucketPolicy": {
            "Type": "AWS::S3::BucketPolicy",
            "Properties": {
                "Bucket": {
                    "Ref": "Bucket"
                },
                "PolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Sid": "DenyIncorrectEncryptionHeader",
                            "Effect": "Deny",
                            "Principal": "*",
                            "Action": "s3:PutObject",
                            "Resource": {
                                "Fn::Sub": "arn:${AWS::Partition}:s3:::${Bucket}/*"
                            },
                            "Condition": {
                                "StringEquals": {
                                    "s3:x-amz-server-side-encryption": "AES256"
                                }
                            }
                        },
                        {
                            "Sid": "DenyPublicReadACL",
                            "Effect": "Deny",
                            "Principal": "*",
                            "Action": [
                                "s3:PutObject",
                                "s3:PutObjectAcl"
                            ],
                            "Resource": {
                                "Fn::Sub": "arn:${AWS::Partition}:s3:::${Bucket}/*"
                            },
                            "Condition": {
                                "StringEquals": {
                                    "s3:x-amz-acl": [
                                        "public-read",
                                        "public-read-write",
                                        "authenticated-read"
                                    ]
                                }
                            }
                        },
                        {
                            "Sid": "DenyPublicReadGrant",
                            "Effect": "Deny",
                            "Principal": "*",
                            "Action": [
                                "s3:PutObject",
                                "s3:PutObjectAcl"
                            ],
                            "Resource": {
                                "Fn::Sub": "arn:${AWS::Partition}:s3:::${Bucket}/*"
                            },
                            "Condition": {
                                "StringLike": {
                                    "s3:x-amz-grant-read": [
                                        "*http://acs.amazonaws.com/groups/global/AllUsers*",
                                        "*http://acs.amazonaws.com/groups/global/AuthenticatedUsers*"
                                    ]
                                }
                            }
                        },
                        {
                            "Sid": "DenyNonHttpsConnections",
                            "Effect": "Deny",
                            "Principal": "*",
                            "Action": [
                                "s3:PutObject",
                                "s3:GetObject"
                            ],
                            "Resource": {
                                "Fn::Sub": "arn:${AWS::Partition}:s3:::${Bucket}/*"
                            },
                            "Condition": {
                                "Bool": {
                                    "aws:SecureTransport": false
                                }
                            }
                        },
                        {
                            "Sid": "Give ReadOnlyRole access to get objects from bucket and list bucket",
                            "Effect": "Allow",
                            "Principal": {
                                "AWS": {
                                    "Ref": "ReadOnlyArn"
                                }
                            },
                            "Action": [
                                "s3:GetObject",
                                "s3:GetObjectTagging",
                                "s3:ListBucket",
                                "s3:ListBucketByTags"
                            ],
                            "Resource": [
                                {
                                    "Fn::Sub": "arn:${AWS::Partition}:s3:::${Bucket}"
                                },
                                {
                                    "Fn::Sub": "arn:${AWS::Partition}:s3:::${Bucket}/*"
                                }
                            ]
                        },
                        {
                            "Sid": "Give the ReadWriteRole access to get and put objects from and to bucket and list bucket and multipart uploads",
                            "Effect": "Allow",
                            "Principal": {
                                "AWS": {
                                    "Ref": "ReadWriteArn"
                                }
                            },
                            "Action": [
                                "s3:DeleteObject",
                                "s3:DeleteObjectTagging",
                                "s3:GetObject",
                                "s3:GetObjectTagging",
                                "s3:ListBucket",
                                "s3:ListBucketByTags",
                                "s3:PutObject",
                                "s3:PutObjectTagging"
                            ],
                            "Resource": [
                                {
                                    "Fn::Sub": "arn:${AWS::Partition}:s3:::${Bucket}"
                                },
                                {
                                    "Fn::Sub": "arn:${AWS::Partition}:s3:::${Bucket}/*"
                                }
                            ]
                        }
                    ]
                }
            }
        }
    },
    "Outputs": {
        "BucketArn": {
            "Description": "ARN of the bucket created.",
            "Value": {
                "Fn::GetAtt": [
                    "Bucket",
                    "Arn"
                ]
            }
        },
        "BucketName": {
            "Description": "Name of the bucket created.",
            "Value": {
                "Ref": "Bucket"
            }
        },
        "KmsKeyAlias": {
            "Description": "Alias of SSE-KMS Customer Managed Key used to encrypt S3 bucket content.",
            "Value": {
                "Ref": "KmsKeyAlias"
            }
        },
        "KmsKeyArn": {
            "Description": "ARN of SSE-KMS Customer Managed Key used to encrypt S3 bucket content.",
            "Value": {
                "Fn::GetAtt": [
                    "KmsKey",
                    "Arn"
                ]
            }
        }
    }
}

モジュール化するテンプレートの準備ができたので、以下のコマンドでモジュールをCloudFormationレジストリに登録します。

$cfn submit

しばらく待つとCloudFormation レジストリへの登録が完了し、以下の用に出力されます。

Successfully submitted type. Waiting for registration with token '7e150599-078e-49c2-9214-3e0e3fdfa00a' to complete.
Registration complete.
{'ProgressStatus': 'COMPLETE', 'Description': 'Deployment is currently in DEPLOY_STAGE of status COMPLETED; ', 'TypeArn': 'arn:aws:cloudformation:ap-northeast-1:<AWSアカウントID>:type/module/Classmethod-S3-Bucket-MODULE', 'TypeVersionArn': 'arn:aws:cloudformation:ap-northeast-1:<AWSアカウントID>:type/module/Classmethod-S3-Bucket-MODULE/00000001', 'ResponseMetadata': {'RequestId': 'be6aad46-28a1-4579-a08f-1e296f3779ea', 'HTTPStatusCode': 200, 'HTTPHeaders': {'x-amzn-requestid': 'be6aad46-28a1-4579-a08f-1e296f3779ea', 'content-type': 'text/xml', 'content-length': '701', 'date': 'Sat, 28 Nov 2020 05:28:59 GMT'}, 'RetryAttempts': 0}}

これで登録完了です。

CloudFormationレジストリの登録内容をAWS CLIから確認してみましょう

$aws cloudformation describe-type --type MODULE --type-name Classmethod::S3::Bucket::MODULE

以下のようなレスポンスが返却されます。

{
    "Arn": "arn:aws:cloudformation:ap-northeast-1:944137583148:type/module/Classmethod-S3-Bucket-MODULE/00000001",
    "Type": "MODULE",
    "TypeName": "Classmethod::S3::Bucket::MODULE",
    "DefaultVersionId": "00000001",
    "IsDefaultVersion": true,
    "Description": "Schema for Module Fragment of type Classmethod::S3::Bucket::MODULE",
    "Schema": "{\n    \"typeName\": \"Classmethod::S3::Bucket::MODULE\",\n    \"description\": \"Schema for Module Fragment of type Classmethod::S3::Bucket::MODULE\",\n    \"properties\": {\n        \"Parameters\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"KMSKeyAlias\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"Type\": {\n                            \"type\": \"string\"\n                        },\n                        \"Description\": {\n                            \"type\": \"string\"\n                        }\n                    },\n                    \"required\": [\n                        \"Type\",\n                        \"Description\"\n                    ],\n                    \"description\": \"The alias for your KMS key. If you will leave this field empty KMS key alias won't be created along the key.\"\n                },\n                \"ReadOnlyArn\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"Type\": {\n                            \"type\": \"string\"\n                        },\n                        \"Description\": {\n                            \"type\": \"string\"\n                        }\n                    },\n                    \"required\": [\n                        \"Type\",\n                        \"Description\"\n                    ],\n                    \"description\": \"Provide ARN of an existing Principal (role) that will be granted with read only access to the S3 bucket (e.g. 'arn:aws:iam::123456789xxx:role/myS3ROrole'). If not specified, access will be granted to current AWS account:root only. CF deployment will fail and rollback for non-existing ARN.\"\n                },\n                \"ReadWriteArn\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"Type\": {\n                            \"type\": \"string\"\n                        },\n                        \"Description\": {\n                            \"type\": \"string\"\n                        }\n                    },\n                    \"required\": [\n                        \"Type\",\n                        \"Description\"\n                    ],\n                    \"description\": \"Provide ARN of an existing Principal (role) that will be granted with read and write access to the S3 bucket (e.g. 'arn:aws:iam::123456789xxx:role/myS3RWrole'). If not specified, access will be granted to current AWS account:root only. CF deployment will fail and rollback for non-existing ARN.\"\n                }\n            }\n        },\n        \"Resources\": {\n            \"properties\": {\n                \"KmsKey\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"Type\": {\n                            \"type\": \"string\",\n                            \"const\": \"AWS::KMS::Key\"\n                        },\n                        \"Properties\": {\n                            \"type\": \"object\"\n                        }\n                    }\n                },\n                \"KmsKeyAlias\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"Type\": {\n                            \"type\": \"string\",\n                            \"const\": \"AWS::KMS::Alias\"\n                        },\n                        \"Properties\": {\n                            \"type\": \"object\"\n                        }\n                    }\n                },\n                \"Bucket\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"Type\": {\n                            \"type\": \"string\",\n                            \"const\": \"AWS::S3::Bucket\"\n                        },\n                        \"Properties\": {\n                            \"type\": \"object\"\n                        }\n                    }\n                },\n                \"BucketPolicy\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"Type\": {\n                            \"type\": \"string\",\n                            \"const\": \"AWS::S3::BucketPolicy\"\n                        },\n                        \"Properties\": {\n                            \"type\": \"object\"\n                        }\n                    }\n                }\n            },\n            \"type\": \"object\",\n            \"additionalProperties\": false\n        }\n    },\n    \"additionalProperties\": true\n}\n",
    "DeprecatedStatus": "LIVE",
    "Visibility": "PRIVATE",
    "LastUpdated": "2020-11-28T05:28:40.236Z",
    "TimeCreated": "2020-11-28T05:28:40.236Z"
}

マネコンからも確認してみましょう。

先程登録したモジュールの内容が表示されています。これでモジュールを利用する準備が整いました。

モジュールを利用するスタックの作成

以下のテンプレートを用意し、先程作成したモジュールを利用して新しくスタックを作成します。リソースFirehoseDestinationのTypeに先程作成したモジュールの名前Classmethod::S3::Bucket::MODULEを指定するのがポイントです。

AWSTemplateFormatVersion: '2010-09-09'
Description: "Create a Firehose stream that writes to S3"
Resources:
  FirehoseDestination:
    Type: <先程作成したモジュール名>
    Properties:
      KMSKeyAlias: !Sub "${AWS::StackName}"
      ReadWriteArn: !GetAtt FirehoseRole.Arn
      ReadOnlyArn: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
  FirehoseRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: AssumeRole1
            Effect: Allow
            Principal:
              Service: firehose.amazonaws.com
            Action: 'sts:AssumeRole'
  FirehosePolicy:
    Type: 'AWS::IAM::Policy'
    Properties:
      PolicyName: "ReadWrite"
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: "KmsEncryptionDecryption"
            Effect: Allow
            Action:
              - 'kms:Decrypt'
              - 'kms:GenerateDataKey'
            Resource: !GetAtt FirehoseDestinationKmsKey.Arn
            Condition:
              StringEquals:
                kms:ViaService: !Sub 's3:${AWS::Region}.amazonaws.com'
              StringLike:
                kms:EncryptionContext:aws:s3:arn: !Sub '${FirehoseDestinationBucket.Arn}/*'
          - Sid: FirehoseAccess
            Effect: Allow
            Action:
            - kinesis:DescribeStream
            - kinesis:GetShardIterator
            - kinesis:GetRecords
            - kinesis:ListShards
            Resource: !GetAtt Firehose.Arn
          - Sid: "S3ListBucket"
            Effect: Allow
            Action:
              - 's3:ListBucket'
              - 's3:ListBucketByTags'
              - 's3:ListBucketMultipartUploads'
              - 's3:GetBucketLocation'
            Resource: !GetAtt FirehoseDestinationBucket.Arn
          - Sid: "S3GetPutDeleteObject"
            Effect: Allow
            Action:
              - 's3:DeleteObject'
              - 's3:DeleteObjectTagging'
              - 's3:GetObject'
              - 's3:GetObjectTagging'
              - 's3:PutObject'
              - 's3:PutObjectTagging'
            Resource: !Sub '${FirehoseDestinationBucket.Arn}/*'
      Roles: 
      - !Ref FirehoseRole
  Firehose:
    Type: AWS::KinesisFirehose::DeliveryStream
    Properties:
      DeliveryStreamName: !Sub "${AWS::StackName}"
      DeliveryStreamType: DirectPut
      S3DestinationConfiguration:
        BucketARN: !GetAtt FirehoseDestinationBucket.Arn
        RoleARN: !GetAtt FirehoseRole.Arn
        EncryptionConfiguration:
          KMSEncryptionConfig:
            AWSKMSKeyARN: !GetAtt FirehoseDestinationKmsKey.Arn

このテンプレートからスタックを作成してみます。

スタックの作成完了後に、作成されたリソースを確認するとモジュール側で定義したS3バケット等のリソースが作成されていることが分かります。

また、「テンプレートタブ」から「処理されたテンプレートの表示」を選択すると以下のように表示されました。

Description: Create a Firehose stream that writes to S3
Parameters: {
  }
Mappings: {
  }
Conditions: {
  }
Rules: {
  }
Resources:
...略
  FirehoseDestinationKmsKeyAlias:
    Type: AWS::KMS::Alias
    Metadata:
      AWS::Cloudformation::Module:
        TypeHierarchy: Classmethod::S3::Bucket::MODULE
        LogicalIdHierarchy: FirehoseDestination
    Properties:
      TargetKeyId:
        Ref: FirehoseDestinationKmsKey
      AliasName:
        Fn::Sub:
        - alias/${KMSKeyAlias}
        - KMSKeyAlias:
            Fn::Sub: ${AWS::StackName}
  FirehoseDestinationBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Metadata:
      AWS::Cloudformation::Module:
        TypeHierarchy: Classmethod::S3::Bucket::MODULE
        LogicalIdHierarchy: FirehoseDestination
    Properties:
      Bucket:
        Ref: FirehoseDestinationBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Condition:
            StringEquals:
              s3:x-amz-server-side-encryption: AES256
          Action: s3:PutObject
          Resource:
            Fn::Sub: arn:${AWS::Partition}:s3:::${FirehoseDestinationBucket}/*
          Effect: Deny
          Principal: '*'
          Sid: DenyIncorrectEncryptionHeader
        - Condition:
            StringEquals:
              s3:x-amz-acl:
              - public-read
              - public-read-write
              - authenticated-read
          Action:
          - s3:PutObject
          - s3:PutObjectAcl
          Resource:
            Fn::Sub: arn:${AWS::Partition}:s3:::${FirehoseDestinationBucket}/*
          Effect: Deny
          Principal: '*'
          Sid: DenyPublicReadACL
        - Condition:
            StringLike:
              s3:x-amz-grant-read:
              - '*http://acs.amazonaws.com/groups/global/AllUsers*'
              - '*http://acs.amazonaws.com/groups/global/AuthenticatedUsers*'
          Action:
          - s3:PutObject
          - s3:PutObjectAcl
          Resource:
            Fn::Sub: arn:${AWS::Partition}:s3:::${FirehoseDestinationBucket}/*
          Effect: Deny
          Principal: '*'
          Sid: DenyPublicReadGrant
        - Condition:
            Bool:
              aws:SecureTransport: false
          Action:
          - s3:PutObject
          - s3:GetObject
          Resource:
            Fn::Sub: arn:${AWS::Partition}:s3:::${FirehoseDestinationBucket}/*
          Effect: Deny
          Principal: '*'
          Sid: DenyNonHttpsConnections
        - Action:
          - s3:GetObject
          - s3:GetObjectTagging
          - s3:ListBucket
          - s3:ListBucketByTags
          Resource:
          - Fn::Sub: arn:${AWS::Partition}:s3:::${FirehoseDestinationBucket}
          - Fn::Sub: arn:${AWS::Partition}:s3:::${FirehoseDestinationBucket}/*
          Effect: Allow
          Principal:
            AWS:
              Fn::Sub: arn:aws:iam::${AWS::AccountId}:root
          Sid: Give ReadOnlyRole access to get objects from bucket and list bucket
        - Action:
          - s3:DeleteObject
          - s3:DeleteObjectTagging
          - s3:GetObject
          - s3:GetObjectTagging
          - s3:ListBucket
          - s3:ListBucketByTags
          - s3:PutObject
          - s3:PutObjectTagging
          Resource:
          - Fn::Sub: arn:${AWS::Partition}:s3:::${FirehoseDestinationBucket}
          - Fn::Sub: arn:${AWS::Partition}:s3:::${FirehoseDestinationBucket}/*
          Effect: Allow
          Principal:
            AWS:
              Fn::GetAtt: FirehoseRole.Arn
          Sid: Give the ReadWriteRole access to get and put objects from and to bucket
            and list bucket and multipart uploads
  FirehoseDestinationBucket:
    Type: AWS::S3::Bucket
    Metadata:
      AWS::Cloudformation::Module:
        TypeHierarchy: Classmethod::S3::Bucket::MODULE
        LogicalIdHierarchy: FirehoseDestination
    Properties:
      PublicAccessBlockConfiguration:
        RestrictPublicBuckets: true
        BlockPublicPolicy: true
        BlockPublicAcls: true
        IgnorePublicAcls: true
      BucketName:
        Fn::Sub: ${AWS::StackName}-${AWS::AccountId}-${AWS::Region}
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - ServerSideEncryptionByDefault:
            SSEAlgorithm: aws:kms
            KMSMasterKeyID:
              Ref: FirehoseDestinationKmsKey
      AccessControl: BucketOwnerFullControl
...略
Outputs:
  FirehoseDestinationBucketArn:
    Description: ARN of the bucket created.
    Value:
      Fn::GetAtt:
      - FirehoseDestinationBucket
      - Arn
  FirehoseDestinationBucketName:
    Description: Name of the bucket created.
    Value:
      Ref: FirehoseDestinationBucket
  FirehoseDestinationKmsKeyAlias:
    Description: Alias of SSE-KMS Customer Managed Key used to encrypt S3 bucket content.
    Value:
      Ref: FirehoseDestinationKmsKeyAlias
  FirehoseDestinationKmsKeyArn:
    Description: ARN of SSE-KMS Customer Managed Key used to encrypt S3 bucket content.
    Value:
      Fn::GetAtt:
      - FirehoseDestinationKmsKey
      - Arn
AWSTemplateFormatVersion: '2010-09-09'
Hooks: {
  }

モジュールで指定したリソースの定義がテンプレートに取り込まれて展開されていることが分かります。これで別のスタックを作成する時も同じS3バケットやKMSの定義を共通利用することができそうですね。

また、スタックの「出力」を確認すると、モジュール側のOutputsセクションで定義した値が出力されていることが分かります。

このようにモジュールを利用するテンプレートとモジュールの間でパラメータを受け渡ししたり、Fn::RefやFn::Subで参照することも可能になっています。これは便利に使えそうですね。

まとめ

CloudFormationのモジュール機能についてご紹介しました。テンプレートの再利用が捗りそうな良いアップデートですね。これまでAWS::Includeやマクロを使っていたユースケースではモジュールへの置き換えを検討しても良いかもしれません。既にGitHubでモジュールのサンプルがいくつか公開されているので、このあたりを参考にモジュールの作成に挑戦してみるのも面白そうです。

https://github.com/aws-cloudformation/aws-cloudformation-samples/tree/adc70365aadaad8a68234c37e3b290b979c7e166/modules

あとはモジュール用のテンプレートがYAMLに対応してくれれば...期待して待ちましょう。