カテゴリー: テクノロジー

WebAssemblyを使ってRubyプログラムをWASIアプリケーションとしてブラウザ外で動かす

はじめに

前回の記事ではRubyプログラムをブラウザ上で動かしてみました。今回はRubyプログラムをブラウザ外でWASIアプリケーションとして動かしてみます。Rubyの実行環境込みでパッケージングすることで、可搬性のある配布物を作ることができます。

WASIパッケージを作る

ruby.wasm のページが参考になります。
Dependencies のところに書かれている wasi-vfs と wasmtime を先にインストールします。

wasi-vfsのインストール

wasi-vfs のサイトの Installation の通りです。

$ export WASI_VFS_VERSION=0.4.0
$ unzip wasi-vfs-cli-x86_64-unknown-linux-gnu.zip
$ sudo mv wasi-vfs /usr/local/bin/wasi-vfs

wasmtimeのインストール

wasmtime のサイトの Installation の通りです。

$ curl https://wasmtime.dev/install.sh -sSf | bash

WASIパッケージを作る

まずビルド済の ruby.wasm をダウンロードして展開します。

$ curl -LO https://github.com/ruby/ruby.wasm/releases/latest/download/ruby-3.2-wasm32-unknown-wasi-full.tar.gz
$ tar xfz ruby-3.2-wasm32-unknown-wasi-full.tar.gz
$ mv 3.2-wasm32-unknown-wasi-full/usr/local/bin/ruby ruby.wasm

実行対象のRubyプログラムを作ります。

$ mkdir src
$ echo "puts 'Hello, World!'" > src/my_app.rb

wasi-vfs でパッケージングします。

$ wasi-vfs pack ruby.wasm --mapdir /src::./src --mapdir /usr::./3.2-wasm32-unknown-wasi-full/usr -o my-ruby-app.wasm

実行

wasmtime を使って実行します。実行対象のRubyプログラムをパッケージ内のパスで指定します。

$ wasmtime my-ruby-app.wasm -- /src/my_app.rb
Hello, World!

[AWS] Laravel で ElastiCache を使うときに負荷分散が効く設定

ElastiCache for Redis に対する通信量が問題になり、複数ノード構成にして通信を分散させようという検討をしたことがあります。

ノードを増やすのなら、ということでクラスターモードを有効にしての検証をしたところ、以外にもプライマリノードにリクエストが集中してしまうことが分かりました。AWSの資料によると、接続してすぐに readonly コマンドを実行しないと読み書き両方の可能性があるためプライマリノードにリダイレクトされてしまうとのこと。

自前で ElastiCache とのやり取りを全部実装しているのであれば readonly を最初に発行することで対応できますが、フレームワークを使っている場合そうもいきません。そのときは Laravel で Predis を使っていたのですが、設定ではなんともならず、対応させるには結構な改造が必要になるので断念しました。

そのときの環境では結局、クラスターモード無効にして書き込み有りのときの接続先設定(プライマリエンドポイントに接続)と読み込み専用の接続先設定(リーダーエンドポイント)を別々に用意してプログラム中で使い分けるのが良いということになりました。もちろん読み込み専用で接続したときにはプログラムからreadonlyコマンドを送るわけです。

database.php はこんな感じで、REDIS_HOST_READ と REDIS_HOST_WRITE は .env でそれぞれリーダーエンドポイントとプライマリエンドポイントを設定します。このリーダーエンドポイントの下にレプリカノードを複数用意しておけば負荷分散になります。

