はじめに
- 少し使ってるバージョンが古いです(Node.jsのバージョン上げれば大丈夫なはずだけで環境用意してなくて未実施。)
- ソースコードは書き殴ったままなので見る人が見たらアレルギー反応出ちゃうかもしれません
- 年末大掃除の1つ。
動作環境、使ってるもの
- Serverless Framework
- AWS Lambda
- Node.js 8.10 (現時点でLambda関数が新規作成できないです)
なぜ作ったか
SQLをプログラム中で組むのが面倒だった。
集計用のSQLは重くなりがちなので、チューニングしやすいようにSQLそのもので管理したかった。
依頼されているレポートをまとめて作りたかった。
レポートを10前後(重いのは2つ程度)を毎日作成することから解放されたかった。
仕組み
- 指定のディレクトリに格納されているすべてのSQLを実行する
- 実行結果をCSVファイルに書き込む
- (記事中に載せてない処理)
- すべてのCSVファイルをzipファイルにまとめる
- zipファイルをSlackに投稿
ソースコード
GetRdsData.js
'use strict';
/**
* 1回の取得件数、繰り返しの回数を指定。
*/
const ONETIME_ROWS = 200000; // 1回あたりの取得件数 大きすぎるとメモリが足りないのでほどほどに。
const GET_TIMES = 20; // 取得回数 ONETIME_ROWSの値と合わせて指定。
const START_OFFSET_CNT = 0; // 最初から取得する場合は0を指定。
/**
* @namespace GetRdsData
*/
// ライブラリ読み込み
const AWS = require('aws-sdk');
const wrapper = require('co-mysql');
const mysql = require('mysql');
const co = require('co');
const fs = require('fs');
const del = require('del');
const dateformat = require('dateformat');
const Json2csvParser = require('json2csv').Parser;
const log = function() {
console.log.apply(console, arguments);
};
/**
* MySQL 接続設定
* @memberof GetRdsData
* @type {Object}
*/
const opts = {
connectionLimit: 1,
host: process.env.RdsEndpoint || '127.0.0.1',
port: process.env.RdsPort || '3306',
user: process.env.RdsUser || 'user',
password: process.env.RdsPassword || 'password',
database: process.env.RdsDbName || 'db',
multipleStatements: true
};
const STAGE_LABEL = process.env.Environment;
/**
* SQLファイル格納先
*
*
* @memberof GetRdsData
*/
const SQL_FILE_DIR = './sql/';
const TMP_DIR = '/tmp/';
const RDS_FILE_PREFIX = 'rds-';
const outputZipFileName = RDS_FILE_PREFIX + STAGE_LABEL + '_' + dateformat(new Date(), 'yyyymmddHHMMss') + '.zip';
const outputZipFilePath = TMP_DIR + outputZipFileName;
/**
*
* データ取得、ファイル作成 <br />
*
*
* @memberof GetRdsData
* @name GetRdsData
* @param {event} event [description]
* @param {context} context [description]
* @param {Function} callback [description]
* @return {} [description]
*/
exports.handler = (event, context, callback) => {
log(opts);
const executeQuery = function*(file, start, limit) {
log('executeQuery', file);
// SQLファイル読み込み
const sql = fs.readFileSync(SQL_FILE_DIR + file, 'utf-8');
// 参照系(SELECT,SET)以外を除外
const checkStr = sql.split(";");
for (let i = 0; i < checkStr.length; i++) {
if ('SELECT' !== checkStr[0].slice(0, 6).toUpperCase() && 'SET' !== checkStr[0].slice(0, 3).toUpperCase()) {
log('sql check NG!!!');
return null;
}
}
// SQL実行
const result = yield p.query(sql, [start, limit]);
if (result == null || result.length == 0) {
return null;
}
log('sql execute ok');
// SQLの実行結果で分岐
if (result[0].constructor.name == 'RowDataPacket') {
// SQLが単独の場合はそのまま返却
return result;
} else {
// SQLが複数の場合は末尾の結果のみを返却
return result[result.length - 1];
}
};
/**
* CSVファイルを出力する
* 出力先はtmpディレクトリ
*/
const outputTmpCsvFile = function*(fileName, object) {
// Excel文字化け対策のためにUTF-8 BOM付きで保存する
fs.writeFileSync(TMP_DIR + fileName, '\ufeff' + object);
};
const pool = mysql.createPool(opts);
const p = wrapper(pool);
const ymdMap = new Map();
co(function*() {
// SQLファイル一覧を取得
const fileList = fs.readdirSync(SQL_FILE_DIR);
// SQLをすべて実行する
for (let file of fileList) {
// SQLファイル以外はスキップ
if (!fs.statSync(SQL_FILE_DIR + file).isFile() || file.indexOf('.sql') < 0) {
continue;
}
log(file);
for (let i = START_OFFSET_CNT; i < START_OFFSET_CNT + GET_TIMES; i++) {
let start = ONETIME_ROWS * (i);
let end = ONETIME_ROWS * (i + 1); // SQL実行には使わない
// SQL実行
const result = yield executeQuery(file, start, ONETIME_ROWS);
log('data convert csv')
// データの取得結果で分岐
if (result != null && result.length > 0) {
log(result.length);
// データが取得できた場合
// 個別集計のデータの場合
const json2csvParser = new Json2csvParser({});
const csv = json2csvParser.parse(result);
log('convert done. file output start.')
// データの出力
yield outputTmpCsvFile(RDS_FILE_PREFIX + file.replace('.sql', '') + '_' + start + 'to' + end + '.csv', csv);
log('file output done.')
} else {
// データが取得できなかった場合
log('get data error', file);
log(result);
break;
}
}
}
// Connection Poolの解放
pool.end();
}).then(() => {
// tmpディレクトリ下に作成したファイルを削除
// csvファイル削除
del.sync([TMP_DIR + RDS_FILE_PREFIX + '*.csv'], {
force: true
});
}).catch((e) => {
log(e);
callback(e, null);
});
}
package.json
{
"name": "get-rds-data",
"version": "0.0.1",
"private": true,
"engines": {
"node": "8.10.0"
},
"main": "GetRdsData.js",
"dependencies": {
"co": "*",
"co-mysql": "*",
"mysql": "*",
"json2csv": "^4.5.1",
"del": "*",
"request": "*",
"archiver": "*",
"dateformat": "*",
"nodejslack": "*"
},
"devDependencies": {
"aws-sdk": "*"
},
"scripts": {}
}
serverless.yml
service: getRdsData
provider:
name: aws
runtime: nodejs8.10
memorySize: 2048
timeout: 120
region: ap-northeast-1
stage: ${opt:stage, 'develop'}
environment:
TZ: Asia/Tokyo
Environment: ${self:provider.stage}
resources:
Description: Get data.
# 環境依存の設定値
custom:
stage:
# ローカル実行
local:
RdsEndpoint: 127.0.0.1
RdsPort: 3306
RdsDbName: db
RdsUser: user
RdsPassword: password
# 機能固有の設定値
functions:
GetRdsData:
handler: GetRdsData.handler
description: Get data.
role:
environment:
RdsEndpoint: ${self:custom.stage.${self:provider.stage}.RdsEndpoint}
RdsDbName: ${self:custom.stage.${self:provider.stage}.RdsDbName}
RdsUser: ${self:custom.stage.${self:provider.stage}.RdsUser}
RdsPassword: ${self:custom.stage.${self:provider.stage}.RdsPassword}
RdsPort: ${self:custom.stage.${self:provider.stage}.RdsPort}
まとめ
だいたいどこ行ってもSQLを簡単に実行できる仕組みは必要だった。