CUEBiC TEC BLOG

キュービックTECチームの技術ネタを投稿しております。

RESTAPIを使って記事一覧を作ってみた

地味に逃げ続けていたGASと格闘してましたmikihoです
今回はWordPressスプレッドシートの連携をRESTAPIを使って実現した方法について説明していきます。

わざわざスプレッドシートに一覧を作る必要があるのか、という疑問もありますが、意外と需要があるのです。
運用側からしたら、わざわざWordPressにログインして、必要な情報によっては投稿などの中に入って一つ一つ確認しなくてはいけないのは手間だそうで。
小規模なメディアならともかく、長く運用を続けていると正直確認だけで工数が…と言うことで、REST APIを使って自動化してみたのです。

今回は方法を大きく2つに分けて解説していきます。

基本的な情報のみの一覧

ここでいう基本的な情報とは、投稿の基本情報「ID」「タイトル」「投稿日」「更新日」などなどを示します。
ここにはカスタムフィールドの値は含んでおりません。

GASのコードとしては下記のような形になります

var SITE = "取得したいサイトURL";
function getArticles() {
var sheetArticleList = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('一覧を吐き出したいシート名');
// WP REST APIでカテゴリを取得する
var categories = getCategories();
// WP REST APIで記事を取得する
var jsonArticleInfo = UrlFetchApp.fetch(SITE + '/wp-json/wp/v2/posts?order=asc&per_page=100', {'method':'get'});
var articleInfos = JSON.parse(jsonArticleInfo.getContentText());
 // 総ページ数
var totalPages = jsonArticleInfo.getHeaders()["x-wp-totalpages"];
// 取得した記事情報から出力するデータを作成
 var output = makeOutputData(articleInfos, categories);
 // シートの2行目から出力する
sheetArticleList.getRange(2, 1, output.length, output[0].length).setValues(output);
// 記事数が2ページ以上ある場合は、総ページ数分、繰り返し処理する
if (totalPages > 1) {
for (var index = 2; index <= totalPages; index++) {
// 最終行
var lastRow = sheetArticleList.getRange('A:A').getValues().filter(String).length;
// WP REST APIで記事を取得する
jsonArticleInfo = UrlFetchApp.fetch(SITE + '/wp-json/wp/v2/posts?order=asc&per_page=100&page=' + index, {'method':'get'});
articleInfos = JSON.parse(jsonArticleInfo.getContentText());
// 取得した記事情報から出力するデータを作成
 output = makeOutputData(articleInfos, categories);
// シート最終行の次の行から出力する
sheetArticleList.getRange(lastRow+1, 1, output.length, output[0].length).setValues(output);
}
}
}  
function getCategories() {
// WP REST APIでカテゴリを取得する
var jsonCategoryInfo = UrlFetchApp.fetch(SITE + '/wp-json/wp/v2/categories?orderby=id&per_page=100', {'method':'get'}); 
var categoryInfos = JSON.parse(jsonCategoryInfo.getContentText());
var categories = [];
for(var index = 0; index < categoryInfos.length; index++){
var categoryInfo = categoryInfos[index];
var row = [];
// ID
row.push(categoryInfo["id"]);
// 名前
row.push(categoryInfo["name"]);
categories.push(row);
}
return categories;
}
function makeOutputData(articleInfos, categories) {
var output = [];
for(var index = 0; index < articleInfos.length; index++){
var articleInfo = articleInfos[index];
var row = [];
// 公開日
row.push(Utilities.formatDate(new Date(articleInfo["date"]), 'JST', 'yyyy/M/d'));
// 更新日
row.push(Utilities.formatDate(new Date(articleInfo["modified"]), 'JST', 'yyyy/M/d'));
// カテゴリ
var articleCategories = articleInfo["categories"];
var category = "";
 for (var articleCategoryIndex = 0; articleCategoryIndex < articleCategories.length; articleCategoryIndex++) {
if (category.length != 0) {
category += ",";
}
category += getCategoryName(categories, articleCategories[articleCategoryIndex]);
}
row.push(category);
 // タイトル
row.push(articleInfo["title"]["rendered"]);   
// 記事ID
row.push(articleInfo["id"]);
// URL
row.push(articleInfo["link"]);
// パス
row.push(articleInfo["link"].replace(SITE, ""));
output.push(row);
}
return output;
}
function getCategoryName(categories, articleCategoryId) {
var ret = "";
for (var index = 0; index < categories.length; index++) {
var category = categories[index];
if (articleCategoryId == category[0]) {
ret = category[1];
break;
}
}
 return ret;
}

これで基本的な情報は取得できます。
ちなみに、あえて本文は取得させておりません。
本文も取得できますが、今回はあくまで記事一覧が欲しいというお話をいただいて作っていたものなので。

※ちなみに、本文の文字数がセルの最大文字数を超える可能性もあるので、必要なければ取得しないことをお勧めいたします...

定期実行させるためのトリガーを設定すれば簡単に自動化が完了するのです。

カスタムフィールドの情報を含ませたい

一覧にカスタムフィールドの情報も表示させたい、となると少し改良が必要になります。
RESTAPIではデフォルトでカスタムフィールドの中身は取得できないので、functions.phpなどに追加するためのコードが必要になるからです。

WordPress側に追加するコードは下記のような形になります

<?php
add_action( 'rest_api_init', 'api_add_custom_fields' );
    function api_add_custom_fields() {
      register_rest_field(
        'post', //カスタムフィールドの情報を追加したいpost_type名
        'custom_field', //取得した際の名前
        array(
          'get_callback'    => 'add_get_custom_field', //カスタムフィールドを取得するための関数名
          'update_callback' => null,
          'schema'          => null,
          )
        );
      }
    
      function add_get_custom_field($object, $field_name, $request) {
        $meta_fields = array(
          'custom_field1',//カスタムフィールド名
          'custom_field2'
          );
          $meta = array();
          foreach ( $meta_fields as $field ) {
            $meta[$field] = get_post_meta( $object[ 'id' ], $field, true );
          }
          return $meta;
      }
?>

排出する内容は自分で整形する必要があるので、そこら辺の処理も一緒に追加しておくことをお勧めします。
GASはコード管理が難しいので、管理がしやすいテーマ側に処理を寄せた方が後々のトラブルを回避できるでしょう。

