CUEBiC TEC BLOG

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

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

    Terraformのdynamicブロックを使って動的にリソースパラメーターを分けてみた

    概要

    こんにちは、キュービックでSREをやっているYuhta28です。キュービック内のテック技術について発信します。
    以前弊社でのTerraformの取り組みについて紹介しました。

    cuebic.hatenablog.com

    この中で環境によってリソース数の差分が生じる、例えば本番環境なら冗長化のためNAT Gatewayを3台稼働させるが、検証環境ならコスト最適化のため1台のみ稼働させると言ったケースではfor_eachを使って動的にリソース数を管理する方法について紹介しました。
    ではAWS CodePipelineで本番環境のみにApprovalステージを追加したいというような要望のときはどうすればよろしいでしょうか🤔

    本番環境ではデプロイ直前に承認作業がある

    CodePipelineのステージはパラメーターの一つで複数行に渡り記述されるのでfor_eachで分岐するとなるとうまくいきません。

    # CodePipeline作成Terraform(一部パラメーター省略)
    resource "aws_codepipeline" "example-pipeline" {
      artifact_store {
        location = var.s3-location
        type     = "S3"
      }
      name     = "pipeline-name"
    
      # ソースステージ
      stage {
        name = "Source"
        action {
          category = "Source"
    
          configuration = {
            Branch               = "main"
            Owner                = "master"
            PollForSourceChanges = "false"
            Repo                 = "pipeline-repo"
          }
    
          name             = "Source"
          namespace        = "SourceVariables"
          output_artifacts = ["SourceArtifact"]
          owner            = "ThirdParty"
          provider         = "GitHub"
          region           = "ap-northeast-1"
          run_order        = "1"
          version          = "1"
        }
      }
    
      # ビルドステージ
      stage {
        name = "Build"
    
        action {
          category = "Build"
    
          configuration = {
            ProjectName = "build-project"
          }
    
          input_artifacts  = ["SourceArtifact"]
          name             = "Build"
          output_artifacts = ["BuildArtifact"]
          owner            = "AWS"
          provider         = "CodeBuild"
          region           = "ap-northeast-1"
          run_order        = "1"
          version          = "1"
        }
      }
    
      # 承認ステージ(本番環境のみ作成)
      stage {
          name = "Confirm"
          action {
            category  = "Approval"
            name      = "RequestApprove"
            owner     = "AWS"
            provider  = "Manual"
            region    = "ap-northeast-1"
            run_order = "1"
            version   = "1"
          }
      }
    
      # デプロイステージ
      stage {
        name = "Deploy"
    
        action {
          category = "Deploy"
    
    
          input_artifacts = ["BuildArtifact"]
          name            = "Deploy"
          owner           = "AWS"
          provider        = "ECS"
          region          = "ap-northeast-1"
          run_order       = "1"
          version         = "1"
        }
      }
    }
    

    承認ステージ部分が本番環境だけに必要なステージです。ここだけを環境ごとに分割したいという状況は一般的にあると思います。
    この問題を解決してくれるのがdynamicブロックとよばれるものです。今回このdynamicブロックを用いて環境毎に異なるリソースパラメータの動的作成について紹介します。

    dynamicブロックについて

    www.terraform.io

    dynamicはブロック単位のループ処理を実装したいときに使います。
    例えばセキュリティグループの作成でdynamicブロックを使ったとします。

    # セキュリティグループ作成
    locals {
      ingress_web = [
      # [description, from_port, to_port, protocol, cidr_blocks]
        ["HTTPS from VPC", 443, 443, "tcp", "10.0.0.0/8"],
        ["HTTP from VPC", 80, 80, "tcp", "192.168.0.0/16"],
      ]
    }
    
    resource "aws_security_group" "web" {
      name        = "allow-web"
      vpc_id      = aws_vpc.main.id
    
      dynamic "ingress" {
        for_each = local.ingress_web
        content {
          description = ingress.value[0]
          from_port   = ingress.value[1]
          to_port     = ingress.value[2]
          protocol    = ingress.value[3]
          cidr_blocks = ingress.value[4]
        }
      }
    
      egress = [
        {
          from_port        = 0
          to_port          = 0
          protocol         = "-1"
          cidr_blocks      = ["0.0.0.0/0"]
        }
      ]
    
      tags = {
        Name = "allow-web"
      }
    }
    

    ここではingressルールでHTTP通信とHTTPS通信の許可設定を付与する箇所でdynamicブロックを使っています。ingressのパラメータはポートが異なっても設定項目は同じなので設定値をingress_webという変数でまとめればingressの記述回数が一回で済みます。
    これを応用してdynamicでループするかどうかをTrue or Falseで分岐するようにすれば本番環境のみに承認ステージを追加することができます。

    dynamicブロックによる分岐設定

    先程のTerraformファイルを以下のように修正します。

    # CodePipeline作成Terraform(一部パラメーター省略)
    resource "aws_codepipeline" "example-pipeline" {
      artifact_store {
        location = var.s3-location
        type     = "S3"
      }
      name     = "pipeline-name"
    
      # ソースステージ
      stage {
        name = "Source"
        action {
          category = "Source"
    
          configuration = {
            Branch               = "main"
            Owner                = "master"
            PollForSourceChanges = "false"
            Repo                 = "pipeline-repo"
          }
    
          name             = "Source"
          namespace        = "SourceVariables"
          output_artifacts = ["SourceArtifact"]
          owner            = "ThirdParty"
          provider         = "GitHub"
          region           = "ap-northeast-1"
          run_order        = "1"
          version          = "1"
        }
      }
    
      # ビルドステージ
      stage {
        name = "Build"
    
        action {
          category = "Build"
    
          configuration = {
            ProjectName = "build-project"
          }
    
          input_artifacts  = ["SourceArtifact"]
          name             = "Build"
          output_artifacts = ["BuildArtifact"]
          owner            = "AWS"
          provider         = "CodeBuild"
          region           = "ap-northeast-1"
          run_order        = "1"
          version          = "1"
        }
      }
    
      # dyanmicを追加、論理型変数でTrue時のみリソースを作成する
      dynamic "stage" {
        for_each = var.production_approval ? [1] : []
        content {
          name = "Confirm"
          action {
            category  = "Approval"
            name      = "RequestApprove"
            owner     = "AWS"
            provider  = "Manual"
            region    = "ap-northeast-1"
            run_order = "1"
            version   = "1"
          }
        }
      }
    
      # デプロイステージ
      stage {
        name = "Deploy"
    
        action {
          category = "Deploy"
    
    
          input_artifacts = ["BuildArtifact"]
          name            = "Deploy"
          owner           = "AWS"
          provider        = "ECS"
          region          = "ap-northeast-1"
          run_order       = "1"
          version         = "1"
        }
      }
    }
    

    dynamicブロックを追加し、production_approvalという変数がTrueの時リソースを一回作成するように設定します。
    production_approvalはTrue/Falseの論理型変数なので、variables.tfには以下のように記載します。

    # variables.tf
    variable "production_approval" {
      type        = bool
      default     = false
      description = "本番環境のみにApprovalステージを設定するためのif分岐"
    }
    

    本番環境のterraform apply実行先のCodePipelineモジュールファイルにtrueを加えます。

    module "cba-codepipeline" {
      source              = "../../modules/codepipeline
    
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
      # trueとすることでfor_eachで承認ステージを作成する
      production_approval = true
    }
    

    これによって本番環境では承認ステージ付きのCodePipelineが作成され、検証環境では承認ステージなしのCodePipelineを作成できます。
    (注:production_approvalのデフォルト値をfalseにしているので検証環境に変数をセット不要です)

    所感

    dynamicブロックを使ってCodePipelineの承認ステージの作成可否を環境ごとに分岐できるようにしました。
    Moduleで汎用化する際、パラメータ単位でリソースを作り分けたいというケースもあると思います。そうしたケースではdynamicブロックを使って環境別の条件別分岐を実装すると良いと思います。
    みなさんもぜひ試してみてください。

    参考文献

    future-architect.github.io

    stackoverflow.com

    kleinblog.net

    関連記事

    Terraformに関連する記事はこちらもご覧ください。

    cuebic.hatenablog.com

    herp.careers

    スイッチURLの管理からサヨウナラ、AWS Extend Switch Rolesを使ってみた

    概要

    こんにちは、キュービックでSREをやっているYuhta28です。キュービック内のテック技術について発信します。
    以前弊社で運用しているAWSマルチアカウントについて記事にしました。

    cuebic.hatenablog.com

    この時、複数のアカウントへのスイッチURLを管理しようとすると結構管理が面倒になるときがあります。ロール履歴では最大5つまでのスイッチ履歴が保存されるので5個までのAWSアカウントでしたら問題はありませんが、それ以上増えるとなるとスイッチ先のAWSアカウントとIAMロールARNを指定する必要があります。

    スイッチURLをドキュメントに一覧化してスイッチしたいアカウントのURLを参照するという方法もありますが、毎回ドキュメントページまで行くのが個人的にはおっくうだと思っています。何か方法はないかなと思い調べてみましたが、AWS Extend Switch RolesというGoogle Chromeアプリを使うことでスイッチURLを簡単にできることがわかりましたので、AWS Extend Switch Rolesを使ったスイッチURL管理について紹介いたします。

    AWS Extend Switch Roles

    chrome.google.com

    上記のChrome ウェブストアからAWS Extend Switch Rolesをインストールします。
    インストール後、アドオンに追加されていますのでConfiguration画面へ移行します。

    Configuration画面では、~/.aws/configファイルで記述するするprofileブロックと同様にスイッチ先のアカウントIDやスイッチIAMロール名、移行先のデフォルトリージョンなどを指定します。

    全部モザイクですとわかりにくいのでサンプルコードを置いておきます。

    [SwitchA]
    aws_account_id = 012345678910
    role_name = SwitchARole
    region = ap-northeast-1
    color = 6f1603
    
    [SwitchB]
    aws_account_id = 123456789100
    role_name = SwitchBRole
    region = ap-northeast-1
    color = 220259
    
    [SwitchC]
    aws_account_id = 234567891001
    role_name = SwitchCRole
    region = ap-northeast-1
    color = f7efee
    
    [SwitchD]
    aws_account_id = 345678910012
    role_name = SwitchDRole
    region = ap-northeast-1
    color = d29665

    color部分はカラーコードを記載すれば色の変更ができますので、画面下から好みの色のカラーコードを生成してペーストしてください。

    AWSコンソール画面でExtend Switch Rolesのアイコンをクリックするとスイッチロールリストが表示されます。

    所感

    AWS Extend Switch Rolesを使ったスイッチURLの切替方法について紹介しました。最初は一つのAWSアカウントからプロダクトを始めても事業が成長すると複数のAWSアカウントを利用するようになると思います。
    そのときにこのアカウントのスイッチURLってなんだっけ?切り替えの煩わしさを少しでも減らせれば開発効率も上がると思います。Chromeアプリ版を紹介しましたが、Firefoxアドオン版もありますのでFirefoxユーザーも気になりましたらぜひチェックしてみてください。

    addons.mozilla.org

    参考文献

    https://dev.classmethod.jp/articles/try-aws-extend-switch-roles-v2-pre-release/
    https://dev.classmethod.jp/articles/introduction-aws-extend-switch-role/