📝

Auto Scalingライフサイクルフックを利用して、AMIを取得するLambda関数

2021/06/25に公開

Auto Scalingライフサイクルフックを利用して、AMIを取得する方法で、実装の全体像を紹介しましたが、こちらの記事ではLambda関数の中身を紹介します。

関数概要

  • ランタイム
    • Node.js 14.x
  • タイムアウト
    • 35秒
  • メモリ
    • 128MB
  • トリガー
    • EventBridge
  • アクセス権限
    • AWSLambdaBasicExecutionRole
    • ec2:DescribeImages
    • e2:CreateImage
    • ec2:CreateTags
    • lambda:UpdateFunctionConfiguration
    • autoscaling:CompleteLifecycleAction
  • 環境変数
    • ASGNAME:現在のAuto Scalingグループ名

コード

index.js

  • 現在のAuto Scalingグループ名と、イベントが発生したAuto Scalingグループ名を取得
  • 起動イベントなら環境変数を更新する
  • 終了イベントの場合、Auto Scalingグループ名を比較
    • 現在のグループ名と同じグループ名のイベントならAMI取得
    • 現在のグループ名と異なるグループ名の場合、リリースによるグループの置換のため、グループの削除を続行
  • 各APIはサービスごとのクラスで呼び出している
index.js
const AWS = require('aws-sdk');
AWS.config.update({ region: 'ap-northeast-1' });
AWS.config.apiVersions = {
  ec2: '2016-11-15',
  lambda: '2015-03-31',
  autoscaling: '2011-01-01',
};
const EC2API = require('./AWS_API/EC2API');
const ec2 = new EC2API(AWS);
const LambdaAPI = require('./AWS_API/LambdaAPI');
const lambda = new LambdaAPI(AWS);
const AutoScalingAPI = require('./AWS_API/AutoScalingAPI');
const autoscaling = new AutoScalingAPI(AWS);

exports.handler = async (event, context) => {
 //ログ出力
  console.log(JSON.stringify(event, null, 2));
  //環境変数から現在のAuto Scalingグループ名を取得
  const currentAsgName = process.env.AsgName;
  //イベントが発生したAuto Scalingグループ名とインスタンスIDを取得
  const eventAsgName = event.detail.AutoScalingGroupName;
  const instanceId = event.detail.EC2InstanceId;
  if (
    event.detail.LifecycleTransition === 'autoscaling:EC2_INSTANCE_LAUNCHING'
  ) {
    //リリースによる新規Auto Scalingグループ起動時に、Lambdaの環境変数を更新
    try {
      await lambda.updateFunctionConfiguration(
        context.functionName,
        eventAsgName
      );
    } catch (err) {
      console.log(err);
    }
  } else {
    if (currentAsgName === eventAsgName) {
      //予期せぬスケールイン時に障害調査用のAMIを取得
      const imageName = `Disability_Image_${new Date().getTime()}`;
      try {
        const imageId = await ec2.createImage(instanceId, imageName);
        const snapshotId = await getSnapshotid(imageId);
        if (snapshotId === undefined) {
          console.log('スナップショットIDを取得できませんでした。');
          return;
        }
        await ec2.createTags(imageId, snapshotId, imageName);
      } catch (err) {
        console.log(err);
      }
    } else {
      //リリースによる古いAuto Scalingグループの削除続行
      const result = await autoscaling.completeLifecycleAction(
        eventAsgName,
        event.detail.LifecycleActionToken,
        instanceId,
        event.detail.LifecycleHookName
      );
      console.log(result);
    }
  }
};

//スナップショットのidを取得するメソッド
//AMI作成直後はスナップショットのIDが生成されないので、30秒だけループ
async function getSnapshotid(imageId) {
  for (let i = 0; i < 10; i++) {
    await new Promise((resolve) => setTimeout(resolve, 3000));
    try {
      const ebs = await ec2.describeImages(imageId);
      if ('SnapshotId' in ebs) {
        return ebs.SnapshotId;
      }
    } catch (err) {
      console.log('getSnapshotidでエラーが発生しました。');
      throw err;
    }
    if (i === 9) {
      console.log('30秒以内にスナップショットのidを取得できませんでした。');
      return undefined;
    }
  }
}

EC2API.js

EC2関連のAPIを詰め込んだクラスです。

  • createImage
    • AMIを作成
  • describeImages
    • AMIの詳細を取得
  • createTags
    • AMIとスナップショットにタグ付け
