12
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

こんにちは。@komasayukiです。

この記事は、Alibaba CloudのkubernetesやServerless製品(SAEなど)でサービス立ち上げてみよう by Alibaba Cloud Advent Calendar 2023 の18日目です。

家のハムスターが年老いてきて、カリカリ(ドライフード)の餌を食べられなくなってしまいました。

そのため、カリカリを粉にして水に溶かしてあげています。ゼリーも食べられないようで、液体食が必須です。ハムスターなのに餌をほっぺに貯められないので死活問題です。

残念ながら、私が家に帰れない日があり、愛ハムスターに遠隔で餌をあげるシステムを作ることにしました。

以下が完成したハムスター餌やり機です。

ハムスターの自動餌やり機

システム構成は以下のような感じです。

システム構成

今回初めてAlibaba Cloudを使ってみました。
自宅側はRaspberry Piです。

コードはGitHubで公開しています。

液カリ(カリカリの水溶き)をどうやって出すか?

犬猫用の自動餌やり機はAmazonにありました。
しかし、液体の餌に対応したものは見つかりません。

自作するしかないようです。

調べていくと、粘度の高い液カリを出すにはソープディスペンサーなどで使われる、蠕動(ぜんどう)ポンプが衛生面でも最適だとわかりました。

しかし液カリはすぐに乾いてしまうため、コップ+ポンプでは液カリが固まってしまいポンプが詰まります。

ここは既製品のソープディスペンサー新品(送料込み約1500円)をフリマで買いました。ソープを入れるものなので簡単には乾きませんし、ポンプ付きです。下手に蠕動ポンプ単体で買うより安いです。

Pump

ソープディスペンサーの改造

このソープディスペンサーは赤外線センサーに手を近づけたらソープが出る仕組みで、手動ソープONボタンはありません。

これをRaspberry PiからソープON/OFF操作するのが目標となります。

分解して解析しましたが、センサーがシリアル通信で距離を吐いているようです。双方向の通信をしているようで、シリアル通信に介入するのは時間がかかりそうです。リレーで内蔵乾電池とポンプをON/OFFすることにしました。

Raspberry Piでリレーを操作、液カリを出す

Raspberry piからはGPIO(3.3V出力)を使って、トランジスタ(2SC1815)で5Vリレー(オムロン G5V-1)を操作します。回路図は以下のような感じです。

回路図

手持ちにリレーがあったのでこのようにしましたが、モーター(蠕動ポンプ)が動けば何でも大丈夫です。

ソフトウェアの開発

ハードウェアの準備ができました。後はソフトウェアです。

以下の3種類のソフトが必要になります。

  • スマホ
    • Alibaba CloudにRESTでPOST(餌やり指示)する
  • Alibaba Cloud
    • POSTを受信して、Raspberry Piから繋いであるWebSocketに餌やりを通知する
  • Raspberry Pi
    • Alibaba CloudにWebSocketを繋いで、餌やり指示が届いたら一定時間リレーをONにする = ポンプを動かす

スマホ側 - Alibaba CloudにRESTでPOST(餌やり指示)する

スマホ側はソフトを自作せずに、API Testerという無料iPhoneアプリを使いました。

設定を入れるだけでREST操作ができるので便利です。

API Tester

このアプリはiOSのショートカットからも呼び出せるため、ホーム画面からワンタッチで(アプリ遷移なしで)実行できます。

Alibaba Cloud側 - POSTを受信して、Raspberry Piから繋いであるWebSocketに餌やりを通知する

コンテナの作成

Alibaba CloudのServerless App Engineは、AWS ECS+Fargateに似ています。

しかし、こちらはコンテナだけでなく、WAR、JAR、PHPはコンテナ化せずにデプロイ可能です。

今回はTypescriptで開発しますので、Dockerコンテナを動かします。

認証はシークレットとして固定文字列を使います。
シークレットはスマホからはクエリーで渡します。

ハムスターをいじめられたくないので、fail2banっぽく認証に失敗すると一定時間無視するようにしています。

以下、POSTを受け付けるコードの抜粋です。

import express from "express";

const app = express();

//...