これでEST APIにカスタムフィールドの値が追加されました。
あとはタイトルなどと変わらない方法で書き出すためのコードをGAS側に追加するだけで完成します。

補足

REST APIはデフォルトではたくさんの情報を取得します。
が、その分重くなりがちなので必要のない情報は取得しない形にまとめておくことが理想です。
functions.phpにて、下記のようなコードでRESTAPI上から情報を消しておきましょう

<?php
    //いらない取得データの削除
      function remove_post_data($response, $post, $request) {
        unset($response->data['id']);
        unset($response->data['slug']);
        unset($response->data['excerpt']);
        unset($response->data['status']);
        unset($response->data['content']);
        unset($response->data['type']);
        unset($response->data['menu_order']);
        unset($response->data['comment_status']);
        unset($response->data['ping_status']);
        return $response;
    }   
    add_filter('rest_prepare_page','remove_post_data' , 10, 3);
?>

応用編

今回紹介したのはあくまで投稿一覧ですが、カスタム投稿の一覧も作ることが可能です。
URLの一部をカスタム投稿のpost_typeに変更するだけで、ほとんどコードを変更する必要なく取得ができます。 ですが、注意しなくてはいけないのは、show_in_resttrueにしておくこと。
カスタム投稿はデフォルトでは、show_in_restfalseのため、REST APIを叩いても404が返ってきてしまいますのでそこだけ注意が必要です。

そして、アクセス先のURLも当然変わってきます。
具体例を挙げると下記のような形になりますね。

    /wp-json/wp/v2/posts

ここを

    /wp-json/wp/v2/pages

などのような形でpost_type名に変更するだけです。
例では固定ページのpost_typeを入れていますが、カスタム投稿のpost_typeを入れれば取得が可能になります!

注意点

REST APIは基本的にドメイン/wp-json/wp/v2/〜という形式ですが、パーマリンクの構造をデフォルト設定にしている場合この形式ではなく別の形式になります。
その場合はドメイン/?rest_route=wp/v2/〜という形式で書かなくてはいけなくなるのです。
パーマリンクの構造を変えてもいい場合は変更してもいいかと思いますが、そうでない場合の方が多いと思いますので、取得の形式が違う、というのは覚えておきましょう。

【便利すぎる】Notionボタン機能-便利な活用方法!

ボタン機能を使い業務の簡素化をしてみた

ポイント! Notionで毎回データベースにページ追加をしたくない人には本当におすすめ!

初心者でも使いやすいボタン機能です!いいねボタンも作れたりするので会社のコミュニケーションがより活発になりますね!
ボタン機能のショートカットキーは「/button」です!

【目次】

1. ボタン機能を使ってみた感想

2. ボタン機能利用例:本の管理

3. ボタン機能利用例:アイディアストック

1. ボタン機能を使ってみた感想

大きな利点としては

1️⃣ルーティン作業の簡素化
2️⃣Notion初心者でも分かりやすい

でした!実際に議事録をボタン一つで追加できるのと、そのボタンがどこのページにも設置可能という点がとても良かったです。また、今まで議事録の追加やプロパティ編集をするために

Notionホーム>議事録データベースページ>データベースにページを追加>プロパティ編集

という4つの段階が必要でしたが、今では
Notionホーム>ボタンを押す>ちょっとだけプロパティ編集

という風に作業が圧倒的に楽になりました!ページ遷移がないのは最高です😆さらにボタンを押すだけなのでNotionに慣れていない人にとっては嬉しすぎる機能であることは間違いありません!

あるページのデータベースを操作するボタンは、どのページにも設置可能なのでそこが最高です。私はNotionでお金管理をしていますが、何か買ったらすぐに記録できるようにNotionのホームにボタンを設置しています。

2. ボタン機能利用例:本の管理

ボタン機能を使うと、「複数人で本を管理する際に、誰がいつ借りて、本の感想はどうで、おすすめかどうか」を研修コストなしで実現できます!ボタンを押すだけですから!

本の管理ではページのプロパティを編集する必要があります。しかし、以下の見た目ならどうでしょうか?

ボタン機能を使った本の管理
下にある3つのボタン「借りる、返す、おすすめ」を押すだけで全てが済みます。ボタン機能にはページのプロパティを編集することができます。試しにおすすめボタンを作成してみましょう!

作成step

step1
step2
step3
step4
step5
step6
他のプロパティを編集するのも基本的に同じ手順で進みます。いいねボタンも簡単に作成可能です!実際にボタンを押してみるとおすすめ欄に自身の名前がつくはずです!

3. ボタン機能利用例:アイディアストック

新しいアイディアが生まれたとき簡単にデータベースの追加がボタン機能で実現できます!今回は見やすさのためにボタンをデータベースと同じページにしていますが、ボタンはどのページにも設置することができます!

ボタン押すとページが追加

上の画像に書いてある通り、ボタンはどこにでも設置できるので手間が大幅に省けますね!

作成step

step1
step2
step3
step4
慣れてくればすらすら使えるようになります!最初は見様見真似で挑戦してみてください!

まとめ

ボタン機能を使えば今までルーティン作業が簡素化し、Notion初心者でも使いやすいのが分かりましたでしょうか?ボタン機能によって作業効率の幅が広がったと感じています!
Notionを使いこなして生産性のある仕事環境にしましょう!

 Notionの記事は以下の記事でも書いています。 cuebic.hatenablog.com

herp.careers

troccoでutf-8の罠にはまったはなし(前編)

ポイント! どうも〜キュービックのテックリードの尾﨑です。 本日はtroccoをKomawo以外でも使ってもらおうと同じチームのトーマスこと東松に勧めたところ、 ハマった事例を紹介します

自己紹介

最初にトーマスに関して軽く紹介します。おーいトーマス〜!

はーい!どうも初めましてトーマスこと東松です

最近サービスリリースしたんですよね?

そうなんです。Sunbyというプロダクトをリリースしました

sunby.jp

ということで今回はSunbyでtroccoを使用して検証していた時に起こった不具合を解消するまでの流れをトーマスとお届けします

Sunbyの詳細はまた、別の機会にさせていただくのでお楽しみに!

troccoを勧めたきっかけ

