RustでGitHub Actionsの実行時間を集計してみた

こんにちは、株式会社ライトハウスでエンジニアをしている宮城です。

今回は、RustでGitHub Actionsの実行時間を集計するツールを作成した話をします。

目次

GitHub Actionsの実行時間を可視化するにいたった背景

要約

困っていたこと

  • 一回の変更で多数のCIが実行され、多くの課金時間が消費される

解消方法

  • ダミーJobを用意して、実行時間を短縮する

この体験を通して気がついた課題

  • GitHub Actionsの情報を把握出来てない

詳細

困ったていたこと

8月より新規プロダクトの開発が始まりました。

このプロダクトは、複数のサービスをオンプレミスサーバー上で動かす想定です。

この複数のシステムは、同一レポジトリ上に存在していた方が開発をしていくフローと相性が良いため、モノレポを採用しました。

また、弊社はGitHub ActionsでCI/CDを行うことが多いため、今回もGitHub Actionsを使用することにしました。

最初にサービスを1つ作成したとき、下記のようなディレクトリ構造になりました。

.
├── .github/workflows/service_a.yaml
└── service_a

また、このサービス配下のディレクトリにコードの変更があったときのみCIを実行するというルールを設定しました。

on:
  push:
    branches:
      - '**'
    paths:
      - 'service_a/**'
      
jobs:
  service-a-lint:
    name: service-a-lint

その後、このjobをブランチプロテクションルールに加えました。

ほどなくして、 2つ目のサービスを作成しました。

ディレクトリ構成は下記のようになりました。

.
├── .github/workflows/service_a.yaml
├── .github/workflows/service_b.yaml
├── service_a
└── service_b

1つ目のサービスと同様に下記のルールでCIを作り、このjobをブランチプロテクションに加えました。

on:
  push:
    branches:
      - '**'
    paths:
      - 'service_b/**'
      
jobs:
  service-b-lint:
    name: service-b-lint

ここで困ったことに遭遇しました。 service_aのみ変更した場合、pathsトリガーではなくブランチプロテクションがトリガーとなり、service_bのCIも起動します。 このとき、service_bに対する変更がないため、いつまでもservice_bのCIが終わりません。

開発の初期で、CI対象のサービスが2つしか存在しなかったため、どのCIも全ての変更に対して実行することにしました。 これにより、pathsルールとブランチプロテクションを併用している際に発生してしまう問題を一旦回避しました。

しかし、開発から2か月後、さらにサービスが増えたことにより1回の変更で実行されるCIが増えていきました。

それに伴い、GitHub Actions上で実行時間も増え、下記のようなメールが届きました。

f:id:LighthouseInc:20220119101707p:plain

ついに会社のGithubアカウント全体に影響を与えてしまう状態になってしまいました。

解消方法

このままではCIが実行できなくなるので、一旦課金設定を行い、全てのリポジトリのCI/CDが止まらないようにしました。

ただ、このプロダクトのサービスが増えるたびに、1回の変更で実行されるJobが増えていきます。

それは、GitHub Actions上の課金時間が増えていくことを表しています。

額が小さいうちは問題ありませんが、いずれ改善しなければいけない項目になるのは目に見えていました。

そこで、 今回はダミージョブを作ることにしました。

具体的には、まずdummy.yamlをworkflows配下に作成します。

.
├── .github/workflows/dummy.yaml
├── .github/workflows/service_a.yaml
├── .github/workflows/service_b.yaml
├── service_a
└── service_b

そして、dummy.yamlの中にservice-a-lint.yamlとservice-b-lint.yamlに定義されているjob名と同一のものを定義します。

このjobに対して、必ず成功で終わるstepを記載します。

name: Dummy CI

on:
  push:
    branches:
      - '**'

jobs:
  dummy-service-a-lint:
    name: service-a-lint
    runs-on: ubuntu-latest
    steps:
      - name: exit with success
        run: exit 0

  dummy-service-b-lint:
    name: service-b-lint
    runs-on: ubuntu-latest
    steps:
      - name: exit with success
        run: exit 0

これにより、あるサービスのディレクトリに変更があったときのみCIを実行しつつ、ブランチプロテクションも行えるようにしました。

この体験を通して気がついた課題

