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を作る
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設定
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設定
{
"presets": [
["env", {
/* AWS Lambda Execution Environment */
"targets": { "node": "4.3.2" },
"include": ["transform-es2015-destructuring"]
}]
]
}
LambdaのNodeバージョンは4.3.2だが、targets設定するだけだとシンタックスエラーが出てしまった
- Serverless Framework設定
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/