LoginSignup
2
1

More than 5 years have passed since last update.

AWS Lambda-backed カスタムリソース - AMIを作成する

Last updated at Posted at 2016-05-03

Lambdaを使うのもカスタムリソースを作成するのもはじめてなので、練習も含めてAMIを作成するカスタムリソースを作成してみた。

つくるもの

  • InstanceIDをパラメータとして、AMIを作成してAMI IDをリターンするカスタムリソース
  • CloudFormationのRequestTypeに応じて、挙動を変える
    • Create: AMIの作成
    • Update: 以前のAMIを削除して、新しく作成
    • Delete: 以前のAMIを削除
  • Optionで削除をしなくできるようにする

使い方イメージ

以下のようなリソースをCloudFormationに入れる感じにしたい。

"InstanceImage" : {
  "Type" : "Custom::CreateImage",
  "Properties" : {
    "ServiceToken"   : { "Fn::GetAtt" : [ "CreateImageLambdaFunction", "Arn" ] },
    "InstanceId"     : { "Ref" : "Instance" }, <-- InstanceIDをパラメータに
    "DeletionPolicy" : "Retain" <-- Optionで削除しないように
  }
}

完成コード

node.jsで開発。CloudFormationテンプレートの中で埋め込みしたかったので、外部ライブラリは使わなかった。

node.js
var AWS      = require('aws-sdk'),
    response = require('cfn-response'),
    domain   = require('domain');

exports.handler = function(event, context) {
  var requestType    = event.RequestType,
      resourceId     = event.PhysicalResourceId,
      instanceId     = event.ResourceProperties.InstanceId,
      deletionPolicy = event.ResourceProperties.DeletionPolicy,
      ec2            = new AWS.EC2(),
      d              = domain.create();

  d.on('error', function (err) {
    console.error(err, err.stack);
    return response.send(event, context, response.FAILED);
  });

  var succeed = function (imageId) {
    response.send(event, context, response.SUCCESS, { ImageId: imageId }, imageId);
  };

  var createImage = function (instanceId, callback) {
    ec2.describeInstances({
      InstanceIds: [ instanceId ]
    }, d.intercept(function (data) {
      var instance = data.Reservations[0].Instances[0];
      var instanceName = instance.Tags.filter(function (tag) {
        return tag.Key == 'Name'
      })[0].Value;

      ec2.createImage({
        InstanceId: instanceId,
        Name: instanceName + '-' + Date.now(),
        NoReboot: true
      }, d.intercept(function (data) {
        callback(data.ImageId);
      }));
    }));
  };

  var deleteSnapshots = function (snapshots, callback) {
    if (snapshots.length === 0) {
      return callback();
    }
    var snapshotId = snapshots.shift();
    ec2.deleteSnapshot({
      SnapshotId: snapshotId
    }, d.intercept(function (data) {
      deleteSnapshots(snapshots, callback);
    }));
  };

  var deleteImage = function (imageId, callback) {
    ec2.describeImages({
      ImageIds: [ imageId ]
    }, function (err, data) {
      if (err) {
        return callback();
      }
      var snapshots = data
      .Images[0].BlockDeviceMappings
      .map(function (blockDevice) { return blockDevice.Ebs })
      .map(function (ebs) { return ebs.SnapshotId });
      ec2.deregisterImage({
        ImageId: imageId
      }, d.intercept(function (data) {
        deleteSnapshots(snapshots, callback);
      }));
    });
  };

  if (resourceId && deletionPolicy !== 'Retain') {
    deleteImage(resourceId, function () {
      if (requestType === 'Delete') {
        return succeed(resourceId);
      }
      createImage(instanceId, succeed);
    });
  } else {
    createImage(instanceId, succeed);
  }
};

サンプル

インスタンス作ってついでにAMIも作成するCloudFormation。