今回は上記案で解消出来たので良かったのですが、本当の課題はGitHub Actions上で動作しているworkflowの情報を把握出来ていないことだと感じました。

具体的には、各workflowがどれくらいの頻度と時間で実行されているか、またそれに対する課金時間はどれくらいなのかということです。

今回はすぐに解消できる問題だから無事に済みましたが、状態や傾向を把握しておかないと、何かあったときにCI/CDが実行できない状態になり開発を止めかねません。

また、実は極端に実行時間が長いジョブや全くデプロイが行われていないサービスなど、改善が必要なCI/CDの発見やサービスそのものの改善を知ることも出来ません。

そのために、GitHub Actions上で動作しているworkflowの情報を把握したくなりました。

Rustでツールを作った背景

上述したとおり、やりたいことは、GitHub Actions上で動作している各workflowの情報を把握することです。

特に欲しい情報は、各workflowの実行時間、実行頻度、課金時間です。

当初はGitHubの支払いから簡単に分かると思っていました。

しかし、リポジトリごとの実行回数は記載されていますが、実際どのジョブにどれくらい時間がかかったかを把握することが出来ませんでした。

そのため、やりたいことを実現するためには、 GitHub CLIGitHubAPIを使う必要がありました。

困ったことにGitHub CLIgh run listだと、課金時間が取れませんでした。

GitHubAPIを使えば実現できることは分かりました。

しかし、リクエストを送らないといけないエンドポイントが複数あり、シェルを書くよりもコード化した方が楽だなと思いました。

これが、今回ツールを作ろうと思ったきっかけです。

また、やりたいことを実現するツールも探し、見つけました。

しかし、以下の理由から、Rustでツールを作ることにしました。 - 今後のIoT開発にRustを使用するための事前調査をしたい。 - ジョブごとの実行時間を集計するだけなので、スコープが狭くて丁度よい。 - Rustかっこいい。

現在、緊急でこのツールが欲しいというわけでもなかったので、個人の時間で作成してみることにしました。

ツール紹介

作成したツールは、こちら にあります。

GitHub Actionsを使用しているリポジトリと期間を指定すると、指定した期間の実行時間をworkflow runごとに取得し、CSV形式で出力するプログラムになっています。

f:id:LighthouseInc:20220119100954g:plain

実行時間の可視化

弊社には、ETLや分析用途のEKSがあります。

上記で作成したバイナリーを実行するCronJobを作り、Big Queryに結果を格納します。

この結果を見るためのダッシュボードをRedash上に作成し、開発メンバーで毎週行っているモニタリングチェック時に見るようにしています。

f:id:LighthouseInc:20220119101001p:plain

実装時にハマった箇所

正直ハマらなかった箇所がないくらい、全ての工程でハマりました。

その中で一番実現に時間を要したのが、非同期処理とプログレスバー表示の組み合わせです。

非同期処理を取り入れた方が良いと考えた理由

実行時間を取得するために、各workflow runのidを用いて、GitHub APIのtimingエンドポイントにリクエストを送る必要があります。 workflowによってはかなりのrun数が存在します。 ツールの開発当初は、同期的にリクエストを送っていましたが、実行時間に20分以上かかることがあるため、改善が必要でした。

プレグレスバーがあったほうが良いと考えた理由

上記実行時に、待ち時間が発生するworkflowが存在するため、処理が進んでいることを伝えるために進捗が可視化されていたほうが良いと感じました。 また、ローディングしてるさまが描画されていると、少しワクワクした気持ちになり退屈さが軽減されるからです。

使用したライブラリ

非同期ランタイムにはtokioプログレスバーの表示にはindicatifを使用しました。

具体的にハマったこと

indicatifのexamplesをみて自分のプログラムに組み込みましたが、非同期処理と組み合わせると、自分が思うような形で進捗を表示することが出来ませんでした。

具体的には、各非同期タスクをmap内で処理すると、最後の集計結果のみがプログレスバーに表示されてしまいました。

let pb = new_progress_bar(workflow_runs.get_length() as u64, workflow.get_name());

let workflow_runs_tasks: Vec<JoinHandle<Result<WorkflowRuns>>> = workflows
    .get_workflows()
    .into_iter()
    .progress_with(pb)
    .map(|w| {
        let api = api.clone();
        tokio::spawn(
            async move { api.find_timing_from_workflow_run(run.get_id()).await },
        )
    })
    .collect::<Vec<_>>();
