CUEBiC TEC BLOG

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

RcloneをECS FargateでGoogleドライブ-S3間のファイル同期を楽々定期実行

背景

こんにちは、キュービックでSREをやっているYuhta28です。キュービック内のテック技術について発信します。
弊社では業務で扱うファイルを格納するストレージにGoogeドライブを利用しており、基本的に参照は社内の人のみに限られます。ただ場合によっては社内のファイルを外部の人に見せたり、別サービスから画像ファイルを出力させたいという要件が発生します。
先日GoogleのLooker Studio1に社内で管理している数GBもある大量の画像ファイルを出力したいという要望がありました。直接Google Driveから参照するのではなくS3経由で画像を出力させたいとのことですが、Googleドライブのファイルは定期的に更新されるので更新されるたびに手動でストレージ間同期するのも面倒なので定期実行できる仕組みが必要だと考えました。

今回はRcloneというOSSを使い、GoogleドライブとS3のファイル同期及び実行環境をECS Fargateにすることで定的に自動実行できるようにしましたのでどのように設定したかについて紹介いたします。

対象読者

  • クラウドストレージ間のファイル同期方法について検討している
  • Rcloneの使い方について知りたい

Rcloneとは

rclone.org

Rcloneはクラウドストレージ上のファイルを管理できるコマンドラインツールです。S3やGoogleドライブなど70以上ものクラウドストレージに対応しており、クラウドストレージとローカル間、クラウドストレージ同士のファイルのコピーや移動、参照コマンドも可能なGo製のOSSとなります。
インストールページを見ても分かる通り幅広い環境で実行できる優れたツールです。

アーキテクチャ

GoogleドライブからS3へのファイル同期図

ファイル同期間にフォーカスをあてたアーキテクチャ図になります。Googleドライブのファイルが更新されたらS3にファイル同期を開始することも検討しましたが、認証周りの設定が面倒かつそこまでリアルタイムでの同期を求めていないということでしたので、毎日朝8時にFargateが起動してRcloneコンテナを実行しGoogleドライブからS3へファイルを同期するという手段を採用しました。

実装方法

Rclone設定

GoogleドライブとS3のRclone設定につきましてはクラスメソッドさんに詳しい手順がありますのでこちらを御覧ください。
一点考慮することとしてS3の設定でRcloneの設定ファイルにIAMユーザーのアクセスキーとシークレットキーをハードコーディングする箇所がありますが、Fargate上で実行する場合IAMロールをアタッチしてS3へファイル同期する権限を渡しますのでここの設定は不要となります。その他は同じ設定で問題ありません。

dev.classmethod.jp

  • S3のRclone設定
#IAMロールで権限を設定するため「2」を選択
Option env_auth.
Get AWS credentials from runtime (environment variables or EC2/ECS meta data if no env vars).
Only applies if access_key_id and secret_access_key is blank.
Choose a number from below, or type in your own boolean value (true or false).
Press Enter for the default (false).
 1 / Enter AWS credentials in the next step.
   \ (false)
 2 / Get AWS credentials from the environment (env vars or IAM).
   \ (true)
env_auth> 2

GoogleドライブとS3のrclone configが完了したら~/.config/rclone/rclone.confに以下の設定が記載されます。

[google-drive]
type = drive
scope = drive
token = {アクセストークン情報}
team_drive = 
root_folder_id = 

[s3]
type = s3
provider = AWS
env_auth = true
region = ap-northeast-1
location_constraint = ap-northeast-1
acl = private
storage_class = STANDARD

このファイルをDockerfileと同じディレクトリにコピーしてRcloneの準備は完了です。

コンテナ設定

作成した設定ファイルをRcloneコンテナで動かすためのDockerfileを構築します。

FROM rclone/rclone

COPY ./rclone.conf  /config/rclone/rclone.conf

CMD ["sync", "google-drive:<同期元Googleドライブフォルダ>", "s3:<同期先S3バケット/フォルダ>"]

# 確認用
# CMD ["sync", "--dry-run", "google-drive:<同期元Googleドライブフォルダ>", "s3:<同期先S3バケット/フォルダ>"]

syncはrcloneのサブコマンドで同期元ストレージと同期先ストレージのファイル階層を同一にします。この時、同期先ストレージに既存ファイルやフォルダなどがある場合同期元ストレージとファイル階層をあわせるため削除されてしまいます。事前に--dry-runオプションをつけて動作確認することを推奨します。
Dockerfileの記載が完了しましたらコンテナイメージをビルドし、ECRへプッシュします。

  • コンテナイメージpush手順