トーマスとは担当プロダクトは違うのですが分析部分で私が、お手伝いをしていたりなこともあり、1on1を週1で行っていました。そんな1on1の中の1コマから始まりました

Looker Studioへのデータ連携を試してるんですけどうまくいかなくて・・・

データ連携ならtroccoで簡略化できるかもよ。そうかトーマスには紹介してなかったですね

招待しました!

ありがとうございまーす!

発生した事象

そして2週間ほどしてトーマスが何やらやっているのを見守っていた時のことです

trocco君・・・

MySQLのutf8mb4をTrocco(UTF-8)経由でスプシに吐き出したら文字化けする。。

あー化けてますね。何か類似のパターンあったかなぁ??UTF-8だから絵文字はだめですよってことでは?

う〜んUnicodeの絵文字のコードを含んでるわけじゃないんですよね。。日本語文字が化てまして

例えばどんなです?

こういうのの「sunby_kansya_感謝」感謝のところが化けてるんです

色々仮説を洗い出していたらスレッドは伸びに伸び

仮説

  • VARCHARでマルチバイト文字の変換時に何か起きているのでは?
  • DB本体のクライアント側の文字コード設定がイけてない
  • その他環境依存の問題
  • troccoの設定の問題

DBのSchemeの型はVarcharなのでそこに問題がある可能性は否定できないです😭

方針

ひとまず、エラー原因の調査は継続しつつ、トーマスをtroccoのSlackコネクトに追加してCSにも相談させていただくことに

はい、ということでトーマスも無事troccoのサポートチャンネルに仲間入りですね。troccerが増えて嬉しいです。次回は実際の原因と対応策などを解説していきたいと思います。お楽しみに!

えっprimeNumberさんのCSに相談投げれたんですか?!もっと早く教えてくださいよー

Tableau DesktopでRedshiftをデータソースに設定する方法

どうも! キュービックのテックリードの尾﨑です
今回は前回「Tableau REST APIのエラーコード400081と格闘した話 - CUEBiC TEC BLOG」 で取り上げたRedshiftをデータソースに設定する工程がそもそも複雑すぎたので、tipsとしてまとめて解説したいと思います

対象

この手順はmac OSODBC接続をする前提で記載しています。参考にさせていただいた情報ソースなどは記載しますが、 SSL認証などセキュアな部分などは自己責任でお願いいたします。

設定手順

設定手順としては以下の順序で解説していきます

手順

  • 1.Redhisftのドライバーを公式からインストールする
  • 2.odbc.iniファイルに設定情報を記載する
  • 3.odbc.iniファイルを適切なパスに移動する
  • 4.iODBCをダウンロードしてSystemDSNを設定する

1.Redhisftのドライバーを公式からインストールする

Tableau Desk TOPからRedshiftに接続するにはAWSの公式のRedshiftのドライバーを入れないといけません。以下からダウンロードしてください。

docs.aws.amazon.com

公式でもそライバーが必要な旨は説明されていますが、ドライバーをインストールした後の話がAWS側の遷移先リンクを読み解く必要があり、 個人的にわかりずらかったです。結局、公式は何について解説をしたいのかよくわからない・・・というのが初見の感想でした。 help.tableau.com

2.odbc.iniファイルに設定情報を記載する

インストールするとルートディレクトリーに以下のようなパスでodbic.iniが配置されているのでこちらに必要情報を追記します。

/opt/amazon/redshift/Setup/odbic.ini

情報追記箇所は以下の3つです

  • Server(hostと同じです)
  • Port(5043です) -Database(設定内容に合わせて指定してください)

SSL認証を必須としたい場合は追加で以下を行う必要があります

3.odbc.iniファイルを適切なパスに移動する

設定ができたら、odbc.iniの配置位置が異なるので以下の要領でコピーファイルを再配置しましょう(シンボリックリンクでも良いとお思います)

sudo cp -p ./opt/amazon/redshift/Setup/odbc.ini  ./Library/ODBC/.

また、ファイルのグループと権限を更新する必要があるため以下の要領で更新してください。

sudo chown root:admin /Library/ODBC/odbc*.ini
sudo chmod 664 /Library/ODBC/odbc*.ini

以下のようになればOKです

参考にさせていただいたリンク github.com

iODBCをダウンロードしてSystemDSNを設定する

odbic.iniに情報を記載しただけではデータソースとして接続することはできません。DSN設定する必要があります。今回はiODBCというツールを使用することにしました。

macで標準でインストールされなくなったようで自前でインストールする必要があります。ダウンロードリンクはこちらになります(数秒経過後に自動ダウンロードとなるので注意です)

ダウンロードが完了したら、System DSNを開いて、以下を設定します。 両方とも、Redshiftの接続する際のUserとPassと一緒です。

  • Useranme
  • Password

接続テストをしてみましょう。

何やらエラーが出ていますね。あっVPNの設定をしていませんでした。 あるあるですね。

Tableau Desktopから接続

ここまで設定ができたら、いよいよTableau Desktopから接続です。

  • サーバーへ>その他>その他のデータベース(ODBC)を押下します

  • iODBCに記載したDSNを選択して、ユーザー名とパスワードを入力します

さぁ緊張の瞬間です

無事つながりました。勝利ですね。

ちなみにiODBCを実施前にドライバー選択でもトライしましたが、Tableau Cloudと同じRedshiftの接続情報でも接続できませんでした。

以上が、手順になります。もう少しスマートなやり方がある気がしますし、Windowsならもっとスムーズに行く可能性もありますが、参考になれば幸いです。

Tableau REST APIのエラーコード400081と格闘した話

ポイント! どうもキュービックのテックリードの尾﨑です。本日はTableau Server REST APIのエラーコード400081と格闘した記録をお話しします。 おそらくベストプラクティスがまとまっていない事例なので、同じ課題を持つ方の参考になれば嬉しいです

はじめに

エラーコード400081とは? Tableau Server REST APIでViewの詳細情報を取得する際に稀に発現するエラー
公式ではGENERIC_QUERY_VIEW_DATA_ERRORとして定義されています

{
    "error": {
        "summary": "Bad Request",
        "detail": "There was a problem querying the data for view '3a912663-29ae-46f7-8634-f663d65b0929'.",
        "code": "400081"
    }
}