let workflow_runs_lst = try_join_all(workflow_runs_tasks)
        .await?
        .into_iter()
        .collect::<Result<Vec<WorkflowRuns>>>()?;

改善策

これを解消するために、チャネルを使用することにしました。

具体的には、mspcを使ってチャネルを生成し、非同期処理内でapiリクエストに対するレスポンスの受け取りとチャネルへの送信を行うようにしました。

これにより、非同期時でも処理が終わる度にカウントアップされ、プレグレスバーが正しく表示されるようになりました。

最後にチャネルのレシーバーをドロップしないと処理が終わらないので注意が必要です。

let pb = new_progress_bar(workflow_runs.get_length() as u64, workflow.get_name());

let (tx, mut rx) = mpsc::channel(1);
for run in workflow_runs.get_workflow_runs().into_iter() {
    let tx = tx.clone();
    let api = api.clone();
    tokio::spawn(async move {
        match api.find_timing_from_workflow_run(run.get_id()).await {
            Ok(timing) => {
                if tx.send(timing).await.is_err() {
                    println!("receiver dropped");
                }
            }
            Err(msg) => println!("failure: {}", msg),
        }
    });
}
// Note: drop the last sender to stop `rx` waiting for message
drop(tx);

while let Some(t) = rx.recv().await {
    // some implementations
    pb.inc(1);
}
pb.finish();

終わりに

Rustでツールを作るための実用的な課題を見つけられたことにより、課題の解決のみでなく、今後のIoT開発に役立てそうな知見を収集することが出来ました。

今後は、今回得た知見をチームにも還元していきます。

弊社は、新しい技術や興味のある技術の導入に対してとても柔軟です。

色んな技術に挑戦したい方には、とてもおすすめです。

現在、エンジニア採用活動中です!

もしドメインや使っている技術に興味もっていただけましたら、カジュアル面談でお話しましょう!

今ふたたびのAnsible入門

こんにちは、ライトハウスでソフトウェアエンジニアをしている北添です。

今回は「今ふたたびのAnsible入門」と題しまして以下の内容を書きます。

  • なぜ今ふたたびAnsibleに入門するのか
  • 基本的な使い方
  • ベストプラクティスに則ったAnsibleのコード設計

なぜこの記事を書いたか

理由は2つあります。

1つ目は、IoTのプロダクトでAnsibleがどのように活躍するのか、ご紹介したかったことです。

私が勤める株式会社ライトハウスは、水産業界の課題解決のために船舶のIoT化を推し進めています。 船舶からセンサーを通じて情報を吸い上げ、クラウドへ送信することでIoT化が達成されます。

通常、センサーで取得したデータは直接クラウドへ送信されるのではなく、船内のサーバーに一度集められた後送信されます。 つまり、IoTの文脈ではオンプレミスサーバーが必要であり、オンプレミスサーバーが必要な以上、構成管理が重要になるのです。

2つ目は、(ちょうど数か月前の私のような)Ansible入門者の道標となるサンプルコードを提供したいと思ったことです。

特に、Ansible公式が推奨するベストプラクティスに則った使い方は、実例となるコードを見ることによって一気に理解できました。 そういった形でこの記事が役に立てば嬉しいです。

なぜ、今ふたたびAnsibleに入門するのか

なぜ、クラウド・コンテナ開発全盛の今に、サーバーの構成管理をするAnsibleに入門する必要があるのでしょうか。

それを知るために、ライトハウスでのAnsibleの活用をご紹介します。

船舶をIoT化する上でまず必要になるのが、センサーとサーバーです。 センサーがなければモノの情報が取得できませんし、サーバーがなくてはインターネットに繋ぐことができません。

そのサーバーの構成管理ツールとして、Ansibleを利用しています。

もしも、構成管理ツールを使わず、手動オペレーションによってサーバーを構築するとどうなるでしょうか。

サーバーがどのような状態なのか把握できなくなり、管理不能な状態に陥ります。

弊社でも、構成管理していないサーバーの障害対応で非常に手痛い思いをしたことがありました… 障害の状況を再現できず、原因究明やリカバリーの難易度が高くなってしまうのです。

