1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CloudFormationで構築したAutoHealing構成で起動テンプレートのAMI設定更新をドリフトさせずに運用する

Posted at

はじめに

ほうき星です。
AutoScalingGroupを使用し最小:1、最大:1のAutoHealingを構成する場合、起動中のインスタンスからAMIを取得し起動テンプレートやAutoScalingGroupの設定を更新する運用を行うことがあります。
この更新作業はLambda関数等を使用し自動化されることが多いですが、CloudFormationを使用してリソースを定義している場合は少し工夫しないとドリフトが発生します。
本記事ではドリフトを回避しつつAMIを更新する運用方法を紹介します。

AutoHealing構成時のAMI設定の更新運用

AutoHealing構成(特に最小:1、最大:1)の時に新しく起動してくるインスタンスを最新の状態に保つため、起動中のインスタンスからAMIを取得し起動テンプレートやAutoScalingGroupの設定を更新する運用を行うことがあります。

AutoHealingAMI更新作業.drawio.png

この運用では、CloudFormationテンプレートでAutoHealing構成を定義している場合に、次の課題が発生します。

課題:AMIの更新によるCloudFormationスタックのドリフト

CloudFormationはAutoScalingGroupの起動テンプレートバージョンの指定において$Latest$Defaultを使用することはできません。

そのため前述の運用において取得したAMIで新しい起動テンプレートバージョンを作成するだけではなく、AutoScalingGroupが参照する起動テンプレートのバージョン指定も併せて変更する必要があり、これによりドリフトが発生します。

image.png
image.png

※起動テンプレートはCloudFormation外で新しいバージョンが作成されてもドリフトを検知しません

解決策

パターン①:SSMパラメータを活用する

起動テンプレートのAMI指定ではAMIを直接指定する他にSSMパラメータを使用することができます。

これを利用しCloudFormationテンプレートで定義したSSMパラメータを起動テンプレートで指定し、AMI取得後にSSMパラメータをLambda等を用いて更新することで、起動テンプレート及びAutoScalingGroupの設定を変更することなく(=ドリフトさせずに)運用することが可能です。

SSMパラメータ(AWS::SSM::Parameter)をCloudFormationテンプレートで定義し、それをLambda等で更新してしまうとSSMパラメータ部分がドリフトしてしまいます。
必ずCloudFormationテンプレートで事前に定義したものを更新します。

CloudFormationテンプレートサンプル

以下はSSMパラメータを使用するAuto Healing構成を定義するCloudFormationテンプレート及びAWSBackupのCOMPLETEDイベントをトリガーにSSMパラメータを作成したAMIで更新するサンプルです。

solution1.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Stack of solution 1

Parameters:
  IamInstanceProfileArn:
    Type: String
    Description: Enter the arn of the instance profile to be used for the instance.

  ImageIdParameterName: # CloudFormationテンプレート外で定義したSSMパラメータ名を指定する
    Type: AWS::SSM::Parameter::Name
    Description: Enter the parameter name of the parameter store where you filled in the ImageId.

  SecurityGroupIds:
    Type: List<AWS::EC2::SecurityGroup::Id>
    Description: Enter the security group ids to be used for the instance.

  VPCZoneIdentifier:
    Type: List<AWS::EC2::Subnet::Id>
    Description: Enter the subnet ids to be used for the instance.

Resources:
  Solution1LaunchTemplate: # 起動テンプレート
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: solution1-launch-template
      LaunchTemplateData:
        IamInstanceProfile:
          Arn: !Ref IamInstanceProfileArn
        ImageId: !Sub resolve:ssm:${ImageIdParameterName} # SSMパラメータから取得
        InstanceType: t2.micro
        SecurityGroupIds:
          !Ref SecurityGroupIds
        TagSpecifications:
          - ResourceType: instance
            Tags:
              - Key: Name
                Value: solution1-instance
              - Key: ImageIdParameterName
                Value: !Ref ImageIdParameterName

  Solution1AutoScalingGroup: # AutoScalingGroup(Auto Healing)
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      AutoScalingGroupName: solution1-auto-scaling-group
      LaunchTemplate:
        LaunchTemplateId: !Ref Solution1LaunchTemplate
        Version: !GetAtt Solution1LaunchTemplate.LatestVersionNumber
      MaxSize: 1
      MinSize: 1
      VPCZoneIdentifier:
        !Ref VPCZoneIdentifier

  • solution1.ymlテンプレートは事前準備のSSMパラメータ名をパラメータImageIdParameterNameとして定義
  • 起動テンプレートのImageIdプロパティでは!Sub resolve:ssm:${ImageIdParameterName}とすることでSSMパラメータを指定