'redis' => [
(略)
    'read_cache' => [
        'url' => env('REDIS_URL'),
        'host' => env('REDIS_HOST_READ', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port' => env('REDIS_PORT', 6379),
        'database' => env('REDIS_CACHE_DB', 1),
    ],

    'write_cache' => [
        'url' => env('REDIS_URL'),
        'host' => env('REDIS_HOST_WRITE', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port' => env('REDIS_PORT', 6379),
        'database' => env('REDIS_CACHE_DB', 1),
    ],

],

jqのクエリをインタラクティブに作成する

JSON のデータを扱うときに jq は必須といって良いツールです。

  • 見やすく表示する
  • データを抽出する
  • 特定のデータだけをまとめて新しいJSONファイルを作る

対象のJSONデータの構造を把握していれば jq のクエリを書くのもそれほど苦労はありませんが、そうでない場合は試行錯誤が必要になります。クエリをちょっとずつ変更しながら毎回 jq を叩くのも良いですが、インタラクティブにクエリを書けるツールを使えばもっと楽にできます。

そういう用途のツールとして jiq があります。以下の画像のようにインタラクティブにクエリを書いてJSONデータを掘り下げることができます。最終的なクエリの文字列を標準出力に吐いて jq コマンドに渡すこともできます。

インストール方法は、Linux 系の場合はプロジェクトのダウンロードページから最新版をダウンロードして展開してパスを通すだけです。

jiq と似た jid というツールもあり、こちらは各種ディストリビューションのパッケージも用意されていてインストールがより簡単です。ただし、jiq の方が対応しているクエリの種類が多いのでこちらを使っています。


AWS Lambda で読みがな変換関数を作成する

とある用途で日本語文字列→読みがなの変換処理が必要となったので、AWS Lambda で動作する関数を作成しました。ひらがなに変換する処理で MeCab を、ひらがなからローマ字に変換する処理で KAKASI を使います。KAKASI にもひらがな変換の機能は有りますが、今回の用途では MeCab の方が精度が良かったため、こういう構成となりました。

Python であれば mecab-python3 と pykakasi を使えばプログラムから呼び出せるので、Lambda レイヤーで mecab-python3, pykakasi の環境を作って Lambda 関数で呼び出すようにします。

mecab-python3, pykakasi がインストールされたディレクトリを zip ファイルにまとめてから Lambda コンソールにアップロードします。Lambda は内部的には Amazon Linux 2023 なので、一旦別の Amazon Linux 2023 環境で mecab-python3, pykakasi をインストールしてから zip ファイルを作ります。Amazon Linux 2023 を利用できる環境としては EC2, Docker イメージ, AWS Cloud9 などがありますが、今回は Docker イメージでやりました。
Lambda のランタイムとしては python3.12 も使えますが、Amazon Linux 2023 でパッケージインストールできる python3.11 を採用しました。

$ mkdir mecab-kakasi-python3.11
$ cd mecab-kakasi-python3.11
$ mkdir layers
$ vi Dockerfile
$ vi docker-compose.yml
$ vi laysers/requirements.txt

Dockerfile

FROM amazonlinux:2023

RUN dnf install -y zip python3.11
RUN dnf install -y python3.11-pip
RUN curl https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py
RUN python3 /tmp/get-pip.py

RUN pip3 install setuptools
RUN mkdir /home/layers
RUN mkdir /home/python

docker-compose.yml

version: '3'
services:
  aws-lambda-layers:
    build: .
    volumes:
      - './layers:/home/layers'
    working_dir: '/home/'
    command: sh -c "python3.11 -m pip install -r layers/requirements.txt -t python/ && zip -r layers/file.zip python/" 

requirements.txt

boto3==1.34.54
mecab-python3==1.0.8
ipadic==1.0.0
pykakasi==2.2.1

Docker コンテナ上で上記の設定ファイルを使って環境構築します。

$ sudo docker compose build
$ sudo docker compose up

layers/file.zip が作られたことを確認します。

$ unzip -l layers/file.zip |head
Archive:  layers/file.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  2024-03-03 16:10   python/
        0  2024-03-03 16:10   python/boto3-1.34.54.dist-info/
    10174  2024-03-03 16:10   python/boto3-1.34.54.dist-info/LICENSE
     7546  2024-03-03 16:10   python/boto3-1.34.54.dist-info/RECORD
     6620  2024-03-03 16:10   python/boto3-1.34.54.dist-info/METADATA
        4  2024-03-03 16:10   python/boto3-1.34.54.dist-info/INSTALLER
       92  2024-03-03 16:10   python/boto3-1.34.54.dist-info/WHEEL

file.zip を S3 にアップロード(約30MBあって Lambda コンソールで直接アップロードできないため)してから Lambda レイヤーを作成します。
Lambda コンソール > レイヤー > レイヤーの作成

  • 名前: mecab-kakasi
  • Amazon S3 のリンク URL: アップロードした先の URL
  • ランタイム: python3.11

Lambda コンソール > Lambda 関数 > 関数の作成

  • 一から作成
  • 関数名: hiragana_roman
  • ランタイム: python3.11
  • アーキテクチャ: x86_64
import json
import MeCab
import ipadic
import pykakasi

def lambda_handler(event, context):
    mecab = MeCab.Tagger(f'-Oyomi {ipadic.MECAB_ARGS}')
    kks = pykakasi.kakasi()
    return {
        'statusCode': 200,
        'body': kks.convert(mecab.parse(event['src']).rstrip())
    }

テスト用のイベントJSONとテスト実行結果です。

{ "src": "SPLOUT株式会社" }
{
  "statusCode": 200,
  "mecab": "SPLOUT カブシキガイシャ\n",
  "body": [
    {
      "orig": "SPLOUT",
      "hira": "SPLOUT",
      "kana": "SPLOUT",
      "hepburn": "SPLOUT",
      "kunrei": "SPLOUT",
      "passport": "SPLOUT"
    },
    {
      "orig": "カブシキガイシャ",
      "hira": "かぶしきがいしゃ",
      "kana": "カブシキガイシャ",
      "hepburn": "kabushikigaisha",
      "kunrei": "kabusikigaisya",
      "passport": "kabushikigaisha"
    }
}

Androidアプリでバーコードリーダーから値を受け取る

先日、Android タブレットにUSBでバーコードリーダーを繋いでアプリで読み取る実装を経験しました。基本的にUSB接続のバーコードリーダーはキーボードと同じです。読み取った文字列のキーコードを1文字ずつ送りつけてエンターで終わります。

Android アプリでは setOnKeyListener の onKey でキーイベントを拾って1文字ずつ処理します。プログラムの流れとしては、ループで1文字ずつ繋げてエンターキーを受け取ったら繋げた文字列を処理用の関数に渡す、となっています。コードはこんな感じです。

    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View rootView = binding.getRoot();
        rootView.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View view, int i, KeyEvent keyEvent) {
                if (keyEvent.getAction() != KeyEvent.ACTION_UP) { // キーが押されたときが ACTION_DOWN, 離されたときが ACTION_UP
                    return false;
                }
                int keyCode = keyEvent.getKeyCode();
                if (keyCode == KeyEvent.KEYCODE_ENTER) {  // エンターキーが押されたら終了。ここまで繋げてきた数字の列を処理する。
                    processValue(scannedDigits); 
                    scannedDigits = "";
                    return false;
                } else if (keyCode < KeyEvent.KEYCODE_0 || KeyEvent.KEYCODE_9 < keyCode) { // 数字以外の文字は無視する
                    return false;
                }
                scannedDigits += (char) (keyCode + 41); // ASCII コードに変換
                return false;
            }
        });

        return rootView;
    }

上記は数値をバーコード化したものを読み取る処理です。 KeyEvent.KEYCODE_0とKeyEvent.KEYCODE_9でキーコードの範囲を限定しています。

最初はバーコードリーダーという自分にとっての未知のデバイスを前に身構えていたものの、PCに接続して読み取った文字列をエディタに反映させたりブラウザのフォームを埋めたりしているうちにキーボードと同じという実感を得ていく作業は大変楽しいものでした。