CUEBiC TEC BLOG

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

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連携」をご紹介します。

NetSuiteとAPI連携してみた_その1(OAuth2.0トークン認証編)

どうもーキュービックでテックリードをやっている尾﨑です。 本日は会計や組織データなどのマスタ管理を行なっているNetSuiteのSuiteTalk REST Web ServicesからREST API連携した時のお話をしたいと思います。
Komawoの全体アーキテクチャ
クラッチで連携した事例は希少だと思うので、困っている方の課題解決に繋がると嬉しいです。 構成が長いので以下の3遍でご紹介したいと思います。
・OAuth2.0トークン認証によるREST API連携★
・OAuth2.0クライアント認証によるRest API連携
・トークンベース認証によるRestlet API連携
本日はOAuth2.0トークン認証によるREST API連携方法とハマったポイントなどをご紹介したいと思います。

アーキテクチャ構成

Komawoの前身であるCBA(CUEBiC Analytics)から先行的に ドメインとして持つべきではないマスタをNetSuiteに退避し、REST APIにて同期を行いました。

困った点

  • NetSuiteの公式のドキュメントが読みづらい
    • すごく重要な設定をサラッと書いている
    • 設定方法が散らばっているので関連性が紐解きづらい
  • ノウハウ系の投稿が少ない
    • SaaSサービスによる連携サイトがほとんどで連携方法自体はブラックボックスなパターンがほとんど
    • 有償サポートでは技術的な課題は回答ができなかった
    • テクニカルサポートはフィリピンなのでビデオ会議まで時間がかかった。