docs.aws.amazon.com

Fargate設定

次にタスク定義を作成します。FargateがS3へファイルを同期するための書き込み権限が必要になりますのでタスクロールに適切なIAMロールをアタッチします。

その他に特別な設定は不要でECRのコンテナイメージURIを指定しましたらデフォルトの設定で問題ありません。

ECRのイメージURLを指定
タスクを実行するECSクラスターも用意できましたら定期実行するためのEventBridge Schedulerを作成します。

EventBridgeScheduler設定

EventBridge Schedulerは2022年に発表されたEventBridgeの機能です。今回の要件でしたら従来のEventBridge ルール2でも実現できますので好みで選んでも問題ありません。

dev.classmethod.jp

スケジュールステップで毎日朝8時にEventBridge Schedulerが起動できるように設定します。

スケジュール設定

ターゲット選択ステップでは先ほど作成したタスクを起動するRunTaskを選択し、実行対象ECSクラスターを決め次のステップに移ります。

ターゲット選択

設定ステップではEventBridge Scheduler自身がFargateタスクを起動できるようにするための新規IAMロールを作成する必要があります。設定ステップ内で新規IAMロール作成を選択した状態で次のステップに移動します。

確認ステップまで進み設定に問題がなければEventBridge Schedulerを作成します。

所感

最終的に毎朝8時にEventBridge SchedulerがECS Fargateタスクを起動し、GoogleドライブからS3へファイルを同期する上記アーキテクチャが完成しました。
デフォルト設定のままでしたらCloudWatch Logsに実行ログが記録されていますので毎朝の実行結果が確認できます。

CloudWatch Logs

弊社はログ運用をDatadogに寄せているのでしばらく動かして問題なさそうでしたらFargateコンテナのログをDatadogに転送しようかなと考えています。

cuebic.hatenablog.com

参考文献

dev.classmethod.jp

REST APIを使ってカスタムフィールドを一括更新してみた

最近WordPressよりGASと戯れる時間が長くなってきているmikihoです。

前回、RESTAPIを使って記事一覧を作りましたが、今回はそれの発展系。
WordPressからスプレッドシートの連携ではなく、スプレッドシートからWordPressへの情報更新を実現させていきます。

調べてみると「投稿する」「更新する」など、単発記事への更新はそこそこありますが、今回目指すのは一括での情報更新です。
なければ作ればいいのということで、更新のためのコードを参考にしながらGASと向き合いました。

CSVデータでも生成させた方が早いのではないか?という疑念は常にありましたが、今回はREST APIを用いた方法を検証してみます。

リストを作るところは前回やったのでいいとしましょう。
気になる方は前回の記事をチェックしてくださいね。

cuebic.hatenablog.com

やってみた

事前準備

今回は情報の更新ということで、WordPress内でアプリケーションパスワードを取得する必要があります。
このアプリケーションパスワードはユーザーに紐づいて発行されるものなので、今回の目的上既存投稿の編集権限を持つアカウントでパスワード発行をしてください。

管理画面からプロフィールに移動して、下の方にパスワードの発行場所があるのでそこからパスワードを発行してください。

WordPressのアプリケーションパスワード発行場所

保存したパスワードと、パスワードが紐づいたアカウントのユーザーIDはスプレッドシートスクリプト プロパティに保存しておきましょう。
名前はなんでもいいんですが今回はapi_keyとapi_userとしておきます。

スプレッドシートのスクリプトプロパティ保存場所

※弊社の場合外部からアクセスできないように制限しているために、一旦このような形での運用しております。
悪用されないようにユーザーIDとパスワードが外部に漏れないように注意してください。

そして次に、今回はカスタムフィールドの情報を含むので、次にfunctions.phpにてregister_rest_fieldで定義させる必要があるので、そこら辺を追加していきます。