どうやらAPIの戻り値云々ではなく、APIのリクエストの段階で問題が起きているようですね

判明している発現パターン

パターン

  • 1.APIのアクセスキーを発行したユーザーの権限が足りない
  • 2.Viewがパブリッシュされていない
  • 3.ワークブックブックがパブリッシュされた直後
  • 4.データソースのパスワードが埋め込まれていない
  • 5.Tableau Cloud WEB でマルチデータソースでカスタムSQLを作成している
  • 6.データソースにRedshiftを使用している

原因と解消方法

他のエンドポイントで判明するもの

以下の3つに関してはViewの詳細をする以前に別のエンドポイントを使用することで切り分けが可能です

  • 1.APIのアクセスキーを発行したユーザーの権限が足りない
  • 2.Viewがパブリッシュされていない
  • 3.ワークブックブックがパブリッシュされた直後

Query Workbooks for Site(WORKBOOK情報取得) ワークブック情報が取得できない場合は、アクセス権限が足りないか、パブリッシュが正常にされていない(完了していない)可能性が高いです

Query Views for Workbook(Viewsリスト取得) Viewの一覧に対象のViewが含まれていない場合は、Viewが下書きモードでパブリッシュされていないなど不完全な状態である可能でしが高いです

APIの使用方法に関してはこちらで詳しく解説しているのでご参照ください

4.データソースのパスワードが埋め込まれていない

原因

データソースの接続設定をした際に、パスワードを埋め込んでいないことにより認証ができていない

解消方法

ワークブックからデータソース>アクション>接続設定の編集へと進み

「必要な場合はユーザーにパスワード用のメッセージを表示」から「接続に埋め込まれたパスワード」を設定すると解消されます

5.マルチデータソースでカスタムSQLを作成している

原因

Tableauではデータソースの設定し、設定したデータソースに対してカスタムSQLを投げることができます。
しかし、TableauのWEB上でカスタムSQLでデータを取得もでき、View上でもデータが確認できるのにREST APIではエラーとなることが判明しました。
※マルチデータソースだけでなく、Mysqlなど1つのデータソースに複数のDBが含まれている場合なども同様でした。

解消方法

Tableau Cloud上でマルチデータソース設定とカスタムSQL設定はあくまで疎通確認ぐらいとし、 運用ベースで設定する際はTableau DesktopまたはTableau Prepで設定を行う

6.データソースにRedshiftを設定している

原因

これが一番難解でした。複数のテーブルの結合などもなく、View自体は認識されているのに詳細は取れない。
どうやらTableau CloudでRedshiftをデータソースとしてパブリッシュしたワークブックのViewは取得できないことがあるようですね。
もし取得方法をご存知の方がいたら是非教えて欲しいです><

解消方法

解決方法! 1.Tableau DesktopでRedshiftのドライバーをインストールする
2.Tableau DesktopでRedshiftをデータソースとして設定して、カスタムSQLで任意のワークブックをpublishする
3.Tableau Server REST APIでパブリッシュしたワークブックのViewを取得する

こちらで実際に解消することはできました。ただ、設定がmysqlのドライバーのインストールする際などより 数倍は手間がかかったため「Tableau DesktopでRedshiftをデータソースに設定する方法 - CUEBiC TEC BLOG」 で詳細は解説したいと思います。

まとめ

エラーコード400081が出たらまずは以下を疑う。

ポイント

  • 1.APIのアクセスキーを発行したユーザーの権限が足りない
  • 2.Viewがパブリッシュされていない
  • 3.ワークブックブックがパブリッシュされた直後
  • 4.データソースのパスワードが埋め込まれていない
  • 5.Tableau Cloud WEB でマルチデータソースでカスタムSQLを作成している
  • 6.データソースにRedshiftを使用している

原因切り分け方法

  • 1〜3は該当のワークブックまたはviewが取得できているかをまずは確認する
  • それでも解決しない場合は4,5,6を疑う

確認された以外でも扱うデータソースによってはまだまだエラーになるパターンは存在しそうです。 発見し次第、更新したいとお思います。
現状は原因に対してエラーコード/メッセージの内容がかなり不親切なので、細分化されたり、公式で トラブルシューティング方法が発表されることを願います。

SREエンジニアがテックリード主催のプログラミング研修に参加してみた

どうもーキュービックのテックリードの尾﨑です。 今回はSREチームのkmsn17と行ったプログラミング研修に関して解説したいと思います。

なぜプログラミング研修を行ったか

元々インターン生の育成カリキュラムの作成に関わっていて、アプリケーションエンジニアの カリキュラムのベースを尾﨑が作っていました。ただ、以下のような課題を抱えていました

課題

  • アサインされるプロダクトやPJによってはカリキュラム通りに進行しない
  • カリキュラム通りに進行しないことにより妥当性がわからない
  • 磨き込みが行われず、浸透しない

そういえば、これって正社員の研修でも使えるかもねという声もあったな・・・

そこで!

  • 尾﨑が研修を開いてビルドアップしちゃえばええやんと思ったのが始まりです

研修内容

カリキュラム

  • javascriptで文法の学習と演習を進めるという単純なもの
  • データ取得、データ整形、データ連携を中心に設計
  • javascriot→node.js→GASと学んだことを再生産しつつ応用を効かせる

前半で基礎をしっかり叩き込めば、あとは自助努力で伸びていくという寸法です

プログラミングって学ぶことを上げ始めるとキリがないですよね。要するにDBの知識やRubyPHPが書けなくても 何かしらの言語でデータ取得、データ整形、データ連携ができればあとは応用でしょう!という発想のもと最重要エッセンスだけを抽出しました

研修の狙い

ゴール

  • GASでも良いので最小工数で課題解決ツールを独力で作成できる状態

狙い

  • カリキュラムの磨き込みを行いたかった
  • 研修受講者の課題解決スキルのエンハンスに繋げたかった
  • 研修受講者に後進育成をしてもらうプロセスを作りたかった
  • 副次的に育成工数代替と後進育成者の理解度の補完

研修の流れ

実際の流れ

  • 受講後にどうなっていたいのかを擦り合わせる
  • 研修の完了日を設定する
  • カリキュラムをベースにWBSを受講者に作成してもらうう
  • WBSをチェックして、現実的なスケジュールで更新する
  • 演習をWBSをもとに 週次で1on1を実施して解説や詰まったポイントを解消
  • Slackのhuddleでペアプロを挟みつつ進行

