puppeteerを利用してグラフを画像として保存しslackに通知する

# 目的
・利用しているサーバの状態を受動的に確認できるようにする
# 概要
puppeteerでブラウザを自動操作して、以下の操作を実施する
1. AWSコンソールからログインする
2. カスタムダッシュボードの画面を開く
3. 目的のグラフを画像をダウンロードし、slackに通知する
# 前提条件
・AWSでBot用のIAMユーザを作成していること
・SlackにAppを作成して特定のchannelに投稿できるように設定していること
# スクリプト
・aws-dashboard.js
```
const fs        = require('fs');
const process   = require('process');
const puppeteer = require('puppeteer');
const request   = require('request');
const common    = require('../lib/common');
// グラフ画像の保存先
const DIR = '/var/bot/aws_dashboard/';
// slackの設定
const CONF_SLACK = {
    'url'     : 'https://slack.com/api/',
    'token'   : '{トークン}',
    'channel' : '{チャンネル}'
};
// URLの設定
const SIGNIN_URL = '{AWSのコンソールのURL}';
const DASHBOARD_URL = '{AWSのカスタムダッシュボードのURL}';
// タイミング調整用の時間設定（ミリ秒）
const WAIT_PAGE_TRANSION =  3000;
const WAIT_LITTLE        =   200;
const WAIT_DASHBOARD     = 10000;
(async () => {
    // 起動引数を取得する
    // ※args[0]はnodeプログラム、args[1]は本スクリプトのパスとなる
    const accountId = process.argv[2];
    const userName  = process.argv[3];
    const password  = process.argv[4];
    
    // ブラウザの初期設定をする
    const browser = await puppeteer.launch({
        headless : true, // ヘッドレスモードで起動（画面を表示しない）
        slowMo   : 20, // Puppeteer の動作を指定されたミリ秒単位で遅らせる
        devtools : false,
        ignoreHTTPSErrors: true, 
        defaultViewport : {
            width  : 1920,
            height : 1080
        },
        args : [
            '--window-size=1920,1080'
        ]
    });
    // ブラウザを起動する
    const page = await browser.newPage();
    // STEP1. AWSコンソール画面からサインインする
    // 通常のサインインの画面を開く
    await page.goto(SIGNIN_URL);
    await page.waitForSelector('#next_button');
    
    // 通常のサインイン画面
    // 以下を選択・入力し、次へを押下する
    // - ユーザ種別
    // - アカウントID
    await common.waitFor(WAIT_PAGE_TRANSION);
    await page.click('input[id="iam_user_radio_button"]');
    await page.type('input[id="resolving_input"]', accountId);
    await common.waitFor(WAIT_LITTLE);
    await page.click('#next_button');
    await page.waitForSelector('#signin_button');
    // IAMユーザとしてサインイン画面
    // 以下を入力し、次へを押下する
    // - ユーザ名
    // - パスワード
    await common.waitFor(WAIT_PAGE_TRANSION);
    await page.type('input[id="username"]', userName);
    await page.type('input[id="password"]', password);
    await common.waitFor(WAIT_LITTLE);
    await page.click('#signin_button');
    await page.waitForSelector('#consoleNavHeader');
    //  STEP2. ダッシュボード画面を開く
    await common.waitFor(WAIT_PAGE_TRANSION);
    await page.goto(dashboardUrl);
    // ローディングが終了するまで待つ
    await page.waitForSelector('.cwdb-widget-container');
    
    //  STEP3. 各画像をダウンロードし、slack通知する
    await common.waitFor(WAIT_DASHBOARD);
    // ダウンロード用のディレクトリを作成する
    const IMAGE_DIR = `${DIR}${common.imageDir()}`;
    fs.mkdirSync(IMAGE_DIR);
　　// ダッシュボードのグラフ画像を特定するためのセレクタ一覧
    const selectors = [
        '.widget-2', // CPU使用率
        '.widget-8'  // メモリ使用量
    ];
    let counter = 1;
    for (const selector of selectors) {
        // グラフを画像として保存する
        const filepath = `${IMAGE_DIR}/widget_${counter}.png`;        
        const clip = await page.evaluate(s => {
            const el = document.querySelector(s)
            const { width, height, top: y, left: x } = el.getBoundingClientRect()
            return { width, height, x, y }
        }, selector);
        await page.screenshot({clip, path: filepath});
        // 保存した画像をslackに通知する
        request.post({
            url: `${CONF_SLACK.url}files.upload`,
            formData: {
                token    : CONF_SLACK.token,
                filename : filepath,
                file     : fs.createReadStream(filepath),
                channels : CONF_SLACK.channel
            },
            function (error, response, body) {
                if (!error && response.statusCode == 200) {
                    common.log(`push file to slack is success. filepath : ${filepath}`);
                } else {
                    common.log(`push file to slack is failed. filepath : ${filepath}, status_code : ${response.statusCode}`);
                }
            }
        });
        counter++;
    };
    await common.waitFor(WAIT_PAGE_TRANSION);
    await browser.close();
})();
```
・lib/common.js
```
exports.log = function(message) {
  let dateStr = this.simpleDate(new Date(), 'YYYY-MM-DD hh:mm:ss') 
  console.log(`${dateStr}\t${message}`);
}
exports.simpleDate = function(date, format) {
    format = format.replace(/YYYY/, date.getFullYear());
    format = format.replace(/MM/  , ('0' + (date.getMonth() + 1)).slice(-2));
    format = format.replace(/DD/  , ('0' + date.getDate()).slice(-2));
    format = format.replace(/hh/  , ('0' + date.getHours()).slice(-2));
    format = format.replace(/mm/  , ('0' + date.getMinutes()).slice(-2));
    format = format.replace(/ss/  , ('0' + date.getSeconds()).slice(-2));
    return format;
}
exports.imageDir = function() {
  return this.simpleDate(new Date(), 'YYYYMMDD_hhmmss') 
}
exports.waitFor = async function(sec) {
    return new Promise((resolve, reject) => {
      setTimeout(resolve, sec);
    });
}
```
# 実行
```
$ node aws-dashboard.js {アカウントID} {IAMユーザ名} {IAMユーザのパスワード}
```
# 実行時のイメージ
・指定したslackのchannelに画像が通知される
![dashboard_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327498/6f7e6d46-56fe-a8d1-2684-fb88745e1f7d.png)

# 参考サイト
・https://pptr.dev/
・https://deep.tacoskingdom.com/blog/41
----------------------------------------------------------------------------------------------------------------------------------------------------------------