[アップデート]AWS CloudFormationが変更セット作成時にカスタム名を指定するリソースを自動でインポートしてくれるようになりました

2023.11.20

初めに

先日のアップデートでCloudFormationが既に存在するカスタム名を指定しているリソースがテンプレートに含まれている場合、変更セット作成時に自動で取り込んでくれるようになりました。

これまでスタックに既存のリソースをインポートする場合、リソースのインポート操作を通常のスタックの作成とは別枠で行う必要があり(通常の変更と一緒にできない)、かつインポート時にはテンプレート自体とは別に取り込むリソースの識別子を入力する必要がありました。

今後はカスタム名が指定されているリソースについては、ImportExistingResourcesパラメータをTrueにすることで上記事のような個別の指定は不要となりCloudFormation側が自動で判別して取り込んでくれるようになります。

なおカスタム名は一部のリソースに存在する個別の属性として名称が指定可能なものとなり対応リソースは以下ページをご参照ください。

試してみる

以前試したCloudFront+S3+WAFのテンプレートあったので、事前にS3バケットを作成した上で以下のテンプレートで変更セットを作成します。ハイライト部分が今回取り込み対象とするリソースです。

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  Prefix:
    Type: String
Mappings:
  CloudFront:
    ManagedCachePolicyId:
      CachingDisabled: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
    ManagedOriginRequestPolicy:
      AllViewerAndCloudFrontHeaders202206: 33f36d7e-f396-46d9-90e0-52428a34d9dc