LHでのAnsible実行環境の紹介

次に、より具体的にライトハウスでどのようにAnsibleを活用しているのかご紹介します。

検証環境と本番環境の2つを用意し、それぞれの環境の構成管理をAnsibleで行っています。

ただし、船に置くサーバーは高価なため、検証用のサーバーとして本番と同じものを買うことはできません。 そこで、検証用の環境には本番と同じOSが入っているEC2インスタンスを使っています。

Ansibleで管理する対象は、船に置くオンプレミスのサーバーとEC2インスタンスとなっているわけです。

Ansible入門(実行編)

さて、ここからAnsibleの具体的な使い方について紹介します。

ここではAnsibleのインストールから、対象のホストにAnsibleでPingできるようになるまでの手順を紹介します。

インストール

Macをお使いの方は、brewでインストールできます。

brew install ansible

実行

まずはAnsibleを通して、対象のホストにpingしてみましょう。 ${HOST_IP}の部分には対象のホストのIPアドレスを入力してください。

ansible all -i ${HOST_IP}, -m ping

うまく疎通ができれば、Pingが成功するはずです。

変数を利用する

Ansibleでは変数を利用できます。

コマンドラインで変数を書くことも、jsonyamlに変数を保存して使うこともできます。

variable.jsonという名前のファイルに変数を定義していると仮定すると、以下のように利用できます。

ansible all -i ${HOST_IP}, -m ping --extra-vars "@variable.json"

Ansible入門(インベントリー編)

Ansibleを実行するときは、実行対象のホストを指定する必要があります。 この「実行対象のホスト」のことをAnsibleでは「インベントリー」と呼びます。

インベントリーの指定方法は2つです。

  1. コマンドラインで与える
  2. ファイルに書いて与える

コマンドラインから与える際は、 -i オプションにホストのIPアドレスかホスト名を与えます。 -i に入る値は、カンマ区切りである点を注意してください。

次に、ファイルに書く場合は以下のようにします。

[webservers]
foo.example.com
bar.example.com

webservers というグループに、 foo.example.combar.example.com というホストが存在している」という意味の設定です。

インベントリーをファイルで設定する場合、ファイル名は hostsとすることがデフォルトです。

インベントリーの書き方(実例)

インベントリーの書き方について、より実践的な例を紹介しておきます。

SSMセッションを利用したSSH接続を行い、EC2インスタンスに対してAnsible実行するためのhostsファイルです。

[ec2-instance]
instance ansible_host={EC2インスタンスID}

[all:vars]
ansible_ssh_common_args=-o StrictHostKeyChecking=no -o ProxyCommand="sh -c \"aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'\""
ansible_user='ec2-user'
ansible_become=true

Ansible入門(設計編)

設計編では、Ansibleのコード管理のベストプラクティスをいくつかご紹介します。

Ansible公式が掲載しているベストプラクティスを参考にしました。

rolesディレクトリの活用

Ansibleでは、rolesディレクトリの配下に、さらに管理対象ごとのディレクトリを作ることで分かりやすく整理するのがベストプラクティスと言われています。

例えば、chrony関連を管理するAnsibleのコードは、 roles/chrony 配下に配置します。

roles/
    chrony/
    network/
.
.
.

さらにそのディレクトリ配下には、filestaskshandlers というディレクトリを作成します。

先程のchronyを例に取り、ディレクトリ構造を示します。

roles/
    chrony/
        files/
            - chrony.conf
        tasks/
            - main.yml
        handlers/
            - main.yml

以上のような形になります。

filesには、サーバーに配置したい設定ファイルを置きます。

引き続きchronyの例でいうと、chronyのサービスが参照する chrony.conf ファイルを置きます。

tasksとhandlersには、サーバーに適用するアクションを定義します。 tasksでもし変更を検知したら、handlersの処理を呼び出すという関係になるようにアクションを置き分けていきます。

引き続きchronyの例を取り上げます。 tasks配下にはchronyの設定ファイルを配置するよう定義しておきます。

- name: deploy chrony.conf
  template:
    src: "./files/chrony.conf"
    dest: "/etc/chrony.conf"
    owner: root
    group: root
    mode: 0644
  notify: restart chronyd.service

ポイントは、 notify というディレクティブです。 これにより「chrony.confに変更があった時だけ」notify が指し示すアクションを実行するようになります。