研修の工夫ポイント

こだわり

  • 受講期間中のモチベーションは自身で維持してもらった
  • WBSの演習項目を調査、学習、実装に分けてもらった
  • 1つ目の演習課題完了後に習熟度や学習プロセスを確認し、クリアリングした
  • 2つ目以降の演習課題はスケジュールを引き直してもらった
  • 指摘に関してはまとめてもらって、同じ指摘はしないスタンスとした
  • WBSを完遂することが目的ではないことを中盤で説明した
  • ライブラリーは利用意図を説明できるようにしてもらった
  • 成りたい像を再ヒアリングしてWBSを改訂してもらった

とはいえモチベーションの維持って、難しいですよね。何か工夫していたポイントってありますか?

う〜ん。色々あるんですけど、一番は時間を作って教えていただいてるので信頼感を裏切りたくない一心で手を動かしましたね笑 最初は焦りもありシンドイナーって気持ちもありましたが、進めていくうちに楽しくなって業務を止めて進めてた時期もありました汗

なるほどなるほど!研修を通して学ぶことの面白さを知ってもらいたかったが大きいので、嬉しいですね

実際のカリキュラム

dom操作

データ整形

  • dom操作で取得したデータを配列に整形しよう

データ出力

  • aタグを生成してblobでcsvファイルを出力しよう

他システム連携

  • node.jsでdom操作で抽出したデータを整形してSlackに通知しよう
  • node.jsでデイリーで抽出した整形データをSlackに通知しよう

応用

苦労した点

実際やってみてkmsn17どうでしたか?

最初は文法が何でこういう書き方なの?から始まりした笑 特に苦労した点はこのあたりですね

苦労した点受講者視点

  • 文法周りが理解できなかった
  • 解説された内容で理解できない部分があった
  • どこを調べればいいのか検討がつけづらかった
  • ライブラリはどれを使えばいいのか?分からなかった

なるほど。ひとつずつ聞いてみたいと思います。まず文法周りで困ったということですが、印象に残ってるものとかありますか?

変数の定義でvar,let,constの違いが分からなかったり、変数の初期化やループ処理や標準出力などのなぜそうなっているのか?という用法を理解するのに苦労しました

あー最初はわかんないですよね〜。そんな中でもどうやって学習を進めていったんですか?

ターニングポイントとしては途中からjavascriptの書籍を購入して、手を動かしながら復習することで体系的な理解を進めることができたのかなと思います

自己学習ができてきたって感じですね。次は解説で分からない部分があったということですが、どの辺ですかね?

カリキュラムをベースにWBSを作った際に、課題のアウトプットがふわっとしたまま進めてしまったところがありました

なので、1on1の時に実際のアウトプットとの乖離を指摘されて、後から意味に気づいたパターンとかがありました

あー!ありましたね!あれってどうやって改善しましたか?

分からないキーワードは事前にリスト化して調べてまずは部分的に理解を進めつつ、アウトプットのイメージを事前にすり合わせてブレがないようにしました。また、他の開発メンバーにも相談したりもしました

なるほど、アドバイスしたものもあれば自主的に取り組んでくれたものもあった感じですね!次は「どこを調べればいいのか検討がつけづらかった」に関しては時間の経過とともに変化したと思います。初期と中盤と終盤でそれぞれ教えてください

開始初期の時は、ひとつ調べると分からない言葉が出てきてまた調べる感じでしたが、 中盤からは情報ソースを見つけやすくはなりました。なぜそれを選んだのかの根拠がふわっとしていたのでそこを求めるようになりました。 終盤は選定方法では実現できないパターンもあったので複数のパターンの代替案を見つけるのに苦労しました

なるほど、なるほど!前半から終盤にかけて結構変わりましたね。中盤から根拠を大事にされた点が良かったのかなと思います

参考までに講師側の苦労した点も書いておきます

苦労した点講師視点

  • 前半は文法理解がまだなので概念理解と演習と文法解説が混ざりがち
  • 30分の1on1でクリアリングって結構大変。次のMTGあるけどあと5分で実装サンプル作る・・・
  • なぜ詰まっているかをまずは見つけるところからだった
  • リモートを挟みつつやったのでjsじゃないと環境的に厳しかったかも的なところも
  • 完了日が近づいてくると講師も焦り始める。これ終わるよね?詰まったら連絡ちょうだい

テックリードからの指摘一覧

実際にメモしてもらったものの抜粋です。最初は文法周りのFBが多かったですね

そうですね。あと、文法を覚えるためにtab補完を使わないようにもしたり、一度指摘された内容は、同じこと言われないように何度も見返しました

第一回

  • var createElementを調査しよう(chromeの書き方が載ってる)
  • デバック力をつける、プログラムを動かしたとき、どこで欠損しているのか?障害の切り分けをまずは覚えよう
  • 構文理解(②の後に、ここが重要)
  • sampleで配列を定義して、ブラウザから出力できることを確認しよう
  • 一回できたら他のサイトでも同じことをやろう
  • コピペは使わない
  • セミコロンをつける癖をつける

第二回

  • 変数の初期化を実施しよう
  • constとvarとletの違いを理解しよう
  • 検証モードのキャッシュのクリア方法を知ろう
  • 一回データを取得したら、consle上で確認をして、その後csvで取得できるのか?確認をする
  • option+command+iでデベロッパーモードが使えるよ
  • 変数の宣言と変数の代入の違い

この辺りで確か情報収集部分で課題があることを発見して軌道修正しましたね

書籍を購入して構文や構成理解を深めたり他のメンバーの知見も吸収し始めました。

第三回

  • 分からなくなったら、まず図に起こして記載する
  • 分からない単語、分からなかった章を紐づけて、記載する
  • paizaも復習で利用しよう
  • jsを体系的に理解する本を購入しよう
  • 他のメンバーと知見や困っている点を質問しよう
  • フロントエンドエンジニアにセレクタを解説してもらおう
  • カリキュラムでブラシュアップできる点は提案して欲しい

node.jsに進みます。ある程度ライブラリーの力も借りつつ環境構築とAPI連携からの通知に挑戦。この時点で自発的に学びに行く学習プロセスの軸が出来上がってきましたね