Resources:
  #----------------------------------
  #----- WAF
  #----------------------------------
  WAF:
    Type: AWS::WAFv2::WebACL
    Properties: 
      Name: !Sub ${Prefix}-web-acl
      Description: !Sub WebACL for ${Prefix}
      Scope: CLOUDFRONT
      DefaultAction:
        Allow: {}
      VisibilityConfig: 
        CloudWatchMetricsEnabled: False
        MetricName: !Sub ${Prefix}-web-acl
        SampledRequestsEnabled: False
  #----------------------------------
  #----- CloudFront
  #----------------------------------
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties: 
      DistributionConfig:
        Enabled: True
        Comment: !Sub Distribution for ${Prefix}
        WebACLId: !Ref WAF
        DefaultCacheBehavior: 
          AllowedMethods: 
            - GET
            - HEAD
            - OPTIONS
            - PUT
            - PATCH
            - POST
            - DELETE
          CachePolicyId: !FindInMap [CloudFront, ManagedCachePolicyId, CachingDisabled]
          OriginRequestPolicyId: !FindInMap [CloudFront, ManagedOriginRequestPolicy, AllViewerAndCloudFrontHeaders202206]
          TargetOriginId: site-bucket
          ViewerProtocolPolicy: redirect-to-https
        DefaultRootObject: index.html
        HttpVersion: http2
        IPV6Enabled: False
        Origins: 
          - Id: site-bucket
            DomainName: !GetAtt SiteBucket.DomainName
            OriginAccessControlId: !Ref CommonOAC
            S3OriginConfig: 
              OriginAccessIdentity: ''
        PriceClass: PriceClass_All
  CommonOAC:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Name: !Sub ${Prefix}-bucket-access-control
        Description: !Sub bucket access control for ${Prefix}
        OriginAccessControlOriginType: s3
        SigningBehavior: never
        SigningProtocol: sigv4
  StaticContentsDefaultCachePolicy:
    Type: AWS::CloudFront::CachePolicy
    Properties: 
      CachePolicyConfig: 
        Name: !Sub ${Prefix}-static-content-cache-policy
        Comment: !Sub cloudfront accesslog in ${Prefix}
        ParametersInCacheKeyAndForwardedToOrigin:
          EnableAcceptEncodingBrotli: False
          EnableAcceptEncodingGzip: True
          CookiesConfig: 
            CookieBehavior: none
          HeadersConfig: 
            HeaderBehavior: none
          QueryStringsConfig: 
            QueryStringBehavior: all
        DefaultTTL: 300
        MinTTL: 300
        MaxTTL: 300
  SiteBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    Properties:
      BucketName: !Sub ${Prefix}-site-contents-{AWS::AccountId}
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - BucketKeyEnabled: True
            ServerSideEncryptionByDefault: 
              SSEAlgorithm: AES256
  SiteBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties: 
      Bucket: !Ref SiteBucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: AccessFromCloudFront
            Effect: Allow
            Principal:
                Service: cloudfront.amazonaws.com
            Action: s3:GetObject
            Resource: !Sub arn:aws:s3:::${SiteBucket}/*
            Condition:
                StringEquals:
                    AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}

バケットは事前に手動で作成しておきます。

今回のオプションはまだマネジメントコンソール上で指定が見当たらなかったのでAWS CLIで操作を行います。

なお現時点ではAWS CLIでもv1系のみが対応しており、v2系はUnknown options扱いとなったためv1側で操作を行っています。

## v2は未対応
$ aws --version
aws-cli/2.13.37 Python/3.11.6 Darwin/22.5.0 exe/x86_64 prompt/off

$ aws create-change-set --region us-east-1 \
      --stack-name new-custom-name-import-test \
      --template-body auto-import.yml \
      --parameters ParameterKey=Prefix,ParameterValue=auto-import \
      --change-set-name import-`date +%Y%m%d-%H%M%S` \
      --import-existing-resources
      --change-set-type CREATE
...
Unknown options: --import-existing-resource

# v1は対応済み
$ /tmp/aws-cli-latest/bin/aws --version
aws-cli/1.30.3 Python/3.11.6 Darwin/22.5.0 botocore/1.32.3

$ /tmp/aws-cli-latest/bin/aws  create-change-set --region us-east-1 \
      --stack-name new-custom-name-import-test \
      --template-body file://auto-import.yml \
      --parameters ParameterKey=Prefix,ParameterValue=auto-import \
      --change-set-name import-`date +%Y%m%d-%H%M%S` \
      --import-existing-resources
      --change-set-type CREATE
{
    "Id": "arn:aws:cloudformation:us-east-1:xxxxx:changeSet/import-20231120-153127/xxxxx",
    "StackId": "arn:aws:cloudformation:us-east-1:xxxxx:stack/new-custom-name-import-test/xxxxx"
}

マネジメントコンソール側で変更セットを確認してみるとS3がImportとなっていることが確認できます。

実行してみると取り込みイベントとしてS3バケットが含まれていることが確認できます。

DeletionPolicyの指定は必要

CloudFormationでインポートするリソースにはDeletionPolicyの指定が必須という制約がありますが、これは今回の方法を使った場合でも同様です。

未指定の場合はAdd扱いではなく変更セットの作成自体がエラーとなります。

一部のリソースはうまく取り込めなかった

検証していた中でWAFやセキュリティグループについてはうまくImportとならずAddのままとなってしまいました。

この2つのリソースについてはカスタム名に対応してはいますが取り込みリソース指定の際ににID等の別の属性が必要となるのでこの辺りの関係でしょうか?
今回はここまで追い切ることができなかったため別途確認が必要な点とはなります。

終わりに

これまでリソースのインポートする形で新たにスタックを作成する場合は一度対象のリソースをコメントアウトしてデプロイし別途インポート処理が必要、既存の追加でも変更処理が入らないように構成する必要があり非常に手間でした。

現時点では全てのリソースが対応しているわけではないですが今回のアップデートによりそういった前作業を気にする必要がなく1デプロイで通せるようになったので非常に有難いアップデートです。

付録: RustでのCreateChangeSet

WAFの件でハマってる時にCloudTrail上でImportExistingResourcesのパラメータが見えなくて本当に渡ってるか不安だった時に別口でrustのsdkも既に対応してそうだったのでそちらで行うようにもしていました。

結果的にはCloudTrailには表示されないパラメータでdescribe-change-setで確認するとTrueになったいたので不要となりましたが今後何かで使うこともあると思いますので個人的なメモとして残しておきます。

use std::{fs::File, io::Read};
use aws_config::BehaviorVersion;
use aws_sdk_cloudformation::{Client, Error, types::Parameter};

#[tokio::main]
async fn main() -> Result<(), Error> {
    let mut contents = String::new();
    let mut f = File::open("auto-import.yml").unwrap();
    f.read_to_string(&mut contents).unwrap();

    let shared_config = aws_config::load_defaults(BehaviorVersion::v2023_11_09()).await;
    let client = Client::new(&shared_config);

    #当初は新規作成の変更セットではなく既存の変更(追加)で考えていたため少し指定が異なります
    let params = Parameter::builder()
                .set_parameter_key(Some("Prefix".to_string()))
                .use_previous_value(true)
                .build();
    
    let req = client.create_change_set()
                .stack_name("custom-name-import-test")
                .change_set_name("import1")
                .template_body(contents)
                .import_existing_resources(true)
                .parameters(params);
    let resp = req.send().await?;
    println!("{:?}", resp);
    Ok(())