// RESTでPOSTされたときの処理
app.post('/', (req, res) => {
    const body = req.body; //POSTされたBody
    const secret = req.query.secret; //secretクエリ(認証に使う)

    const remoteAddress = req.socket.remoteAddress as string;

    if(isBanned(remoteAddress)) { //Banリストに接続IPが含まれているか確認
        res.status(401).send();
        console.log("POST / :", remoteAddress, "is in ban list");
        return;
    }

    if (secret !== SECRET) { //secretクエリが指定した固定文字列と一致するか確認
        console.log("POST / :", remoteAddress, "sent wrong secret:", secret);
        banRemoteAddress(remoteAddress);
        res.status(401).send("Unauthorized");
    }
    else { 
        // secretクエリが一致したので、WebSocketコネクションにブロードキャスト
        connections.forEach((conn) => {
            conn.send(body);
        });

        res.send("Feeding..."); 
    }
});

WebSocketの受付にはexpress-wsを使い、RESTと同一ポートで対応するようにします。

import http from "http";
import expressWs from "express-ws";
import * as ws from "ws";

//...

const httpServer = http.createServer(app);
const appWs = expressWs(app, httpServer);

appWs.app.ws('/', function (ws, req) {

    const remoteAddress = req.socket.remoteAddress as string;

    if(isBanned(remoteAddress)) { //Banリストに接続IPが含まれているか確認
        ws.close();
        console.log(remoteAddress, "is in ban list");
        return;
    }

    if(req.query.secret !== SECRET) { //secretクエリが指定した固定文字列と一致するか確認
        console.log("Websocket", remoteAddress, "sent wrong secret:", req.query.secret, req.query);
        banRemoteAddress(remoteAddress);
        ws.close();
        return;
    }

    console.log("Websocket", remoteAddress, "connected");
    connections.push(ws);

    ws.on('close', function (msg) { //WebSocketが切断されたらブロードキャスト先から削除
        console.log('close');
        connections.splice(connections.indexOf(ws), 1);
    });

});

Typescriptをビルドして、以下のDockerfileでコンテナ化します。脆弱性を減らすためにdistrolessを実行用イメージとして使います。

ARG BUILD_IMAGE=node:20.1.0
ARG RUN_IMAGE=gcr.io/distroless/nodejs20-debian11

# Build stage
FROM $BUILD_IMAGE AS build-env
COPY . /app
WORKDIR /app
RUN npm ci && npm run build

# Prepare production dependencies
FROM $BUILD_IMAGE AS deps-env
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# Create final production stage
FROM $RUN_IMAGE AS run-env
WORKDIR /usr/app
COPY --from=deps-env /node_modules ./node_modules
COPY --from=build-env /app/dist/index.js ./index.js
COPY package.json ./

ENV NODE_ENV="production"
EXPOSE 8080
CMD ["/usr/app/index.js"]

あとはdocker buildしてimageを作ります。

% docker build -t feed-server .

Alibaba Cloud側の前準備

Serverless App Engineの利用前に

  • VPC
  • vSwitch (= AWSではサブネット)
  • Security Group

を先に作る必要があります。VPCの画面に辿り着けば簡単に作れます。

VPC

AWSと違い、VPCやSecurity Groupは最初から存在しません。しかし、近年、AWSでもデフォルトセキュリティグループは使わないプラクティスが流行っていますし、これらは意識して作ると安全かもしれません。

コンテナレジストリにPush

Docker imageはContainer Registry(AWSではECR)にpushします。
AWS ECRと同じくブラウザでサンプルコマンドが表示され、dockerコマンドでpushできます。
Container RegistryにはPersonalとEnterpriseの2種類があり、Personalを使いました。

Registry

Serverless App Engineのセットアップ

Create Applicationを押すと、アプリを簡単に立ち上げできます。

SAE1

最安のインスタンスで、1分当たり0.0005292ドルかかると表示されています。
1ヶ月で3500円ぐらいですね。
(AWS ECSの最弱インスタンスだと6500円ぐらいなので安いです)

Nextを押します。

Screenshot 2023-12-14 at 17.11.48.png

次の画面では、先程pushしたコンテナイメージを選択します。
あと環境変数SECRETを設定して、ヘルスチェックを指定しました。

これでインスタンスが立ち上がります。

Server Load Balancerの追加

しかし、まだインターネットからアクセスできません。SAEのBasic Informationから、
Public Endpoint: Add Internet-facing SLB Instanceを押して、SLB(Server Load Balancer)を追加します。

SLB

ここではhttpのPortバインディングを入力します。
SLBはWebSocketに対応しています。
(ws -> http, wss -> httpsで設定できます)

Port