<?php
add_action( 'rest_api_init',  function() {
    register_rest_field(
        'post',        // カスタムフィールドを利用しているpost_type名
        'post_meta',   // rest-apiに追加するキー
        array(
            'get_callback'  => function(  $object, $field_name, $request  ) {
                // 出力したいカスタムフィールドのキーをここで定義
                array_push($meta_fields ,'カスタムフィールド名'); 
                $meta = array();
                foreach ( $meta_fields as $field ) {
                    $meta[ $field ] = get_post_meta( $object[ 'id' ], $field, false );
                }
                return $meta;
            },
            'update_callback' => function(  $value, $post, $field_name) {
                if (!$value) {return;}
                foreach($value as $key => $data){
                if(is_array($data)){
                    foreach($data as $record){
                        update_post_meta($post->ID, $key, $record);
                    }
                }else{
                    update_post_meta($post->ID, $key, $data);
                }
            }
            },
                'schema'=> null,
            )
        );
    });
?>

事前準備はこれだけ。
ここからGASのコードを交えて解説していきます。

とりあえず全部更新させてみた

GAS側の処理としてはこんな感じ。

var siteUrl = '更新したいWPサイトURL';  
var Key = PropertiesService.getScriptProperties().getProperty('api_key');
var User = PropertiesService.getScriptProperties().getProperty('api_user');
var sheetArticleList = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('シートの名前');
function postReport() {
  var lastRow = sheetArticleList.getLastRow();  
  for(var i=2;i < lastRow + 1;i++){ //1行目は項目名なので、2行目からスタート
    var headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Basic ' + Utilities.base64Encode(User + ":" + Key)
    };
    //post_idとtitleの取得
  var postId = sheetArticleList.getRange(i,2).getValue();
  var title = sheetArticleList.getRange(i,3).getValue();
  if(postId){
    var post_meta = {};
    var index = 4;//更新したいカスタムフィールドの位置
        post_meta['カスタムフィールド名'] = sheetArticleList.getRange(i,index).getValue();
    var arguments = { 
            'title': title,
        post_meta
      };
    var apiUrl = siteUrl + '/wp-json/wp/v2/post_type名/'+ postId;
      var options = {
        'method': 'POST',
        'muteHttpExceptions': true,
        'headers': headers,
        'payload': JSON.stringify(arguments)
        };
        var response = UrlFetchApp.fetch(apiUrl, options);
        var responseJson = JSON.parse(response.getContentText());
        
        JSON.parse(UrlFetchApp.fetch(apiUrl, options));
    }
  }
}

これは例なのでわかりやすくタイトルとテストのカスタムフィールドのみ更新が行えるコードになってます。
※タイトルやカスタムフィールドの値を取得するための数値は適宜環境ごとに変更してください。

参考: motoki-design.co.jp

実際に処理を走らせたんですが…
基本全記事に対してRESTAPIへ更新処理を走らせる必要があるので、非常に時間がかかる…!

テスト実装していたメディアには160を超えるデータがあったので30分程度の時間がかかりました。
データが増えればtimeoutするという、とんでもない代物となり却下。

mikihoは 力尽きた...

なんて茶番が頭によぎった途端、死んでしまうとは情けないという言葉に叩き起こされたのでもうちょっと改良していきます。

更新したものだけ抽出してみた

最初からそうしろという話ではありましたが、更新したデータだけにすれば負荷軽くなるじゃん!ということでコードをかなり書き換え。

少々原始的な方法を使いながら更新データを抽出して更新処理をするという方法に変えてみました。

まず、更新されたデータの収められた行が何行目なのか、を判別させるためのコードを書きます。
こんな感じ。

function showStatus(){
  var mySheet = SpreadsheetApp.getActiveSheet(); //シートを取得
  var myCell = mySheet.getActiveCell(); //アクティブセルを取得
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('更新データを貯めたいシート名');
  if(myCell.getColumn() === 3 || myCell.getColumn() > 4){ //アクティブセルが更新対象フィールドかの判別
   var lastRow = sheet.getRange('A:A').getValues().filter(String).length;
   sheet.getRange(lastRow+1,1).setValue(myCell.getRow());
  }
}

そう、更新データというタブを増やして、そこに更新行だけひたすらためていくためのシートを作ったんです。
今回は特定のフィールドの更新しかさせないために、特定列の更新が入ったタイミングでのみ更新行を書き出すようにif文を仕込みました。

あとはこの関数を、シート更新時にトリガーを設定しておくだけ。
これで更新した行数が自動でシートに書き込まれていきます。

さて、次に最初に作ったコードをこんな感じに変えていきます。

