9
4

More than 3 years have passed since last update.

今更ながらAWS CDKを使ってみた

Posted at

はじめに

今更ですが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インスタンスを停止させ、最後にレコードを削除するという処理をしています。

4b89e2e2-bd60-479d-aa93-45c38fc4e157.png

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を確認してもしっかり作成されています。

スクリーンショット 2020-04-18 13.04.22.png

次のコマンドでコードと実際のインフラの差分を見ることができます。

cdk diff

インフラの削除は次のコマンドで行なえます。

cdk destroy

最後に

AWS CDKはプログラミングの知識が必要となるので、今までなんとなくYAMLでいいか…などと考えていましたが、実際に使ってみると便利で使いやすく感じました。

なんといってもエディタ(IDE)の補完が効くのがいいです。
コーディングの最中に必須のパラメーターが足りないよ、追加しようか?などIDEが聞いてくるのでとても捗りました。

これからはAWS CDKを使ってバシバシデプロイしていきます!

9
4
0

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
9
4