【AWS CloudFormation】スタック作成時はパラメータストアから最新AMI取得し、スタック更新時はAMI更新させない方法 〜ネストされたスタック利用〜

記事タイトルとURLをコピーする

こんにちは、屋根裏エンジニアの折戸です。

タイトル長いですが、あしからず。

経緯

以前、CloudFormationの変更セットで本番稼働中のEC2を削除しかけた話 を投稿しました。
blog.serverworks.co.jp

当時の対策としてはCloudFormation(以下、CFn)のスタックの変更は利用せずに手動でコンソールから任意のパラメータを更新することでインスタンスのTerminateを防ぎました。
しかしこの方法は実環境とCFnスタック間でドリフトが発生している状態であり、これはあまり良い状況とは言えません。

そこで今回はそもそもの話、
スタック作成時はパラメータストアから最新AMI取得したい、でもスタック更新時にはAMIは更新させたくない!
という方法をご紹介します。

元のテンプレート

まずは問題となっていたテンプレートをおさらい。

AWSTemplateFormatVersion: 2010-09-09
Description: test-instance

Parameters:
  ImageId:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-arm64-gp2

Resources:
  EC2Instance:
    Type: AWS::EC2::Instance
    DeletionPolicy: Delete
    Properties:
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            DeleteOnTermination: true
            Encrypted: false
            VolumeSize: 8
            VolumeType: gp3
      DisableApiTermination: false
      ImageId: !Ref ImageId
      InstanceInitiatedShutdownBehavior: stop
      InstanceType: t4g.micro
      Monitoring: false
      KeyName: test-keypair
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeviceIndex: 0
          GroupSet:
            - !ImportValue SecurityGroup-web
          SubnetId:
            !ImportValue SubnetTestPrivateA

肝となるのは以下の部分

  ImageId:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-arm64-gp2

AWS Systems Manager Parameter Store(以下、パラメータストア)の
/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-arm64-gp2 を参照して最新のAMI IDを取得して設定する部分です。

このテンプレートの仕組みにより、変更セット実行のタイミングによって ImageIdの値が変わってしまうということが原因で、思わぬインスタンスのTerminateを引き起こしてしまう可能性があります。

対策

ネストされたスタックという方法を利用します。

docs.aws.amazon.com

ネストされたスタックは、他のスタックの一部として作成されたスタックです。

インフラストラクチャが大きくなるにつれ、複数のテンプレートで同じコンポーネントを宣言する共通パターンができてきます。これらの共通するコンポーネントを他と分類し、専用テンプレートを作成できます。

本来の用途とはズレている気はしますが、今回はこれを利用します。

テンプレート分割

元のテンプレートを、親スタック用と子スタック用へ分割します。 それぞれの役割とテンプレートは以下のとおり

親スタック用テンプレート

役割

最新のAMI IDをパラメータストアから参照し、Stackリソースを通して子スタックへ値を渡す

nest-stack-parent.yml

AWSTemplateFormatVersion: 2010-09-09
Description: nest-stack-parent

Parameters:
  ImageId:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-arm64-gp2

Resources:
  ImageIdStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub https://nest-stack-${AWS::AccountId}.s3.ap-northeast-1.amazonaws.com/nest-stack-child.yml
      Parameters:
        ImageId: !Ref ImageId

子スタック用テンプレート

役割

親スタックから渡されたAMI IDをString型でParametersへ保持し、Instanceリソースを作成する

nest-stack-child.yml

AWSTemplateFormatVersion: 2010-09-09
Description: nest-stack-child

Parameters:
  ImageId:
    Type: String

Resources:
  EC2Instance:
    Type: AWS::EC2::Instance
    DeletionPolicy: Delete
    Properties:
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            DeleteOnTermination: true
            Encrypted: false
            VolumeSize: 8
            VolumeType: gp3
      DisableApiTermination: false
      ImageId: !Ref ImageId
      InstanceInitiatedShutdownBehavior: stop
      InstanceType: t4g.nano
      Monitoring: false
      KeyName: test-keypair
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeviceIndex: 0
          GroupSet:
            - !ImportValue SecurityGroup-web
          SubnetId:
            !ImportValue SubnetTestPrivateA