var siteUrl = '更新したいWPサイトURL';  
var Key = PropertiesService.getScriptProperties().getProperty('api_key');
var User = PropertiesService.getScriptProperties().getProperty('api_user');
var sheetArticleList = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('シートの名前');
function postReport() {
  //重複した更新データの削除
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('更新データを貯めたシート名');
  var range = sheet.getDataRange();
  range.removeDuplicates();
  //更新するデータ行の取得
  var values = sheet.getDataRange().getValues();
  for(i=0;i < values.length;i++){
    var headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Basic ' + Utilities.base64Encode(User + ":" + Key)
    };
    var postId = sheetArticleList.getRange(values[i],2).getValue();
    var title = sheetArticleList.getRange(values[i],3).getValue();
    if(postId){
      var post_meta = {};
      post_meta['カスタムフィールド名'] = sheetArticleList.getRange(i,index).getValue();
    var arguments = { 
     'title': title,
      post_meta
      };
    var apiUrl = siteUrl + '/wp-json/wp/v2/post_type名/'+ postId;
      var options = {
        'method': 'POST',
        'muteHttpExceptions': true,
        'headers': headers,
        'payload': JSON.stringify(arguments)
        };
        var response = UrlFetchApp.fetch(apiUrl, options);
        var responseJson = JSON.parse(response.getContentText());
        
        JSON.parse(UrlFetchApp.fetch(apiUrl, options));
    }
    }
    //更新したデータを貯めたシートをクリアする
    sheet.clear();
  }

更新データのシートは同じ行が更新されるとその都度追加してしまうので、最初のタイミングで同じデータが入ってるものは削除してまとめます。
重複したデータが無くなった状態の内容を改めて取得。
あとは、データ数分ループを回して更新処理を走らせるだけ

こんな感じの処理にすることによって、更新データ数を制限するので、最初の処理よりかは遥かに軽くなりました。

更新したいデータにのみチェックを入れてもらうという案もあったものの、そもそも一括で更新して工数を減らしたい、というのにわざわざ手順を一つ増やす意味があるのか?
と、自問自答した結果更新データも自動で抽出する形にしてみました。

もう少し上手いやり方はあると思うのですが、期限的に今はとにかくアウトプットを優先ということで、この仕様で納品いたしました。

やってみて

記事の本文はともかく、カスタムフィールドを多用していると意外といちいち編集画面に入って、更新して…という手間がかかって気づいたら工数がかさむということが運用側はそこそこあるようでした。
ですが、調べてみてもスプレッドシートから記事を投稿・更新するというのは情報が色々あるんですが一括はなかったです。

おそらくそこまでやりたいならCSVファイルでの一括更新がプラグインなどで手軽に実装できるので必要性がなかったのでしょう。(多分)

作ってみた感想としてはCSVファイルに落とし込んだほうが早いのでは…?というところはありました。
既存のものは手軽だし、どのプラグインを選んだかに左右されるもののある程度セキュリティ担保もされていますし...
とはいえ、今回のやり方を使えばファイルを落としてきてアップロードするという手間は省けます。

かといってCSVより今回のやり方の方がよかったとは言い切れません。
ケースバイケースとしか言え無さそうだと感じました。
ダウンロードしてきたCSVファイルの取り扱いの運用のめんどくささを考えるとシートから直接更新の方に多少のメリットがありそう、とは思いましたが。
運用体制も含めて、どちらが最適なのかのかを検討する必要がありそうです。

ハッカソンとは?実はエンジニアだけのものではない!開催のメリットと初運営の注意点!

こんにちは〜キュービックインターンのguparupaです。
 今回は私が主導で開催をした『ハッカソン』について述べていきます。今回社内で初めての取り組みとなっており、様々な視点から企画の立案を行なっているので今後開催する人の助けになればと思っています。ハッカソンはエンジニアのためというイメージを持っている人はそれを改めてもらう機会にもなると思います。

こんな人に読んで欲しい
  • ハッカソンってなんだろう?言葉の由来は?
  • ハッカソンを開催するメリットはなに?
  • 社内で初めて開催したいけどノウハウがない
  • ハッカソン開催で注意したい点が知りたい

ハッカソンのダイジェスト動画をYouTubeに載せたのでみてみて下さい。運営者もインタビューも載っています。雰囲気が分かるかと思います。 youtu.be

目次

  1. ハッカソンとは?
  2. ハッカソンで得られる成果とは?
  3. 開催前にしたこと、開催の注意点
  4. 今後の展望〜ハッカソンの未来〜
  5. まとめ

