- 投稿日:2020-12-16T21:44:41+09:00
GmailのAPIでのエラー: Metadata scope doesn't allow format FULL
users.messages.get
のAPIを使おうとしてのエラーメモです。https://developers.google.com/gmail/api/reference/rest/v1/users.messages/get
Metadata scope doesn't allow format FULL
というエラーが出ました。
ドキュメントにこれらのパーミッションが必要と書いてあるので許可してたのですが......
https://mail.google.com/ https://www.googleapis.com/auth/gmail.modify https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/gmail.metadata https://www.googleapis.com/auth/gmail.addons.current.message.metadata https://www.googleapis.com/auth/gmail.addons.current.message.readonly https://www.googleapis.com/auth/gmail.addons.current.message.action検証環境
- Node.js v15.3.0
- googleapiモジュール v66
試したコード
//省略 gmail.users.messages.get({ userId: 'me', id: `xxxx`, // format: 'FULL' }, (err, res) => { if (err) return console.log('The API returned an error: ' + err); console.log(res); }); //省略ちなみに
allow format FULL
となっていたのは、何も指定しないとデフォルトのフォーマットがFULLになるからという理由です。
- MINIMAL
- FULL (デフォ)
- RAW
- METADATA
https://developers.google.com/gmail/api/reference/rest/v1/Format
/auth/gmail.metadata
のパーミッションを許可しない
Metadata scope doesn't allow format FULL gmail
とのことなのでそのままなのですが、APIドキュメントのテスターで試そうとしたら、/auth/gmail.metadata
のパーミッションがチェック外れている状態でした。試しにチェックして試すと、テスター上も同様のエラーが発生。
ということでトークンを作成する際に
/auth/gmail.metadata
を許可しなければOKです。トークン作成する際のスコープから
/auth/gmail.metadata
をはずしてトークンを作成しなおしたらいけました。const SCOPES = [ `https://mail.google.com/`, `https://www.googleapis.com/auth/gmail.modify`, `https://www.googleapis.com/auth/gmail.readonly`, // `https://www.googleapis.com/auth/gmail.metadata`, `https://www.googleapis.com/auth/gmail.addons.current.message.metadata`, `https://www.googleapis.com/auth/gmail.addons.current.message.readonly`, `https://www.googleapis.com/auth/gmail.addons.current.message.action` ];まとめ
これはドキュメントミスな気がするけどどうなんでしょう。。
- 投稿日:2020-12-16T18:39:30+09:00
SVG + CSS + Node.js + receiptline で電子レシートを発行してみよう
マークダウン言語で紙のレシートや電子レシートを簡単に作れる receiptline。
https://github.com/receiptline/receiptline
https://www.npmjs.com/package/receiptline今回は receiptline 本来の用途と考えられる、電子レシートの発行です!
レシートを設計する
デザインツール
ReceiptLine Designer を使います。使い方はこの連載記事の初回から。
いますぐ試したい方は、開発元のホームページで公開されているのでこちらへ。
ロゴの背景を透明に
以前の記事で作成したロゴ画像を再利用します。
GIMP の「色を透明度に」 (Color to Alpha) 機能で、背景色を白から透明にしました。
2023年度インボイス制度対応
前回の記事で学習した、簡易適格請求書等の記載事項も追加しておきます。
- ① 適格請求書発行事業者の氏名又は名称及び登録番号
- ② 課税資産の譲渡等を行った年月日
- ③ 課税資産の譲渡等に係る資産又は役務の内容(課税資産の譲渡等が軽減対象資産の譲渡等である場合には、資産の内容及び軽減対象資産の譲渡等である旨)
- ④ 課税資産の譲渡等の税抜価額又は税込価額を税率ごとに区分して合計した金額
- ⑤ 税率ごとに区分した消費税額等又は適用税率
作成したレシートデータ
ReceiptLine{image:iVBORw0KGgoAAAANSUhEUgAAASAAAAAwAgMAAADMTE88AAAACVBMVEVwAAsAAAD///9xeVj9AAAAAXRSTlMAQObYZgAAAdZJREFUSMftlsFu4zAMRMUD73sI/0c59M4Amv//lXJI2XEWaGSgAVrsxuihtqgnmjOk09r7+pcv7esYAEtMM19yrCkWxwnOgEbk1Fc5nwBpRNgCpGdAhKi/4NVYaFmD1qqlYi8ACU6Y6BRo/DaQjk3hiARv4BeB834aVXBDk1bL0lHlCEs0Qz5zP4A89FVW3rgWNwGxWrRuCVIC7Ao3HjBCamY6mt7GHSQ9/rnQVEGZIBmlBPL0Fsvoig8Mg9N6KJAAB5ASFPDYzUcJytgEFXAQzpf2/ajMCMeMjCAwMX8A9ZQjQUJQ7mZs5G4TZMcaXRJd3bKBLOZLz4gC9Q00Y8030FE1Vga5egf5put0rTwDbT4SalV6fQmyp6DZIqCONXe+Aqk/A82mlcF3RO7eQeMRZH0JElePJ3P336BNfjwtdg42CV9PHynu8ldjSO7NIhxAD/Lvo1YDZONPgdIlg26JvwT4BAmud1AZstsOkhz+Fo3pERHhdLxiMCy9z89VgcAmnaCyPfCxg2hqnn4NWr/MtWgD0Fdeqt9qLAwbOyiaNtXRXV3eDeZlfAmKlPrxK6WbD2umR02i0diyyBmTdVEmvhhR3//Mv0H/M8jwIhBWv7ve189enyEmqNlnG50wAAAAAElFTkSuQmCC} 柳都市星降町7丁目8番9号 登録番号 T1234567890123 2020年12月16日(水)12:34 #0903 {border:line; width:22} ^領 収 書 {border:space; width:3,*,3,8; text:nowrap} 166003 |2021葱鏡餅 水引 | 3個| ¥1,620* 691004 |洗面器45RPM N025 | 1個| ¥1,210~ - {width:auto; text:wrap} 小 計 |4点 | ¥2,830~ (税率10%対象 | ¥1,210) (内消費税等10% | ¥110) (税率 8%対象 | ¥1,620) (内消費税等 8% | ¥120) - 合 計 | ^¥2,830 お預り | ^¥3,000 お釣り | ^¥170 |*印は軽減税率対象商品です {code:202012160903; option:code128,2,48}電子レシート発行サーバーを作る
Node.js
Node.js で電子レシートを発行する HTTP サーバーを作ります。
receipt.js
の内部構成は、以下のようになっています。
- ReceiptLine
- 作成したレシートデータ (固定) を SVG に変換する
- HTML
- 作成した SVG を HTML に埋め込む
- HTTP サーバー
- 任意の GET リクエストを受けて、作成した HTML を返す
receipt.jsconst http = require('http'); const receiptline = require('receiptline'); // ReceiptLine const text = `{image:iVBORw0KGgoAAAANSUhEUgAAASAAAAAwAgMAAADMTE88AAAACVBMVEVwAAsAAAD///9xeVj9AAAAAXRSTlMAQObYZgAAAdZJREFUSMftlsFu4zAMRMUD73sI/0c59M4Amv//lXJI2XEWaGSgAVrsxuihtqgnmjOk09r7+pcv7esYAEtMM19yrCkWxwnOgEbk1Fc5nwBpRNgCpGdAhKi/4NVYaFmD1qqlYi8ACU6Y6BRo/DaQjk3hiARv4BeB834aVXBDk1bL0lHlCEs0Qz5zP4A89FVW3rgWNwGxWrRuCVIC7Ao3HjBCamY6mt7GHSQ9/rnQVEGZIBmlBPL0Fsvoig8Mg9N6KJAAB5ASFPDYzUcJytgEFXAQzpf2/ajMCMeMjCAwMX8A9ZQjQUJQ7mZs5G4TZMcaXRJd3bKBLOZLz4gC9Q00Y8030FE1Vga5egf5put0rTwDbT4SalV6fQmyp6DZIqCONXe+Aqk/A82mlcF3RO7eQeMRZH0JElePJ3P336BNfjwtdg42CV9PHynu8ldjSO7NIhxAD/Lvo1YDZONPgdIlg26JvwT4BAmud1AZstsOkhz+Fo3pERHhdLxiMCy9z89VgcAmnaCyPfCxg2hqnn4NWr/MtWgD0Fdeqt9qLAwbOyiaNtXRXV3eDeZlfAmKlPrxK6WbD2umR02i0diyyBmTdVEmvhhR3//Mv0H/M8jwIhBWv7ve189enyEmqNlnG50wAAAAAElFTkSuQmCC} 柳都市星降町7丁目8番9号 登録番号 T1234567890123 2020年12月16日(水)12:34 #0903 {border:line; width:22} ^領 収 書 {border:space; width:3,*,3,8; text:nowrap} 166003 |2021葱鏡餅 水引 | 3個| ¥1,620* 691004 |洗面器45RPM N025 | 1個| ¥1,210~ - {width:auto; text:wrap} 小 計 |4点 | ¥2,830~ (税率10%対象 | ¥1,210) (内消費税等10% | ¥110) (税率 8%対象 | ¥1,620) (内消費税等 8% | ¥120) - 合 計 | ^¥2,830 お預り | ^¥3,000 お釣り | ^¥170 |*印は軽減税率対象商品です {code:202012160903; option:code128,2,48}`; const svg = receiptline.transform(text, { cpl: 32, encoding: 'cp932', spacing: true }); // HTML const style = 'float: left; padding: 24px; background: lavender;'; const html = `<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>レシート</title> </head> <body> <div style="${style}">${svg}</div> </body> </html>`; // HTTP Server const server = http.createServer((req, res) => { switch (req.method) { case 'GET': res.end(html); break; default: res.end(); break; } }); server.listen(8080, "127.0.0.1", () => { console.log('Server running at http://127.0.0.1:8080/'); });実行
電子レシート発行サーバーを起動します。
$ node receipt.js
Web ブラウザーで
localhost:8080
を開きます。
receiptline ライブラリが動作しない IE11 でも表示 OK。
作成された SVG データ
レシートデータから生成された SVG データです。
中身はパス、画像、テキスト、Web フォント、フィルター、いろいろ入っています。SVG<svg width="384px" height="684px" viewBox="0 0 384 684" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"><style type="text/css"><![CDATA[@import url("https://fonts.googleapis.com/css2?family=Kosugi+Maru&display=swap");]]></style><defs><filter id="receiptlineinvert" x="0" y="0" width="100%" height="100%"><feFlood flood-color="#000"/><feComposite in="SourceGraphic" operator="xor"/></filter></defs><g font-family="'Kosugi Maru', 'MS Gothic', 'San Francisco', 'Osaka-Mono', 'Courier New', 'Courier', monospace" fill="#000" font-size="24" dominant-baseline="text-after-edge"><g transform="translate(48,0)"><image xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASAAAAAwAgMAAADMTE88AAAACVBMVEVwAAsAAAD///9xeVj9AAAAAXRSTlMAQObYZgAAAdZJREFUSMftlsFu4zAMRMUD73sI/0c59M4Amv//lXJI2XEWaGSgAVrsxuihtqgnmjOk09r7+pcv7esYAEtMM19yrCkWxwnOgEbk1Fc5nwBpRNgCpGdAhKi/4NVYaFmD1qqlYi8ACU6Y6BRo/DaQjk3hiARv4BeB834aVXBDk1bL0lHlCEs0Qz5zP4A89FVW3rgWNwGxWrRuCVIC7Ao3HjBCamY6mt7GHSQ9/rnQVEGZIBmlBPL0Fsvoig8Mg9N6KJAAB5ASFPDYzUcJytgEFXAQzpf2/ajMCMeMjCAwMX8A9ZQjQUJQ7mZs5G4TZMcaXRJd3bKBLOZLz4gC9Q00Y8030FE1Vga5egf5put0rTwDbT4SalV6fQmyp6DZIqCONXe+Aqk/A82mlcF3RO7eQeMRZH0JElePJ3P336BNfjwtdg42CV9PHynu8ldjSO7NIhxAD/Lvo1YDZONPgdIlg26JvwT4BAmud1AZstsOkhz+Fo3pERHhdLxiMCy9z89VgcAmnaCyPfCxg2hqnn4NWr/MtWgD0Fdeqt9qLAwbOyiaNtXRXV3eDeZlfAmKlPrxK6WbD2umR02i0diyyBmTdVEmvhhR3//Mv0H/M8jwIhBWv7ve189enyEmqNlnG50wAAAAAElFTkSuQmCC" x="0" y="0" width="288" height="48"/></g><g transform="translate(0,72)"><text x="54,78,102,126,150,174,198,210,234,258,270,294,306">柳都市星降町7丁目8番9号</text></g><g transform="translate(0,102)"><text x="54,78,102,126,150,162,174,186,198,210,222,234,246,258,270,282,294,306,318">登録番号 T1234567890123</text></g><g transform="translate(0,132)"><text x="0"> </text></g><g transform="translate(0,162)"><text x="12,24,36,48,60,84,96,108,132,144,156,180,192,216,228,240,252,264,276,288,300,312,324,336,348,360">2020年12月16日(水)12:34  #0903</text></g><g transform="translate(48,192)"><text x="0,12,24,36,48,60,72,84,96,108,120,132,144,156,168,180,192,204,216,228,240,252,264,276">╔══════════════════════╗</text></g><g transform="translate(48,216)"><text transform="scale(1,1)" x="0,12,24,36,48,60,72,84,96,108,120,132,144,156,168,180,192,204,216,228,240,252,264,276">║                      ║</text><text transform="scale(2,1)" x="24,48,60,84,96">領 収 書</text></g><g transform="translate(48,240)"><text x="0,12,24,36,48,60,72,84,96,108,120,132,144,156,168,180,192,204,216,228,240,252,264,276">╚══════════════════════╝</text></g><g transform="translate(0,270)"><text x="0,12,24">166</text><text x="48,60,72,84,96,120,144,168,180,204">2021葱鏡餅 水引</text><text x="240,252">3個</text><text x="300,312,324,336,348,360,372">¥1,620*</text></g><g transform="translate(0,300)"><text x="0,12,24">691</text><text x="48,72,96,120,132,144,156,168,180,192,204,216">洗面器45RPM N02</text><text x="240,252">1個</text><text x="300,312,324,336,348,360,372">¥1,210 </text></g><g transform="translate(0,330)"><text x="0,12,24,36,48,60,72,84,96,108,120,132,144,156,168,180,192,204,216,228,240,252,264,276,288,300,312,324,336,348,360,372">════════════════════════════════</text></g><g transform="translate(0,360)"><text x="0,24,48">小 計</text><text x="132,144">4点</text><text x="300,312,324,336,348,360,372">¥2,830 </text></g><g transform="translate(0,390)"><text x="0,12,36,60,72,84,96,120">(税率10%対象</text><text x="300,312,324,336,348,360,372">¥1,210)</text></g><g transform="translate(0,420)"><text x="0,12,36,60,84,108,132,144,156">(内消費税等10%</text><text x="324,336,348,360,372">¥110)</text></g><g transform="translate(0,450)"><text x="0,12,36,60,72,84,96,120">(税率 8%対象</text><text x="300,312,324,336,348,360,372">¥1,620)</text></g><g transform="translate(0,480)"><text x="0,12,36,60,84,108,132,144,156">(内消費税等 8%</text><text x="324,336,348,360,372">¥120)</text></g><g transform="translate(0,510)"><text x="0,12,24,36,48,60,72,84,96,108,120,132,144,156,168,180,192,204,216,228,240,252,264,276,288,300,312,324,336,348,360,372">════════════════════════════════</text></g><g transform="translate(0,540)"><text x="0,24,48">合 計</text><text transform="scale(2,1)" x="120,132,144,156,168,180">¥2,830</text></g><g transform="translate(0,570)"><text x="0,24,48">お預り</text><text transform="scale(2,1)" x="120,132,144,156,168,180">¥3,000</text></g><g transform="translate(0,600)"><text x="0,24,48">お釣り</text><text transform="scale(2,1)" x="144,156,168,180">¥170</text></g><g transform="translate(0,630)"><text x="0,12,36,60,84,108,132,156,180,204,228,252,276">*印は軽減税率対象商品です</text></g><g transform="translate(91,636)"><path d="M0,0h4v48h-4zM6,0h2v48h-2zM12,0h6v48h-6zM22,0h4v48h-4zM30,0h2v48h-2zM36,0h6v48h-6zM44,0h4v48h-4zM52,0h2v48h-2zM58,0h6v48h-6zM66,0h2v48h-2zM70,0h4v48h-4zM78,0h6v48h-6zM88,0h2v48h-2zM94,0h6v48h-6zM102,0h4v48h-4zM110,0h4v48h-4zM118,0h2v48h-2zM124,0h2v48h-2zM132,0h2v48h-2zM138,0h2v48h-2zM144,0h4v48h-4zM154,0h4v48h-4zM162,0h2v48h-2zM166,0h6v48h-6zM176,0h4v48h-4zM186,0h6v48h-6zM194,0h2v48h-2zM198,0h4v48h-4z" fill="#000"/></g></g></svg>電子レシートの背景をカスタマイズする
ソースコードの CSS を変更して、電子レシートの背景をカスタマイズします。
単色
お店のキャンペーンでよく使われるピンクレシートと黄色いレシート。
receipt.jsconst style = 'float: left; padding: 24px; background: pink;';receipt.jsconst style = 'float: left; padding: 24px; background: #ff9;';多色
サーマルロール紙にはできないグラデーション。
receipt.jsconst style = 'float: left; padding: 24px; background: linear-gradient(skyblue, lightyellow);';receipt.jsconst style = 'float: left; padding: 24px; background: linear-gradient(lightgreen, snow, beige);';透かし
「複写」「COPY」の画像をタイル状に並べます。画像は CSS に埋め込みます。
画像の Data URI 形式への変換は ReceiptLine Designer を使うと簡単です。
画像をロードしてimage:
をdata:image/png;base64,
に置き換えます。
receipt.jsconst style = 'float: left; padding: 24px; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANgAAADYAQMAAACz7r+uAAAABlBMVEXc3tv5+/j5MJA8AAADWklEQVRYw+2YPW7bMBSAqTAwMxTmBVwqR2g3G1AiH8W9gYMsCuLGSg3UW3yBwDlElwLNYMGDlza+QFsoMFqvDAy0NCpENQqRj3yCshfRmyh9En8eH98PCamllucih0+wPjT3MFPQZAh5GbQ5YvQ7tH3E2ATaAWK8Ae0MMXEAXUjEgpZpDjGLzCA0TxGTkelSIeal0tP/XSJGk9Uo1t/N0PJGi0UV478nS9MHYiL3Hw2L0fIuWpnF9t3lRQrGTps2GwQS2Fg5O3H88h765NmxzXx//tXowRcDZw18PIVJc2WrpcG5ME8tZ5cYYQKYoLZGOWkEwJpsZg2429hoYA1+mQPrxATWR9gNy0FvpzMvBcOmj3xu2It1aivfy3zYJ38pma18dUzMTP2pcuy8HwDjt1nDOW+Rxc6u8YEzjG3ayCI9YD/P4wrWYNnaZbAk7m8XLuuxpHjB8uHNO3cufFJ0SrMwv3KZr7d3p6M/U2SSLNPsy8Y9q35GivNIPydr5R4BRQoTpVcJOo+7JWjGt3mMfZE2bb5dILJnGNu+x+4mNiy5wgz+W01LTBhvc11i2qewqF1i2lxZEJXmopVBRR+7zEtZ5U2JB4piJbXksyq3sXOG0EVcB8Faaqmllv9TIGW5KMWHGUSKEksh/S3FAAg5CUpHyX7U1SzGaXMz0NFLEMyEiWwBwTGuxWUl6yw0i0p9vp7LcupUyInFephNZLnG0WFzIrsVjOajdjFMm3C3nuCrUccw381twsWHT4UaRT+8ddjbpfILxuXAydLJ2bdMMyaVW2e92T7qisZLpMu8zU2mZ0dTxNZh2jN5Fard7k7BKGLEWmsJuQbKRI7u+sCEW2CG1+Y/SkTDzSM7CsYLnE4l31gs82Pbctn6o3lIVAgm2qAzurT2Mo3ALDiLPSuZG85k36pXnJoztGuEQPRI5GSjkNq1A0mcaia1WCQPbcPj0mIPst+1D0cEIxytlJMfBqGCMmRx65TGEZdQhkxajur7LAUmhEBlP7AgDKpcQ6iGUZVLOZe/ZBV7uP+BS2PD0vkc+RQa6xddNka+iI6hXirdJkD66y7vX0UDllu6TYB5H+BU3DK0V/jGwKqFT7C7GdiWVX1ZEsZVnrZ8IWK7reYTAWC/joG1PBv5C4viMg7LEHCAAAAAAElFTkSuQmCC);';地紋
領収書やチケットでよく見かける地紋です。小さな素片をタイル状に並べます。
const style = 'float: left; padding: 24px; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADAQMAAACplL1tAAAABlBMVEX/pQD////52iT3AAAAFElEQVQI12M4l/+AYf/mCQyFdwoAJNIF3T0xRTsAAAAASUVORK5CYII=);';また何か作ったら投稿します。ではまた!
- 投稿日:2020-12-16T16:57:54+09:00
Node.js + NoSQL(Couchbase) アプリ開発 ステップバイステップガイド (1)
はじめに
Node.js + NoSQL(Couchbase) を使ったアプリ開発をステップバイステップで解説していきます。本記事は、シリーズの初回として位置付けられます。
Node.jsコミュニティに向けて
なぜ、NoSQLデータベースを使うのか
NoSQLの中でも、ここで扱うドキュメント指向データベース(ドキュメントストア)は、データとしてJSONドキュメントを格納します。言わずもがなですが、JSONとは「JavaScript Object Notation」の略で、Node.jsアプリ開発のバックエンドデータベースとしてドキュメントストアを利用することは、MEANスタック(MongoDB、Express、AngularJS、Node.js)にもみられように、自然と言えます。
なぜ、Couchbaseを使うのか
NoSQL技術同士の比較については、この記事の目的ではないため、多くは触れません。
先にあげたMEANスタックからもわかるように、MongoDBがまずは筆頭に上がる状況を踏まえ、あまり目にする機会のない、日本語によるCouchbaseについての紹介として、興味を持っていただければ幸いです。
一つだけ、Couchbaseに独自の部分として、SQLのJSONドキュメント用の拡張である、Couchbaseのクエリ言語、N1QL(Non 1st-normal-form Query Language: 非第一正規化クエリ言語)の利用について、最後に解説します。
前提
- Node.js
- 開発環境がセットアップされている
- Couchbase Server
- 開発環境にインストールされている(
localhost
でアクセス可能)- 管理者ユーザが作成されている(ID:
Administrator
、パスワード:password
)- Bucket(
test
)が作成されている- コマンドラインツールにパスが通っている
動作確認環境
- Mac OS Catalina Version 10.15.6
- コマンドラインツールパス:
/Applications/Couchbase Server.app/Contents/Resources/couchbase-core/bin/
- Node.js
- node v15.4.0
- npm 7.0.15
- Couchbase Server 7.0 BETA
プロジェクト準備
プロジェクト用のディレクトリを作成し、その中で作業を開始します。
はじめに下記を実行します。
$ npm init -ypackage.jsonファイルが作成されます。
Couchbase SDKをインストールし、package.jsonに追記します。
$ npm install couchbase --savepackage.jsonに以下のような依存関係が追記されます。
"dependencies": { "couchbase": "^3.1.0" }プログラム作成
index.js
ファイルを作成し、以下の内容を追加していきます。Couchbaseへの接続
const couchbase = require("couchbase"); const cluster = new couchbase.Cluster("couchbase://localhost", { username: "Administrator", password: "password", });バケットへの接続
const bucket = cluster.bucket("test"); const collection = bucket.defaultCollection();サンプルデータ定義
const user = { type: "user", id: 1, name: "田中", };ドキュメント登録関数定義
const upsertDocument = async (doc) => { try { const key = `${doc.type}_${doc.id}`; const result = await collection.upsert(key, doc); console.log("Upsert: "); console.log(result); } catch (error) { console.error(error); } };ドキュメント取得関数定義
const getDocumentByKey = async (key) => { try { const result = await collection.get(key); console.log("Get: "); console.log(result); } catch (error) { console.error(error); } };処理実行部
upsertDocument(user); getDocumentByKey("user_1");プログラム実行
$ node index.js下記のような結果が出力されます。
Upsert: { cas: CbCas { '0': <Buffer 00 00 05 1b f9 1e 51 16> }, token: CbMutationToken { '0': <Buffer d8 18 d7 a9 dd 24 00 00 03 00 00 00 00 00 00 00 2f 00 00 00 00 00 00 00 74 65 73 74 00 00 00 00 28 a2 03 05 01 00 00 00 40 a2 03 05 01 00 00 00 28 a2 ... 230 more bytes> } } Get: { cas: CbCas { '0': <Buffer 00 00 05 1b f9 1e 51 16> }, content: { type: 'user', id: 1, name: '田中' } }Ctrl+Dでプロセスを抜けてください。
実行結果確認
Couchbase Serverのコマンドラインツール(
cbq
)を使って、登録されたデータを確認します。$ cbq -u Administrator Enter Password: Connected to : http://localhost:8091/. Type Ctrl-D or \QUIT to exit.クエリ(
SELECT * from test;
)を実行してみます。以下のようなエラーになりました。
cbq> SELECT * from test; { "requestID": "00fda8f3-d64e-4d8c-85c1-ad810f921bd2", "errors": [ { "code": 4000, "msg": "No index available on keyspace test that matches your query. Use CREATE INDEX or CREATE PRIMARY INDEX to create an index, or check that your expected index is online." } ], "status": "fatal", "metrics": { "elapsedTime": "438.814µs", "executionTime": "368.037µs", "resultCount": 0, "resultSize": 0, "serviceLoad": 2, "errorCount": 1 } } cbq>メッセージ("No index available on keyspace test that matches your query. Use CREATE INDEX or CREATE PRIMARY INDEX to create an index, or check that your expected index is online.")にもある通り、クエリを実行するには、インデックスが作成されている必要があります。
プライマリインデックスを作成します。
cbq> CREATE PRIMARY INDEX test_primary ON test; { "requestID": "2c482905-b794-4092-bb0b-8d8cb76bc535", "signature": null, "results": [ ], "status": "success", "metrics": { "elapsedTime": "1.872931032s", "executionTime": "1.872853887s", "resultCount": 0, "resultSize": 0, "serviceLoad": 2 } } cbq>先ほどのクエリを再度、実行します。
以下のようにクエリの結果が表示されました。
cbq> SELECT * from test; { "requestID": "9b80a8eb-3605-4ff8-84fb-0a435959e4e1", "signature": { "*": "*" }, "results": [ { "test": { "id": 1, "name": "田中", "type": "user" } } ], "status": "success", "metrics": { "elapsedTime": "78.632291ms", "executionTime": "78.571384ms", "resultCount": 1, "resultSize": 113, "serviceLoad": 2 } }結果("results"の値)が幾分冗長なので、以下のようにクエリを微修正して、実行します。
cbq> SELECT test.* from test; { "requestID": "14dae1e2-cf81-45ae-8c55-2f64ad975743", "signature": { "*": "*" }, "results": [ { "id": 1, "name": "田中", "type": "user" } ], "status": "success", "metrics": { "elapsedTime": "1.034558ms", "executionTime": "969.805µs", "resultCount": 1, "resultSize": 73, "serviceLoad": 2 } }最後に
今回の記事は以上です。
今回のプログラムでは、キーによるアクセスのみを扱い、クエリでのアクセスについては、コマンドラインツールのみで扱いましたが、クエリをプログラムで実行することも、当然可能です。また、クエリについては、今回紹介したような単純なものではなく、一般にSQLを使った開発で用いられているようなもっと複雑なものが利用可能です。これについては、今後の記事で紹介できればと思っています。
また、Node.jsの本来のあり方を考えると、上記のような一度だけ実行するプログラムを作成・実行することは、直感的ではないかもしれません。サーバ・プログラム化についても、今後の記事で紹介していきたいと考えています。
今回は、非常に単純なプログラムとして作成したことで、データベースへの接続と、データの登録・取得という、本質的な部分のみを見て行くことができたかと思います(Node.js経験者にとっては、それ以外の部分は、よく知られているところかとも思います)。また、MongoDBの経験者が読まれた際に、Couchbaseでの実装について、MongoDBと比べて、どのような違い(と共通点)があるかが分かりやすいものになっていれば幸いです。
最後に、ここで紹介したプログラムを下記に公開していますので、適宜ご参照・ご利用ください。
https://github.com/YoshiyukiKono/couchbase_step-by-step_node_jp
- 投稿日:2020-12-16T16:19:50+09:00
GitHub ActionsでNodeサーバーでbuildを走らせる
目的:
記事にヘッドレスCMS+SSGを導入することに当たってNodeサーバー構築し、
GitHubActionでpush時に自動でgitから最新ソースをpull後にnodeサーバーでbuildする仕組みを実装前提:
下記のNodeサーバー構築完了
https://qiita.com/kerry/items/7a414f6fa2ca793c407e必要な情報をGitHubに登録
下記の必要なサーバーへ接続情報(envに設定する)をGitHubに登録する。
GitHubのSettingsタブの一番下のSecretsメニューを選択して順番に登録する。AWS用のACCESS_KEY(secrets.AWS_ACCESS_KEY_ID) AWS用のSECRET_ACCESS_KEY(secrets.AWS_SECRET_ACCESS_KEY_ID) AWS用のSECURITY_GROUP_ID(secrets.AWS_SECURITY_GROUP_ID_STG) SSH用の鍵(secrets.PRIVATE_KEY_STG) SSH用のユーザー名(secrets.USER_NAME) SSH用のHost(secrets.HOST_NAME_STG)GitHubActionsのworkflow作成
GitHubのActionsタブに入って新しくTempleteを利用して
.github/workflows/stag.ymlファイルを作成処理の流れ:
・Github ActionsのイメージのIPを取得
・AWS CLIをインストール
・AWS CLIの設定
・EC2のセキュリティグループにルールを追加
・SSH接続する
・サーバーでGit pull処理
・nodeサーバーでbuild処理
・EC2のセキュリティグループからルールを削除実装コート:
name: deploy stag on: push: branches: [stag] jobs: deploy: runs-on: ubuntu-latest if: github.ref == 'refs/heads/stag' steps: - uses: actions/checkout@v2 - name: deploy stag env: ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_ID }} SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_ID }} SECURITY_GROUP: ${{ secrets.AWS_SECURITY_GROUP_ID_STG }} PRIVATE_KEY: ${{ secrets.PRIVATE_KEY_STG }} USER_NAME: ${{ secrets.USER_NAME }} HOST_NAME: ${{ secrets.HOST_NAME_STG }} run: | IP_ADDRESS=`curl https://api.ipify.org/ -s` # AWS CLIインストール curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip awscliv2.zip sudo ./aws/install aws --version # AWS CLI設定 printf "${ACCESS_KEY}\n${SECRET_ACCESS_KEY}\nap-northeast-1\njson\n" | aws configure --profile blog-stag aws configure get aws_access_key_id --profile blog-stag # セキュリティグループにルールを追加 aws --profile blog-stag ec2 authorize-security-group-ingress --group-id ${SECURITY_GROUP} --protocol tcp --port 22 --cidr "$IP_ADDRESS"/32 # sshキーをコピー mkdir -p /home/runner/.ssh touch /home/runner/.ssh/private_key.pem echo "$PRIVATE_KEY" > /home/runner/.ssh/private_key.pem chmod 600 /home/runner/.ssh/private_key.pem # known_hostsに追加 ssh-keyscan ${HOST_NAME} >> ~/.ssh/known_hosts # テスト環境にclone #cd ./deploy && git clone git@github.com:test/blog.git /home/ec2-user/deploy # SSH接続して、git pullする ssh -o StrictHostKeyChecking=no -i /home/runner/.ssh/private_key.pem ${USER_NAME}@${HOST_NAME} ' source ~/.bash_profile && cd /home/ec2-user/deploy/blog && git checkout stag && git fetch --all && git reset --hard origin/stag && git pull origin stag && chmod +x /home/ec2-user/deploy/deploy_node.sh && sh /home/ec2-user/deploy/deploy_node.sh ' # セキュリティグループからルールを削除 aws --profile blog-stag ec2 revoke-security-group-ingress --group-id ${SECURITY_GROUP} --protocol tcp --port 22 --cidr "$IP_ADDRESS"/32上記の実行しているシェルの中身
sh deploy_node.sh
```!/bin/sh
nodenv local 14.15.1
npm ci
npm run build
```今回はサーバー上に静的htmlを吐き出したがったので、pull後に直接nodeサーバー内でbuildする仕組みを実装したが、下記のようにS3に保存することも可能
S3にアップ
上記の情報以外に下記の情報が追加で必要
AWS用のS3_BUCKET名(secrets.AWS_S3_BUCKET)実装コート:
name: deploy stag on: push: branches: [stag] jobs: deploy: name: git pull origin runs-on: ubuntu-latest if: github.ref == 'refs/heads/stag' steps: - uses: actions/checkout@v2 - name: deploy stag env: ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_ID }} SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_ID }} SECURITY_GROUP: ${{ secrets.AWS_SECURITY_GROUP_ID_STG }} PRIVATE_KEY: ${{ secrets.PRIVATE_KEY_STG }} USER_NAME: ${{ secrets.USER_NAME }} HOST_NAME: ${{ secrets.HOST_NAME_STG }} run: | IP_ADDRESS=`curl https://api.ipify.org/ -s` # AWS CLIインストール curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" unzip awscliv2.zip sudo ./aws/install aws --version # AWS CLI設定 printf "${ACCESS_KEY}\n${SECRET_ACCESS_KEY}\nap-northeast-1\njson\n" | aws configure --profile blog-stag aws configure get aws_access_key_id --profile blog-stag # セキュリティグループにルールを追加 aws --profile blog-stag ec2 authorize-security-group-ingress --group-id ${SECURITY_GROUP} --protocol tcp --port 22 --cidr "$IP_ADDRESS"/32 # sshキーをコピー mkdir -p /home/runner/.ssh touch /home/runner/.ssh/private_key.pem echo "$PRIVATE_KEY" > /home/runner/.ssh/private_key.pem chmod 600 /home/runner/.ssh/private_key.pem # known_hostsに追加 ssh-keyscan ${HOST_NAME} >> ~/.ssh/known_hosts # テスト環境にclone #cd ./deploy && git clone git@github.com:com:test/blog.git /home/ec2-user/deploy # SSH接続して、git pullする ssh -o StrictHostKeyChecking=no -i /home/runner/.ssh/private_key.pem ${USER_NAME}@${HOST_NAME} ' cd /home/ec2-user/deploy/blog && git checkout stg && git fetch --all && git reset --hard origin/stg && git pull origin stg ' # セキュリティグループからルールを削除 aws --profile blog-stag ec2 revoke-security-group-ingress --group-id ${SECURITY_GROUP} --protocol tcp --port 22 --cidr "$IP_ADDRESS"/32 build: name: build and deploy to s3 runs-on: ubuntu-latest if: github.ref == 'refs/heads/stag' strategy: matrix: node-version: [14.15.1] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run build --if-present - run: npm test - name: s3 sync uses: jakejarvis/s3-sync-action@master with: args: --acl public-read --follow-symlinks --delete env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY_ID }} AWS_REGION: 'ap-northeast-1' AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }} SOURCE_DIR: 'out'今回ハマったところ
nodenvコマンドのところで下記のエラーが発生。
bash: line 5: nodenv: command not found
直接サーバーでは問題なくbuildできたのに、、
ssh接続後下記を追加で解決
source ~/.bash_profile
https://superuser.com/questions/564926/profile-is-not-loaded-when-using-ssh-ubuntu
- 投稿日:2020-12-16T16:05:51+09:00
BOXのダウンロードの流れとSDK無しでダウンロードする方法
BOXからファイルをダウンロードする時にSDKを使わないと少し戸惑うのでメモ。
大まかな処理の流れ
利用するAPIの説明はこれです。
https://ja.developer.box.com/reference/get-files-id-content/このAPIを叩くと、レスポンスが302で帰って来る時は、レスポンスヘッダーで
Location
が戻されます。
このLocation
には、https://dl.boxcloud.com/d/1/b1!V1LhT0hCN....
のようなURLが入ってきます。
このLocation
から実際にファイルをダウンロードします。レスポンスは2パターンあって、ステータスコードが
302
の場合は、ヘッダーにLocation
、202
の場合は、ヘッダーにRetry-After
が戻され、クライアントがファイルをダウンロードできるようになるまでの秒数を示します。ためしても
202
がもどされなかったので、202
が発生して、Retry-After
が戻される時、curlとかaxiosではどういう挙動になるのか不明です。
ここに、curl 7.66.0からRetry-After
に従うようなことがかいてあるけど、どうなんだろう。試してないので動くかわかんないです。SDKにはこのあたりの処理が間違いなくちゃんと入ってます。
オートメーションとか連携に使うなら、アクセストークンの取り回しなどもあるので、SDKで(特にJWT認証で)使うのがいいでしょうね。APIをcurlで実行
ダウンロード用URLの取得
まず、Locationをフォローしないかたちで(
-L
を付けずに)以下のように実行してみます。curl -i -X GET "https://api.box.com/2.0/files/690849498295/content" -H "Authorization: Bearer XXXXXXXXXX"以下のようなレスポンスが帰ってきます。
HTTP/1.1 302 Found Date: Wed, 16 Dec 2020 02:28:43 GMT Transfer-Encoding: chunked Strict-Transport-Security: max-age=31536000 Cache-Control: no-cache, no-store BOX-REQUEST-ID: 17d4c6d940827c69342f2705eb8df260c Location: https://dl.boxcloud.com/d/1/b1!V1LhT0hCNCBXXWBMjANGzmCxSWNA0BsyXMMYGOAdG7ZSlBs4FzPrGPjDASb06VFsGc (めちゃくちゃ長いので省略) jLux9WOM./download Connection: closeふむふむ、
Location
が入ってます。ダウンロード用URLまで続けて実行
curlでは、
-L
を付けて実行すると、上のレスポンスヘッダーのLocationに入ってきたURLに自動的に移動してくれるので、ヘッダーからLocationを取り出すような操作は不要です。curl -i -X GET "https://api.box.com/2.0/files/690849498295/content" -H "Authorization: Bearer XXXXXXXXXX" -L以下が帰ってきます。
HTTP/1.1 200 OK Date: Wed, 16 Dec 2020 06:32:08 GMT Content-Type: image/png Content-Length: 86195 Connection: keep-alive Accept-Ranges: bytes Cache-Control: private X-Envoy-Upstream-Service-Time: 116 Content-Disposition: attachment;filename="baseball_girl.png";filename*=UTF-8''baseball_girl.png X-Robots-Tag: noindex, nofollow Encryption_Policy_Id: 0 X-Content-Type-Options: nosniff Strict-Transport-Security: max-age=31536000 Warning: Binary output can mess up your terminal. Use "--output -" to tell Warning: curl to output it to your terminal anyway, or consider "--output Warning: <FILE>" to save to a file.
-L
をつけると、一気にファイルを含んだレスポンスが帰ってきます。
--output
つけてファイルに出力しろって警告してますね。
ファイルに出力するんだったら、-i
で詳細情報だすのは不要です。ファイルに保存
-i
を外し、-o/--output
で、出力先のファイルを指定すると、ファイルを直接保存可能になります。
APIに対して1発でファイルをダウンロードするコマンドは以下のようになります。curl -X GET "https://api.box.com/2.0/files/690849498295/content" -H "Authorization: Bearer XXXXXXXXXX" -L -o sample.pngこれで一気にファイルがダウンロードできました。
Node.js(Axios)からの実行
Node.jsから実行してみるとこんな感じです。
axiosをつかっていますが、デフォルトで1つめのレスポンスに含まれるLocationをフォローしてくれますね。
ここではresponseType
にstream
を指定して、そのままWriteStreamに書き出してます。const axios = require("axios"); const fs = require("fs"); const accessToken = "XXXXXXXXXX"; axios .get("https://api.box.com/2.0/files/690849498295/content", { headers: { Authorization: `Bearer ${accessToken}` }, responseType: "stream", }) .then((result) => { const output = fs.createWriteStream("sample.png"); result.data.pipe(output); }) .catch((error) => { console.log(error.message); });Box node SDKを使う
ちなみに、Box Node SDKをつかうとこんな感じです。
わざわざアクセストークンを貼るなどせず、JWT認証をつかって認証周りの制御もやってます。
自動化するならBox公式SDKをつかうのが一番らくでしょうね。const boxSDK = require("box-node-sdk"); const config = require("./config.json"); const fs = require("fs"); const sdk = boxSDK.getPreconfiguredInstance(config); const client = sdk.getAppAuthClient("enterprise"); client.files.getReadStream("690849498295", null, function (error, stream) { if (error) { return; // handle error } const output = fs.createWriteStream("./sample.png"); stream.pipe(output); });
- 投稿日:2020-12-16T14:23:58+09:00
Jest のテストがメモリ不足で落ちまくっていたので、調査方法を調べてみた
TL;DR
簡易的にメモリの利用状況を知りたいのであれば次のオプションで jest 実行
実行例:
$ node --expose-gc ./node_modules/.bin/jest --runInBand --logHeapUsage PASS test/a.e2e-spec.ts (10.43 s, 160 MB heap size) PASS test/b.e2e-spec.ts (18.028 s, 197 MB heap size) PASS test/c.e2e-spec.ts (5.313 s, 215 MB heap size) ...ちなみにこの実行例は実際にメモリリークが発生してるテストなので、リークしていなければ基本的にここの値は大きく変わらないはず
Google Chrome の inspect 機能を使いたい場合は次のオプションで jest 実行
実行例:
$ node --inspect-brk ./node_modules/.bin/jest --runInBand Debugger listening on ws://127.0.0.1:9229/8ede11f0-7073-49bc-8e17-c16e661197d2 For help, see: https://nodejs.org/en/docs/inspector # Chrome の chrome://inspect を開いて `inspect` ボタンをクリックするとを押すと進む Debugger attached. # Chrome の inspect ツールから breakpoint からの開始ボタンをクリックすると進む RUNS test/operations/event.e2e-spec.ts ...ちなみに「これで最低限作業できる」ってオプションなので、「他のオプションを組み合わせたらもっと有用なデータが取れる」みたいなことはあるかもしれない。
もしご存知の方がいればコメントいただけると幸い。オプションたち
--runInBand
Jestを実行したプロセスをそのままテスト実行プロセスとして利用し、全てのテストを直列で実行するデバッグ用のオプション。
--maxWorkers=1
と似ているが子プロセスを生成しないという点が違う。はず。
内部の実装を見ていないのでもしかすると同じなのかもしれないけど。あと
--runInBand
と--maxWorkers
を同時に指定すると「バカなの?」と聞かれてコマンドが終了するので「わぁ、同時に指定しちゃった!どっちだ!」みたいなことにはならない。--logHeapUsage
それぞれのテストでのメモリ使用量を出力してくれるオプション。
Jest の--runInBand
オプションと node (v8) の --expose-gc オプションの設定が必要とのこと。付けると時間情報の部分にメモリの利用量も表示されるようになる。
例:
# オプション無し PASS test/a.e2e-spec.ts (10.43 s) # オプション有り PASS test/a.e2e-spec.ts (10.43 s, 160 MB heap size)「メモリ利用量」と連呼しているけど、あくまでも「ヒープメモリ」であることには注意。
ただデバッグしてる時はそこまで気にする事でも無い。--expose-gc
Node.js のオプションかと思ったら v8 のオプションらしい。
Web 上でのオプションのドキュメントは見当たらなかったのでオプションのありかだけ。$ node --v8-options | grep expose-gc --expose-gc (expose gc extension) --expose-gc-as (expose gc extension under the specified name)要は GC の情報を外部のアプリケーションからも触れるようにしてあげることでメモリの利用状況をより細かに取得できるようにしているんだと理解している。
--logHeapUsage
を取りたいなら必要だと言われているから付ける。以上。--inspect-brk
こちらは Node.js のオプション。
デバッグ用のinspector
というサービスを立ち上げると同時に、ユーザーのアプリケーション実行前に Break してくれる。--inspect
というオプションを使うとデバッガーを接続した瞬間にアプリケーションコードが実行される。基本的に自分が手動で実行する分には--inspect-brk
で良いと思う。ToDo
あとで chrome を使った inspect 周りを書く
参考文献
https://dev.to/pustovalov_p/reducing-jest-memory-usage-1ina
「こうすれば Jest のメモリ利用状況把握できるよ」ってのを教えてくれた記事
ただ、結論が「--logHeapUsage
するとその度に gc が走るからそれでパフォーマンス改善したよ。」だったので対処法は別途調べ中。https://www.alexkras.com/simple-guide-to-finding-a-javascript-memory-leak-in-node-js/
Node のメモリリークのデバッグ方法を書いている記事
細かいメモリリークへの対応が書いてあるけど、「自分のコードにメモリの stats 出すコードを埋め込んで調査する」が基本スタイルなので、今回の jest のメモリ利用状況把握とは若干噛み合わない部分も。
- 投稿日:2020-12-16T14:11:28+09:00
nodemailerでWindowsパスを使って添付ファイルを指定する。
CSVでメールの情報を受け取ってnodemailerで送信するスクリプトを書いていた。
CSVには添付ファイルの情報がwindows形式パスで入ってくる。C:\hoge\hoge.jpg↓以下のコードで送信できると思って一生懸命実行しまくっていた
let message = { from: config.FromAddress, to: targetMail.to, subject: targetMail.subject, text: targetMail.body, attachments: [ { filename: 'hoge.jpg', path: 'C:\\hoge.jpg' }, ] };送れない。。。
結果、これで送ることが出来たlet message = { from: config.FromAddress, to: targetMail.to, subject: targetMail.subject, text: targetMail.body, attachments: [ { filename: 'hoge.jpg', content: fs.createReadStream('C:\\hoge.jpg') }, ] };パスではなくストリームを渡す
- 投稿日:2020-12-16T13:54:00+09:00
ヘッドレスCMS+SSGを導入することに当たってNodeサーバー構築
目的:
担当する記事サイト改修でSSG/ヘッドレスCMSを導入する
問題:
・Vercelの利用を考えましたが、ドメイン変更が必要。
・SSG/ヘッドレスCMSを導入したいけど、ドメイン変更したくないというディレクターの要望。検討
・とりあえず既存サーバー内にNodeサーバー構築し、いろいろ試すことに
nodenvとは
・nodeのバージョンを管理するためのコマンド
・プラグイン(nodenv-build)を入れることで、nodeのインストールが可能nodenvのダウンロード
GitHubからnodenvのソースをcloneする
git clone https://github.com/nodenv/nodenv.git ~/.nodenv
nodenvのビルド
nodenvをビルドして実行可能な状態にする
cd ~/.nodenv && src/configure && make -C src
pathを通す
echo 'export PATH="$HOME/.nodenv/bin:$PATH"' >> ~/.bash_profile echo 'eval "$(nodenv init -)"' >> ~/.bash_profilepathを通してコマンドを叩けるようになる
シェルを再起動する
exec $SHELL -lログインシェルを再起動して新しい設定ファイルを読ませる
nodenvバージョンの確認
nodenv --versionプラグインのインストール
プラグイン用のディレクトリ作成
mkdir -p "$(nodenv root)"/pluginsプラグインのインストール(clone)
git clone https://github.com/nodenv/node-build.git "$(nodenv root)"/plugins/node-buildnodenvアップデート用プラグインのインストール(clone)
git clone https://github.com/nodenv/nodenv-update.git "$(nodenv root)"/plugins/nodenv-updatenodenv updateとコマンドを叩くことで、nodenvとそのプラグインを自動的にアップデート可能
nodeのインストール
インストール可能nodeバージョンの確認
nodenv install --listnodeのインストール(14.15.1は現時点でのLTSバージョン)
nodenv install 14.15.1rehash実行
nodenv rehash・nodenvからnodeやグローバルなnpmパッケージを見えるようにするため
・新しいnodeのバージョンを入れたり、npm install -gなどを行ったときに実行する必要があるnodeバージョンの確認
nodenv versions * 14.15.1 (set by /home/ec2-user/***_deploy/blog/.node-version)nodeバージョンの指定
プロジェクトで使うnodeのバージョンを指定
プロジェクトのディレクトリでのみ使うnodeのバージョンを指定する
cd /home/ec2-user/xxx_deploy/blog
nodenv local 14.15.1
プロジェクトのディレクトリ (/home/ec2-user/xxx_deploy/xxx_blog)内に.node-versionという名前の不可視ファイルが生成され、その中にそのディレクトリで使うnodeのバージョンが記述ちなみにグローバルで使う場合のnodeバージョン指定
nodenv global 14.15.1
グローバルとローカル両方指定した場合、ローカルで指定した方が優先らしい。build
npm ci(少し時間かかる)npm run buildCompiled successfully、Export successfulメッセージでbuildが完了し、プロジェクトのディレクトリにout(デフォルト)というフォルダが出来て色々吐き出される。
outフォルダ内の記事のhtmlファイルを開くと問題なく記事が表示されたのでbuildが正常に行われたと判断。その他nodeコマンド
nodeアンインストール
nodenv uninstall 14.15.1nodenvのアップデート
nodenv updatenodenvのアンインストール
~/.nodenvのディレクトリを消すだけ
rm -rf $(nodenv root)
サーバーで直接コマンド実行でbuildが正常に行われたので次はGitHubActtionでpush時に自動でgitから最新ソースをpull後にnodeサーバーでbuildする仕組みを下記のように実装。
https://qiita.com/kerry/items/5568232516b8ee323d4b
- 投稿日:2020-12-16T13:34:01+09:00
特定のパッケージを使用しているローカルリポジトリを探す
はじめに
この記事は、grepコマンドによる依存パッケージの調査方法についてのメモです。
GitHubのDependabotなど、依存パッケージの脆弱性警告ツールが普及しました。しかしその警告に従い、複数のリポジトリをまたいで依存パッケージを調査するのは骨の折れる作業です。
こうした状況で便利なコマンドがgrepです。grepは多くの環境に移植されていますので、使い方を一度憶えてしまえば役立つ資産となります。
grep
grepは正規表現によりファイルを検索するUNIXコマンドです。
このコマンドを利用すれば、ファイルを横断した本文検索ができます。
使用例
% grep -lr <pattern> --include="package.json" --exclude-dir=node_modules ./*このコマンドの内容を書き下すと、以下のようになります。
カレントディレクトリ
./*
以下で、ファイル名package.json
の<pattern>
を含む行を探す。ただしディレクトリnode_modules
は除外する。使用しているオプションは以下の通りです。
-r
: ディレクトリを再帰処理(デフォルトではディレクトリ直下のファイルを検索する)-l
: ファイル名のみを表示(デフォルトでは該当する行も同時に表示する)他にも検索パターンやオプションを変更すれば、grepは様々な問題の解決に役立ちます。皆様の問題に合わせてオプションを変更してみてください。
以上、ありがとうございました。
- 投稿日:2020-12-16T11:51:54+09:00
秘伝のServerlessFrameworkをアップデートする
はじめに
こんにちは。本年度からSREになりました蔭山です。
今回はサービスローンチ時より使い続けているServerlessFrameworkを(作業当時の)1系最新バージョンだった1.83.0へバージョンアップ対応を行いました。
そこでどのようにアップデートを行ったのか、実際の取り組み内容を元にご紹介します。当時の状況
私達のチームが運営しているサービスはAWSのLambdaをメイン基板としたシステムとなっています。サービスローンチ時より、アプリケーション側のリリース管理フレームワークとしてServerlessFrameworkを利用してきました。
ServerlessFrameworkを導入してからバージョンは1.36.2(2019/01/21リリース) で固定し、サービスを成長させてきました。発生した問題
このような状態でプロダクトを成長・運用してきていましたが、月日が経つと以下のような問題も発生してきました。
- クラウドベンダー(AWS)でのアップデートに追従できない
- ServerlessFrameworkの新機能に追従できない
- npmパッケージの脆弱性に対応できない
クラウドベンダー(AWS)でのアップデートに追従できない
Lambdaのようなサーバーレスなサービスは近年注目されていることもあり、クラウドベンダーでのアップデートは頻繁に実施されています。そういったアップデートでの新機能は有効なものが多く、プロダクトに取り込みたいものが数多くありました。
ですがServerlessFramework側はバージョン固定しているため基本的には対応できておらず、採用を見送ることも多々ありました。ServerlessFrameworkの新機能に追従できない
もちろんですがServerlessFrameworkも日々バージョンアップが進められています。そういったアップデートでは記述の簡略化など開発体験の向上につながるものを多くあります。
そういった新機能はもちろん利用できないためServerlessFrameworkのテンプレート上にリソース定義を書く、独自のServerless Pluginsを作成しフックする、別のCloudFormationテンプレート上に起こすなどの冗長的な対応が発生していました。npmパッケージの脆弱性に対応できない
アプリケーションはPythonでできているためnode.js製のパッケージが本番環境に乗ることはありませんが、ServerlessFrameworkのバージョンを固定してることで依存しているパッケージの脆弱性に対応できていない状況でした。
アプリケーションのコードはGithubで管理しているため脆弱性が公表されるたびにDependabotがアップデート対応のPRを生成してくれますが、対応を取り込むことによってServerlessFrameworkが動かなくなる懸念が拭えず取り込みを断念してきました。このような問題を解決するために、今回ServerlessFrameworkおよび利用しているServerless Pluginsのバージョンアップ対応を推し進めることにしました。
やったこと
上記の問題を解消するために、以下の対応を行いました。
- ServerlessFrameworkのバージョン検討
- ローカル環境・CI環境でのnode.jsのバージョンアップ
- package.json、package-lock.json更新
- 動作確認
ServerlessFrameworkのバージョン検討
アップデートを検討した段階で、Serveless Frameworkは2系がすでにリリースされていました。そのため、1系のままで最新版にするか、メジャーバージョンまで上げるかの検討を行いました。
当時の結論としては2系へのメジャーバージョンアップは行わず、1系の最新バージョンまでアップデートすることとしました。
主な理由としては以下となります。
- 当時2系がリリースされたばかりで潜在の不具合がある可能性があった
- 1系の古いバージョンからのアップデートであるため2系への変更時のbreaking changeやその他の影響が読めない
- 2系へ上げることのメリットが上記の懸念に比べると大きくなかった
ローカル環境・CI環境でのnode.jsのバージョンアップ
リリース時よりローカル環境・CI環境もnode.jsのバージョンを古いもの(8.11.1)で固定していた状態でしたので、まずはnode.jsのバージョンを14.10.0までアップデートを行いました。
ローカル環境は開発者の好みでさまざまなnode.jsのバージョン管理ツールを利用している状況だったため、アップデート方法に関しては開発者各自で行ってもらいました。
CI環境については独自にDocker Imageを準備していましたので、そちらのImageで対応を行いました。package.json、package-lock.json更新
package.jsonの更新はnpm-check-updatesを利用し、ServerlessFrameworkのバージョンアップを実施しました。
lockファイルに関してはpackage.json更新後にnpm install
を行って更新を行いました。動作確認
実際に開発環境にリリースし、以下の観点で動作確認を行いました。
- ServerlessFrameworkで生成されるCloudFormationスタックのリソースで消えたリソースはないか
- CloudFormationスタックで不審な変更はないか
- アプリケーションのパッケージング内容に差はないか
- 実際にアプリケーションを動かし、動作に問題ないか
問題
動作確認が無事完了し、実際に本番リリースすることができました。ですが、実際に本番へリリースしてみると以下の問題が発生してしまいました。
一時的にAWSリソースへの権限がなくなり、アクセスできなくなる
リリース直後に一時的に一部の処理でDynamoDBやS3、SQSといったAWSリソースへのアクセス権限がなくなり、エラーとなる事象が発生しました。
急遽切り戻して確認を行っていくと、LambdaのIAMロールのインラインポリシーの名前で命名規則が修正された改修があり、こちらが原因でAWS内部で一時的に権限が確認できなかったと仮説を立てました。
更に調査を進めて行くと発生率としては0.5%程度、発生期間は10分ほどであったため、リリース時のみの瞬断として対応を行うことなくリリースを続行しました。まとめ
以上、今回対応したServerlessFrameworkのバージョンアップ対応について紹介しました。
リリース時には問題はあったものの、その後は安定稼働を続けており冒頭で挙げた問題点も解消されています。ですが、今回は1系の最新バージョンにバージョンアップする方針としたため、まだ最新版に追いつけていないのが現状となります。
こちらに関しても引き続き対応を進めていく所存です。それでは、快適なServerlessライフを!
- 投稿日:2020-12-16T11:08:55+09:00
WebAssemblyをNode-REDで使う (後編)
前編の内容
前編では、Node-REDでWebAssemblyを使う方法として、functionノードにWebAssemblyバイナリを埋め込む方法と、独自ノード内でWebAssemblyを呼び出す方法の前準備までを説明しました。
後編では、前編で作成したWebAssemblyコンパイル済みのPNGトリミングルーチンを使って独自ノードを作っていきます。
(承前) 第二段階: PNG画像のトリミングをする独自ノードの作成
Node-REDノードの作成
Node-RED Nodeの作り方のドキュメントを参考にしながら、モジュールを作っていきます。
まずnpmモジュールの雛形を作ります。
% mkdir node-red-contrib-png-crop % cd node-red-contrib-png-crop % npm init -y ... %次に、
package.json
を更新します。package.json"name": "node-red-contrib-png-crop", "version": "1.0.0", "description": "Cropping PNG image", "keywords": [], "author": "", "license": "ISC", "node-red": { "nodes": { "png-crop": "png-crop.js" } }, "dependencies": { } }ランタイム側のロジックは
png-crop.js
として記述します。png-crop.jsconst pngcropwasm = require('./pngcrop'); module.exports = (RED) => { function PngCropNode(config) { RED.nodes.createNode(this,config); const node = this; node.on('input', (msg, send, done) => { if (Buffer.isBuffer(msg.payload.img)) { const x = msg.payload.x || 0; const y = msg.payload.y || 0; const width = msg.payload.width || 100; const height = msg.payload.height || 100; const cropped_img = pngcropwasm.croppng(msg.payload.img, x, y, width, height); msg.payload.img = cropped_img; send(msg); done(); } else { done('no image'); } }); }; RED.nodes.registerType('png-crop', PngCropNode); }冒頭の
require()
でWebAssemblyバイナリにコンパイルされたPNGトリミングライブラリをモジュールとして読み込んでいます。そして、Node-REDのメッセージハンドラで、ペイロードに含まれているイメージファイルを引数としてトリミング関数を呼び出し、結果をメッセージとして送信しています。あとは、エディタ側の記述です。とくに設定インタフェースはないので、最低限の記述のみしてあります。
png-crop.html<script type="text/javascript"> RED.nodes.registerType('png-crop', { category: 'function', color: '#F3B567', defaults: { name: {value: ""} }, inputs: 1, outputs: 1, icon: "font-awesome/fa-crop", label: function() { return this.name ||"png-crop";} }); </script> <script type="text/html" data-template-name="png-crop"> <div class="form-row"> <label for="node-input-name"><i class="fa fa-tag"></i> Name</label> <input type="text" id="node-input-name" placeholder="Name"> </div> </script> <script type="text/html" data-help-name="png-crop"> <p>A simple node that crops the image</p> </script>独自ノードのインストールとフローの作成
これをインストールして、フローを作ってみましょう。
% cd ~/.node-red/ % npm install ....../node-red-contrib-png-crop ... % cd ...../node-red % npm startfunctionカテゴリに"png crop"というノードができています。
試しにWeb上のPNG画像をトリミングしてダッシュボードに表示するフローを作ってみます。
[{"id":"924181c4.4467c","type":"tab","label":"Flow 1","disabled":false,"info":""},{"id":"fc70fd9.aa3a5","type":"inject","z":"924181c4.4467c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":100,"wires":[["aef153f1.97613"]]},{"id":"aef153f1.97613","type":"http request","z":"924181c4.4467c","name":"Ferris the crab","method":"GET","ret":"bin","paytoqs":"ignore","url":"https://rustacean.net/assets/rustacean-flat-happy.png","tls":"","persist":false,"proxy":"","authType":"","x":340,"y":100,"wires":[["b33daa13.f05df8"]]},{"id":"b33daa13.f05df8","type":"change","z":"924181c4.4467c","name":"","rules":[{"t":"move","p":"payload","pt":"msg","to":"payload.img","tot":"msg"},{"t":"set","p":"payload.x","pt":"msg","to":"480","tot":"num"},{"t":"set","p":"payload.y","pt":"msg","to":"350","tot":"str"},{"t":"set","p":"payload.height","pt":"msg","to":"300","tot":"str"},{"t":"set","p":"payload.width","pt":"msg","to":"300","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":540,"y":100,"wires":[["1f7259b8.b4e1e6"]]},{"id":"7497a53c.595bdc","type":"function","z":"924181c4.4467c","name":"Base64 encode","func":"const cropped = Buffer.from(msg.payload.img);\nmsg.payload = cropped.toString('base64');\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":340,"y":180,"wires":[["6dd7310.e04afd"]]},{"id":"6dd7310.e04afd","type":"ui_template","z":"924181c4.4467c","group":"c744f26f.11e5f","name":"","order":0,"width":0,"height":0,"format":"<div style=\"height: 300px; width: 300px\">\n<img src=\"data:image/png;base64,{{msg.payload}}\"\n alt='cropped image'\n />\n </div>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","x":520,"y":180,"wires":[[]]},{"id":"1f7259b8.b4e1e6","type":"png-crop","z":"924181c4.4467c","name":"","x":160,"y":180,"wires":[["7497a53c.595bdc"]]},{"id":"c744f26f.11e5f","type":"ui_group","z":"","name":"Default","tab":"22fdfd7c.238b92","order":1,"disp":true,"width":"6","collapse":false},{"id":"22fdfd7c.238b92","type":"ui_tab","z":"","name":"Home","icon":"dashboard","disabled":false,"hidden":false}]RustマスコットのFerris the Crabの画像を持ってきて、顔の部分を切り出して
ui_template
ダッシュボードに表示するフローになっています。速度は?
気になるのが変換速度になります。
5K画像を100回トリミングする簡単なスクリプトを書いて、速度を調べました。pure JS版(png-crop, pngjs利用)const pngcrop = require('png-crop'); for (let i=0; i<100; i++) { pngcrop.crop('./test.png', './testout.png', {width: 1000, height: 1000, top: 10, left: 10}, (err) => { if (err) throw err; }); }Rust+WebAssembly+JS版const pngcrop = require('./pngcrop'); const fs = require('fs'); for (let i = 0; i < 100; i++) { const input = fs.readFileSync('./test.png', {flag:'r'}); const cropped = pngcrop.croppng(input, 10,10,1000,1000); fs.writeFileSync('./testout.png',cropped); }結果は下記の通りです。プログラムの作りが違うので直接は比べられませんが、WebAssemblyベースのほうが約1.4倍高速という結果になっています。もちろん、ネイティブコードで実装すればさらに高速化できますが、ここではNode.jsで完結するところに利点を見出しています。
所要時間 pure JS比 pure JS版 98秒 1 Rust+Wasm+JS版 68秒 0.69 今回は
wasm-bindgen
で生成したファイルを手でコピーしてnpmモジュールに追加していますが、wasm-pack
を使うとnpmモジュールの作成まで自動的に行えます。Node-REDのノードモジュールとして使うときには、wasm-bindgen
の生成物をそのままコピーで十分かと思います。最後に
やや無理やりな使い方ではありますが、RustとWebAssemblyの組み合わせでNode-REDノードが記述できることを示しました。
WebAssemblyによって、多様な言語で記述したプログラムがブラウザ上やNode.js上で安全に実行できるようになりました。また、WebAssembly System Interface(WASI)によってWebAssemblyはサンドボックスを備えた軽量な実行環境としても使われるようになってきています。今後は、エッジコンピューティングなどより広い応用範囲で使われる技術になっていくと期待しています。
参考文献
- 投稿日:2020-12-16T11:06:23+09:00
WebAssemblyをNode-REDで使う (前編)
はじめに
Node-REDの独自ノードを作るには、一般にはJavaScriptとHTMLを使います。高速化のためにNode.jsのnative addonを使ってランタイム側にC++などで書いたコードをつかうこともできますが、環境に応じて再コンパイルする必要がでてきます。
そこで、WebAssemblyを使ってみることを考えてみます。WebAssemblyは
- JavaScriptより高速な処理が期待できる
- Node.js(8以上)が動けばどこでも動く
- C,C++,Rustなどの言語のコンパイル対象になっている
という特長があります。
ここでは、Rustで書いたコードからWebAssemblyのバイナリにコンパイルし、それをNode-REDから活用する方法を説明します。
環境整備
ここでは下記の環境を使いました。
- macOS Catalina version 10.15.7
- Node-RED v1.2.6
- Node.js v14.15.1
- Rustc 1.48.0 (target=wasm32-unknown-unknown)
- wasm-bindgen 0.2.69
各ツールのインストール方法はそれぞれのツールのWebページの説明をご覧ください。
第一段階: Node-REDから呼び出せることを確認する
最初から独自ノードを作っていくのも大変なので、まずは動くことを確認するためにfunctionノードをつかって無理やり使ってみましょう。
Rustのライブラリを作る
Cargoコマンドで、プロジェクトの雛形を作りましょう。この時点ではとくにWebAssemblyを意識することはありません。
% cargo new --lib hellowasm Created library `hellowasm` package %C形式の動的ライブラリを生成してWebAssemblyにコンパイルできるようにするために
Cargo.toml
を編集します。最後の2行は、最適化のための設定です。Cargo.toml[package] name = "hellowasm" version = "0.1.0" edition = "2018" [lib] crate-type = ["cdylib"] [profile.release] lto = trueプログラム本体は、
src/lib.rs
に書きます。ここでは単純な浮動小数点の掛け算をします。なお、WebAssembly自体には32,64bitの整数、浮動小数点しか型がありません。src/lib.rs#[no_mangle] pub extern "C" fn mul_f64_f64(x: f64, y: f64) -> f64 { x * y }WebAssemblyにコンパイルする
それでは、これをWebAssemblyにコンパイルしましょう。
% cargo build --release --target wasm32-unknown-unknown Compiling hellowasm v0.1.0 (.../hellowasm) Finished release [optimized] target(s) in 0.70sWebAssemblyのバイナリが
target/wasm32-unknown-unknown/release/hellowasm.wasm
に生成されています。% ls -l target/wasm32-unknown-unknown/release/hellowasm.wasm -rwxr-xr-x 2 ktoumura staff 243 Dec 3 16:03 target/wasm32-unknown-unknown/release/hellowasm.wasm243byteと小さいですね。なお、最適化の設定(link time optimization)を行わないと1513648byteのファイルになります。
functionノードでwasmをロードする
今回は小さいファイルですので、Base64エンコードをして直接functionノードから読み込ませてしまいましょう。
% base64 target/wasm32-unknown-unknown/release/hellowasm.wasm AGFzbQEAAAABBwFgAnx8AXwDAgEABAUBcAEBAQUDAQAQBhkDfwFBgIDAAAt/AEGAgMAAC38AQYCAwAALBzMEBm1lbW9yeQIAC211bF9mNjRfZjY0AAAKX19kYXRhX2VuZAMBC19faGVhcF9iYXNlAwIKCQEHACAAIAGiCwAPDi5kZWJ1Z19hcmFuZ2VzABUEbmFtZQEOAQALbXVsX2Y2NF9mNjQATQlwcm9kdWNlcnMCCGxhbmd1YWdlAQRSdXN0AAxwcm9jZXNzZWQtYnkBBXJ1c3RjHTEuNDguMCAoN2VhYzg4YWJiIDIwMjAtMTEtMTYp %
これを使って、functionノードのsetupタブでWebAssemblyのロードを行います。Setup時にロードすることで、メッセージを受けるたびにWebAssemblyのロードが行われるコストを削減します。
const wasmcode = "AGFzbQ...(略)..."; WebAssembly.instantiate(Buffer.from(wasmcode, 'base64'),{}) .then(result => { if (context.get("mul_f64_f64") === undefined) { context.set("mul_f64_f64", result.instance.exports.mul_f64_f64) } });このような形で関数定義を取り込み、ノードのコンテキストにセットしておきます。本体のコードはFunctionタブにセットしましょう。
const mul_f64_f64 = context.get("mul_f64_f64"); msg.payload = mul_f64_f64(msg.payload, 2.5); return msg;それではデプロイして実行してみましょう。
$12345.6 \times 2.5=30864$が計算できてますね。第二段階: PNG画像のトリミングをする独自ノードの作成
さて、もう一歩実用的な使い方に進みましょう。次は、独自ノードの中でWebAssemblyを使うことにします。ここでは「PNG画像をうけとって、それをトリミングした画像を出力する」ノードを作ります。
前節の説明ではRust-WebAssembly-JavaScriptの間のつなぎを全て手作業で実施しました。このままですと文字列を渡すのも一苦労です。このような作業を補助してくれるツールとしてwasm-bindgenがあります。
RustでPNGのトリミングを記述
前節と同様に、Rustのプロジェクトを作りましょう。
% cargo new --lib pngcrop Created library `pngcrop` package %今回は
wasm-bindgen
を使うためにCargo.tomlを下記のように変更します。Cargo.toml[package] name = "pngcrop" version = "0.1.0" edition = "2018" [lib] crate-type = ["cdylib"] [dependencies] image = "0.23.12" wasm-bindgen = "0.2" [profile.release] lto = true
dependencies
に画像を扱うためのimage
クレートと、先ほど述べたwasm-bindgen
を加えています。RustでのPNG画像のトリミングのコード(
src/lib.rs
)は下記になります。まだRustに慣れていないので不自然なコードになっているかもしれません...src/lib.rsuse image::*; use wasm_bindgen::prelude::*; #[wasm_bindgen] pub fn croppng(buf: &[u8], x: u32, y: u32, width: u32, height: u32) -> Vec<u8> { let mut img = load_from_memory(buf).unwrap().to_rgb8(); let cropped = imageops::crop(&mut img, x, y, width, height).to_image().into_raw(); let mut data = Vec::new(); let encoder = codecs::png::PngEncoder::new(&mut data); encoder.encode(&cropped, width, height, ColorType::Rgb8).unwrap(); data }croppng()関数は、PNG形式のバイナリのスライスとトリミングのパラメータを受け取って、トリンミング済みのPNG形式のバイナリを返します。
#[wasm_bindgen]
というアトリビュートをつけることで、後に置かれた関数がバインディング生成の対象であることを示します。このライブラリをビルドし、
wasm-bindgen
コマンドでNode.js用のバインディングを生成します。% cargo build --release --target wasm32-unknown-unknown Compiling autocfg v1.0.1 ... Compiling wasm-bindgen-macro v0.2.69 Compiling image v0.23.12 Compiling pngcrop v0.1.0 (.../pngcrop) Finished release [optimized] target(s) in 44.06s % wasm-bindgen target/wasm32-unknown-unknown/release/pngcrop.wasm --target nodejs --out-dir . --no-typescript % ls -l total 1640 -rw-r--r-- 1 ktoumura staff 11555 Dec 3 17:57 Cargo.lock -rw-r--r-- 1 ktoumura staff 178 Dec 3 18:10 Cargo.toml -rw-r--r-- 1 ktoumura staff 1945 Dec 3 19:07 pngcrop.js -rw-r--r-- 1 ktoumura staff 815413 Dec 3 19:07 pngcrop_bg.wasm drwxr-xr-x 3 ktoumura staff 96 Dec 3 16:52 src drwxr-xr-x 6 ktoumura staff 192 Dec 3 18:55 target %
pngcrop_bg.wasm
がコンパイルされたWebAssemblyバイナリ、pngcrop.js
がWebAssemblyへのインタフェース用のJavascriptプログラムになります。Javascriptのプログラム側からは、pngcrop.js
をrequire()
して使います。後編では、この2つのファイルを使って、Node-REDの独自ノードに仕立てていきます。