AWSBackupをトリガーにSSMパラメータを更新するServerlessFunctionサンプル

solution1-update-parameter.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Lambda function to update SSM Parameter Store with AMI ID after AWS Backup completes.

Resources:
  UpdateImageIdInParameterStoreFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: UpdateImageIdInParameterStore
      Runtime: python3.12
      Handler: lambda_function.lambda_handler
      MemorySize: 128
      Timeout: 30
      Policies:
        - AWSLambdaBasicExecutionRole
        - Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Action:
                - ec2:DescribeImages
                - ssm:PutParameter
              Resource: "*"
      Events:
        BackupCompletedEvent:
          Type: EventBridgeRule
          Properties:
            Pattern:
              source:
                - "aws.backup"
              detail-type:
                - "Backup Job State Change"
              detail:
                state:
                  - "COMPLETED"

lambda_function.py
import boto3

def lambda_handler(event, context):
    print(f"{event=}")

    # AWSBackupイベントから、ImageIdを取得
    image_id: str = event["resources"][0].split("/")[-1]

    # Imageに付与された'ImageIdParameterName'タグを取得
    describe_images_response: dict = boto3.client("ec2").describe_images(ImageIds=[image_id])
    specific_tag: list[str] = [
        tag["Value"]
        for tag in describe_images_response["Images"][0]["Tags"]
        if tag["Key"] == "ImageIdParameterName"
    ]
    if not specific_tag:
        print("Tag: ImageIdParameterName is not found, Skipped.")
        return

    parameter_name: str = specific_tag[0]

    # 取得したタグを元に、SSMパラメータにImageIdを書き込む
    boto3.client("ssm").put_parameter(
        Name=parameter_name,
        Value=image_id,
        Overwrite=True
    )

    print(f"ImageId: {image_id} is written to SSM Parameter Store: {parameter_name}")

  • AWSBackupの完了をトリガーにLambda関数を実行
  • 作成されたAMIに特定のタグ:ImageIdParameterName が存在するかチェック、存在すれば値として設定されたSSMパラメータを作成されたAMIで更新
AWSBackup後の動作結果

solution1_lambda.jpg
solution1_update後.png

  • AWSBackupの完了をトリガーにLambda関数が動作、SSMパラメータを更新

image.png

  • CloudFormationテンプレートで作成したSSMパラメータを更新しただけなので、ドリフト無し

パターン②:AMIをCloudFormationのパラメータとして定義し、スタックの更新を行う

CloudFormationのパラメータとしてImageIdを定義し、AMI取得後にLambda等を使用しImageIdパラメータに最新のAMIを指定しスタックを更新することで、こちらも(当たり前ですが)ドリフトなく運用することができます。

1つのテンプレートを使用して複数のAutoHealing構成を定義している場合、スタック更新のタイミング等を考慮・検討する必要があります。

CloudFormationテンプレートサンプル

solution2.yml
AWSTemplateFormatVersion: '2010-09-09'
Description: Stack of solution2

Parameters:
  IamInstanceProfileArn:
    Type: String
    Description: Enter the arn of the instance profile to be used for the instance.

  ImageId:
    Type: AWS::EC2::Image::Id
    Description: Enter the image id to be used for the instance.

  SecurityGroupIds:
    Type: List<AWS::EC2::SecurityGroup::Id>
    Description: Enter the security group ids to be used for the instance.

  VPCZoneIdentifier:
    Type: List<AWS::EC2::Subnet::Id>
    Description: Enter the subnet ids to be used for the instance.

