AWS テスト環境のRDS/EC2 インスタンスの起動停止自動化を数コマンドで実現する、AWS CDK templateを作った話

# はじめに
こんにちわ。Wano株式会社のnariと申します。
会社のgithubアカウントのpublic repositoryで、**掲題の内容のtemplateを公開**しましたので、その内容説明として記事を書いています。

**repositoryは[GitHub - wano/auto-startstop-instance-cdk-template: auto-startstop-instance-cdk-template](https://github.com/wano/auto-startstop-instance-cdk-template)**
こちらのReadMeの方にも導入方法を書いていますので、そちらを合わせてご覧になってください。

# なぜやりたいか
- **テスト環境のECS/RDSのインスタンスの起動時間を平日のある時間帯だけに限定して、コストカットしたい**
- 巷の自動停止のtemplateでRDS、EC2両方対応したものが見つからないので欲しい
- **AWS CDKを試してみたかった**  

## 開発環境
- MacOS Mojave version 10.14.6
- Go 1.12.0
- TypeScript 3.6.2
- CDK z1.6.0

# そもそも**AWS CDK(AWS Cloud Development Kit) とは？？**　
ざっくり言ってしまえば、**AWSインフラリソースの定義やプロビジョニングをアプリケーションの言語でかけるフレームワーク**です。これによって、jsonやyml形式などで書かれた**インフラ用のテンプレートファイルと格闘する必要がなくなります。**
アプリケーションチームがインフラの管理まで行っている場合、非常に有益なツールになり得ます。(アプリケーションのナレッジが転用できる、deployの前と後の処理を非常に簡単に組み込めるetc)

例えば以下のように、TypeScriptで書けて、しかもわずか10行でLambdaとAPI Gatewayが宣言できたりします。
![carbon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321850/4f273503-892e-6a20-024f-250216c6e55b.png)

実際の挙動としては、CDK は**クラウドインフラストラクチャの「コンパイラ」**のようなもので、一旦CloudFormationのソースに変換され実行されます。
[AWS クラウド開発キット – アマゾン ウェブ サービス](https://aws.amazon.com/jp/cdk/)

# 簡単なソースコードの説明
## デプロイリソースの定義
- CDKは **App > Stack > Constracts** という大きさの順での概念を扱うが、ここではStack、Constractsを定義する
- **Stack**はCloudFormationのスタックと同一で**デプロイ可能な単位**で、リージョンとアカウントを保持する
- **Constracts**は**AWS個別リソース**を定義する
- これだけで、ec2停止起動のlamda,Iam Role,Iam Policy,cronでec2停止起動のlambdaを発火するCloudWatch Eventが定義できている

```auto-startstop-instance-cdk-template/auto_start_stop_instance_cdk/lib/auto_start_stop_instance_cdk-stack.ts

import events = require('@aws-cdk/aws-events');
import iam = require('@aws-cdk/aws-iam');
import targets = require('@aws-cdk/aws-events-targets');
import { Function, Runtime, Code } from "@aws-cdk/aws-lambda"
import cdk = require('@aws-cdk/core');
import fs = require('fs');

export class AutoStartStopInstanceCdkStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const stackConfig = JSON.parse(fs.readFileSync('example.json', {encoding: 'utf-8'}));

    const lambdaFn = new Function(this, 'singleton', {
      functionName: "auto_ec2_startstop", // 関数名
      runtime: Runtime.GO_1_X, // ランタイムの指定
      code: Code.asset("./lambdaSources/source"), // ソースコードのディレクトリ
      handler: "main", // handler の指定
      memorySize: 256, // メモリーの指定
      timeout: cdk.Duration.seconds(10), // タイムアウト時間
    });

    lambdaFn.addToRolePolicy(new iam.PolicyStatement({
      actions: [
        'ec2:DescribeInstances',
        'ec2:StartInstances',
        'ec2:StopInstances',
        "rds:StopDBInstance",
        "rds:StartDBInstance",
        "rds:StartDBCluster",
        "rds:StopDBCluster"
      ],
      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',InstanceNames:stackConfig.targets.instanceNames
        ,DBInstanceIdentifies:stackConfig.targets.dbInstanceIdentifies,DBClusterIdentifies:stackConfig.targets.dbClusterIdentifies
        ,WebHookURL:stackConfig.slack.webHookUrl,SlackChannel:stackConfig.slack.channel})
    }));

    // 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',InstanceNames:stackConfig.targets.instanceNames
        ,DBInstanceIdentifies:stackConfig.targets.dbInstanceIdentifies,DBClusterIdentifies:stackConfig.targets.dbClusterIdentifies
        ,WebHookURL:stackConfig.slack.webHookUrl,SlackChannel:stackConfig.slack.channel})
    }));

  }
}

```

## プロビジョニングの定義

- 先ほど出てきた**App**という概念を扱う。 **実行可能なプログラム(CloudFormationテンプレート)へのコンパイルとデプロイ**に利用される
- デプロイ前に、Goのソースをbuildして、デプロイ後に削除している

```auto-startstop-instance-cdk-template/auto_start_stop_instance_cdk/bin/auto_start_stop_instance_cdk.ts
    
#!/usr/bin/env node
import 'source-map-support/register';
import cdk = require('@aws-cdk/core');
import { AutoStartStopInstanceCdkStack } from '../lib/auto_start_stop_instance_cdk-stack';



const util = require('util');
const exec = util.promisify(require('child_process').exec);

async function deploy(){
    await exec('go get -v -t -d ./lambdaSources/function/... && GOOS=linux GOARCH=amd64 go build -o ./lambdaSources/source/main ./lambdaSources/function/**.go')
    const app = new cdk.App();
    new AutoStartStopInstanceCdkStack(app, 'AutoStartStopInstanceCdkStack');
    app.synth()
    await  exec('rm ./lambdaSources/source/main')
}

deploy()
```

## このテンプレートで何ができるか
- **使っていない間にinstances(EC2,RDS)を落としておいて、コストを削減できる**
- **あなたのケースに合わせて設定できる**
    - ec2インスタンスだけ、RDSインスタンスだけ、クラスタだけ、etc
    - 複数台のインスタンスの設定にも対応
- **オプションで、自動起動と停止の際に、slackにメッセージを飛ばすことができる**

## どう使うか
### 1.[GitHub - wano/auto-startstop-instance-cdk-template: auto-startstop-instance-cdk-template](https://github.com/wano/auto-startstop-instance-cdk-template)をクローンする
```
git clone https://github.com/wano/auto-startstop-instance-cdk-template.git
```
### 2.あなたの設定に合わせてexample.jsonをいじる
- **クーロンの時間設定はUTCなことに注意する**

| |  ec2|  rds(cluster) | rds(instance)|Slack Notification|
|---|----|---|---|---|
|case1| ◯ |　◯ | ◯ | ◯ |
|case2| ◯ |　  | ◯ | ◯ |
|case3|　 |　  | ◯　|  |

- **case1**

```json
{
  "events": {
    "cron": {
      "start": "00 00 ? * MON-FRI *", 
      "stop": "00 11 ? * * *"
    }
  },
  "targets": {
    "ec2region": "your_region",
    "instanceNames": ["your_instance_name_1","your_instance_name_ 2"],
    "dbClusterIdentifies": ["your_cluster_name_1"],
    "dbInstanceIdentifies": ["your_db_instance_name_1","your_db_instance_name_ 2"]
  },
  "slack": {
    "webHookUrl":"https://hooks.slack.com/services/xxxxxxxxx/xxxxxxxxxxxxx/xxxxxxxxxxxxxxxxxx",
    "channel":"xxxxxxxxxxxxxx"
  }
}
```

- **case2**

```json
{
  "events": {
    "cron": {
      "start": "00 00 ? * MON-FRI *", 
      "stop": "00 11 ? * * *"
    }
  },
  "targets": {
    "ec2region": "your_region",
    "instanceNames": ["your_instance_name_1","your_instance_name_ 2"],
    "dbClusterIdentifies": [""],
    "dbInstanceIdentifies": ["your_db_instance_name_1","your_db_instance_name_ 2"]
  },
  "slack": {
    "webHookUrl":"https://hooks.slack.com/services/xxxxxxxxx/xxxxxxxxxxxxx/xxxxxxxxxxxxxxxxxx",
    "channel":"xxxxxxxxxxxxxx"
  }
}
```

- **case3**

```json
{
  "events": {
    "cron": {
      "start": "00 00 ? * MON-FRI *",
      "stop": "00 11 ? * * *"
    }
  },
  "targets": {
    "ec2region": "ap-northeast-1",
    "instanceNames": [""],
     "dbClusterIdentifies": ["your_cluster_name_1"],
    "dbInstanceIdentifies": [""]
  },
  "slack": {
    "webHookUrl":"",
    "channel":""
  }
}
```

### 3.CDKをインストールし、デプロイする
```shell
npm install -g aws-cdk
cd ./auto_start_stop_instance_cdk
npm install @aws-cdk/core @aws-cdk/aws-lambda @aws-cdk/aws-iam @aws-cdk/aws-events @aws-cdk/aws-events-targets
npm run build
export AWS_ACCESS_KEY_ID="xxxxxxxxxxx"
export AWS_SECRET_ACCESS_KEY="xxxxxxxxxxx"
cdk bootstrap
cdk deploy
```
# 終わりに
意外とRDSとEC2の自動起動停止の設定の良いIaCテンプレートがなかったので、この機会に是非お使いください。
**AWS CDKに少しでも興味を持った方は、2019/09/22に開催される技術書典7で、CDKやGoに入門する技術書を出しますので、是非チェックしてください！(露骨な宣伝)** 
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">✅AWS CDK<br>の登場で、インフラ管理はTypeScriptなどアプリの言語で管理する時代へ！<br><br>✅例えばわずか10行でLambdaとAPI Gatewayが宣言できる！<br><br>そんなAWS CDKとGoで対話型SlackBotを開発しながら楽しく学べる本を書きました！<br><br> <a href="https://twitter.com/hashtag/%E6%8A%80%E8%A1%93%E6%9B%B8%E5%85%B8?src=hash&amp;ref_src=twsrc%5Etfw">#技術書典</a> お28Cで待ってます！<a href="https://t.co/bro4vxsTDn">https://t.co/bro4vxsTDn</a> <a href="https://t.co/SpBWmTSIjB">pic.twitter.com/SpBWmTSIjB</a></p>&mdash; nari@技術書典7 お28C📖 GoとAWS CDKで作る本格SlackBot (@fukubaka0825) <a href="https://twitter.com/fukubaka0825/status/1168871843769409536?ref_src=twsrc%5Etfw">September 3, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>