1. ハッカソンとは?

 ハッカソン(hackathon)を一言で説明すると『短期間で集中してテーマに沿った創造物を開発すること』です。テーマは様々で例えば「DX」であったり、「〇〇県を盛り上げる」など抽象的なものが多いです。ハッカソンの言葉の由来は「hack」と「marathon(マラソン)」の混成語であり、1999年頃からアメリカで開催され始め、今現在の日本でも流行り続けています(Wikipedia)。

 ハッカソンは元々システム開発者を中心に参加していましたが、今ではビジネスマンやデザイナーなど、コードを書く人以外も参加して多様性を活かしています。イメージが難しいのでYouTubeに載っている動画を見るのもおすすめします。

ハッカソンイメージ画像

 上の画像はキュービックで初めてハッカソンを開催したときの画像です。ワイワイしてます。

2. ハッカソンで得られる成果とは?

 ハッカソンするメリットは多くあり、上流から下流までの開発経験や、短期間でプロダクト開発新しいコミュニティの創造新しい技術の挑戦自分の実力の証明物作り体験、……。これらのメリットの中で、特に得られる三つに注目して社内でハッカソンを開催するメリットを述べていきます。

2-1. プロダクトの完成体験

 1つ目のメリットはプロダクトの完成体験です。エンジニアの方でも自分で考えたものを作って発表してフィードバックを貰うことは少ないと思います。ましてや、エンジニアとして働いている方で短期間のうちにプロダクトの完成させたことがある人は多くないでしょう。しかし、ハッカソンではチームで0からプロダクトの開発を進めて開発させることができます。これにより、自信に繋げることもできますし、コードを書くだけではない開発の難しさとやりがいも知ることができるでしょう。
 さらに、作りたいものがある人はそのアイディアを実際に形にすることもできます。子供が自由にアイディアを発散するように、自分のアイディアを形にする際の楽しさを味わえます。

2-2. コミュニティの創造

 2つ目のメリットはコミュニティの創造です。エンジニアは一人で黙々と取り組むイメージを持っている人もいますが、実際は活発にコミュニケーションを取らなければなりません。なぜなら、コミュニケーション不足で仕様の食い違いなどが起こるからです。ハッカソンでは個人よりもチームで開発を進めるため、コミュニケーションを活発にとり、コミュニティの創造にも貢献することができます。社内で開催すると、初めて話をする人との出会いや、その人の技術に触れる貴重な機会となります。これは今後の開発においても大いに役立つことができます。また、初参加の方にとっては話題作りがしやすく、コミュニティの形成も促進されます。

2-3. トレーニング&チャレンジ

 3つ目のメリットは新しい技術への挑戦です。ハッカソンではチームを組んでもどうしても新しい技術を学び、使う場面が生まれます。チームメンバーの技術を学び、活用することもあるでしょう。作りたいものを作るために、最低限の学びと最大限の活用を行います。新しいツールを利用する機会にもなるので、視野が広がり、短期間という焦りからものすごい集中力で吸収することができます。これにより新しいアイディアの発見や社内の技術力向上にもなります。ハッカソンは何よりもトレーニングになり、新しい技術にチャレンジする機会になります。そこから生まれる成長はかけがえのないものです。

3. 開催前にしたこと、開催の注意点

私たちの会社にはハッカソンのノウハウがなかったため、そのノウハウを持っている人に話を伺いました。そして会社のCore Valueなどの思いを元に企画の立案を進めていきました。

3-1. 他社にインタビューを実施

 今回インタビューでは2つの異なる企業とハッカソン開催をしたことがある人にお話を伺いました。特に、初開催の注意点や開催中に生まれる問題、開催するきっかけの話がとても大事でした。実際にインタビューする際は開催したい目的は何かをはっきりしておくといいでしょう。発表の形式や、テーマ決めの背景、開催頻度とその理由が目的によって変化していくからです。私たちが今回開催したハッカソンのテーマの背景は下に記載しているのでぜひ参考にしてみてください。

3-2. 企画のプロセス

 目的をはっきりさせ、その目的にあった企画を立てていきます。私たちが得たい成果は上で記載した3つだったので実際にどのような流れで決まっていったのかみていってください。