そして、handlers配下には以下のようにサービスのrestartが行われるように定義します。

- name: restart chronyd.service
  systemd:
    name: chronyd.service
    state: restarted
    enabled: yes
    daemon_reload: yes

こうすることで、chrony.confに変更があるときだけ、chronydのサービスが再起動して設定が読み込まれるようになります。

ちなみに、 state には restarted の他にも reloadedstopped などがあります。 こうした選択肢は公式のドキュメントに載っていますので、適宜調べて最適なものを選ぶ必要があります。

stateを最適化する例を1つ紹介します。

restarted に指定していると、サービスが一度ダウンした後起動する挙動をします。 一方で、 reloaded に指定していると、サービスのダウンなしに立ち上がり直す挙動をします。

そうした性質を踏まえて、ダウンタイムなしでサービスに設定を反映させたい場合は reloaded を使うなどの判断が必要です。 例えばnginxのサービスなどは、ダウンタイムがないとより好ましいでしょう。

あとがき

ここまでお読みいただき、ありがとうございます。 IoTにご興味のある方はAnsibleを使えて損はありません。この記事を入門の第一歩をしてくれたら嬉しいです。

エンジニアを募集しています

ライトハウスではエンジニア募集しています。

船舶のIoT化や水産業界のDXを推し進める、社会的な意義の深い仕事に取り組めます!

少しでもご興味が湧いたら、ぜひカジュアル面談へいらしてください!

未知を拓く。共に船舶海洋プラットフォームを構築するエンジニア募集

サウナで分析基盤を構築し、漁業のサステナビリティを向上させる取り組みを考えた話

こんにちは、株式会社ライトハウスでエンジニアをしている平塚です。

今回はサウナで分析基盤を作った話を致します。

なぜサウナなのか

休憩時間に本気のリフレッシュできるからです。

逆に長時間働き過ぎると眠くなってくるので(個人の主観)夢中になってやりすぎ防止出来る面は 良いかなと思っています。

分析基盤を作成した場所

今回利用させて頂きました施設は両国湯屋江戸遊 です。

両国の江戸遊を選択した理由は複数あるのですが、以下の通りです。

会議室の利用

会議室は当日に伺うと埋まっている事が多いです、 事前予約が可能なので予約することをお勧めします。

サウナの種類

男湯には以下2種類のサウナが用意されています。

水風呂は1つあり、18度くらいに設定されています。

若干ぬるめかと思ったのですが、サウナの温度設定が良いのか 水質が良いのか分かりませんが水風呂がすごく気持ちよかったと感じました。

分析基盤とは

世の中ではデータの収集、蓄積、分析、可視化などの機能を有したものであるなど、色々ありますが ライトハウスでもそれら4つの機能を実現できる基盤を構築しています。

収集: Airflowを使用してISANAを利用したユーザのデータ(個人情報はマスクしています)や利用状況などの集計。

蓄積: Google BigQueryに保存。

分析:Redash Queryの利用。

可視化: Redash Dashboardの利用。

RedashのDashboard全社員が共通の指標を追うことができる状況を目指しています。

なぜEKSを利用したのか

AWSを選択する理由

AWS(S3やRDS)に大量のデータが存在するため、以下の理由から他のクラウドサービスではなくAWSを利用する選択をしました。

  • VPC内実行による通信コストの削減
  • VPC内実行による通信速度の向上
  • AWS利用実績

EKSを選択する理由

正直機能だけを考慮すればEC2やECSなどで十分ですが、 選択した理由は以下の通りです。

  • ただただKubernetesを触りたいという欲望
  • どれを使っても基盤部分は2,3時間で環境作れる気がする
  • BigQueryを利用しているのでいつでもGKEに移行できること

アーキテクチャの説明

KubernetesにRedashとAirflowを稼働させ、各種データソースから取得したデータを Bigqueryに保存し、Redashで分析します。

Redashを使用する理由

BIツールとして利用実績が十分あること。

私自身もコントリビュートしているためある程度バグが存在しても修正でき、 独自機能を追加することも十分可能であると考えたからです。

Redashの構築説明

redashの構築はコード付きで解説すると長くなるのでサンプルコードを作成しました。 ログインにはgoogle oauthを利用する想定で記載しています。