解決方法

  • YouTubeの英語の解説動画を探して、何度も停止・再生をしながら設定を学ぶ
  • 設定方法とNetSuite公式ドキュメントを比較して社内ドキュメントを体系的にまとめる
  • それでも解決しない場合はテクニカルサポートに連絡して、日本語対応スタッフとテレビ電話でペアプロする
  • 事前準備

  • OAuth 2.0 Authorization Code Grant Flowによる認証方法を使用して連携を行います
  • 公式ドキュメント:NetSuite Applications Suite
  • 以下の3STEPで設定を行います。

    Q.よくある疑問 OAuth2のトークンを取得するために前提情報として[クライアントID]が必要だが、どこで設定しているのか??

    A.NetsuiteのUI上で事前設定が必要です 設定>インテグレーション

    インテグレーション

    トークンベース認証

    インテグレーション

    APIの設定

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

    前提
    1.手順1Oauth2.0承認コードの取得
    2.手順2.refresh tokenの取得
    3.手順3.アクセストークンの取得
    4.手順4.疎通テスト

    前提

    Oauth2.0の承認codeを取得するには以下の事前設定が必要 NetSuite上で設定>SuitsCloudタブ

    認証を管理>OAUTH2.0 チェックボックスを有効化にし、機能を有効化にして保存

    上記ステップを踏まない場合は、リダイレクトURLにリダイレクト前に以下のようなエラーが表示される

    手順1.Oauth2.0承認コードの取得

    NetSuiteのインテグレーションで設定した以下の情報をもとにOauth2.0の承認コードを取得する 1.API TESTERなどで、以下のエンドポイントに対して、パラメータを設定しURLを生成する

    エンドポイント

    大項目 小項目
    エンドポイント https://{account_id}.app.netsuite.com/app/login/oauth2/authorize.nl?
    HTTPメソッド -


    パラメータ 概要 /設定値
    response_type code
    client_id インテグレーションで発行したクライアントID
    redirect_uri インテグレーションで設定したリダイレクトURL
    scope rest_webservices
    state 任意の24文字以上の文字列

    2.生成したURLをブラウザにコピペして実行する。

    3.NetSuiteのログイン画面が表示されるので、2要素認証まで突破する

    4.認証画面が表示されるので、許可を押下する

    5.設定したリダイレクト先のURLにリダイレクトされる

    リダイレクトされた際のURLのパラメータからcodeを取得

    手順2.refresh tokenの取得

    codeが取得できたので、次はrefresh tokenの取得を行う

    • 事前準備:HEADERSのAuthorizationに設定するBasicコードの取得

    以下で取得した項目をheader-generaterなどで生成して取得する

    項目 概要
    クライアントID インテグレーション登録時に初回発行時のみ参照可能
    アクセスキー インテグレーション登録時に初回発行時のみ参照可能


    大項目 小項目
    エンドポイント https://{account_id}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token?
    HTTPメソッド POST


    ヘッダー 設定値
    Content-Type application/json
    Authorization Basicコード


    パラメータ 概要 /設定値
    code 手順1で取得したcode
    redirect_uri インテグレーションで設定したリダイレクトURL
    grant_type authorization_code

    APIを実行して実行結果からrefresh tokenを取得する

    手順3.アクセストークンの取得

    手順2で取得したrefresh tokenを使用してアクセストークンを取得する

    大項目 小項目
    エンドポイント https://{account_id}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token?
    HTTPメソッド POST


    ヘッダー 設定値
    Content-Type application/x-www-form-urlencoded
    Authorization Basicコード


    パラメータ 概要 /設定値
    grant_type authorization_code
    refresh_token 手順2で取得したrefresh token

    APIを実行し、アクセストークンを取得する

    手順4.疎通テスト

    手順3で取得したアクセストークンをHEADERSに設定し、APIでクエリの疎通をこなう

    大項目 小項目
    エンドポイント https://{account_id}suitetalk.api.netsuite.com/services/rest/query/v1/suiteql?
    HTTPメソッド POST


    ヘッダー 設定値
    Authorization Bearer + 手順3で取得したアクセストーク
    Content-Type application/json


    パラメータ 概要 /設定値
    limit 表示したい件数

    リクエストbodyサンプル

    {
        "q": "SELECT * FROM customrecord_cb_anken"
    }
    

    ※1000件以上の場合は再度リクエストが必要になる

    実行が成功すると以下のようにNetsuiteのテーブル情報が取得される

    はまった点

    • アクセストークンが1週間ぐらいで切れる
    • アクセストークンが切れるとNetSuite上で規約同意を行う必要がある
    • ヘッドレスブラウザでも使わない限り自動化不可
    • カスタムフィールドに値で設定していないデータはAPIで項目として認識されない

    リフレッシュトークンが3600秒なのは分かりますが、アクセストークン2週間はかなり厳しいですね。 ECSのタスク定義の環境変数に認証情報を設定していたのであくまで素通用で運用レベルでは使えないと断定しました。 次回はこちらの弱点を克服したクライアント証明書方式による認証をご紹介したいと思います。

    Datadogに送るログをフィルタリングする

    背景

    こんにちは、キュービックでSREをやっているYuhta28です。キュービック内のテック技術について発信します。
    Datadogに移行してから数ヶ月が経ちました。メディアサーバーへのエージェント導入や外形監視設定など一通り完了し今の所運用できていますが、しばらくして課題が出てきました。

    ログストレージコスト増加

    Datadogのログ料金はログの保持期間、量によって変わる従量課金制1です。弊社は15日間の保持期間で契約していて、それより古いログはAmazon S3アーカイブされる運用にしています。

    アーキテクチャ

    アーキテクチャ

    以前の記事でも書いたとおりDatadogへのログ転送はOSSのFluentd2を採用しており、対象ログの中身すべてをDatadogに転送する設定にしています。
    結果としてDatadogが取り込むログの量が膨大に膨れ上がれ、ログ周りのコストが高くなってしまいました。なのでログコスト削減を目的にDatadogで収集するログの中身をフィルタリングすることにしました。

    Fluentdフィルタリング

    Datadogへのログの転送に使用しているFluentdには、不要なログの除外や必要なログのみの抽出が可能なfilterプラグイン3があります。LTSV形式のログならキーを指定することでそのキー内の特定の条件にマッチしたログの抽出もしくは除外設定が可能となります。
    例えば今回ようにELBからのヘルスチェックのログと画像ファイルやcssファイルなどの静的コンテンツファイルを除外したい場合は以下のように記述しました。

    <filter nginx>
      @type grep
      <or>
        <exclude>
          key path
          pattern /\.(jpg|jpeg|gif|png|css|js|swf|ico|pdf|svg|eot|ttf|woff)/
        </exclude>
        <exclude>
          key agent
          pattern ^.*ELB-HealthChecker\/2\.0$
        </exclude>
      </or>
    </filter>
    

    この設定を反映した結果がこちらです。

    メディアアクセスログ
    11:30を境にアクセス量が半分近くまで減らせることができました。これで当初のやりたかったことは達成できましたが一つ課題が出てきました。

    生ログの保存

    画像が表示されていないなど画像起因のサイト表示エラーが起きた場合に画像ファイルのログがないと調査が困難になりますので、生ログは別に保存しておきたいという要望がありました。そこでDatadogのアーカイブ保存用S3とは別にFluentdから直接生ログを転送するためのS3を用意して保存することにしました。

    生ログ保存用S3を追加したアーキテクチャ

    複数保存先のへの転送失敗

    最初複数の転送先へログを送るときはこう書きました。

    <match nginx>
      @type s3
      s3_bucket cuebic-mediaXXXXXXXXXXXXX
      s3_region ap-northeast-1
      path media/nginx/
      time_slice_format year=%Y/month=%m/day=%d/hour=%-H/
      s3_object_key_format %{path}%{time_slice}%{uuid_flush}.json.%{file_extension}
      <format>
        @type json
      </format>
    </match>
    
    <filter nginx>
      @type grep
      <or>
        <exclude>
          key path
          pattern /\.(jpg|jpeg|gif|png|css|js|swf|ico|pdf|svg|eot|ttf|woff)/
        </exclude>
        <exclude>
          key agent
          pattern ^.*ELB-HealthChecker\/2\.0$
        </exclude>
      </or>
    </filter>
    <match nginx>
      @type copy
      <store>
        @type datadog
        @id nginx-access
        api_key "#{ENV['ddapikey']}"
    
        include_tag_key true
        tag_key @log_name
    
        dd_source 'nginx'
        service   'access-log'
      </store>
    </match>
    

    Fluentdに詳しい人ならピンと来ると思いますが、matchに一致したイベントは次のmatchセクションを参照しません。つまりnginxイベントは最初のmatchセクションに拾われS3へログは送られますが、次のmatchセクションで定義しているDatadogへのログ転送が反映されなくなります。 解決策としてcopyプラグイン4を使って2つのmatch内に含まれているログ転送情報をまとめましたが、copyプラグイン内でfilterプラグインは使えないらしくFluentdの再起動時に警告が出ました。
    どうしようかと悩みましたが、詳しい人からrelabelプラグインを使えば解決するというアドバイスをいただいたことで解決まで持っていくことができました。

    relabelプラグインの使用

    docs.fluentd.org

    relabelプラグインは一致したイベントに再度ラベリングをつけてくれるプラグインです。まずnginxイベント内にS3へ送るラベルとDatadogへ送るラベルを付与します。

    <match nginx>
      @type copy
      <store>
        @type relabel
        @label @s3lognginx
      </store>
      <store>
        @type relabel
        @label @filternginx
      </store>
    </match>
    

    その後各ラベルがついたログイベントに対して生ログのままS3へ送る処理とフィルタリングした状態でDatadogへ送る処理を定義しました。

    <label @s3lognginx>
      <match nginx>
        @type s3
        s3_bucket cuebic-mediaXXXXXXXXXXXXX
        s3_region ap-northeast-1
        path media/nginx/year=%Y/month=%m/day=%d/hour=%H/
        s3_object_key_format %{path}%{time_slice}%{uuid_flush}.json.%{file_extension}
        <format>
          @type json
        </format>
        <buffer tag,time>
          timekey 1m
        </buffer>
      </match>
    </label>
    
    <label @filternginx>
      <filter nginx>
        @type grep
        <or>
          <exclude>
            key path
            pattern /\.(jpg|jpeg|gif|png|css|js|swf|ico|pdf|svg|eot|ttf|woff)/
          </exclude>
          <exclude>
            key agent
            pattern ^.*ELB-HealthChecker\/2\.0$
          </exclude>
        </or>
      </filter>
      <match nginx>
        @type datadog
        @id nginx-access
        api_key "#{ENV['ddapikey']}"
        include_tag_key true
        tag_key @log_name
        dd_source 'nginx'
        service   'access-log'
      </match>
    </label>
    

    これによりS3には完全な生ログが保存され、Datadogにはフィルタリングされた状態のログが転送されるようになりました。

    所感

    Datadogへ送るログのフィルタリング方法について紹介しました。単純にフィルタリングしたログをDatadogへ送るだけでしたがそんなに難しくありませんでしたが、複数の転送先にログを送るときにcopyプラグインが使えなくてどうすればいいのか解決に苦労しました。
    このことをTwitterでつぶやくと親切な人からrelabelを使った解決方法について教えていただきました。
    解決できましたので私もこの記事を通して同じように悩んでいる人への手助けになれればと思います。

    Tableau REST APIを使ってスプシ連携ツールを作ってみた

    どうもキュービックのテックリードをやっている尾﨑です。2月10日にデブサミに登壇してきました。

    アーキテクチャの中のサービスに関して1つ1つ詳細をお話しすることができなかったので、利用技術やハマった点などを交えてご紹介していきたいと思います。 今回はTableau REST APIを使ってスプレッドシート連携を自動化した事例をご紹介します。 ボリュームがあるので、手っ取り早くTableau APIの良し悪しと用法を把握したい方は目次の5以降を参照ください。

    1.なぜTableau REST APIでスプシ連携ツールを作ろうと思ったか

    キュービックではBIツールとしてTableauを使用しており、収集したデータを加工・集計してビジネスサイドがモニタリングできるようにアウトプットをTableauのワークブック上にビジュアライゼーションしています。TableauにはエクセルやCSVダウンロード機能はあるものの、標準でスプレッドシート連携機能がなかったので、こちらを最小工数で自動化できないか?と考えたのがきっかけでした。

    2.スプシ連携ツールの構成

    構成1

    構成2

    3.スプシ連携ツール作成時に工夫したところ

    • 1.ちゃんと使ってもらえるツールを考えたところ

      • 敢えてWEBアプリケーション化しないことで運用上の不要なアクション数を減らした
    • 2.運用者FB/トライアル期間を挟んでリリースしたこと

    • 3.GASでデータ取得と整形に関するロジックはライブラリー化したこと

      • 別途テンプレートファイルを作成し、メインロジックが汚染されないようにした

    4.スプシ連携ツールの改善点

    • 問題
      • スプシのテンプレートに更新があった際にバージョン追随ができない
      • 運用者に最新テンプレートに乗り換えてもらう必要がある
    • 課題(解決策)
      • 移行コストを如何に削減するか?を考慮する方向で検討 (WEBアプリ化はしない)

    ★5.Tableau REST APIの主要な機能

    作成したスプシ連携ツールでは、TableauのView情報を取得(参照)することを目的としているため、 Tableauに対してAPIで登録/更新/削除を実行するようなものは触れません。

    ★6.スプシ連携ツールの実装サンプル

    構成としては以下に分けています。

  • 1.REST APIで取得する部分
  • 2.スプシからパラメータ情報の読み込みと取得結果の出力
  • 今回は1を中心に解説します。
    以下のような方向けにできるだけ分かりやすく解説していきます

    • これからTableauのREST APIを「使ってみよう」と思っている方
    • TableauのREST APIで「疎通」しているけどうまくいかない
    • TableauのREST APIで「エラーの解消方法が分からない」
    • TableauのREST APIで「参考」になる情報を探している
    • TableauのREST APIで「簡単」なツールを作りたい

    アクセストークン取得

    まずは、個人用アクセストークンを発行し、アクセストークンとsite-idを取得します。

    リクエスト情報

    大項目 小項目
    エンドポイント https://10az.online.tableau.com/api/3.4/auth/signin
    HTTPメソッド POST


    ヘッダー 設定値
    Content-Type application/json
    Accept application/json


    パラメータ 概要 /設定値
    personalAccessTokenName 2要素認証対応のアクセストークンを生成時に設定した名前 個人用アクセストークンを参照
    personalAccessTokenSecret 2要素認証対応のアクセストークンを生成時に生成されたキー個人用アクセストークンを参照
    contentUrl tableauのURLのsite/の後の文字列https://10az.online.tableau.com/#/site/{contentUrl}/

    ソースコードサンプル

    1.リクエスト情報のパラメータの設定値に指定した以下の情報を設定してください。

    • personalAccessTokenName
    • personalAccessTokenSecret
    • contentUrl
    function getTableauAcccessToken() {
      // POSTの値
      var requestPayload = {
        "credentials": {
          "personalAccessTokenName": "your Access Key Name",//personalAccessTokenNameを設定
          "personalAccessTokenSecret": "your Access Key",//personalAccessTokenSecretを設定
          "site": {
            "contentUrl": "your content URL"//contentUrlを設定
          }
        }
      }
      // リクエストヘッダー
      var requestHeaders = {
          'Content-Type': 'application/json',
          'Accept' : 'application/json'
      }
      // リクエストオプション
      var requestOptions = {
          'method' : 'post',
          'payload' : JSON.stringify(requestPayload),
          'headers' : requestHeaders,
      }
      var requestUrl = 'https://10az.online.tableau.com/api/3.4/auth/signin';
      var response = UrlFetchApp.fetch(requestUrl, requestOptions);
      var responseCode = response.getResponseCode();
      var responseText = response.getContentText();
      var result = JSON.parse(responseText)['credentials']//アクセストークンなどの認証情報を取得
      return result;
    }
    

    2.APIのresponse結果としてcredentialsを取得します

    • リクエストが成功するとsiteのid(site-id)とtoken(アクセストークン)が取得できます
    {
        "credentials": {
            "site": {
                "id": "sample",
                "contentUrl": "sample"
            },
            "user": {
                "id": "sample"
            },
            "token": "sample",
            "estimatedTimeToExpiration": "270:11:00"
        }
    }
    

    ワークブック情報取得

    次に、Tableauのワークブックの情報を取得します。

    リクエスト情報

    大項目 小項目
    エンドポイント https://10az.online.tableau.com/api/3.17/sites/{site-id}/workbooks
    HTTPメソッド GET
    • ※site-idに関しては「アクセストークン取得」にて取得


    ヘッダー 設定値
    X-Tableau-Auth 「アクセストークン取得」で取得したアクセストーク
    Accept application/json


    パラメータ 概要 /設定値
    pageSize 取得データ件数(defaultは100件)

    1.アクセストークン取得で取得した以下の情報を使用します

    • siteのid(siteId)
    • token(アクセストークン)

    ソースコードサンプル

    function getWorkBookInfo() {
      tokenInfo = getTableauAcccessToken();
      var pageNumber = 1;
      // リクエストヘッダー
      var requestHeaders = {
          'X-Tableau-Auth' : tokenInfo['token'],//アクセストークンをリクエストヘッダーに指定
          'Accept' : 'application/json'
      }
    
      // リクエストオプション
      var requestOptions = {
          "method" : "get",
          "headers" : requestHeaders
      }
      var siteId = tokenInfo['site']['id'];//siteのidを設定
      var requestUrl = `https://10az.online.tableau.com/api/3.17/sites/${siteId}/workbooks?pageSize=1000`;
      var response = UrlFetchApp.fetch(requestUrl, requestOptions);
      var responseCode = response.getResponseCode();
      var responseText = response.getContentText();
      var result = JSON.parse(responseText)['workbooks'];//ワークブックの情報を取得
      return  result;
    }
    

    2.APIのresponse結果としてworkbooksを取得します

    • リクエストが成功すると以下のような情報が取得できます
    {
        "pagination": {
            "pageNumber": "1",
            "pageSize": "100",
            "totalAvailable": "304"
        },
        "workbooks": {
            "workbook": [
                {
                    "project": {
                        "id": "0c5b75e8-8895-4d17-a9fb-f3047a",
                        "name": "転職"
                    },
                    "location": {
                        "id": "0c5b75e8-8895-4d17-a9fb-f3047a",
                        "type": "Project",
                        "name": "転職"
                    },
                    "owner": {
                        "id": "3f5dfad5-1f00-41ed-9eb8-7",
                        "name": "sample@cuebic.co.jp"//オーナーのメールアドレス
                    },
                    "tags": {
                    },
                    "dataAccelerationConfig": {
                        "accelerationEnabled": false
                    },
                    "id": "e25e09f0-a2db-49dc-999e-eb6f6",
                    "name": "分析用",
                    "description": "",
                    "contentUrl": "_0",
                    "webpageUrl": "https://10az.online.tableau.com/#/site/contentUrl/workbooks/1521246",//ワークブックのURL
                    "showTabs": "true",
                    "size": "99",
                    "createdAt": "2019-05-04T04:04:37Z",
                    "updatedAt": "2021-07-13T05:21:00Z",
                    "encryptExtracts": "false",
                    "defaultViewId": "45776cde-f128-442f-92b"
                },
      ]
        }
    }
    

    3.もし必須情報を取得したい場合は以下をresultの後続処理に追加すると良い感じにデータが取得できます

      var kaigyo ="\n";
      var workBookInfoArray = [];
      workBookInfoArray[0] =[
          `ワークブック名${kaigyo}name`,
          `ワークブックID${kaigyo}id`,
          `ワークブックURL${kaigyo}webpageUrl`,
          `プロジェクトID${kaigyo}project:id`,
          `プロジェクト名${kaigyo}project:name`,
          `オーナーID${kaigyo}owner:id`,
          `オーナーメールアドレス${kaigyo}owner:name`
      ]
      var j= 0;
      var cnt = 1;
      for(i=0; i<result['workbook'].length; i++){
        if(!result['workbook'][j]['project']){
          j++
          continue;
        }
        workBookInfoArray[cnt]= [
          result['workbook'][j]['name'],
          result['workbook'][j]['id'],
          result['workbook'][j]['webpageUrl'],
          result['workbook'][j]['project']['id'],
          result['workbook'][j]['project']['name'],
          result['workbook'][j]['owner']['id'],
          result['workbook'][j]['owner']['name']
        ]
        //console.log( workBookInfoArray[i]);
        j++
       cnt ++
      }
    

    View一覧取得

    次に、TableauのワークブックのViewのリストを取得します。

    リクエスト情報

    大項目 小項目
    エンドポイント https://10az.online.tableau.com/api/3.17/sites/{site-id}/workbooks/{workbook-id}/views
    HTTPメソッド GET
    • ※site-idに関しては「アクセストークン取得」にて取得
    • ※workbook-idに関しては「ワークブック情報取得」にて取得


    ヘッダー 設定値
    X-Tableau-Auth 「アクセストークン取得」で取得したアクセストーク
    Accept application/json


    パラメータ 概要 /設定値
    pageSize 取得データ件数(defaultは100件)

    1.ワークブック情報取得で取得したworkbookのid(workBooksId)を使用してViewの一覧を取得します

    ソースコードサンプル

    function getViewID(name,workBooksId) {
      tokenInfo = getTableauAcccessToken();
      // リクエストヘッダー
      var requestHeaders = {
          'X-Tableau-Auth' : tokenInfo['token'],
          'Accept' : 'application/json',
          'pageSize' : 1000
      }
    
      // リクエストオプション
      var requestOptions = {
          "method" : "get",
          "headers" : requestHeaders
      }
      var siteId = tokenInfo['site']['id']
      var requestUrl = `https://10az.online.tableau.com/api/3.17/sites/${siteId}/workbooks/${workBooksId}/views`;//siteIdとワークブック情報取得で取得したworkBooksIdを設定してください
      var response = UrlFetchApp.fetch(requestUrl, requestOptions)
      var responseText = response.getContentText()
      var result = JSON.parse(responseText)['views'];
      return result;
    }
    

    2.APIのresponse結果としてviewsを取得します

    • リクエストが成功すると以下のような情報が取得できます
    • これで取得したいワークブックのIDとView(シート)のIDが取得できました。
    {
        "views": {
            "view": [
                {
                    "tags": {
                    },
                    "id": "bb9aeee4-b144-49e3-9e66-2749c9",//ViewのID
                    "name": "広告アカウント_抽出",//View(ワークブックのシート名)
                    "contentUrl": "AD/sheets/PBID_",
                    "createdAt": "2023-02-15T13:09:57Z",
                    "updatedAt": "2023-02-21T02:41:15Z",
                    "viewUrlName": "PBID_"
                },
      ]
        }
    }
    

    View情報取得

    これでView情報を取得するのに必要な情報が集まりました。 今度は、TableauのワークブックのViewの詳細情報を取得します。

    リクエスト情報

    大項目 小項目
    エンドポイント https://10az.online.tableau.com/api/3.17/sites/{site-id}/views/{view-id}/data
    HTTPメソッド GET
    • ※site-idに関しては「アクセストークン取得」にて取得
    • ※workbook-idに関しては「ワークブック情報取得」にて取得
    • ※view-idに関してはViewsリスト取にて取得


    ヘッダー 設定値
    X-Tableau-Auth 「アクセストークン取得」で取得したアクセストーク
    Accept application/json


    パラメータ 概要 /設定値
    vf_ {fieldname} vf_{filter_name}の表記でパラムを設定してTableauのView上のフィルターと同等の絞り込みを実現できる

    1.View一覧取得で取得したviewIdを指定してViewの詳細情報を取得します

    ソースコードサンプル

    function getViewinfo() {
      var tokenInfo = getTableauAcccessToken();
      
     // リクエストヘッダー
      var requestHeaders = {
          'X-Tableau-Auth' : tokenInfo['token'],
          'Accept' : 'application/json'
      }
    
      // リクエストオプション
      var requestOptions = {
          "method" : "get",
          "headers" : requestHeaders,
          "muteHttpExceptions" : true
      }
      var siteId = tokenInfo['site']['id']
      //filterを適用したい場合は末尾に{vf_フィルター名=フィルタ内容}を設定
      var requestUrl = `https://10az.online.tableau.com/api/3.17/sites/${siteId}/views/${viewId}/data`;//siteIdとView一覧取得で取得したviewIdを設定する
      var response = UrlFetchApp.fetch(requestUrl, requestOptions)
      var responseText = response.getContentText();
      result = Utilities.parseCsv(responseText);
      return result;
    }
    

    2.APIのresponse結果としてviewの詳細情報を取得します - リクエストが成功すると以下のようなCSVデータが取得できます

    ad_media,Measure Names,Month of day,Year of day,粗利,Measure Values
    FB2,imp,August,FY 2020,"115,343","94,140"
    FB2,click,August,FY 2020,"115,343","1,480"
    FB2,ctr,August,FY 2020,"115,343",0.015721266
    FB2,cpc,August,FY 2020,"115,343",85.376351351
    FB2,cost,August,FY 2020,"115,343","126,357"
    FB2,avg.position,August,FY 2020,"115,343",
    FB2,preCV,August,FY 2020,"115,343",219

    ★7.Tableau REST APIの良かったところ

    • 1. ワークブックの一覧が取れる

      • ワークブックのURLやワークブックのオーナーのメールアドレスが取得できる
      • 運用調整時に誰に相談すれば良いのか?が分からないシーンで助かりました
      • 結果的に調査工数削減にもつながりました
    • 2.Viewの一覧が取れる

      • ワークブックと同じくあのViewの所在確認で困らなくなりました
      • Viewの詳細が取得できないものはこちらの時点で一覧に存在しないパターンが多いです(編集中/未publishなど)
    • 3.Viewの詳細が取れる

      • 本命です。運用者が普段使用してるスプレッドシートにシームレスで情報連携が可能になりました
    • 4.Viewのフィルターを指定できる

      • ワークブックを再保存しなくてもフィルター適用状態で取得が可能でした
      • 特定期間のデータや特定組織/メディアの情報を取得したい際に重宝しました
      • APIでデフォルトで取得されるのはワークブック保存時のフィルタ情報のようです
    • 5.Tableauのアカウント保持者以外もデータが擬似的に参照できるようになった

      • ワークブックの設計者以外も参照可能になりました
      • 結果として運用スキルとコストの削減につながりまし

    ★8.Tableau REST APIの改善して欲しいところ

    • Viewで定義されているフィルター情報の一覧をView毎に取得することができない 
      • 現在はスプレッドシート上に最大公約数的にフィルターに相当するパラメータを用意しておき、運用者に埋めてもらっている状態
      • Tableauで運用者がviewのフォーマットを意図せずに更新するとエラーが起きてしまう可能性があります
    • API keyが最大で1年しか持たないところ
      • 連続したアクセスが2週間以上ない場合は非有効化されるのでGASでトリガーを設定しています
      • アラートを設定したとしてもツール作成者居なくなったら負債化する可能性があります
    • CSVでエクスポートしたものとAPIで取得したデータの出力フォーマットが異なる
      • CSVでダウンロードしたものはtableau側でよしなにピボットしてくれておりViewに近い形で出力されます
      • APIで取得したデータは列が行として認識されるものがあり、個別に整形が必要になります
    • エラー内容が不親切
      • 「フォーマットが未対応」のような抽象的なエラーのみがエラーメッセージとして返ってきます
      • ワークブック上で誰かが編集モードで掴んでいるViewや正式にpublishされていないViewは取得できないことがありました

    EC2上で動かしていたDatadog監視をECSクラスター上に構築してみました

    概要

    こんにちは、キュービックでSREをやっているYuhta28です。キュービック内のテック技術について発信します。
    前回の記事で弊社のモニタリング運用をDatadogに集約したことを紹介しました。OpenSearch時代に活用していた集約サーバーのFluentdの設定ファイルをDatadogに向き先を変えて、さらにDatadogエージェントをインストールして、集約サーバーから弊社メディアへの外形監視とDBのパフォーマンスモニタリングを実施しています。

    アーキテクチャ

    アーキテクチャ

    課題

    ただ単なる集約サーバーだったものにDatadogエージェントを導入して、外形監視も担うとなるとサーバー運用が必要となり、負荷が生じてきました。
    そこでEC2ではなく、ECSに移行してDatadogモニタリングクラスターを構築してみることにしました。

    アーキテクチャ

    アーキテクチャ

    FargateのアーキテクチャはFluentdのECSサービスとDatadogのECSサービスを組み合わせたクラスタ構成になります。

    Fluentdコンテナ構築

    図では省略していますが、FluentdのECSに転送する際にメディアサーバー群とFargateの間にNLBを置いています。これによってIPアドレス単位で集約先を指定したものをNLBのドメイン単位で宛先を指定することが可能になります。

    # fluentd設定ファイル(メディアサーバー側)
    <source>
      @type tail
      path /var/log/nginx/access.log
      format ltsv
      time_format %d/%b/%Y:%H:%M:%S %z
      types size:integer,status:integer,upstream_response_time:float,time:time,response_time:float
      tag nginx
    </source>
    <match nginx>
      @type forward
      <server>
    #    host XXX.XXX.XXX.XXX  #IPアドレス
        host XXXXXXXXXXXXXXX.elb.ap-northeast-1.amazonaws.com
        port XXXXXXX
      </server>
    </match>
    
    # fluentd設定ファイル(fluentd ECS側)
    <source>
      @type forward
      port 24224
      bind 0.0.0.0
    </source>
    
    <match nginx>
      @type copy
      <store>
        @type datadog
        @id nginx-access
        api_key "#{ENV['ddapikey']}"  #fluentd設定ファイル内の環境変数設定
    
        include_tag_key true
        tag_key @log_name
    
        dd_source 'nginx'
        service   'access-log'
      </store>
    </match>
    

    幸いなことにFluentdのDockerイメージは公式がDocker Hubに用意していました。後は先のfluentd設定ファイルをコンテナにコピーするDockerfileを書いてECSにデプロイします。

    # Fluentdコンテナ
    FROM fluent/fluentd:v1.15.3-debian-1.0
    USER root
    
    RUN buildDeps="sudo make gcc g++ libc-dev" \
        && apt-get update && apt-get upgrade -y \
        && apt-get install -y --no-install-recommends $buildDeps \
        && sudo gem install fluent-plugin-datadog \
        && sudo gem sources --clear-all \
        && SUDO_FORCE_REMOVE=yes \
           apt-get  purge -y --auto-remove \
                   -o APT::AutoRemove::RecommendsImportant=false \
                   $buildDeps \
        && rm -rf /var/lib/apt/lists/* \
        && rm -rf /tmp/* /var/tmp/* /usr/lib/ruby/gems/*/cache/*.gem
    
    COPY  fluentd/etc/fluent.conf /fluentd/etc/
    
    USER fluent
    

    ちなみにFluentdの設定ファイルにDatadogのAPIキーを埋め込む場合、ECSタスク定義から環境変数を設定しますが、認証情報系は直打ちせずにSecrets Manager*1やSystems Managerのパラメータストア*2から呼び出すようにしておいたほうが安全です。

    "secrets": [
        {
            "name": "ddapikey",
            "valueFrom": "arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:parameter/datadog/apikey"
        }
    ]
    

    Datadogエージェントコンテナ構築

    Datadogエージェントも公式がコンテナイメージを配布しているので、必要な環境変数をセットしてECSサービスに組み込みました。

    # Datadogエージェントコンテナ
    FROM public.ecr.aws/datadog/agent:latest
    
    USER dd-agent
    RUN mkdir /etc/datadog-agent/secrets
    
    # HTTPチェック設定ファイル配置
    COPY etc/datadog-agent/conf.d/http_check.d/conf.yaml /etc/datadog-agent/conf.d/http_check.d/conf.yaml
    
    # DB Application Performance Monitoring設定ファイル&DBパスワード情報配置(db_password情報は暗号化)
    COPY etc/datadog-agent/conf.d/mysql.d/conf.yaml /etc/datadog-agent/conf.d/mysql.d/conf.yaml
    COPY etc/datadog-agent/secrets/db_password /etc/datadog-agent/secrets/db_password
    

    http_check.d/conf.yamlの中に監視したいメディアの情報を記載しています。

    # コーポレートサイト
      - name: cuebic website
        url: https://cuebic.co.jp/
        timeout: 5
        tags:
            - "env:prd"
    

    実は当初DatadogエージェントのコンテナはApp Runner*3を使って実装してみようと思いました。App RunnerならECRにコンテナイメージをセットして指定するだけで自動デプロイの実装までマネージドにやってくれるので、新規メディアへの外形監視作業も先述のHTTPチェックの設定ファイルを更新し、ECRにプッシュするだけで自動的に監視されて運用が楽になるからだと考えました。
    ところがApp RunnerではDatadogエージェントのようなコンテナからのプッシュ型通信とは相性が悪くうまくいかないことが判明しました。このあたりの詳細はこちらのブログを御覧ください。 zenn.dev

    妥協案としては自前でGitHub Actions☓ECSの自動デプロイ基盤を実装することで解決しました。ECSへデプロイしてくれるGitHub Actionsワークフローファイルは公式が用意してくれているのでそちらを活用します。*4

    GitHub Actions☓ECS自動デプロイアーキテクチャ

    デプロイアーキテクチャ

    これでHTTPチェック設定ファイルに新規メディア情報を記載してGitHubリポジトリにプッシュすることでGitHub Actionsが起動し、リポジトリ内のDockerfileをビルド、プッシュしてECRに格納されます。その後ECRのイメージを基にFargateへデプロイすることでECSによるDatadogモニタリングクラスタ基盤が構築できます。

    所感

    EC2上で動かしていたDatadog監視をECSへ移行できました。ガッツリとECSクラスタをイチから構築するのは今回が初めてで、セキュリティ設定ミスやネットワーク設定の不備など不慣れから生じるミスが多かったですが、最終的に実現したいことができてよかったと思います。App Runnerを使った挑戦は残念ながらできませんでしたが、違う機会で再挑戦してみようと思います。