AWS CDK を使って CloudWatch Event + Lambda で EC2 を自動起動・停止させる環境を作ってみた
2019-08-07GA になってから間もない AWS CDK ですが、既にそのバージョンは 1.3.0 になっています。そんな AWS CDK を使って、特定のタグを持つ EC2 インスタンスの自動起動・停止をする環境を構築してみました。
概要
特定のタグ名と値を持つ EC2 インスタンスを自動で停止・起動させる環境を構築します。
自動停止・起動の処理は Lambda で実装し、それを CloudWatch Events のスケジュールで定期的に実行します。この方法自体は以前にも書いた通りですが、今回は Lambda 関数の作成やスケジュールの設定などの環境構築を AWS CDK でやってしまおう という話です。
CDK アプリケーションの作成
それでは早速 AWS CDK アプリケーションを作成していきます。
AWS CDK については公式のドキュメント、およびリファレンスをご参照ください。
CDK はこの記事作成時点で最新の 1.3.0
を使用します。
$ cdk --version
1.3.0 (build bba9914)
TypeScript で実装していきます。
$ mkdir auto-start-stop-ec2
$ cd auto-start-stop-ec2
$ cdk init app --language=typescript
Applying project template app for typescript
Initializing a new git repository...
Executing npm install...
...
# Useful commands
* `npm run build` compile typescript to js
* `npm run watch` watch for changes and compile
* `cdk deploy` deploy this stack to your default AWS account/region
* `cdk diff` compare deployed stack with current state
* `cdk synth` emits the synthesized CloudFormation template
次のようなディレクトリ構成ができあがります。
.
├── README.md
├── bin
│ └── auto-start-stop-ec2.ts
├── cdk.json
├── lib
│ └── auto-start-stop-ec2-stack.ts
├── node_modules
│ ├── @aws-cdk
...
├── package-lock.json
├── package.json
└── tsconfig.json
パッケージのインストール
まずは必要なパッケージをインストールします。
CDK では、コアとなる @aws-cdk/core
の他に、必要となるサービスごとのパッケージを使用します。
$ npm install @aws-cdk/aws-events @aws-cdk/aws-events-targets @aws-cdk/aws-lambda @aws-cdk/aws-iam
準備が整ったので、 lib/auto-start-stop-ec2-stack.ts
を編集して必要なリソースを作成するコードを書いていきます。
Lambda
まずは Lambda 関数です。
今回は予め Python で書かれた Lambda 関数用の auto-start-stop-ec2.py
ファイルを、新たに作成した lambda/
ディレクトリに用意しておき、それを使うように実装します。
また、 Lambda では EC2 インスタンスの起動と停止を実行するので、必要な IAM ポリシーも付与します。
const lambdaFn = new lambda.Function(this, 'singleton', {
code: new lambda.InlineCode(fs.readFileSync('lambda/auto-start-stop-ec2.py', {encoding: 'utf-8'})),
handler: 'index.main',
timeout: cdk.Duration.seconds(300),
runtime: lambda.Runtime.PYTHON_3_7,
});
lambdaFn.addToRolePolicy(new iam.PolicyStatement({
actions: [
'ec2:DescribeInstances',
'ec2:StartInstances',
'ec2:StopInstances'
],
resources: ['*']
}));
ちなみに外部のファイルを読み込む際のパスは、 CDK アプリケーションのルートディレクトリからの相対パスになります。この lib/*.ts
からの相対パスではありません。
CloudWatch Events
続いて、 Lammda 関数を定期実行するための CloudWatch Events を実装します。
const stackConfig = JSON.parse(fs.readFileSync('stack.config.json', {encoding: 'utf-8'}));
// STOP EC2 instances rule
const stopRule = new events.Rule(this, 'StopRule', {
schedule: events.Schedule.expression(`cron(${stackConfig.events.cron.stop})`)
});
stopRule.addTarget(new targets.LambdaFunction(lambdaFn, {
event: events.RuleTargetInput.fromObject({Region: stackConfig.targets.ec2region, Action: 'stop'})
}));
// START EC2 instances rule
const startRule = new events.Rule(this, 'StartRule', {
schedule: events.Schedule.expression(`cron(${stackConfig.events.cron.start})`)
});
startRule.addTarget(new targets.LambdaFunction(lambdaFn, {
event: events.RuleTargetInput.fromObject({Region: stackConfig.targets.ec2region, Action: 'start'})
}));
CloudWatch Events の定義としては、まず events.Rule
オブジェクトを作成し、 addTarget()
メソッドでターゲット (今回の場合は Lambda 関数) を追加するという形になります。
targets.LambdaFunction
のコンストラクタの第 2 引数は任意のパラメータで、 CloudWatch Events で Lambda 関数を呼ぶ時の引数を指定することができます。
今回は 起動と停止の時間、対象となる EC2 インスタンスがあるリージョンを stack.config.json
という JSON ファイル内で定義しておいて、その値を使用するようにしています。
ちなみにここでも指定するファイルパスは、 CDK アプリケーションのルートディレクトリからの相対パスになります。
{
"events": {
"cron": {
"start": "55 23 ? * SUN-THU *",
"stop": "0 12 ? * * *"
}
},
"targets": {
"ec2region": "ap-northeast-1"
}
}
これで CDK アプリケーションの実装は完了です。
auto-start-stop-ec2-stack.ts
の全体としては下記のような感じです。
import events = require('@aws-cdk/aws-events');
import iam = require('@aws-cdk/aws-iam');
import lambda = require('@aws-cdk/aws-lambda');
import targets = require('@aws-cdk/aws-events-targets');
import cdk = require('@aws-cdk/core');
import fs = require('fs');
export class AutoStartStopEc2Stack extends cdk.Stack {
constructor (app: cdk.App, id: string) {
super(app, id);
const stackConfig = JSON.parse(fs.readFileSync('stack.config.json', {encoding: 'utf-8'}));
const lambdaFn = new lambda.Function(this, 'singleton', {
code: new lambda.InlineCode(fs.readFileSync('lambda/auto-start-stop-ec2.py', {encoding: 'utf-8'})),
handler: 'index.main',
timeout: cdk.Duration.seconds(300),
runtime: lambda.Runtime.PYTHON_3_7,
});
lambdaFn.addToRolePolicy(new iam.PolicyStatement({
actions: [
'ec2:DescribeInstances',
'ec2:StartInstances',
'ec2:StopInstances'
],
resources: ['*']
}));
// STOP EC2 instances rule
const stopRule = new events.Rule(this, 'StopRule', {
schedule: events.Schedule.expression(`cron(${stackConfig.events.cron.stop})`)
});
stopRule.addTarget(new targets.LambdaFunction(lambdaFn, {
event: events.RuleTargetInput.fromObject({Region: stackConfig.targets.ec2region, Action: 'stop'})
}));
// START EC2 instances rule
const startRule = new events.Rule(this, 'StartRule', {
schedule: events.Schedule.expression(`cron(${stackConfig.events.cron.start})`)
});
startRule.addTarget(new targets.LambdaFunction(lambdaFn, {
event: events.RuleTargetInput.fromObject({Region: stackConfig.targets.ec2region, Action: 'start'})
}));
}
}
あとは npm run build
を実行して完了です。
CloudFormation のテンプレートを出力してみる
CDK アプリケーションとして実装した auto-start-stop-ec2-stack.ts
は、最終行の空行も含めてちょうど 50 行でした。
これを CloudFormation の template (yaml) として出力してみます。
$ cdk synth
Resources:
singletonServiceRole9C9ECF4A:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service:
Fn::Join:
- ""
- - lambda.
- Ref: AWS::URLSuffix
Version: "2012-10-17"
ManagedPolicyArns:
- Fn::Join:
- ""
- - "arn:"
- Ref: AWS::Partition
- :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Metadata:
aws:cdk:path: AutoStartStopEc2Stack/singleton/ServiceRole/Resource
singletonServiceRoleDefaultPolicyFDD8CA90:
Type: AWS::IAM::Policy
Properties:
PolicyDocument:
Statement:
- Action:
- ec2:DescribeInstances
- ec2:StartInstances
- ec2:StopInstances
Effect: Allow
Resource: "*"
Version: "2012-10-17"
PolicyName: singletonServiceRoleDefaultPolicyFDD8CA90
Roles:
- Ref: singletonServiceRole9C9ECF4A
Metadata:
aws:cdk:path: AutoStartStopEc2Stack/singleton/ServiceRole/DefaultPolicy/Resource
singleton69FEA30F:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: >
import boto3
import json
import traceback
def main(event, context):
try:
region = event['Region']
action = event['Action']
client = boto3.client('ec2', region)
responce = client.describe_instances(
Filters=[{'Name': 'tag:AutoStartStop', "Values": ['TRUE']}])
target_instans_ids = []
for reservation in responce['Reservations']:
for instance in reservation['Instances']:
tag_name = ''
for tag in instance['Tags']:
if tag['Key'] == 'Name':
tag_name = tag['Value']
break
target_instans_ids.append(instance['InstanceId'])
print(target_instans_ids)
if not target_instans_ids:
print('There are no instances subject to automatic {}.'.format(action))
else:
if action == 'start':
client.start_instances(InstanceIds=target_instans_ids)
print('started instances.')
elif action == 'stop':
client.stop_instances(InstanceIds=target_instans_ids)
print('stopped instances.')
else:
print('Invalid action.')
return {
"statusCode": 200,
"message": 'Finished automatic {action} EC2 instances process. [Region: {region}, Action: {action}]'.format(region=event['Region'], action=event['Action'])
}
except:
print(traceback.format_exc())
return {
"statusCode": 500,
"message": 'An error occured at automatic start / stop EC2 instances process.'
}
Handler: index.main
Role:
Fn::GetAtt:
- singletonServiceRole9C9ECF4A
- Arn
Runtime: python3.7
Timeout: 300
DependsOn:
- singletonServiceRoleDefaultPolicyFDD8CA90
- singletonServiceRole9C9ECF4A
Metadata:
aws:cdk:path: AutoStartStopEc2Stack/singleton/Resource
singletonAllowEventRuleAutoStartStopEc2StackStopRule6999EBDE48367C0B:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName:
Fn::GetAtt:
- singleton69FEA30F
- Arn
Principal: events.amazonaws.com
SourceArn:
Fn::GetAtt:
- StopRule00306666
- Arn
Metadata:
aws:cdk:path: AutoStartStopEc2Stack/singleton/AllowEventRuleAutoStartStopEc2StackStopRule6999EBDE
singletonAllowEventRuleAutoStartStopEc2StackStartRule682FBE6A53E17D5B:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName:
Fn::GetAtt:
- singleton69FEA30F
- Arn
Principal: events.amazonaws.com
SourceArn:
Fn::GetAtt:
- StartRule40F02F2E
- Arn
Metadata:
aws:cdk:path: AutoStartStopEc2Stack/singleton/AllowEventRuleAutoStartStopEc2StackStartRule682FBE6A
StopRule00306666:
Type: AWS::Events::Rule
Properties:
ScheduleExpression: cron(0 12 ? * * *)
State: ENABLED
Targets:
- Arn:
Fn::GetAtt:
- singleton69FEA30F
- Arn
Id: Target0
Input: '{"Region":"ap-northeast-1","Action":"stop"}'
Metadata:
aws:cdk:path: AutoStartStopEc2Stack/StopRule/Resource
StartRule40F02F2E:
Type: AWS::Events::Rule
Properties:
ScheduleExpression: cron(55 23 ? * SUN-THU *)
State: ENABLED
Targets:
- Arn:
Fn::GetAtt:
- singleton69FEA30F
- Arn
Id: Target0
Input: '{"Region":"ap-northeast-1","Action":"start"}'
Metadata:
aws:cdk:path: AutoStartStopEc2Stack/StartRule/Resource
CDKMetadata:
Type: AWS::CDK::Metadata
Properties:
Modules: aws-cdk=1.3.0,@aws-cdk/assets=1.3.0,@aws-cdk/aws-applicationautoscaling=1.3.0,@aws-cdk/aws-autoscaling=1.3.0,@aws-cdk/aws-autoscaling-common=1.3.0,@aws-cdk/aws-autoscaling-hooktargets=1.3.0,@aws-cdk/aws-cloudformation=1.3.0,@aws-cdk/aws-cloudwatch=1.3.0,@aws-cdk/aws-ec2=1.3.0,@aws-cdk/aws-ecr=1.3.0,@aws-cdk/aws-ecr-assets=1.3.0,@aws-cdk/aws-ecs=1.3.0,@aws-cdk/aws-elasticloadbalancingv2=1.3.0,@aws-cdk/aws-events=1.3.0,@aws-cdk/aws-events-targets=1.3.0,@aws-cdk/aws-iam=1.3.0,@aws-cdk/aws-kms=1.3.0,@aws-cdk/aws-lambda=1.3.0,@aws-cdk/aws-logs=1.3.0,@aws-cdk/aws-s3=1.3.0,@aws-cdk/aws-s3-assets=1.3.0,@aws-cdk/aws-servicediscovery=1.3.0,@aws-cdk/aws-sns=1.3.0,@aws-cdk/aws-sns-subscriptions=1.3.0,@aws-cdk/aws-sqs=1.3.0,@aws-cdk/aws-ssm=1.3.0,@aws-cdk/core=1.3.0,@aws-cdk/custom-resources=1.3.0,@aws-cdk/cx-api=1.3.0,@aws-cdk/region-info=1.3.0,jsii-runtime=node.js/v10.15.1
CDK のメタデータも含まれていますが、約 170 行の YAML ファイルになりました。
YAML で CloudFormation のテンプレートをしっかり書いたことはないで、これをゼロから書けと言われると辛いです。今回のようなシンプルな構成であれば良いですが、もっと複雑な構成になるとさらに辛そうです。
前に参加した CDK Meetup では、 1,000 行を超えるテンプレートを書いた みたいな話もありました。
デプロイ
CDK アプリケーションが完成したので、あとはデプロイするだけです。
デプロイもいたって簡単で、 cdk deploy
コマンドを実行するだけです。
cdk deploy
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:
<diff が良い感じに出力されます>
Do you wish to deploy these changes (y/n)? y
AutoStartStopEc2Stack: deploying...
AutoStartStopEc2Stack: creating CloudFormation changeset...
0/9 | 15:41:41 | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata
0/9 | 15:41:41 | CREATE_IN_PROGRESS | AWS::IAM::Role | singleton/ServiceRole (singletonServiceRole9C9ECF4A)
0/9 | 15:41:41 | CREATE_IN_PROGRESS | AWS::IAM::Role | singleton/ServiceRole (singletonServiceRole9C9ECF4A) Resource creation Initiated
0/9 | 15:41:43 | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata Resource creation Initiated
1/9 | 15:41:43 | CREATE_COMPLETE | AWS::CDK::Metadata | CDKMetadata
...
9/9 | 15:43:38 | CREATE_COMPLETE | AWS::CloudFormation::Stack | AutoStartStopEc2Stack
✅ AutoStartStopEc2Stack
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:123456789876:stack/AutoStartStopEc2Stack/51a4b4f0-b8de-11e9-ad2d-0ea27dfbb62e
作成されるリソースの内容が出力され、 Do you wish to deploy these changes (y/n)?
で y
を入力すればリソースの作成が始まるので、あとは待つだけです。途中でエラーが発生すれば、その旨 出力されます。
ちなみに cdk deploy
でデプロイされる先は、 AWS CLI の設定ファイル .aws/config
および .aws/credentials
に依存しています。なので、 --profile
オプションを使えばデフォルト以外のアカウントへのデプロイも可能です。
まとめ
AWS CDK を使って CloudWatch Event + Lambda で EC2 を自動起動・停止させる環境を作ってみた話でした。
今まで CloudFormation はほとんど触ったことがなく、 YAML でテンプレートを書くという作業にも食わず嫌いでした。そんなところに AWS CDK が現れて、 TypeScript, Python といった普段の開発に使っている言語でインフラコードが書けるというのは、かなり画期的なことだと思いました。
CDK Meetup のときに AWS の福井さんが仰っていた 「Infrastructure as Code ではなくて Infrastructure is Code だ」 という言葉が非常にしっくりきました。
CDK 自体に関しては GA になって 1 ヶ月も経っていませんが、既にバージョンが 1.3.0
になっており、引き続き結構なスピードでアップデートされていくのかなーという印象です。
CDK きっかけでインフラのコード化に足を踏み入れたので、置いて行かれないようにしたいと思います。
今回作ったもの
今回作った CDK アプリケーションは GitHub に置いていますので、よかったら使ってみてください。書き方おかしいとかあれば指摘も大歓迎です。
comments powered by Disqus