①現状の把握

 社内にハッカソンを経験したことがある人が極端に少なくハッカソンのイメージを持ちにくいことがありました。また、ハッカソンはコードをばりばり書ける人しかできないものであるという固定概念もありました。これではハッカソンの参加者が極端に少なくなってしまう可能性がありました。そのためハッカソンのイメージを持ってもらうために他社のハッカソン開催の動画を共有したり、技術メンターを設け初心者でも安心して参加できるような体制を作りました。

②テーマ決め

 当初のテーマは「プロダクト開発」でした。しかしこれでは抽象的すぎてハッカソンの経験が少ない人にとっては難しいのではないかと考えました。そこで初開催はハッカソン初心者を経験者にするということを焦点にし、テーマを「業務改善」とすることでより参加しやすくなるようにしました。

③やる気作り

 イベントでもっとも不安なのが参加者が集まらないことです。参加者が集めるために参加者が参加しやすい環境、報酬、参加者が要望を考え尽くしました。テーマもそのうちの一つで普段不満が溜まり改善したいと思う業務改善にすることと、メンターの力により参加者のモチベーションを維持しました。

3-3. 注意点

 初開催の際に注意して欲しい点があります。私自身も自社のエンジニアチーム一人ひとりの能力を把握していませんでした。必要なことは、「1️⃣目的を明確にすること」「2️⃣技術メンターを用意すること」「3️⃣モチベーションを維持する施策を考えること」「4️⃣スケジュールの共有は早めにすること」です。

1️⃣ 目的によってどのようなハッカソンになるかが変化します。技術力アップが目的ならテーマは「新しい技術」となりますし、コミュニティの創造ならコミュニティを強制的に作ることをしなければなりません。今回、私たちが最終的に目指したのはハッカソン経験者を増やすことでした。だから参加しやすいテーマ設定や宣伝を行いました。

2️⃣ またエンジニア初心者が多い場合は必ず技術メンターを用意しましょう。今回私たちは技術メンターを各チーム一人配置することができましたが、人数が増えるとそうはいきません。メンターの実力の選定や関わらせ方、参加者にメンターを使ってもらえるように促すなど、プロダクトの開発で難儀しにくい環境作りが必要です。

3️⃣ イベントに参加してもらうためにモチベーション作りが必要です。モチベーションが低い状態だと良いアイディアが生まれず、プロダクトの完成もできません。イベントを開催するのでせっかくなら途中で諦めることもして欲しくないのです。そのために参加者の立場に立ち、モチベーションを維持してもらえるように考える必要があるのです。

4️⃣ スケジュールの共有は余裕を持って行いましょう。初の開催ではいつ開催できるようになるか未定な部分が多いと思います。しかし、いざ準備ができたときに開催できる日が直近しかないという状況にもなりかねません。企画ができてなくても開催するという意思がある場合はある程度日程の希望を社内の参加者に伝えておくと良いでしょう。直前の共有で参加できない人が多くなってしまうからです。(これは私が最も反省しています。)

4. 今後の展望

4-1. ハッカソンはもうエンジニアだけではない!?

 現在、流行りに流行っているChatGPTの影響で今後のハッカソンはエンジニアだけではなくなる可能性があります。全くの初心者では難しいですが、ある程度知識がある人と組むことでエンジニア以外の人がハッカソンに参加してプロダクトの開発ができるようになるでしょう。初開催のハッカソンでもエンジニア歴が短い参加者がいますがChatGPTで開発を進めることができていました。AIの影響でハッカソンがより多様になってきて、アイディアの価値が上がるでしょう。また、多様性によって生まれたプロダクトがビジネスに発展するかもしれません。ChatGPTについて知りたい方は以下の記事を参考にしてみてください!

cuebic.hatenablog.com

4-2. 今後の取り組み

 今後私たちキュービックはハッカソンを通して横断的な社内の交流をメインに取り組み、最終的には社外とのハッカソンも行いたいと考えています。ハッカソンはもうエンジニアだけのものではなく、多様性と個性を活かしたアイディアが重要になってきます。それだけではなく、ハッカソン本来の目的であるプロダクト開発の技術力も必要になってきます。社外に向けた取り組みによってブランディングの強化にもなります。また良いアイディアをビジネスになるような動線作りも私たちは作っています。ハッカソンを最大限活かして今よりも社会に大きな影響を与える存在になります。