https://github.com/jasonsmithj/eks-redash

詳細な解説については次回以降で行います。

Airflowを使用する理由

ライトハウスに携わる以前はDigdagとEmbulkを利用していました。 今回Airflowの採用に踏み切った理由は以下の通りです。

  • Pythonを利用して自由度の高い処理の実装が容易である
  • GCPのマネージドサービスであるCloud Composerの実績が増えてきている
  • 今まで触っていなかった技術に触れたかった

Airflowの構築説明

redashの構築はコード付きで解説すると長くなるのでサンプルコードを作成しました。 ログインにはgoogle oauthを利用する想定で記載しています。

https://github.com/jasonsmithj/eks-airflow

詳細な解説については次回以降で行おうといます。

今後の展望

漁業という独特なドメインに対してIoTを利用しているため、 他の会社には無いようなデータが数十億レコード存在するので 様々な観点で分析する計画を進めています。

また、現在は取得できていませんが、外部データ(気象情報、海底地形、漁協の水揚げ)も取得し 漁業者だけでなく流通や水産資源保護などのトレーサビリティ、サステナビリティの観点でも分析を進めていきます。

まとめ

初めて扱う技術ではハマる事が多く、 合間にサウナを利用する事でリフレッシュできるため、 体感でtypoなどの単純なミスに気付くのが早くなりました。

また、ライトハウスでは漁業者に直接お会いするなど、 出張先のサウナで作業をする機会が多く九州や関西地域のサウナについても 紹介させて頂きますのでサウナ好きの方は楽しみにしてもらえると嬉しいです。

漁師に出会ったエンジニアがDynamoDBで困った話

こんにちは、ライトハウスの開発をしている熊谷です。

今回、ライトハウスのエンジニアはコード書くだけが仕事ではないということをお伝えしたいと思い、ブログを書きました。

それは、新しい機能やサービスについて考えることです。

開発に限らず社員全員がサービス改善の提案と実行をおこない、 希望をすればエンジニアも顧客に会い、直接ユーザの声を聞くことができます。

このような環境で私の体現した印象深い事例をお伝えします。

大阪ミートアップ

関西エリアの多くの漁師さんに集まっていただきました。 f:id:LighthouseInc:20200629121036j:plain

ご出席いただいた漁師さんには、以下のようなご意見を頂きました。

「漁労機器の一部として無くてはならない存在になりつつある」
「これからも、もっといいサービスを作る会社だと期待している」

ISANAを必要と感じ我々に期待をして下さっていると強く感じ、 自信を持って提供して行きたいと思うと共に、期待を裏切らないよう努力し続けて行きたいと感じることが出来ました。

また、ISANAを利用しているユーザはどのように利用しているのか、どのようなモノが理想なのか意見交換を行い、 漁法や地域によって課題が異なるとご意見を頂き、詳細を確認させて頂きながら現在改善を行っています。

顧客対応への同行

設計中の機能が必要とされているか確認したいという思いがあり、既存顧客のデバイスの不具合対応に3船団同行しました。

不具合対応で伺ったのであまり良い反応をされないだろうと思っていたのですが、実際には「わざわざ来てくれてありがとう」と言って頂き嬉しさと同時に、ご不便おかけして申し訳ない気持ちでいっぱいでした。 漁師の方々は本当にみんな優しいなと感じることができました。

また、不具合対応後に船団の方々をお話をさせて頂き、「いい魚探の反応や反応によっての魚種の判定を写真撮って若手の教育に使う」など若手の教育にISANAを利用していると伺い、これは力になれると確信しました。

地域の異なるいくつかの船団の方々のお話を伺い、改めてISANAは漁労機器の一つとして利用されているケースが多く、海上以外で利用されることがまだ少ないと感じ、 漁労機器を越えその先の世界観を実現させていきたいと考えている我々としては、それを実現するための一つの機能として漁獲記録を3月にリリースしました。

漁獲記録

漁獲記録という機能は手書きでノートに残していた漁獲高をアプリで管理するようにしたモノです。
「いろんな条件で、魚のいるポイントは違ったりするけど過去どこで/何を/どれくらい取ったか見たいな」という声を活かすことができました。
こちらの機能を使って過去の漁のデータを見ることで若い漁師さん成長に役立つことができます。