8080でRESTとWebSocketを両方使えるようにしているので、Portバインディングは1つだけです。実運用前にはhttpsに切り替えてください。同じ画面で設定できます。(証明書が必要です)

SLBが作れたら、Public IPが表示されます。インターネットからアクセス可能になっています。

Serverless App Engineにインターネットから直アクセスしたい場合は、EIPをアタッチする方法もあります。(AWS ECSと違い、インスタンス=TaskにPublic IPは発行してないため、設定が必要)

これはAWSのEIP的なものですが、Alibaba CloudのEIPは通信帯域 or データ転送量で課金されます。

小規模にhttpsを利用したい場合、SLB経由より、EIP(データ転送量課金)でインターネットアクセス可能にして、httpsの構成はSLBのインスタンス内で行うと安価になります。

では、これで構築が完成していますので、コマンドラインでテストしましょう。

# Websocketで接続、次のcurl POSTコマンドを実行した後に、feedと表示されたらOK
% wscat -c "ws://12.34.56.78?secret=YOUR-SECRET-TOKEN"
Connected (press CTRL+C to quit)
< feed
# スマホ側でPOSTする操作と同じことをcurlで実行する
% curl -X POST -d 'feed' "http://12.34.56.78?secret=YOUR-SECRET-TOKEN"

これでAlibaba Cloud側は準備完了です。

Serverless App Engineはアプリケーションを簡単にStopできますが、SLBは残りますので、課金を完全に止めたい場合はSLBを削除しましょう。

Raspberry Pi側 - Alibaba CloudにWebSocketを繋いで、餌やり指示が届いたら一定時間リレーをONにする

Raspberry Piからは、まずWebSocketでAlibaba CloudのServerless App Engineに接続します。(Server Load Balancer経由で)

import WebSocket from 'ws';

//urlにはServer Load Balancerのws://IP-ADDRESS?secret=YOUR-SECRET-TOKENが入っている
const ws = new WebSocket(url);

ws.on('open', () => {
    console.log('Connected to the server');
});

スマホからPOSTすると、Serverless App EngineがWebSocketでRaspberry Piにリレーします。メッセージが来たら一定時間リレーをONにします。


ws.on('message', (message: string) => {
    console.log(`Received message from server: ${message}`);

    if(message === 'feed'){
        startFeeding();
    }
});

実際のリレーON処理は以下のようになっています。

import GPIO from 'rpi-gpio'

const PIN_GPIO_4 = 7; //Raspberry PiのGPIO4は7番ピンに規定されている

function startFeeding(){

    GPIO.write(PIN_GPIO_4, true);

    setTimeout(() => {
        GPIO.write(PIN_GPIO_4, false);
    }, 1000);
}

Raspberry PiのGPIO4がトランジスタのBaseに繋がっています。
GPIO.writetrueにしてあげると、「リレーON = 餌が出る」 になります。

止めないと餌が出っぱなしになりますので、一定時間でfalseにします。

いろいろ試した結果、1秒間リレーをオンにしてポンプを動かすと、ちょうどいい具合に餌やりできました。(餌が少なければ複数回指示をすればいい)

ここまで読んだ方は
「液カリをハムスターの頭上にぶちまけないか?」 と不安になったんじゃないでしょうか?

安心してください。ハムスターのケージにWifiカメラをつけています。いつもニヤニヤしながら愛ハムを見ています。タイミングよく、液カリを出すためにポーリングではなくWebsocketなんです。

Alibaba Cloudを初めて使ってみて

私は普段、AWSを使っており、GCPやAzureの経験も少しあります。

Alibaba Cloudを使ったのは初めてでしたが、今回使ったServerless App Engineはとてもスムーズに使えて驚きました。仕組みがAWSに似ているので、AWSを使っている人は違和感なく利用できるんじゃないかと思います。

Serverless App Engine以外のコストも比較してみたのですが、全体的にコストがAWSより安くなっていて、コストメリットもありそうです。

最後に

実はAlibaba Cloudのアカウント作成で、本人確認がうまくいかず、サポートと10回以上やり取りをしました。

その中で、Alibaba Cloudのサポートが概ね1時間ぐらいで返事をしてくれるのに本当に驚きました。私はAWSのエンタープライズサポートを使っていますが、こんなに早くは返事がきません。

熱心なサポートをしてくれた Alibaba Cloud International Support と、問題解決をしてくれたBackend Teamの方に感謝します。

12
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?