Resources:
  Solution2LaunchTemplate: # 起動テンプレート
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: solution2-launch-template
      LaunchTemplateData:
        IamInstanceProfile:
          Arn: !Ref IamInstanceProfileArn
        ImageId: !Ref ImageId # CloudFormationパラメータを指定
        InstanceType: t2.small
        SecurityGroupIds:
          !Ref SecurityGroupIds
        TagSpecifications:
          - ResourceType: instance
            Tags:
              - Key: Name
                Value: solution1-instance

  Solution2AutoScalingGroup: # AutoScalingGroup(Auto Healing)
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      AutoScalingGroupName: solution2-auto-scaling-group
      LaunchTemplate:
        LaunchTemplateId: !Ref Solution2LaunchTemplate
        Version: !GetAtt Solution2LaunchTemplate.LatestVersionNumber
      MaxSize: 1
      MinSize: 1
      VPCZoneIdentifier:
        !Ref VPCZoneIdentifier

  • solution2.ymlテンプレートはパラメータImageIdを定義
  • 起動テンプレートのImageIdプロパティでは!Ref ImageIdとしてパラメータImageIdを指定

AWSBackupをトリガーにスタックを更新するServerlessFunctionサンプル

solution2-update-stack.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Lambda function to update CloudFormation stack's ImageId parameter after AWS Backup AMI completed.

Resources:
  UpdateStackImageIdFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: UpdateStackImageIdFunction
      Runtime: python3.12
      Handler: lambda_function.lambda_handler
      MemorySize: 128
      Timeout: 30
      Policies:
        - AWSLambdaBasicExecutionRole
        - Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Action:
                - ec2:DescribeImages
                - cloudformation:DescribeStacks
                - cloudformation:UpdateStack
              Resource: "*"
      Events:
        BackupCompletedEvent:
          Type: EventBridgeRule
          Properties:
            Pattern:
              source:
                - "aws.backup"
              detail-type:
                - "Backup Job State Change"
              detail:
                state:
                  - "COMPLETED"

lambda_function.py
import boto3

def lambda_handler(event, context):
    print(f"{event=}")

    # AWSBackupイベントから、ImageIdを取得
    image_id: str = event["resources"][0].split("/")[-1]

    # Imageに付与された'StackName'タグを取得
    describe_images_response: dict = boto3.client("ec2").describe_images(ImageIds=[image_id])
    specific_tag: list[str] = [
        tag["Value"]
        for tag in describe_images_response["Images"][0]["Tags"]
        if tag["Key"] == "StackName"
    ]
    if not specific_tag:
        print("Tag: StackName is not found, Skipped.")
        return

    stack_name: str = specific_tag[0]

    # スタックの現在パラメータを取得
    cfn = boto3.client("cloudformation")
    stack: dict = cfn.describe_stacks(StackName=stack_name)["Stacks"][0]
    current_parameters: list[dict] = stack.get("Parameters", [])

    # パラメータを更新するための新しいリストを作成
    new_parameters: list[dict] = []
    image_id_updated: bool = False

    for param in current_parameters:
        if param["ParameterKey"] == "ImageId":
            new_parameters.append({
                "ParameterKey": "ImageId",
                "ParameterValue": image_id
            })
            image_id_updated = True
        else:
            # 変更しないパラメータは「UsePreviousValue: True」で指定
            new_parameters.append({
                "ParameterKey": param["ParameterKey"],
                "UsePreviousValue": True
            })

    if not image_id_updated:
        print(f"Parameter: ImageId not found in stack {stack_name}. Skipped update.")
        return

    # スタック更新を実行
    response: dict = cfn.update_stack(
        StackName=stack_name,
        UsePreviousTemplate=True,
        Parameters=new_parameters,
        Capabilities=["CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND"]
    )
    print(f"Stack update started: {response["StackId"]}")

  • AWSBackupの完了をトリガーにLambda関数を実行
  • 作成されたAMIに特定のタグ:StackName が存在するかチェック、存在すれば値として設定されたスタックのImageIdパラメータを作成されたAMIでスタックを更新
AWSBackup後の動作結果

update実施ログ.jpg
image.png

  • AWSBackupの完了をトリガーにLambda関数が動作、ImageIdパラメータを変更しスタックの更新を実施

image.png

  • 当然ドリフトもありません

さいごに

この記事ではドリフトを回避しつつAMIを更新する方法を2つ紹介しました。
ドリフトを発生させることなく柔軟な運用を実現するためのテクニックの一つとして誰かの役に立てば幸いです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?