f:id:LighthouseInc:20200629121119j:plain ※機能の一部の画像です。検証用のデータで顧客のデータではありません。

まだ構想段階でしかないですが、サスティナビリティーやトレーサビリティーで活用していくこともできるんじゃないか。 結果的に魚価が上がって漁師さんに貢献できるのではないかと考えています。 この機能が評価頂き、水産業に関わる国内外のさまざまなプレイヤーが集まる、東京サステナブル・シーフード・シンポジウム2019でサステナブルシーフードアワードを受賞しました。 f:id:LighthouseInc:20200629121147j:plain

DynamoDBで一度に大量のデータが取得できない

漁獲記録を開発する時に、ハマったポイントがあります。

取得するデータというのは3秒に一回、魚探/ソナー/船上カメラ/位置情報のデータをそれぞれDynamoDBに保存しています。
そのデータをApp API(Rails)で取得してタブレットで表示してます。
検証環境でテストを行っていたところ取得したデータが一部取れていない事が判明しました。

船の機器の映像を保存してタブレットで表示されるまでの、アーキテクチャ図です。 f:id:LighthouseInc:20200629121208p:plain

ドキュメント確認したところDynamoの一度に取得には制限があることが判明しました。

Queryの結果セットは、呼び出しあたり 1 MB に制限されます。

「一回でそんなにいっぱい取ってくるな」ということですね。

取得するデータ量を計算してみました。

  • 1件あたりのデータ:約1KB
  • 1時間あたりのデータ数:3600秒 / 3(3秒に一回のデータアップロード) = 1200レコード
  • 1時間あたりのデータ量:1200 x 1KB = 1.2MB

つまり一度に取得できる期間の限界は50分ということがわかりましたので、余裕を持ち、30分毎データを分けて取得するように変更しました。

dynamodb = Aws::DynamoDB::Client.new
dynamodb.query(
 table_name: TABLE_NAME,
 index_name: 'PartitionKey-Time-index',
 select: 'ALL_ATTRIBUTES',
 limit: limit,
 consistent_read: false,
 scan_index_forward: false,
 key_conditions: {
   PartitionKey: {
     attribute_value_list: [partition_key],
     comparison_operator: 'EQ'
   },
   Time: {
     attribute_value_list: [from, to],
     comparison_operator: 'BETWEEN'
   }
 }
)

fromとtoはリクエストパラメータで取得したいデータの起点と終点を指定します。こちらは30分以上の期間を指定した場合エラーが出るようにチェックしています。 この調査で半日以上使ってしまいましたが、予定通りスプリント内にリリースできました。

全員が設計に関わる

受託で開発する場合は仕様が決まっているものを作ることが多いですが、弊社はサービス設計から全員が関わることができます。自分たちで仮説を立てて、顧客対応する担当者がヒアリングをしたり、希望すれば実際に漁師さんにお会いして声を聞くことができます。自分の作ったサービスに自信を持って働ける素晴らしい環境だと思っています!

ISANAの開発について

 みなさんこんにちは、ライトハウスで開発に携わっている石川です。

 当社は、現在主に漁業のサポートを行うプロダクトであるISANA(「いさな」と読みます。)を漁師さんに使っていただいています。今回はそのISANAの開発についてざっくりと紹介したいと思います。

 ただ、技術的な話に入る前に、ISANAが対象としている漁法について簡単に触れさせてください。

 漁業といっても多種多様な漁法が存在しますが、ISANAは主に船びき網漁とまき網漁の漁師さんを対象としたプロダクトとなっています。

 そこで「船びき網漁」と「まき網漁」について簡単に説明いたします。

対象の漁法

船びき網漁とまき網漁は以下の図のとおり、複数の船によって漁が行われます。

船びき網漁(ばっち網漁):2隻の漁船で袋状になった網を曳いて魚を獲る漁法

https://www.maff.go.jp/j/tokei/census/img/funa_biki2.jpg

まき網漁:魚群を網で巻くように包囲して、網を絞り込んで獲る漁法

  • 探索船:魚群を発見する船
  • 網船:運搬船を起点としてぐるっと網で魚群を囲いこむ船
  • 運搬船:捕獲した魚を保存する船

