LoginSignup
5

More than 5 years have passed since last update.

Serverless FrameworkでEC2のAMIバックアップを自動化

Posted at

TL;DR

  • AWS Lambda を使うとサーバレスに AMI 作成を自動化できる
  • Serverless Framework を使うと Lambda のコードを管理できる
  • でもこのくらいのことは直で Lambda Function 作った方が早いと思います。 Serverless Framework を使ってみたかっただけです
  • ソース

手順

環境構築

  • Node.js v4以上をインストール

  • Serverless Frameworkをインストール

$ npm install -g serverless
  • AWS IAMで、アクセス権限にAdministratorAccessを持つユーザを作り、アクセスキーIDとシークレットアクセスキーを用意

  • プロバイダ証明書を設定

$ serverless config credentials --provider aws --key アクセスキーID --secret シークレットアクセスキー

実装

  • serviceを作成
$ serverless create --template aws-nodejs --name ami
  • 試しにデフォルトのfunctionをローカル実行してみる
$ serverless invoke local --function hello
{
    "statusCode": 200,
    "body": "{\"message\":\"Go Serverless v1.0! Your function executed successfully!\",\"input\":\"\"}"
}
  • ES2015+対応用のwebpackプラグインとbabelと仲間たちをインストール
$ npm install babel-core babel-loader babel-preset-env serverless-webpack webpack --save-dev
  • aws-sdkをインストール
$ npm install aws-sdk
  • AMI作成して、期限切れAMI削除するfunctionを作る
handler.js
import AWS from 'aws-sdk';

const AWS_REGION = 'ap-northeast-1'; // リージョン
const AMI_RETENTION_PERIOD = 7; // 保管日数

AWS.config.region = AWS_REGION;
const ec2 = new AWS.EC2();

/**
 * List EC2 instances
 * @return {Promise.<Array>} instances
 */
const listInstances = () => {
  console.log('listInstances');

  // describeInstances
  // http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EC2.html#describeInstances-property
  return ec2.describeInstances({
    Filters: [{ Name: 'tag:Backup', Values: ['yes'] }],
  }).promise()
  .then((data) => {
    const instances = data.Reservations.length === 0 ? data.Reservations : data.Reservations
    .map(reservation => reservation.Instances.map(instance => ({
      InstanceId: instance.InstanceId,
      Tags: instance.Tags,
    })))
    .reduce((previousValue, currentValue) => previousValue.concat(currentValue));

    return instances;
  });
};

/**
 * Create AMIs
 * @param {Array} instances
 * @returns {Promise.<Array>} AMIs
 */
const createImages = (instances) => {
  console.log('createImages target instances =', JSON.stringify(instances));

  return Promise.all(instances.map((instance) => {
    const name = instance.Tags.some((tag) => {
      if (tag.Key === 'Name') {
        return true;
      }
      return false;
    }) ? instance.Tags.find((tag) => {
      if (tag.Key === 'Name') {
        return true;
      }
      return false;
    }).Value : instance.InstanceId;

    // createImage
    // http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EC2.html#createImage-property
    return ec2.createImage({
      InstanceId: instance.InstanceId,
      Name: `${name} on ${new Date().toDateString()}`,
      NoReboot: true,
    }).promise();
  }));
};

/**
 * Create Tags
 * @param {array} images - AMIs
 * @returns {Promise.<Array>} null
 */
const createTags = (images) => {
  console.log('createTags target images =', JSON.stringify(images));

  // createTags
  // http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EC2.html#createTags-property
  return Promise.all(images.map(image => ec2.createTags({
    Resources: [image.ImageId],
    Tags: [{ Key: 'Delete', Value: 'yes' }],
  }).promise()));
};

/**
 * List expired AMIs
 * @return {Promise.<Array>} AMIs
 */
const listExpiredImages = () => {
  console.log('listExpiredImages');

  // describeImages
  // http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EC2.html#describeImages-property
  return ec2.describeImages({
    Owners: ['self'],
    Filters: [{ Name: 'tag:Delete', Values: ['yes'] }],
  }).promise()
  .then((data) => {
    const expiredImages = data.Images
    .filter((image) => {
      const creationDate = new Date(image.CreationDate);
      const expirationDate = new Date(Date.now() - (86400000 * AMI_RETENTION_PERIOD));

      if (creationDate < expirationDate) {
        return true;
      }
      return false;
    })
    .map(image => ({
      ImageId: image.ImageId,
      CreationDate: image.CreationDate,
      BlockDeviceMappings: image.BlockDeviceMappings.map(mapping => ({
        Ebs: { SnapshotId: mapping.Ebs.SnapshotId },
      })),
    }));

    return expiredImages;
  });
};