一定の理解ができたので調査すべきポイントが掴めたり、あと書籍を再読して理解を深めたのもよかったのかなと。話題のChatGPTにも聞いて難しい内容だけ分かりやすく説明してもらったりしてました笑

第四回

  • 通知のデバッグは即日でも設定次第でできる
  • node.jsでもスケジューリングはできる
  • promptを使って運用レベルで機能を拡張しよう
  • Slackで通知する情報のクオリティも工夫しよう
  • ソースファイルは階層で作ろう
  • そろそろ関数化しよう

後々禍根を残さないようにライブラリーの良し悪しを判断する見識眼をインストール。この辺りだと自分の言葉で実装方針が説明できたり、質問の確度も応用レベルに上がってきてましたね。

そうですね!あと、何でこのライブラリを選んだの?と聞かれて答えられなかったりマズイ!!と危機感があったので説明できるように調べました

第五回

  • ライブラリを安易に入れると後で互換性で苦労するよ
  • ライブラリーが何をしているのか理解しよう
  • ライブラリーの選定理由は説明できるようになろう
  • 処理を目的ごとに分割しよう
  • スプレッドシートからデータを取得すると2次元配列になるのでflat()を使おう
  • 値が参照できない?getValues()しよう
  • 単なる出力だけじゃなくてセルにに色付けや罫線を引いたりもしよう
  • 安易にアプリケーション実装には知らずにデータの参照→更新→登録→削除の順番で進めよう

加速度的に理解が上がってますね。もう尾﨑いらないのでは?いやまだまだ教えることはきっとあるハズ・・・

言われたこと書かれたことをやるのではなく、こういう工夫必要だよね?おっ分かってるじゃん!やるじゃん!と思われるように試行錯誤してました。いっぱい教えて欲しいです笑

第六回

  • パラメータをシートから取得して動的な設定ができるような関数を実装しよう
  • 実務の課題を解決するプロトタイプをつくってみよう
  • シート情報を一括で取得する場合はsheet.getDataRange()を使おう

最後もなかなハラハラする展開でしたね。今思うとGASでdom操作って結構厳しかったんじゃないかと

そうですねwなんとか終わって良かったです

研修を終えた感想とこれからについて

約2ヶ月に及ぶ研修お疲れ様でした。どうでしたか?

ハラハラ、ドキドキの2ヶ月でした

自分からやりたいって言った手前、なんとしても終わらせないと!って気持ちで頑張りましたw

研修の中で完了させることが目的ではないという話をしたと思います。これから何かやりたいこととかありますか?

開発知識がついたのでSREとして運用効率化や信頼性の向上などのメディア改善などに応用していきたいです

実は受講前にも同じ質問をしたんですよね。受講前と今とで何か変わったところとかありますか?

受講前は何ができるか?という点がふわっとしていましたが、ある程度やることが見えてきたり、課題を聞いたりする中でこういうふうにすれば解決できるかも!的なものが出てくるようになりました

もう大丈夫ですね!ここからは応用の繰り返しなので、自分で磨いて行ってください

今回受講してみて、自分みたいにスクリプティングができることでやれることが広がるみたいなのってあるんだろうなって思いました。もし次回の開催とかがあれば自分もお手伝いしたいです

ぜひお願いします。一緒に盛り上げていきましょう

おまけ

そーいえば、以前kmsn17氏が上げてた記事はアクセス好調でしたよね!なんとかズラだったよーな・・・

cuebic.hatenablog.comですね!

投稿してすぐにアクセス爆上がりでしたよね。twitterでも同じことありましたよね!?

世一さんのtwitterですね。社長との洋服ポーカです。3年越しの正体バラシ

NetSuiteとAPI連携してみた_その2(クライアント証明書認証編)

どうもーキュービックでテックリードをやっている尾﨑です。 本日は会計や組織データなどのマスタ管理を行なっているNetSuiteとREST API連携した時のお話をしたいと思います。
Komawoの全体アーキテクチャ
クラッチで連携した事例は希少だと思うので、困っている方の課題解決に繋がると嬉しいです。 今回は前回のOAuth2.0認証に続いて、クライアント証明書認証によるREST API連携をご紹介したいと思います。

・OAuth2.0トークン認証によるREST API連携
・OAuth2.0クライアント認証によるREST API連携★
・トークンベース認証によるRestlet API連携

本日はOAuth2.0クライアント証明書認証によるREST API連携方法とハマったポイントなどをご紹介したいと思います。

アーキテクチャ構成

Komawoの前身であるCBA(CUEBiC Analytics)から先行的に ドメインとして持つべきではないマスタをNetSuiteに退避し、REST APIにて同期を行いました。 こちらのアーキテクチャでは前回のOAuth2.0認証ではリフレッシュトークンの有効期限が短く、本番運用では利用できないという欠点を克服しました。

アーキテクチャ

事前準備

前提

インテグレーションの発行が完了している前提になります。インテグレーションの発行方法は 前回のOAuth2.0認証をご参照ください。

事前に以下の準備が必要です

1.クライアント証明書の発行
2.NetSuite上でのクライアント証明書の設定

1.クライアント証明書の発行

NetSuiteの公式サイト上で指定されているopen sslで発行を行います

openssl req -x509 -newkey rsa:4096 -sha256 -keyout auth-key.pem -out auth-cert.pem -nodes-days730

上記のコマンドをターミナルなるなどで入力すると、対話形式で入力を求められます。 質問内容と解答例を示します。

質問内容 解答例(例です)
Country Name JP
State or Province Name Tokyo
Locality Name Shinjyuku-ku
Organization Name sample company
Organizational Unit Name sample department
Common Name sample.co.jp
Email Address 不要

上記設定値を設定していくとローカルに以下の2ファイルが生成されます

ファイル名 概要/用途
auth-cert.pem publicファイル(CERTIFICATE) NetSuite上に登録
auth-key.pem private key  JWTの生成で使用

参考にさせていただいたリンク 自己署名証明書の作成方法 qiita.com

2.NetSuite上でのクライアント証明書の設定

次にNetSuite上にクライアント証明書を登録します

設定>インテグレーション>OAuth2.0クライアント資格証明(M2M)設定へすすむ