EC2API.js
module.exports = class EC2API {
  constructor(AWS) {
    this.ec2 = new AWS.EC2();
  }

  //AMIを作成するメソッド
  async createImage(InstanceId, Name) {
   //Windowsインスタンスの場合
    const params = {
      InstanceId /* required */,
      Name /* required */,
      BlockDeviceMappings: [
        {
          DeviceName: '/dev/sda1',
          Ebs: {
            DeleteOnTermination: true,
            Encrypted: false,
            //Iops: 'NUMBER_VALUE',
            //KmsKeyId: 'STRING_VALUE',
            //SnapshotId: 'STRING_VALUE',
            //Throughput: 'NUMBER_VALUE',
            VolumeSize: 30,
            VolumeType: 'gp2',
          },
          //NoDevice: 'STRING_VALUE',
          //VirtualName: 'STRING_VALUE'
        },
        /* more items */
      ],
      Description: Name,
      DryRun: false,
      NoReboot: true,
    };

    try {
      const result = await this.ec2.createImage(params).promise();
      return result.ImageId;
    } catch (err) {
      console.log('createImageでエラーが発生しました。');
      throw err;
    }
  }

  //AMIのスナップショットidを取得するメソッド
  async describeImages(imageId) {
    const params = {
      ImageIds: [imageId],
    };

    try {
      const result = await this.ec2.describeImages(params).promise();
      return result.Images[0].BlockDeviceMappings[0].Ebs;
    } catch (err) {
      console.log('describeImagesでエラーが発生しました。');
    }
  }

  //AMIとスナップショットにタグ(名前)を付けるメソッド
  async createTags(imageId, snapshotId, Value) {
    const params = {
      Resources: [imageId, snapshotId],
      Tags: [
        {
          Key: 'Name',
          Value,
        },
        {
          Key: 'Name',
          Value,
        },
      ],
    };

    try {
      await this.ec2.createTags(params).promise();
    } catch (err) {
      console.log('createTagsでエラーが発生しました。');
      throw err;
    }
  }
};

LambdaAPI.js

LambdaのupdateFunctionConfigurationを使用するためのクラス

  • updateFunctionConfiguration
    • Lambdaの環境変数を更新
LambdaAPI.js
module.exports = class LambdaAPI {
  constructor(AWS) {
    this.lambda = new AWS.Lambda();
  }

  //Lambdaの環境変数を更新するメソッド
  async updateFunctionConfiguration(FunctionName, newAsgName) {
    const params = {
      FunctionName,
      Environment: {
        Variables: {
          AsgName: newAsgName,
          /* '<EnvironmentVariableName>': ... */
        },
      },
    };
    try {
      await this.lambda.updateFunctionConfiguration(params).promise();
    } catch (err) {
      console.log('updateFunctionConfigurationでエラーが発生しました。');
      throw err;
    }
  }
};

AutoScalingAPI.js

Auto ScalingのcompleteLifecycleActionを使用するためのクラス

  • completeLifecycleAction
    • ライフサイクルフックの一時停止を解除
AutoScalingAPI.js
module.exports = class AutoScalingAPI {
  constructor(AWS) {
    this.autoscaling = new AWS.AutoScaling();
  }

  //Auto Scalingグループのライフサイクルアクションを完了させるメソッド
  async completeLifecycleAction(
    AutoScalingGroupName,
    LifecycleActionToken,
    InstanceId,
    LifecycleHookName
  ) {
    const params = {
      AutoScalingGroupName,
      LifecycleActionResult: 'CONTINUE',
      LifecycleActionToken,
      InstanceId,
      LifecycleHookName,
    };
    try {
      const result = await this.autoscaling
        .completeLifecycleAction(params)
        .promise();
      return result;
    } catch (err) {
      console.log('completeLifecycleActionでエラーが発生しました。');
      throw err;
    }
  }
};

その他

EventBridgeのイベントパターンは以下の通りです。
こちらはEventBridgeコンソールで簡単に設定可能です。

{
  "source": ["aws.autoscaling"],
  "detail-type": ["EC2 Instance-launch Lifecycle Action", "EC2 Instance-terminate Lifecycle Action"]
}

まとめ

今回は、Auto Scalingライフサイクルフックを利用して、AMIを取得するLambda関数を紹介しました。
参考になれば幸いです。

Discussion