/**
 * Delete AMIs
 * @param {Array} images - AMIs
 * @returns {Promise.<Array>} block device mappings
 */
const deleteImages = (images) => {
  console.log('deleteImages target images =', JSON.stringify(images));

  // deregisterImage
  // http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EC2.html#describeImages-property
  return Promise.all(images.map(image => ec2.deregisterImage({
    ImageId: image.ImageId,
  }).promise()))
  .then(() => {
    const mappings = images.length === 0 ? images :
    images
    .map(image => image.BlockDeviceMappings)
    .reduce((previousValue, currentValue) => previousValue.concat(currentValue));

    return mappings;
  });
};

/**
 * Delete Snapshots
 * @param {Array} mappings - block device mappings
 * @returns {Promise.<Array>} null
 */
const deleteSnapshots = (mappings) => {
  console.log('deleteSnapshots target mappings =', JSON.stringify(mappings));

  // deleteSnapshot
  // http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EC2.html#deleteSnapshot-property
  return Promise.all(mappings.map(mapping => ec2.deleteSnapshot({
    SnapshotId: mapping.Ebs.SnapshotId,
  }).promise()));
};

/**
 * Lambda function: create AMIs and delete expired AMIs
 */
const createAndDeleteAMI = () => {
  listInstances()
  .then(instances => createImages(instances))
  .then(images => createTags(images))
  .then(() => listExpiredImages())
  .then(images => deleteImages(images))
  .then(mappings => deleteSnapshots(mappings))
  .then(() => console.log('Done'))
  .catch(err => console.error(err));
};

export { createAndDeleteAMI };
  • webpack設定
webpack.config.js
module.exports = {
  entry: './handler.js',
  target: 'node',
  // AWS Lambda Available Libraries
  externals: { 'aws-sdk': 'aws-sdk' },
  module: {
    loaders: [{
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'babel',
    }],
  },
};
  • Babel設定
.babelrc
{
  "presets": [
    ["env", {
      /* AWS Lambda Execution Environment */
      "targets": { "node": "4.3.2" },
      "include": ["transform-es2015-destructuring"]
    }]
  ]
}

LambdaのNodeバージョンは4.3.2だが、targets設定するだけだとシンタックスエラーが出てしまった

  • Serverless Framework設定
serverless.yml
service: ami

plugins:
  - serverless-webpack

provider:
  name: aws
  runtime: nodejs4.3
  region: ap-northeast-1
  iamRoleStatements:
    - Effect: Allow
      Action:
        - ec2:DescribeInstances
        - ec2:CreateImage
        - ec2:CreateTags
        - ec2:DescribeImages
        - ec2:DeregisterImage
        - ec2:DeleteSnapshot
      Resource: "*"

functions:
  createAndDeleteAMI:
    handler: handler.createAndDeleteAMI
    events:
      - schedule: cron(0 0 * * ? *) # 毎日0時に実行

スケジュール構文はこちらを参考に

確認・デプロイ

  • ローカルで動作確認してみる
$ serverless webpack invoke --function createAndDeleteAMI
  • デプロイ
$ serverless deploy
  • Lambda 上で動作確認してみる
$ serverless invoke --function createAndDeleteAMI --log

参考

http://blog.powerupcloud.com/2016/10/15/serverless-automate-ami-creation-and-deletion-using-aws-lambda/
https://github.com/serverless/serverless
https://github.com/serverless/examples
https://github.com/americansystems/serverless-es6-jest
https://github.com/elastic-coders/serverless-webpack
https://babeljs.io/docs/plugins/preset-env/
https://github.com/apex/apex/issues/217#issuecomment-194247472
http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EC2.html
https://serverless.com/framework/docs/providers/aws/guide/functions#permissions
https://serverless.com/framework/docs/providers/aws/events/schedule/

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
5