1.新規作成ボタンを押下
2.エンティティで紐づけたいアカウントを選択
3.ロールは:「Administrator」など高位のロールを選択
4.アプリケーション:インテグレーションで設定したものを選択
5.証明書ファイルを選択:1.クライアント証明書の発行で生成した「auth-cert.pem」を選択
6.保存ボタンを押下して問題なければOK

成功すると以下のように証明書がNetSuite上に設定されます

APIの設定

以下の順番で説明していきます

1.JWTの発行
2.アクセストークンの取得

1.JWTの発行

アクセストークンを取得するためのパラメータの「client_assertion」がJWT方式で生成しないといけません。 NetSuiteの公式で指定されているリクエストークン構造でトークンを発行します。

『JWTとは?』

JSON WEB TOKENの略
以下の3つの構成でカンマ区切りでトークンを発行する
・トークンヘッダー
・ペイロード
・クライアント証明のCERTIFICATE
トークンヘッダーとペイロードに認証情報を含んでおり、クライアントはその情報を元に
情報を複合して一致していた場合に認証やトークン発行を行なっている

トークンヘッダー

パラメータ 設定値
typ JWT(固定)
alg RS256(HS256は使用できないので注意)
kid 「2.NetSuiteでのクライアント証明書の設定」で登録したクライアント証明書のID

トークペイロード

パラメータ 設定値
iss インテグレーションで発行したクライアントID
scope rest_webservices
aud NetSuiteのtokenのエンドポイント
exp UNIX時間で設定/トークンの有効期限 ※過去日付はNG ※iatより1時間以内であること
iat UNIX時間で設定/トークン発行日時 ※過去日付はNG

NetSuiteのtokenのエンドポイントの例

https://<accountID>.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token

JWTの生成の実装サンプル

公式がチラッとライブラリーが必要な旨を記載しています・・・ 残念ながらJWTは普通に生成できません。

ここでは、node.jsを使用した生成例を解説します。

Node.jsのjsonwebtokenライブラリを使用します

『選定理由』

『苦労した点』

トークンヘッダー

NetSuite ライブラリー
typ 設定不要。JWTが固定で設定される
alg algorithm
kid keyid

トークペイロード

NetSuite ライブラリー
iss iss
scope なし
aud audience
exp exp
iat 設定不要:timestampが設定される

ソースコード

Node.jsは入っている前提になります。

以下のライブラリーをインストールします

npm install --save sleep
npm install --save @types/sleep

「creat_jwt.js」をnode.jsがあるディレクトリと同じディレクトリに作成します

var fs = require('fs')
var jwt = require('jsonwebtoken')
const private_key = fs.readFileSync('./auth-key.pem', 'utf8')
const public_key = fs.readFileSync('./auth-cert.pem', 'utf8')

const jwtheader = { algorithm: 'RS256', keyid: 'your kid ' };