子スタック用テンプレートをS3へアップロード

コンソールのCFnメニューからは子スタックは作成できません。
親スタックから参照できるように先に子スタック用テンプレートを任意のS3へアップロードしておきます。
適宜バケットを準備してnest-stack-child.ymlをアップロードしてください。
今回は nest-stack-【AWSアカウントID】 の名前でバケットを作成し、そこへnest-stack-child.ymlをアップロードしています。

親スタック用テンプレートの、

…
      TemplateURL: !Sub https://nest-stack-${AWS::AccountId}.s3.ap-northeast-1.amazonaws.com/nest-stack-child.yml
…

の部分がアップロードされているテンプレートを参照します。

親スタックを作成

コンソールのCFnメニューから親スタックを作成します。

CloudFormation > スタック > スタックの作成

テンプレートファイルのアップロード:nest-stack-parent.yml
次へ

スタックの名前:nest-stack
次へ

スタックオプションの設定
次へ

レビュー
スタックの作成

スタック確認

CloudFormation > スタック > nest-stack nest-stack がCREATE_COMPLETEとなってます。

リソースタブ ImageIdStackのStackリソースが作成されてます。

左側のスタック一覧にもネストされたスタックとして、子スタックが確認できます。

CloudFormation > スタック > nest-stack-ImageIdStack-******** ネストされたスタック(子スタック)から、Instanceリソースが作成されていることを確認できます。

パラメータとして、親スタックから渡されたAMI IDがセットされてます。

ここまでで、最新のAMIからインスタンスを作成できました。
ここからが本題です。

前回と同じ様なシナリオでインスタンスのAMI以外のパラメータを更新しようとした場合にインスタンスの置換(AMIが更新されない)が発生しないことを確認しましょう。

子スタックの更新

パラメータの更新は子スタック用のテンプレートを編集します。

子スタック用テンプレート編集

今回はインスタンスタイプを更新する様にテンプレートを書き換えます。
nest-stack-child.yml

        ImageId: !Ref ImageId
        InstanceInitiatedShutdownBehavior: stop
--      InstanceType: t4g.nano
++      InstanceType: t4g.small
        Monitoring: false
        KeyName: test-keypair

ネストされたスタック(子スタック)の変更セットの作成から更新します。

CloudFormation > スタック > nest-stack-ImageIdStack-******** 変更セットの作成

既存テンプレートを置き換える
テンプレートファイルのアップロード
テンプレートファイルのアップロード:編集した nest-stack-child.yml
次へ

ImageIdはもちろん変更しません。そのまま
次へ

スタックオプションの設定
次へ

レビュー
変更セットの作成

置換の確認

置換ステータスはTrueではなく、Conditionalとなってます。

変更セットの実行

実行 をクリックし、実際にスタックを更新してみます。
インスタンスはTerminateされず、AMIは変更されずにインスタンスタイプのみ変更されていることを確認しました。

実は

この時点で スタック作成 から スタック変更によるパラメータの更新 の間に最新のAMIは変わっていなかったため、本当の意味で確認はできていません。

ただし、子スタック上ではAMI IDがパラメータストアからの参照ではなく固定値で保持されているので、最新のAMIが変わっていたとしても子スタックの更新には影響が及ばないはずです。

最後に

CFnで単純にインスタンスを作成したいだけのケースを考えると今回の方法はS3へアップロードするオペレーションが増えてしまうためやりすぎ感があり、得策とは言いがたいです。
より良い方法があれば続編を書こうと思います。

では。

折戸 亮太(執筆記事の一覧)

2021年10月1日入社
クラウドインテグレーション部技術1課

屋根裏エンジニア