今後の展望

 今後の取り組みの中でアイディアソンにも力を入れるつもりです。アイディアソンで生まれたアイディアをハッカソンで実現します。アイディアソンは誰でも参加できるような仕組み作りも必要になります。アイディアソンではチームビルディングの活動から初心者でも参加しやすい体制を進めていけるといいと思っています。

5. まとめ

 ハッカソンの概要や社内で開催するメリット、初開催の注意点などを述べてきました。取り入れる際はぜひ参考にしてみてください。ハッカソン開催する際は「目的」を明確にし、時にはインタビューを通して視野を広げてみるといいと思います。また今後の展望で述べたようにハッカソンを通して多様性を生み、交流を深め、アイディアの価値を創造していこうと思っています。最後まで読んでいただきありがとうございました!😀

Terraform Registryで公開されているTerraform Modulesが便利だった件

背景

こんにちは、キュービックでSREをやっているYuhta28です。キュービック内のテック技術について発信します。

過去何回かTerraformに関する記事を執筆しました。

cuebic.hatenablog.com

cuebic.hatenablog.com

この頃はTerraformを使い始めて日が浅く、作成リソースは全部手動で書かないといけないと考えていました。しかし、別のプロジェクトでAWS CDK1に触れる機会があり、AWS CDKのL2コンストラクトで構築してみたところ少ない記述量で必要な構成をすぐに用意してくれる便利さに魅了されました。
Terraformでも同じことがしたいと考え調べてみると、Terraform Registryで公開されているモジュールを使えば少ない記述量でリソースを作成できるみたいでしたのでモジュールを使ったTerraform IaCについて紹介いたします。

対象読者

  • TerraformでIaCしている人
  • 記述量が多くてもっと効率化したいと考えている人
  • Terraform Modulesの使い方を知りたい人

Terraform Modulesについて

registry.terraform.io

TerraformにはTerraform Registyという自由にダウンロードして使えるモジュールがいくつか存在します。例えばAWS関連のモジュールにはVPCを関連リソースを含めて作成してくれたり、EC2やRDSならセキュリティグループやIAMロールもセットに作成してくれるモジュールがあります。

https://registry.terraform.io/browse/modules?provider=aws

セットで書けるというのはどういうことかと言いますと一例を出します。
Amazon VPCを作成する場合、Terraformで書くと以下のようになります。

resource "aws_vpc" "main" {
  cidr_block  = "10.0.0.0/16"
  tags = {
    Name = "Cuebic-VPC"
  }
}

これをデプロイすればAWSCuebic-VPCというVPCが作成されます。しかしVPCにはサブネット、ルートテーブル、インターネットゲートウェイなど付随するリソースが数多くあり、Terraformはそれらすべてを別々のリソースとして記述しなければ作成されません。
パブリックサブネットとプライベートサブネットを含めた一般的なVPC関連リソースをTerraformでコード化すると、記述量が多くなり煩雑化してしまいます。

モジュールを使わないVPC作成

resource "aws_vpc" "terraform-vpc" {
  cidr_block           = var.cidr_block
  enable_dns_hostnames = true
  tags = {
    Name = "${var.Tag_Name}-vpc"
  }
}

resource "aws_internet_gateway" "terraform-igw" {
  vpc_id = aws_vpc.terraform-vpc.id
  tags = {
    Name = "${var.Tag_Name}-igw"
  }
}
#--------------------------------------------------
# パブリックサブネットリソース
resource "aws_subnet" "terraform-public-subnet" {
  for_each                = var.public-AZ
  vpc_id                  = aws_vpc.terraform-vpc.id
  cidr_block              = each.value
  availability_zone       = "ap-northeast-1${each.key}"
  map_public_ip_on_launch = true
  tags = {
    Name = "terraform-${var.Tag_Name}-public-subnet-${each.key}"
  }
}
resource "aws_route_table" "terraform-public-rt" {
  for_each = var.public-AZ
  vpc_id   = aws_vpc.terraform-vpc.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.terraform-igw.id
  }
  tags = {
    Name = "${var.Tag_Name}-public-rt-${each.key}"
  }
}
resource "aws_route_table_association" "terraform-public-rt-assoc" {
  for_each       = var.public-AZ
  subnet_id      = aws_subnet.terraform-public-subnet[each.key].id
  route_table_id = aws_route_table.terraform-public-rt[each.key].id
}

