はじめに
今更ですがAWS CDKを使ってみました。
CDKはCloud Development Kitの略で、これを使うとインフラをコードで表現することができます。
CloudFormationやTerraformでも同じようなことができますが、大きな違いは次の点となります。
- 高レベルAPIが用意されていて少ない行数で書ける
- エディタの補完が効く
- テストがかける
- CLIが用意されていて、デプロイやdiff等の操作が簡単に行える
- プログラムとして書けるのでforやifなども思いのまま
公式: AWS クラウド開発キット – アマゾン ウェブ サービス
なお内部的にはCloudFormationが用いられますが、CDKがラップしているのでCloudFormationには一切手を触れることはありません。
何を作ったのか
構成はこんな感じです。図中のAWS SDK部分は既存のサービスで、それと連携するAWSサービスを今回CDKを使って構築してみました。
図には書いていませんが、LambdaではDynamoDBに記録されている日時とEC2インスタンスIDのレコードを取得して、次に実行している日時と一致した場合そのEC2インスタンスを停止させ、最後にレコードを削除するという処理をしています。
CDKインストール
このコマンドを実行するとCDKのインストールと初期化が行われコーディングに入れます。
npm install -g aws-cdk
mkdir sample
cd $_
cdk init --language typescript
yarn add '@aws-cdk/aws-dynamodb' '@aws-cdk/aws-lambda' '@aws-cdk/aws-iam' '@aws-cdk/aws-events' '@aws-cdk/aws-events-targets'
コーディング
実行の起点となるファイルはbinディレクトリ内にあるファイルです。
ファイル名やクラス名などは親ディレクトリのものが使われます。初期状態ではこんな感じになっています。
※なので親ディレクトリ名はよく考えたほうがいいです
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { SampleStack } from '../lib/sample-stack';
const app = new cdk.App();
new SampleStack(app, 'SampleStack');
lib/sample-stack.tsにコードを書いていきます。
LambdaでEC2を停止させる処理があるのでその権限も付けています。
特筆するべきはLambdaにDynamoDBへのアクセス権を付与している箇所です。
コードを抜粋すると次の一行です。すごくないですか?私は感動しました。
table.grantFullAccess(lambda_function);
import * as cdk from '@aws-cdk/core';
import { Table, AttributeType } from "@aws-cdk/aws-dynamodb";
import { Function, AssetCode, Runtime } from '@aws-cdk/aws-lambda';
import { PolicyStatement } from "@aws-cdk/aws-iam";
import { Rule, Schedule } from '@aws-cdk/aws-events';
import { LambdaFunction } from '@aws-cdk/aws-events-targets';
export class InfraStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const table: Table = new Table(this, "Ec2ManagerTable", {
partitionKey: {
name: "scheduled_at",
type: AttributeType.STRING
},
sortKey: {
name: "instance_id",
type: AttributeType.STRING
},
readCapacity: 1,
writeCapacity: 1,
tableName: 'ec2-manager'
});
const lambda_function: Function = new Function(this, 'Ec2Operator', {
runtime: Runtime.NODEJS_10_X,
code: AssetCode.fromAsset('src/lambda'),
handler: 'index.handler',
timeout: cdk.Duration.seconds(30),
environment: {
TZ: "Asia/Tokyo",
TABLE_NAME: table.tableName
}
});
table.grantFullAccess(lambda_function);
lambda_function.addToRolePolicy(
new PolicyStatement({
resources: [`arn:aws:ec2:*:*:*`],
actions: [
"ec2:stopInstances",
],
})
);
const rule = new Rule(this, 'Rule', {
schedule: Schedule.expression('cron(0 * * * ? *)')
});
rule.addTarget(new LambdaFunction(lambda_function));
}
}
次にLambdaのコードを書いていきます。
AssetCode.fromAsset('src/lambda')
としたのでsrc/lambda配下にindex.jsとして書いていきます。
exports.handler = async function(event, context) {
var AWS = require("aws-sdk");
var dynamo = new AWS.DynamoDB.DocumentClient();
var ec2 = new AWS.EC2();
const today = new Date();
const month = today.getMonth() + 1; // 0 start
const day = today.getDate();
const hour = today.getHours();
const targetDate = month + '/' + day + ' ' + hour;
const dynamo_param = {
TableName: process.env.TABLE_NAME,
KeyConditionExpression: "#k = :val",
ExpressionAttributeValues: { ":val": targetDate },
ExpressionAttributeNames: { "#k": "scheduled_at" }
};
let ec2_params = {
InstanceIds: []
};
const query_result = await dynamo.query(dynamo_param, function(err, data) {
if (err) {
console.log(err, err.stack);
} else {
return data;
}
}).promise();
query_result.Items.forEach(function(data) {
ec2_params.InstanceIds.push(data.instance_id);
});
const ec2_result = await ec2.stopInstances(ec2_params, function(err, data) {
if (err) {
console.log(err);
} else {
return data;
}
}).promise();
for (let instance of ec2_result.StoppingInstances) {
const params = {
TableName: process.env.TABLE_NAME,
Key: {
"scheduled_at": targetDate,
"instance_id": instance.InstanceId
}
};
await dynamo.delete(params, function(err, data) {
console.log('DELETE: ' + JSON.stringify(data));
if (err) {
console.log(err);
} else {
console.log(JSON.stringify(data));
}
}).promise();
};
};
デプロイ
いよいよデプロイです。
デプロイする前に、初めてCDKを使う場合は次のコマンドを実行する必要があります。
これによりCDKToolkit
という名前でCloudFormationスタックが作成され、CDK用のS3バケットが作成されます。
なおAWSクレデンシャルのプロファイルを切り替えたい場合は普通に--profile (profile名)
とすれば切り替えられます。
cdk bootstrap
一度TypeScriptからJavaScriptのコードを出力する必要があるので次のコマンドを実行します。
npm run build
もしくは次のコマンドを実行すると、コードに更新が発生次第自動で出力してくれます。
npm run watch
最後に次のコマンドを実行するとデプロイが行われます。
cdk deploy
実行するとログが出力されながら作成されてゆきます。
途中IAMリソースの作成確認がされますがy
と回答します。
(node:3699) ExperimentalWarning: The fs.promises API is experimental
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:
IAM Statement Changes
┌───┬─────────────────────────────────┬────────┬───────────────────────┬─────────────────────────────────┬───────────────────────────────────────────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼─────────────────────────────────┼────────┼───────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────┤
│ + │ ${SampleLambda.Arn} │ Allow │ lambda:InvokeFunction │ Service:events.amazonaws.com │ "ArnLike": { │
│ │ │ │ │ │ "AWS:SourceArn": "${Rule.Arn}" │
│ │ │ │ │ │ } │
├───┼─────────────────────────────────┼────────┼───────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────┤
│ + │ ${SampleLambda/ServiceRole.Arn} │ Allow │ sts:AssumeRole │ Service:lambda.amazonaws.com │ │
├───┼─────────────────────────────────┼────────┼───────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────┤
│ + │ ${SampleTable.Arn} │ Allow │ dynamodb:* │ AWS:${SampleLambda/ServiceRole} │ │
├───┼─────────────────────────────────┼────────┼───────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────┤
│ + │ arn:aws:ec2:*:*:* │ Allow │ ec2:stopInstances │ AWS:${SampleLambda/ServiceRole} │ │
└───┴─────────────────────────────────┴────────┴───────────────────────┴─────────────────────────────────┴───────────────────────────────────────────────┘
IAM Policy Changes
┌───┬─────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│ │ Resource │ Managed Policy ARN │
├───┼─────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${SampleLambda/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴─────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)
Do you wish to deploy these changes (y/n)? y
SampleStack: deploying...
[0%] start: Publishing 42294a6a15b0d7af1c80cfb86028837abdd1bcfe8ac996fcb6a197311e11f113:current
[100%] success: Published 42294a6a15b0d7af1c80cfb86028837abdd1bcfe8ac996fcb6a197311e11f113:current
SampleStack: creating CloudFormation changeset...
0/8 | 12:58:47 PM | CREATE_IN_PROGRESS | AWS::DynamoDB::Table | SampleTable (SampleTable08FBB2D0)
0/8 | 12:58:47 PM | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata
0/8 | 12:58:47 PM | CREATE_IN_PROGRESS | AWS::IAM::Role | SampleLambda/ServiceRole (SampleLambdaServiceRoleB1A8618F)
0/8 | 12:58:47 PM | CREATE_IN_PROGRESS | AWS::DynamoDB::Table | SampleTable (SampleTable08FBB2D0) Resource creation Initiated
0/8 | 12:58:48 PM | CREATE_IN_PROGRESS | AWS::IAM::Role | SampleLambda/ServiceRole (SampleLambdaServiceRoleB1A8618F) Resource creation Initiated
0/8 | 12:58:49 PM | CREATE_IN_PROGRESS | AWS::CDK::Metadata | CDKMetadata Resource creation Initiated
1/8 | 12:58:49 PM | CREATE_COMPLETE | AWS::CDK::Metadata | CDKMetadata
2/8 | 12:59:03 PM | CREATE_COMPLETE | AWS::IAM::Role | SampleLambda/ServiceRole (SampleLambdaServiceRoleB1A8618F)
3/8 | 12:59:18 PM | CREATE_COMPLETE | AWS::DynamoDB::Table | SampleTable (SampleTable08FBB2D0)
3/8 | 12:59:20 PM | CREATE_IN_PROGRESS | AWS::IAM::Policy | SampleLambda/ServiceRole/DefaultPolicy (SampleLambdaServiceRoleDefaultPolicy7CCBB3B5)
3/8 | 12:59:22 PM | CREATE_IN_PROGRESS | AWS::IAM::Policy | SampleLambda/ServiceRole/DefaultPolicy (SampleLambdaServiceRoleDefaultPolicy7CCBB3B5) Resource creation Initiated
4/8 | 12:59:36 PM | CREATE_COMPLETE | AWS::IAM::Policy | SampleLambda/ServiceRole/DefaultPolicy (SampleLambdaServiceRoleDefaultPolicy7CCBB3B5)
4/8 | 12:59:39 PM | CREATE_IN_PROGRESS | AWS::Lambda::Function | SampleLambda (SampleLambdaB2FF4FA1)
4/8 | 12:59:39 PM | CREATE_IN_PROGRESS | AWS::Lambda::Function | SampleLambda (SampleLambdaB2FF4FA1) Resource creation Initiated
5/8 | 12:59:40 PM | CREATE_COMPLETE | AWS::Lambda::Function | SampleLambda (SampleLambdaB2FF4FA1)
5/8 | 12:59:42 PM | CREATE_IN_PROGRESS | AWS::Events::Rule | Rule (Rule4C995B7F)
5/8 | 12:59:43 PM | CREATE_IN_PROGRESS | AWS::Events::Rule | Rule (Rule4C995B7F) Resource creation Initiated
5/8 Currently in progress: Rule4C995B7F
6/8 | 1:00:43 PM | CREATE_COMPLETE | AWS::Events::Rule | Rule (Rule4C995B7F)
6/8 | 1:00:45 PM | CREATE_IN_PROGRESS | AWS::Lambda::Permission | SampleLambda/AllowEventRuleSampleStackRuleB7F44284 (SampleLambdaAllowEventRuleSampleStackRuleB7F44284B9B1AC02)
6/8 | 1:00:46 PM | CREATE_IN_PROGRESS | AWS::Lambda::Permission | SampleLambda/AllowEventRuleSampleStackRuleB7F44284 (SampleLambdaAllowEventRuleSampleStackRuleB7F44284B9B1AC02) Resource creation Initiated
✅ SampleStack
これでデプロイが完了しました!CLIが用意されているので簡単ですね。
CloudFormationを確認してもしっかり作成されています。
次のコマンドでコードと実際のインフラの差分を見ることができます。
cdk diff
インフラの削除は次のコマンドで行なえます。
cdk destroy
最後に
AWS CDKはプログラミングの知識が必要となるので、今までなんとなくYAMLでいいか…などと考えていましたが、実際に使ってみると便利で使いやすく感じました。
なんといってもエディタ(IDE)の補完が効くのがいいです。
コーディングの最中に必須のパラメーターが足りないよ、追加しようか?などIDEが聞いてくるのでとても捗りました。
これからはAWS CDKを使ってバシバシデプロイしていきます!