AWSの使用料金をSlackに投稿するLambdaファンクション

![スクリーンショット 2017-09-08 22.33.48.png](https://qiita-image-store.s3.amazonaws.com/0/21172/04a33463-de30-77ba-ef98-40f7eceb79c1.png)


## 概要

* CloudWatchのapiのListMetricsで課金情報が取得できるAWSのサービス一覧を取得して、それを元にapiのGetMetricStatisticsを呼ぶことで各AWSのサービス毎の料金を取得できる
* AWSの管理画面から請求アラートを受け取るを有効にしておく必要がある

![スクリーンショット 2017-09-07 1.01.11.png](https://qiita-image-store.s3.amazonaws.com/0/21172/c551c114-2401-f288-a9f4-71cb1de66a5c.png)

* Promiseを使ってるため、nodejsのバージョンは6.10を選択する

## 許可が必要なIAMポリシー

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": ["cloudwatch:ListMetrics", "cloudwatch:GetMetricStatistics"],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}
```

## ソースコード

* 環境編SLACK_API_TOKENとPOST_CHANNELは任意の値を設定する

```json:package.json
{
  "name": "estimated-charges",
  "version": "1.0.0",
  "main": "index.js"
  "license": "MIT",
  "dependencies": {
    "@slack/client": "^3.10.0",
    "aws-sdk": "^2.85.0",
    "moment": "^2.18.1"
  }
}
```

```js:index.js
'use strict';

const WebClient = require('@slack/client').WebClient
const AWS = require('aws-sdk')
const moment = require('moment')

const cloudwatch = new AWS.CloudWatch({
  region: 'us-east-1',
  endpoint: 'http://monitoring.us-east-1.amazonaws.com'
})

const listMetrics = () => {
  return new Promise((resolve, reject) => {
    cloudwatch.listMetrics({ MetricName: 'EstimatedCharges' }, (error, data) => {
      if (error) {
        reject(error)
      } else {
        resolve(data)
      }
    })
  })
}

const getMetricStatistics = (serviceName, startTime, endTime) => {
  const dimensions = [
    { Name: 'Currency', Value: 'USD' }
  ]
  if (serviceName) {
    dimensions.push({ Name: 'ServiceName', Value: serviceName })
  }
  return new Promise((resolve, reject) => {
    const params = {
      MetricName: 'EstimatedCharges',
      Namespace: 'AWS/Billing',
      Period: 86400,
      StartTime: startTime,
      EndTime: endTime,
      Statistics: ['Maximum'],
      Dimensions: dimensions
    }
    cloudwatch.getMetricStatistics(params, (error, data) => {
      if (error) {
        reject(error)
      } else {
        resolve({ name: serviceName, data: data })
      }
    })
  })
}

exports.handler = (event, context, callback) => {
  listMetrics().then(data => {
    const now = moment().toISOString()
    const yesterday = moment(now).subtract(1, 'd').toISOString()
    const promises = data['Metrics'].map(metric => {
      return metric['Dimensions'][0]
    }).filter(dimension => {
      return dimension['Name'] == 'ServiceName'
    }).map(dimension => {    
      return dimension['Value']
    }).filter((serviceName, index, self) => {
      return self.indexOf(serviceName) === index
    }).map(serviceName => {
      return getMetricStatistics(serviceName, yesterday, now)
    })
    promises.unshift(getMetricStatistics(null, yesterday, now))
    Promise.all(promises).then(data => {
      const results = data.filter(result => {
        const datapoints = result.data['Datapoints']
        return (datapoints.length != 0 && datapoints[0]['Maximum'] != 0)
      })
      if (results.length == 0) {
        return
      }
      const fields = results.filter(result => {
        return result.name != null
      }).sort((result1, result2) => {
        const charge1 = result1.data['Datapoints'][0]['Maximum']
        const charge2 = result2.data['Datapoints'][0]['Maximum']
        return (charge2 - charge1)
      }).map(result => {
        const datapoint = result.data['Datapoints'][0]
        return { title: result.name, value: `$${datapoint['Maximum']}`, short: true }
      })
      const datapoint = results[0].data['Datapoints'][0]
      const client = new WebClient(process.env.SLACK_API_TOKEN)
      client.chat.postMessage(process.env.POST_CHANNEL, `AWS料金 $${datapoint['Maximum']}`, { attachments: [{ color: 'good', fields: fields }] }, (error, response) => {
        if (error) { console.error(error) }
      })
    }, reason => {
      console.error(reason)
    })
  }, reason => {
    console.error(reason)
  })
}
```