resource "aws_nat_gateway" "terraform-nat" {
  for_each  = toset(var.eip-NAT-AZ)
  subnet_id = aws_subnet.terraform-public-subnet[each.key].id
  depends_on = [
    aws_internet_gateway.terraform-igw
  ]
  allocation_id = aws_eip.terraform-nat-eip[each.key].id
  tags = {
    Name = "${var.Tag_Name}-nat-${each.key}"
  }
}
resource "aws_eip" "terraform-nat-eip" {
  for_each = toset(var.eip-NAT-AZ)
  tags = {
    Name = "${var.Tag_Name}-eip-${each.key}"
  }
  depends_on = [
    aws_internet_gateway.terraform-igw
  ]
}
#--------------------------------------------------
#--------------------------------------------------
# プライベートサブネットリソース
resource "aws_subnet" "terraform-private-subnet" {
  for_each          = var.private-AZ
  vpc_id            = aws_vpc.terraform-vpc.id
  cidr_block        = each.value
  availability_zone = "ap-northeast-1${each.key}"
  tags = {
    Name = "terraform-${var.Tag_Name}-private-subnet-${each.key}"
  }
}
resource "aws_route_table" "terraform-private-rt" {
  for_each = var.private-AZ
  vpc_id   = aws_vpc.terraform-vpc.id
  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = var.Tag_Name == "dev" ? aws_nat_gateway.terraform-nat["a"].id : aws_nat_gateway.terraform-nat[each.key].id
  }
  tags = {
    Name = "${var.Tag_Name}-private-rt-${each.key}"
  }
}
resource "aws_route_table_association" "terraform-private-rt-assoc" {
  for_each       = var.public-AZ
  subnet_id      = aws_subnet.terraform-private-subnet[each.key].id
  route_table_id = aws_route_table.terraform-private-rt[each.key].id
}

サブネットとルートテーブルは2つをアタッチするためのリソースとしてaws_route_table_associationも必要となり手作業で作成するとかなり苦労します。VPC関連リソースはサービス毎に大きく構成が変わるものではなく、基本的にパブリックサブネット、プライベートサブネット、データベースサブネットを各AZ毎内に作成されれば十分であるケースも多いです。

先程のTerraform RegistryにはVPC向けのモジュールもありましたのでモジュールを使ったリソース作成がどのようなものか見ていきましょう。

モジュールを使ったVPC作成

https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/lates

モジュールを使う場合、resourceブロックではなくmodulesブロックを使用します。sourceにはTerraform Registryで公開されているモジュールのパスを指定し、パラメーターに必要な値を入力しterraform applyすればVPC関連リソースが作成されます。

module "vpc" {
  source = "terraform-aws-modules/vpc/aws"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
  public_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  private_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
  database_subnets = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]

  enable_nat_gateway = true

  tags = {
    Terraform = "true"
    Environment = "dev"
  }
}

上記のコードを記述しますと3つのAZにパブリックサブネット、プライベートサブネット、データベースサブネット及びNATゲートウェイが作成されます。

VPCアーキテクチャ

パラメーターも多くはデフォルト値が設定されていますので最初から多く書く必要はなく必要なリソースを作成したいときに適宜パラメーターを変更すればOKです。

VPNゲートウェイを追加したい場合
enable_vpn_gateway = true

所感

Terraform Registryで公開されているモジュールを活用したTerraform運用について紹介しました。 最初の頃は頑張ってリソースをすべて書いていましたが、記述量が多くなりしんどいと感じていたところにモジュールを活用した方法を見つけられてTerraform開発が楽になりました。
Terraform Cloud2を使えば社内限定のプライベートモジュールも作成できますのでオリジナルモジュールを作成してぜひTerraform開発を効率化してみてください。

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

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

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

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

基本的な情報のみの一覧

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

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

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

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

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

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

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

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

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

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

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

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

補足

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

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

応用編

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

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

    /wp-json/wp/v2/posts

ここを

    /wp-json/wp/v2/pages

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

注意点

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

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

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

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

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

【目次】

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

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

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

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

大きな利点としては

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

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

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

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

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

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

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

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

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

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

作成step

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

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

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

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

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

作成step

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

まとめ

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

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

herp.careers

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

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

自己紹介

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

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

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

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

sunby.jp

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

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

troccoを勧めたきっかけ

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

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

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

招待しました!

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

発生した事象

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

trocco君・・・

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

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

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

例えばどんなです?

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

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

仮説

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

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

方針

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

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

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