{
  "AWSTemplateFormatVersion": "2010-09-09",

  "Description" : "Sample Template",

  "Parameters" : {
    "KeyName" : {
      "Description"           : "Name of an existing EC2 KeyPair to enable SSH access to the instances.",
      "Type"                  : "AWS::EC2::KeyPair::KeyName",
      "ConstraintDescription" : "can contain only alphanumeric characters, spaces, dashes and underscores."
    },
    "InstanceName" : {
      "Description"           : "Instance Name for node.",
      "Type"                  : "String",
      "AllowedPattern"        : "[-a-zA-Z0-9]+"
    },
    "InstanceType" : {
      "Description"           : "Instance type for node.",
      "Type"                  : "String",
      "Default"               : "t2.nano",
      "AllowedValues"         : [ "t2.nano", "t2.micro","t2.small","t2.medium","m3.medium","m3.large","m3.xlarge","m3.2xlarge","c3.large","c3.xlarge","c3.2xlarge","c3.4xlarge","c3.8xlarge" ],
      "ConstraintDescription" : "must be a valid T2, M3 or C3 instance type."
    },
    "SecurityGroups" : {
      "Description"           : "SecurityGroups.",
      "Type"                  : "List<AWS::EC2::SecurityGroup::Id>"
    },
    "Subnets": {
      "Description"           : "ID of your existing subnets.",
      "Type"                  : "List<AWS::EC2::Subnet::Id>"
    }
  },

  "Mappings": {
    "AmazonAMI" : {
      "ap-northeast-1"        : { "AMI" : "ami-383c1956" }
    }
  },

  "Resources" : {

    "LambdaExecutionRole" : {
      "Type" : "AWS::IAM::Role",
      "Properties" : {
        "AssumeRolePolicyDocument": {
          "Version"  : "2012-10-17",
          "Statement": [{
            "Effect"    : "Allow",
            "Principal" : { "Service" : [ "lambda.amazonaws.com" ] },
            "Action"    : [ "sts:AssumeRole" ]
          }]
        },
        "Path" : "/",
        "Policies" : [{
          "PolicyName"     : "root",
          "PolicyDocument" : {
            "Version"   : "2012-10-17",
            "Statement" : [{
              "Effect"   : "Allow",
              "Action"   : [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ],
              "Resource" : "arn:aws:logs:*:*:*"
            },
            {
              "Effect"   : "Allow",
              "Action"   : [ "ec2:*" ],
              "Resource" : "*"
            }]
          }
        }]
      }
    },

    "CreateImageLambdaFunction" : {
      "Type" : "AWS::Lambda::Function",
      "Properties" : {
        "Code" : {
          "ZipFile" :  { "Fn::Join" : [ "\n", [
            "var AWS      = require('aws-sdk'),",
            "    response = require('cfn-response'),",
            "    domain   = require('domain');",
            "exports.handler = function(event, context) {",
            "  var requestType    = event.RequestType,",
            "      resourceId     = event.PhysicalResourceId,",
            "      instanceId     = event.ResourceProperties.InstanceId,",
            "      deletionPolicy = event.ResourceProperties.DeletionPolicy,",
            "      ec2            = new AWS.EC2(),",
            "      d              = domain.create();",
            "  d.on('error', function (err) {",
            "    console.error(err, err.stack);",
            "    return response.send(event, context, response.FAILED);",
            "  });",
            "  var succeed = function (imageId) {",
            "    response.send(event, context, response.SUCCESS, { ImageId: imageId }, imageId);",
            "  };",
            "  var createImage = function (instanceId, callback) {",
            "    ec2.describeInstances({",
            "      InstanceIds: [ instanceId ]",
            "    }, d.intercept(function (data) {",
            "      var instance = data.Reservations[0].Instances[0];",
            "      var instanceName = instance.Tags.filter(function (tag) {",
            "        return tag.Key == 'Name'",
            "      })[0].Value;",
            "      ec2.createImage({",
            "        InstanceId: instanceId,",
            "        Name: instanceName + '-' + Date.now(),",
            "        NoReboot: true",
            "      }, d.intercept(function (data) {",
            "        callback(data.ImageId);",
            "      }));",
            "    }));",
            "  };",
            "  var deleteSnapshots = function (snapshots, callback) {",
            "    if (snapshots.length === 0) {",
            "      return callback();",
            "    }",
            "    var snapshotId = snapshots.shift();",
            "    ec2.deleteSnapshot({",
            "      SnapshotId: snapshotId",
            "    }, d.intercept(function (data) {",
            "      deleteSnapshots(snapshots, callback);",
            "    }));",
            "  };",
            "  var deleteImage = function (imageId, callback) {",
            "    ec2.describeImages({",
            "      ImageIds: [ imageId ]",
            "    }, function (err, data) {",
            "      if (err) {",
            "        return callback();",
            "      }",
            "      var snapshots = data",
            "      .Images[0].BlockDeviceMappings",
            "      .map(function (blockDevice) { return blockDevice.Ebs })",
            "      .map(function (ebs) { return ebs.SnapshotId });",
            "      ec2.deregisterImage({",
            "        ImageId: imageId",
            "      }, d.intercept(function (data) {",
            "        deleteSnapshots(snapshots, callback);",
            "      }));",
            "    });",
            "  };",
            "  if (resourceId && deletionPolicy !== 'Retain') {",
            "    deleteImage(resourceId, function () {",
            "      if (requestType === 'Delete') {",
            "        return succeed(resourceId);",
            "      }",
            "      createImage(instanceId, succeed);",
            "    });",
            "  } else {",
            "    createImage(instanceId, succeed);",
            "  }",
            "};"
          ]]}
        },
        "Handler" : "index.handler",
        "Runtime" : "nodejs",
        "Timeout" : "30",
        "Role"    : { "Fn::GetAtt" : [ "LambdaExecutionRole", "Arn" ] }
      }
    },

    "Instance" : {
      "Type"           : "AWS::EC2::Instance",
      "Properties" : {
        "InstanceType" : { "Ref" : "InstanceType" },
        "KeyName"      : { "Ref" : "KeyName" },
        "ImageId"      : { "Fn::FindInMap" : [ "AmazonAMI", { "Ref" : "AWS::Region" }, "AMI"] },
        "Tags": [
          { "Key" : "Name", "Value" : { "Ref" : "InstanceName" } }
        ],
        "NetworkInterfaces": [ {
          "AssociatePublicIpAddress" : true,
          "DeviceIndex"              : 0,
          "SubnetId"                 : { "Fn::Select" : [ "0", { "Ref" : "Subnets" } ] },
          "GroupSet"                 : { "Ref" : "SecurityGroups" }
        } ],
        "UserData" : { "Fn::Base64" : { "Fn::Join" : [ "", [
          "#!/bin/bash\n",
          "yum update -y\n",
          "yum install -y nginx\n",
          "/etc/init.d/nginx start\n",
          "chkconfig nginx on\n",
          "/opt/aws/bin/cfn-signal -e 0 --stack ", { "Ref": "AWS::StackName" },
          " --resource Instance ",
          " --region ", { "Ref" : "AWS::Region" }, "\n"
        ]]}}
      },
      "CreationPolicy" : {
        "ResourceSignal" : {
          "Count"      : 1,
          "Timeout"    : "PT10M"
        }
      }
    },

    "InstanceImage" : {
      "Type" : "Custom::CreateImage",
      "Properties" : {
        "ServiceToken" : { "Fn::GetAtt" : [ "CreateImageLambdaFunction", "Arn" ] },
        "InstanceId"   : { "Ref" : "Instance" }
      }
    }
  }
}
2
1
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
2
1