Created
July 13, 2020 15:45
-
-
Save akira345/fe525844c34e09b65caa4d0b0c82c6d8 to your computer and use it in GitHub Desktop.
EC2バックアップラムダのnodeJS版
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
const AWS = require( 'aws-sdk' ); | |
AWS.config.update( { region: 'ap-northeast-1' } ); | |
const ec2 = new AWS.EC2(); | |
const moment = require( 'moment-timezone' ); | |
/** | |
* 起動しているEC2インスタンスIDを返します。 | |
*/ | |
const getRunnningEc2InstanceIds = async () => { | |
// リージョン内にあるすべてのEC2インスタンスを列挙 | |
var params = { | |
Filters: [ | |
{ | |
Name: 'instance-state-name', | |
Values: [ 'running' ] | |
} | |
] | |
}; | |
let nextToken = ''; | |
const instanceIds = []; | |
while ( true ) { | |
let response; | |
if ( nextToken ) { | |
params.NextToken = nextToken; | |
response = await ec2.describeInstances( params ).promise(); | |
} else { | |
response = await ec2.describeInstances( params ).promise(); | |
} | |
response.Reservations.forEach( reservation => { | |
reservation.Instances.forEach( instance => { | |
instanceIds.push( | |
instance.InstanceId | |
); | |
} ); | |
} ); | |
if ( response.NextToken ) { | |
nextToken = response.NextToken; | |
} else { | |
break; | |
} | |
} | |
return instanceIds; | |
}; | |
/** | |
* 指定したインスタンスIDのタグ配列を返します。 | |
* @param {*} instanceId | |
*/ | |
const getTagSets = async ( instanceId ) => { | |
const tagSets = await ec2.describeInstances( { | |
InstanceIds: [ | |
instanceId | |
] | |
} ).promise(); | |
return tagSets.Reservations[ 0 ].Instances[ 0 ].Tags; | |
}; | |
/** | |
* 指定したインスタンスIDのバックアップ設定を返します。 | |
* @param {*} instanceId | |
*/ | |
const getBackupConfig = async ( instanceId ) => { | |
let backupConfig = { | |
backupSw: false, | |
generation: 3 | |
}; | |
const tagSets = await getTagSets( instanceId ); | |
tagSets.forEach( tagSet => { | |
if ( tagSet.Key.toLowerCase() == 'backup' && tagSet.Value.toLowerCase() == 'on' ) { | |
backupConfig.backupSw = true; | |
} | |
if ( tagSet.Key.toLowerCase() == 'generation' ) { | |
backupConfig.generation = tagSet.Value; | |
} | |
} ); | |
return backupConfig; | |
}; | |
/** | |
* バックアップの世代が多いものを特定する | |
* @param {*} instanceId | |
*/ | |
const searchDeleteImages = async ( instanceId ) => { | |
const backupConfig = await getBackupConfig( instanceId ); | |
const ret = await ec2.describeImages( { | |
Owners: [ "self" ], | |
Filters: [ | |
{ | |
Name: "name", | |
Values: [ '*_' + instanceId + '-*' ] | |
}, | |
{ | |
Name: "state", | |
Values: [ | |
"available" // 地味にAMI作成に失敗するのがあるので成功しているもののみ対象とする | |
] | |
} | |
] | |
} ).promise(); | |
const imageLists = ret.Images; | |
//AMIイメージ名を降順にソート(日付が含まれているので新しいもの順にする) | |
const sortLists = imageLists.sort( ( a, b ) => { | |
if ( a.Name < b.Name ) return 1; | |
if ( a.Name > b.Name ) return -1; | |
return 0; | |
} ); | |
// 並べ替えたAMIイメージ名の上位から指定した世代数以降を返す | |
return sortLists.slice( backupConfig.generation ); | |
}; | |
async function asyncFilter ( array, asyncCallback ) { | |
const bits = await Promise.all( array.map( asyncCallback ) ); | |
return array.filter( ( _, i ) => bits[ i ] ); | |
} | |
/** | |
* main | |
* @param {*} event | |
* @param {*} context | |
* @param {*} callback | |
*/ | |
exports.handler = async ( event, context, callback ) => { | |
try { | |
const instanceIds = await getRunnningEc2InstanceIds(); | |
const targetInstanceIds = await asyncFilter( instanceIds, async instanceId => { | |
const backupConfig = await getBackupConfig( instanceId ); | |
if ( backupConfig.backupSw && backupConfig.generation != null ) { | |
console.log( "Start backup in " + instanceId ); | |
return true; | |
} else { | |
console.log( "Backup configuration are not correctly in " + instanceId ); | |
return false; | |
} | |
} ); | |
await Promise.all( | |
targetInstanceIds.map( async instanceId => { | |
let amiName = '_' + instanceId + '-' + moment().tz( 'Asia/Tokyo' ).format( 'YYYYMMDDHHmm' ); | |
const comment = "Automatically Backup AMI"; | |
const tagSet = await getTagSets( instanceId ); | |
tagSet.forEach( tag => { | |
if ( tag.Key.toLowerCase() == "name" && tag.Value != null ) { | |
// 使えない文字を変換 | |
amiName = tag.Value.replace( /\*/g, 'x' ) + "_" + amiName; | |
} | |
} ); | |
// バックアップ対象であればバックアップ、異なれば終了 | |
try { | |
await ec2.createImage( { | |
InstanceId: instanceId, | |
Description: comment, | |
Name: amiName, | |
NoReboot: true | |
} ).promise(); | |
} catch ( err ) { | |
if ( err.code == 'InvalidAMIName.Duplicate' ) { | |
console.log( "Wait for minuts!" ); | |
} | |
} | |
// バックアップされているもので不要となった世代のものを削除する | |
const deleteImages = await searchDeleteImages( instanceId ); | |
if ( deleteImages != null ) { | |
deleteImages.map( async images => { | |
try { | |
console.log( "Remove old backup:" + images.ImageId ); | |
await ec2.deregisterImage( { | |
ImageId: images.ImageId | |
} ).promise(); | |
} catch ( err ) { | |
if ( err.code == 'InvalidAMIID.Unavailable' ) { | |
console.log( "にせもの" ); | |
} | |
} | |
images.BlockDeviceMappings.filter( device => { | |
// ephemeral領域は取れないので除外 | |
if ( 'VirtualName' in device && device.VirtualName.startsWith( 'ephemeral' ) ) { | |
return false; | |
} | |
return true; | |
} ).map( async device => { | |
await ec2.deleteSnapshot( { | |
SnapshotId: device.Ebs.SnapshotId | |
} ).promise(); | |
} ); | |
} ); | |
} | |
} ) ); | |
console.log( "OK" ); | |
} catch ( error ) { | |
console.log( "エラー" ); | |
console.log( error ); | |
throw Error; | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment