CUEBiC TEC BLOG

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

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を使った挑戦は残念ながらできませんでしたが、違う機会で再挑戦してみようと思います。

    デブサミ2023の登壇資料の構成をデータ分析で作成してみた_その2

    キュービックでテックリードをやっている尾﨑です。Developers Summit 2023にCTO加藤と以下のテーマで登壇します。

    2023年2月10日(金)11:50 : データウェアハウス構築時のアンチパターンを克服したサクセスストーリー

    • 登壇にあたって、単にやったことを発表したのでは面白くない
    • 何か今まで学んだデータサイエンスを活かしつつ登壇資料を作れないだろうか?

    ということで前回は、登壇資料の構成をデータ分析でやったら面白いのでは?という仮説の基、 プロセス1〜3を紹介しました。

    今回は続きの4から見ていきます。

    1.企画のスコープを決める
    2.検討/導入/運用/改善の4工程のイベントを洗い出す
    3.工程ごとのデータの散らばりを観測する
    4.データを可視化する★
    5.企画で取り上げるべきイベントを可視化する
    4.データを可視化する

    前回は「3.工程ごとのデータの散らばりを観測する」でデータの散らばりを散布図で観測するところまで実施しました。

    このままでは分かりにくいので、もう少し見やすくします。

    導入工程にフォーカスすると、 「要件、人、インフラ、プロセス」に散らばりが見られました。 これはサービス選定時に重視していたポイントと一致します💡

    運用工程にフォーカスすると、 「コード、設計、欠陥、ビルド」に散らばりが見られました。 これは既存の運用で認識していた負債や課題と言えるでしょう💡

    改善工程にフォーカスすると、 「アーキテクチャー、サービス」に散らばりが見られました。 これは導入時に重視したポイントと、既存の運用課題を鑑みて改善を行った点が考えられます💡

    5.企画で取り上げるべきイベントを可視化する

    今度は企画の4象限の③④に該当するデータを4.とマージして 工程別に取り上げるべきデータを可視化します。

    結果としてはこのようになりました。

    これで登壇で取り上げる各工程のテーマが判明しました。 いかがでしたか?今回は登壇資料の構成を分析でやってみました。 当日はこちらで分析した内容をもとに作成した内容を発表します。

    最後にアーキテクチャーをちょっとだけお見せします

    面白そう!と思ったらエントリーしてもらえると嬉しいです。 対象のエンジニアはデータエンジニア、データサイエンティスト、バックエンドエンジニア、データベースアドミニストレータ、Webアプリケーションエンジニアとなります。是非エントリーお願いします。

    2023年2月10日(金)11:50 : データウェアハウス構築時のアンチパターンを克服したサクセスストーリー


    speakerdeck.com


    尾﨑 勇太(おざき ゆうた)

    プログラミング未経験から金融系のSierを3年経て、教育関係のWEBベンチャーを1年、品質管理を3年、キュービック(現在)と業界歴9年目。 結婚・子育てで開発を離れていたが、自社開発をしたいという思いが募り、キュービックに参画。 残される側の立場を複数社で経験し、技術負債の返済と再構築を実施してきたためマイナスからゼロ。ゼロからイチが得意。 現在はDWHの基盤構築をしながら新規プロダクトのデータ分析などを中心に実施。

    デブサミ2023の登壇資料の構成をデータ分析で作成してみた_その1

    キュービックでテックリードをやっている尾﨑です。Developers Summit 2023にCTO加藤と以下のテーマで登壇します。

    2023年2月10日(金)11:50 : データウェアハウス構築時のアンチパターンを克服したサクセスストーリー

    • 登壇にあたって、単にやったことを発表したのでは面白くない
    • 何か今まで学んだデータサイエンスを活かしつつ登壇資料を作れないだろうか?

    ということで今回は、登壇資料の構成をデータ分析でやったら面白いのでは?という仮説の基、 構成を考えたプロセスをご紹介します。

    以下の流れで実施しました。

    1.企画のスコープを決める
    2.検討/導入/運用/改善の4工程のイベントを洗い出す
    3.工程ごとのデータの散らばりを観測する
    4.データを可視化する
    5.企画で取り上げるべきイベントを可視化する
    1.企画のスコープを決める

    以前、採用施策を企画した際に活用させていただいたマケフリさんの4象限を使用。

    makefri.jp

    ここで、私なりに仮説を立てました。

    結果、スコープはいかに限定して進めました。

    ③正解がないが、自分が体験したこと
    ④正解があり、自分が実践していること(あれば)
    2.検討/導入/運用/改善の4工程のイベントを洗い出す

    イメージのように以下の項目をそれぞれ洗い出してマッピングしました。

    フロー
    問題となった種別
    問題の原因となった種別
    企画の4象限
    工程の4象限

    ちなみに問題となった種別と問題の原因となった種別に関しては、技術負債の洗い出しマニュアルを 部署内で作成して運用を開始したので、技術負債の13の種類を応用しました。

    3.工程ごとのデータの散らばりを観測する

    データが洗い出せたので、工程ごとにデータの散らばり具合をみていきます。

    工程は以下の不確実性とコストを4象限で表すことにします。

    ここに、2で洗い出した結果を重み付けして散布図で表すと以下のようになりました。

    ※母集団とのずれを小さくするためにコストと不確実性の重みの平均を軸で表現しています。

    導入/運用フェーズにデータのばらつきが見られますね。 既存システムの運用課題に対しての導入検討と導入フェーズでの課題が見えてきました。 さぁここからどのように分析していくのか。 続きは、その2でお伝えしたいと思います。

    面白そう!と思ったらエントリーしてもらえると嬉しいです。 対象のエンジニアはデータエンジニア、データサイエンティスト、バックエンドエンジニア、データベースアドミニストレータ、Webアプリケーションエンジニアとなります。是非エントリーお願いします。

    2023年2月10日(金)11:50 : データウェアハウス構築時のアンチパターンを克服したサクセスストーリー


    speakerdeck.com


    尾﨑 勇太(おざき ゆうた)

    プログラミング未経験から金融系のSierを3年経て、教育関係のWEBベンチャーを1年、品質管理を3年、キュービック(現在)と業界歴9年目。 結婚・子育てで開発を離れていたが、自社開発をしたいという思いが募り、キュービックに参画。 残される側の立場を複数社で経験し、技術負債の返済と再構築を実施してきたためマイナスからゼロ。ゼロからイチが得意。 現在はDWHの基盤構築をしながら新規プロダクトのデータ分析などを中心に実施。

    OpenSearchの古いインデックスを定期的に削除できるようにした

    概要

    前回メディアのログをOpenSearchに集約させたことをお話しました。

    cuebic.hatenablog.com

    このとき課題としてストレージに上限があり、溜まり続けると新しいログが転送できないという問題がありました。
    OpenSearchAPIを叩くことで削除することが可能ですが、手動でAPIリクエストを送るのは面倒です。

    curl  -XDELETE https://endpoint.com/<index>

    今回EventBridge + Lambdaで自動で古いインデックスを削除できるようにしましたので、紹介いたします。
    (シェルスクリプトのままLambdaに実装もできなくはないですが、他のエンジニアにも読めるように見よう見まねでPythonで書いてみました)

    アーキテクチャ構成図

    アーキテクチャ

    かなりシンプルなアーキテクチャ構成図ですが使っているものは実際にこれだけです。
    EventBridgeが毎日深夜1時にトリガーをかけてLambdaを実行させます。

    Lambdaソースコード

    import requests
    import json
    import os
    import boto3
    from datetime import date,timedelta
    import datetime
    import calendar
    from requests.exceptions import Timeout
    
    def lambda_handler(event, context): 
        
        #パラメーターストアからOpenSearch 認証情報を取得
        ssm = boto3.client('ssm','ap-northeast-1')
        OpenSearch_user     = ssm.get_parameter(
            Name='/opensearch/cuebic-media-log/user',
            WithDecryption=True
        )["Parameter"]["Value"]
        
        OpenSearch_password = ssm.get_parameter(
            Name='/opensearch/cuebic-media-log/pass',
            WithDecryption=True
        )["Parameter"]["Value"]
     
        # 過去日付情報取得
        ut = datetime.datetime.now()
        ago_30 = ut-timedelta(days=30)
        ago_90 = ut-timedelta(days=90)
        
        # 対象URLリスト
        index_list = [ ~~~~~~ ]
    
        # タイムアウト値設定(コネクションタイムアウト3.0秒、リードタイムアウト7.5秒)
        connect_timeout = 3.0
        read_timeout    = 7.5
        try:
            for index in index_list:
                #30日過ぎたログをクローズさせる
                response = requests.post('https://endpoint.com/'+index+ago_30.strftime('%Y%m%d')+'/_close', auth=(OpenSearch_user, OpenSearch_password), timeout=(connect_timeout, read_timeout))
                #90日すぎたログを削除する
                response = requests.delete('https://endpoint.com/'+index+ago_90.strftime('%Y%m%d'), auth=(OpenSearch_user, OpenSearch_password), timeout=(connect_timeout, read_timeout))
    
            
        except Timeout:
            print('Timeout')
            return
    
        except:
            print('Unkown exception')
            return
        
        return {
            "statusCode": 200,
            "headers": {
                "Content-Type": "application/json"
            },
            "body": json.dumps({
                "message": response.text
            })
        }
    

    ソースコード解説

    LambdaがOpenSearchへアクセスする際に使用する認証情報はSystems Managerのパラメーターストアを利用しています。

    Systems Manager パラメーターストア
    Secrets Managerと違ってパラメーターのローテーション機能はありませんが、無料で使えるので今回はこちらを採用しました。

        ssm = boto3.client('ssm','ap-northeast-1')
        OpenSearch_user     = ssm.get_parameter(
            Name='/opensearch/cuebic-media-log/user',
            WithDecryption=True
        )["Parameter"]["Value"]
    

    各種ログのインデックス名には日付を付けていますので、インデックス名の日付が30日以上前のものはクローズにし画面上から検索できないようにする。90日以上前のものはOpenSearchから完全に削除するようにしています。

    基本的にインデックス名はログ名+yyyymmddでしたので、ログ名部分を配列変数にしてfor構文でAPIを実行するようにしています。

    課題

    今の所うまくいっているので問題にはなっていませんが、メッセージ出力が最後に実行された responseに対する結果のみを出力しますのでもし実行に失敗した場合のログ調査ができない問題があります。

        return {
            "statusCode": 200,
            "headers": {
                "Content-Type": "application/json"
            },
            "body": json.dumps({
                "message": response.text #リストの最後に格納されているインデックス削除の結果しか出力されない
            })
        }
    

    ログを配列に格納すれば解決できそうな気がしますので、Pythonに詳しい人に相談してみようと思います。

    所感

    OpenSearchに溜まり続けるインデックスの削除をEventBridge + Lambdaで自動化してみました。OpenSearchの料金は事前に確保したEBSのストレージサイズに応じてコストが上がるので、事前確保分を抑えて定期的に削除する運用にすれば良い改善になるのではないかなと思います。

    おまけ

    curlAPIを実行する方法をどうやってPythonで実行すればいいのか最初わからなかったですが、こちらのコンバーターサイトを使えばcurlコマンドの内容をGoやPythonに変換してくれるのでプログラミングの苦手な私には重宝しました。みなさんも使ってみてください。

    curlconverter.com