https://www.maff.go.jp/j/tokei/census/img/makiami2.jpg

 こういった複数船隻で行われる漁の場合、これまでは無線を使って各船の魚探やソナーの情報を口頭で共有を行い、漁を行うポイントを決めていました。

 しかし、以下のような複雑な形状を持つ画像の情報を口頭で伝えるのは至難の技ですよね。

f:id:LighthouseInc:20200605145547p:plain

 そこで、ISANAを使ってタブレットで船の位置情報と画像の共有(画面共有機能)を行い、より効率的な漁を行おうという訳です。百聞は一見にしかずです。

 なお、ISANAには、上述した画面共有機能の他にも、その日の出港から帰港までどういったルートを辿ったかを閲覧する機能(操業ログ機能)、投網ごとに漁獲量を記録する機能(漁獲記録機能)などがありますが今回は割愛します。

ISANAのシステム構成

 それでは、ISANAがどのように使われるかが分かったと思うので、続いては、どのようなシステム構成でサービスの提供がなされているかを紹介したいと思います。

ISANAのシステムは、大まかに以下の二つに分けられます。

  • 船上のGPS位置情報や魚探・ソナー画像をアップロードする側
  • アップロードされた情報をタブレットから閲覧する側

構成図にすると、以下の通りです。

f:id:LighthouseInc:20200605152501p:plain

船上のGPS位置情報や魚探・ソナー画像をアップロードする

 ISANAをご利用いただいている漁師さんの船にはISANA専用デバイスが取り付けてあります。

 このISANAデバイスは、船から電源供給を得ておりスイッチを入れると、接続されている魚探やソナー、船上カメラ、GPSの各機器から位置情報や画像データを受け取り、それを専用のAPIに向けて一定間隔で送信しています。

 それを受けとったサーバーサイド側では、画像データはAPIからS3に直接保存し、位置情報(緯度、経度、時間、電波強度、補足しているGPS衛星数)などはDynamoDBに保存しています。

 ISANAデバイスは既に100船団以上に設置されていますが、同時に稼働している船が多い時間帯だと一隻あたり数秒間隔でリクエストがなされるので、多い時ですと8000リクエスト/分にもなります。

 そのため、負荷が高まった際にDynamoDBへPUTされるデータに欠落が生じないように、途中でKinesisを噛ませています。

 私がJOINした頃のISANA開発初期プロトタイプ版ではDynamoDBではなくMySQLにデータに大量の位置情報を保存していて、データが増えるにつれてRSDBSでは捌ききれず画面が全く更新されなくなっていたのを覚えています(笑

 現在もMySQL(Aurora)は使っていますが、数秒間隔で更新される位置情報などはDynamoDBへ保存し、その他のデータについてはMySQLに保存という使い分けを行っています。

 なお、現在のところISANAデバイスのプログラムはPython、Producer, ConsumerはGolangで動いています。

アップロードされた情報をタブレットから閲覧する

 漁中の漁師さんは、操舵室にタブレットを設置し、ISANAの画像共有画面で各船の情報を把握しながら漁を行います。

 タブレット上では、ISANAはAndroid アプリとして動いていて、アプリが情報を取得するために叩くAPIRailsで動いています。この辺はよくある構成だと思うので詳細は割愛します。

 なお、当初は、AngularのWebアプリでしたが、Map上に複数隻の大量の航跡を表示する際にサクサクと動かなくなってきたので、Android アプリに乗り換えたという経緯もあります。

ユーザーの声

 ISANAを使っていただいているユーザさんの声を動画としてまとめているのでよろしければご覧ください。

課題

 もちろんISANAにも課題はあります。

 船によっては、携帯基地局から電波が届く範囲外で漁を行うこともあり、そういった状況下においては、ISANAデバイスからのデータの送信やタブレットからアプリの閲覧することはどうしてもできなくなってしまいます。 画面共有機能の更新間隔は数秒に一回となっていて、もっと間隔を短くして欲しいといった要望もありますが、SIMカードの通信容量の制約やコストの側面などにより実現に至っていません。

総括

 ISANAは様々な課題を抱えていますが、今後もUIや機能の改善を行っていき、漁師さんにもっとご利用いただけるプロダクトを目指して行きたいと考えています。

 ISANAを設置したい場合はこちらのフォーム画面よりお問い合わせください。