const jwtPayload = { iss: 'your iss', scope: 'rest_webservices', aud: 'your audience', exp: 1654498589, //The value must not be more than one hour greater than the value of the iat parameter. };

const token = jwt.sign( jwtPayload , {key: private_key ,passphrase:'passphrase here'}, jwtheader) console.log( '=== JWT TOKEN RS256==='); console.log(token.toString('base64')); const verified = jwt.verify(token, public_key , { algorithms: 'RS256'}, function(err, decoded) { if (err) { // 認証NGの場合 console.log(err.message); } else { // 認証OKの場合 console.log(decoded); } });

トークンの発行と発行トークンの認証確認を行います
node creat_jwt.js

成功したらこんな感じの結果が得られます(生成値は正しくないものに変えています)

=== JWT TOKEN RS256===
eyJhbGciOiJSUzI1NiIsInR5cCeyJpc3MiOiI0OTljYjM3MzkyYTM2ZjEyN2IzYjNiYjk3NDAzM2FiNmI5YjRmYzE2M2E5YTM5MjZlNjk4MDc2MDRjNzJjNjQ1Iiwic2NvcGUiOiJyZXN0X3dlYnNlcnZpY2VzIiwiYXVkIjoiaHR0cHM6Ly81Mzg1NzI3LnN1aXRldGFsay5hcGkubmV0c3VpdGUuY29tL3NlcnZpY2VzL3Jlc3QvYXV0aC9vYXV0aDIvdjEvdG9rZW4iLCJleHAMTY1NDQ5NTEzMH0.KL26d4vO827ki6zGwqKMoD4HEUxx1TNi6iGhj17-WPnKpekqxp-HFSk_rddynMPZz_vm2HBnNKxoRlQKh831QoTOB_9jcjsR33GMeMP-smgcf3NZRxVrvBUufph9XHHTmS9ZcvbAf0XQGijVzDB0GZBMmdAMUtQdkG0Lh9hmhr1IR8Mu1P7Tgys-eAfhenb-N5GbsjON-f4BeEao-dUxKvsbkphNSFsEBrO7xvRTJN2VywKwPFScFh89GVYPS1VLYaBjw4xj4_APLYCOtUKp9JPXAfoTaB1hztjQ44GdggT1k5h42cY0qO1_X6JDPDs-mvPLSuQ6kmyW5txRe217vFFoviutQS4zyy5HCEbQUA6ik7ThI7ITd4-sGiVZtaUtWzDOaNOmllrtmpV1eSRbpTHjr5ydxt_6aXgV889sFw0kgiP6zhTkCFPo7Xd6dS8fYucRbWXOzJfJG-7NB-qpKxsEvtb1VXHDNMqXspoPTT-r8Zw2RA6eqh74H0VGYUJL0_Lsll541k9df6E
{
  iss: 'your iss',
  scope: 'rest_webservices',
  aud: 'your aud',
  exp: 1654498589,
  iat: 1654495130
}

2.アクセストークンの取得

JWTが取得できたので、ようやくアクセストークンの取得へと移ります。

公式で公開されているサンプル

リクエスト形式:POST
エンドポイント:https://<accountID>.app.netsuite.com/services/rest/auth/oauth2/v1/token?

POST /services/rest/auth/oauth2/v1/token HTTP/1.1
Host: <accountID>.suitetalk.api.netsuite
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&client_assertion=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiIxYzM0M2E3MTZjMWRjZWI2MGU3ZmMxNDlmYTY3MzU5MjllZjc3ZDI4ZmUxNjI5M2Y4OTI5NzZkZGU3ZDhlM2UyIiwic2NvcGUiOlsicmVzdGxldHMiLCAicmVzdF93ZWJzZXJ2aWNlcyJdLCJhdWQiOiJodHRwczovL3J1bmJveC5jb3JwLm5ldHN1aXRlLmNvbS9zZXJ2aWNlcy9yZXN0L2F1dGgvb2F1dGgyL3YxL3Rva2VuIiwiZXhwIjoxNjI3OTA5MzAzLCJpYXQiOjE2Mjc5MDU3MDN9.j7fhtd0qQP-iD7ns9q_fuG8Arz2aWJyoSvZ8sHRVA8HXOJG3pAQbT5J5F8MLkWIXA9ZuSxHdCWNwQLoRUeKlGURYFFqDHP_yjoWFWWtq5Wb-AnaZg_jBVL8TaOFGY2WByFM8rHsJVopFegwEQsU6bkcwqiFttEKxso-MiSAc5lE9SBgi6Fus2btiYGIFcNrKalFXEWDy6Ah5yVCo3wxkk9dfiPmT6JgLdjFkCc3v7tMCD9CrRHXrmhQvL8aoeyTMzJILURw5rnuy9zAs9ngymtX_iiwes8XpkBeCJbX4totI-EY4myi7L4fc2NgeWT-bvLWo6_sWjXE4BKyewqjtreUJscR9bhJ5Fi7S8nIoGDQbZrwhIgoKM_UI9Waw6kRLwRer_c0QDFY-sMLeGT3HL5vihHRFNXd-cKb-AWplkRiSJrdHXJtuGHLniHRpkK0-A1AFalIzYw4SSykxfck0qsPdf-oFPuawUsKR9lDCcYlyOaDZdQsBNsbjOsp5gGtyCuBwPBS8xz7I6gqLVEfNuzTfDDk8SMw1fN9MQ0NJtZMqMxm-WY_bLjZVkI3gqsvgDS-ADBPC7cymVZGfPUqummDUeG-Ks7SkLaHpfY6i-aZS8KUAY4aN5Do3GWT56aoEM9s1YB_1ZF_YxsBmK_gcX_mmlwUxbvCVpuHJTvKAQzY

パラメータ

項目 設定値
grant_type client_credentials
client_assertion_type urn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertion JWTの生成で発行したトーク

ヘッダー

項目 設定値
Content-Type application/x-www-form-urlencoded

レスポンス

項目 設定値
access_token アクセストーク
Expires_in アクセストークンの有効時間常に3600秒(1時間)
token_type 常に「Bearer」

curlコマンドなどで確認してみましょう

curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'grant_type=client_credentials&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&client_assertion={your client_assertion}' https://{accountID}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token

成功すると以下のようなレスポンスが返ってきます(access_tokenは適当な値に直しています)

{"access_token":"eyJraWQiOiJjLjUzODU3MjcuMjAyMi0wNC0yNF8xOS00My00OCIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiIzOzgyMCIsImF1ZCI6WyIxRTZEMTI2My0wODIxLTREM0YtQkU1My1FQTk0ODVEQzY4MDM7NTM4NTcyNyIsIjQ5OWNiMTY1NDUwMDI1NSwiZXhwIjoxNjU0NTAzODU1LCJpYXQiOjE2NTQ1MDAyNTUsImp0aSI6IjUzODU3MjcuYS1jLm51bGwuMTY1NDUwMDI1NTUyNSJ9.tbpaJ8xF6Gmpy_NClWszHaSX1t3wiyICzvew9MuCbTI5ut3totCdzV6UIIa7Yyg2gbNm2eL37RGhK4NaAFwMbuxXx6snL6dXaUG5NsfODfCRD3XZIyUA7At1VUUDxycULnkVh5Mf7zGWZvvs5QwASA0itF6B0y5FZ8vpq-Spog021m6K4Y2Y9Vs-uC3nBAc4DUgkK8hFXlf_KxN2A5562mm-UyGrtjG9ZJvrFupxSHwyVQHhyxs35aykwlddbPjLUZbApS6_3GQndIkaqxrWwdFTVsACryfJfUB8F8hYNU","expires_in":"3600","token_type":"Bearer"}

はまった点

『 クライアント証明書のやってはダメなこと』

  • 証明書の有効期限が切れている(最長2年です)
    • 証明書を再発行した上でNS上に再度アップロードしてください
  • 証明書に紐づくアカウントのロールが証明書設定時のものではないものに変更された
    • 証明書を新規ロールで作り直すor元のロールを再度割り当ててください
  • 証明書に紐づくインテグレーションが削除または変更された
    • インテグレーションを再設定した上で、証明書を再発行してください

『JWT(jason web token)の発行で困ったこと』

  • JWTのGENERSTERサイト
  • 割とJWT界隈では有名らしいです・・・が、
  • ヘッダーとペイロードがライブラリーで保証されていませんでした。
  • NetSuiteの認証要求に耐えられないので断念しました。
  • トークン発行後にコピペしたらパラメータの複合はできたので私のやり方の問題かも・・・

『NetSuiteのinternal_idが連番ではない』

  • API連携と直接関係しない話ですがNetSuiteではレコードのナンバリングにinternal_idを使用しています
  • このIDが曲者で、ある段階から急に100→500に飛んだりするのでマスタを退避して設定している際にRDBのIDと乖離が起きました
    • 一括インポートでやれば大丈夫と思ったそこのあなた!新規レコードの追加時も毎回洗い替えしますか?
  • 結果として同期する際にRDBスキーマにinternal_idに相当するカラムを追加する必要が出てきました。

課題

『 更新トリガー』

  • 現状はNetSuiteの更新をトリガーに同期をしているわけではありません
    • ECSのタスクスケジューラで定期的にバッチ処理を実行
    • 任意のタイミングでWEBアプリケーション上から同期リクエス
    • 対象のマスタ情報を全て取得後にRDSのデータと比較して差分があったもののみを更新
  • 理想系
    • NetSuiteの更新トリガーで登録/更新レコードのみが同期される

次回はこちらの課題も考慮した「トークンベース認証によるRestlet API連携」をご紹介します。