Briswell Tech Blog

ブリスウェルのテックブログです

AIサービス展示会!文書生成から動画検索まで #ITExpo2024

IT Expo 2024春の季節がやってきました。 ブリスウェルはここ数年は毎年春・秋と出展していまして、社内は準備の最終段階です。 今回はAIサービスを中心に展示する予定です。

展示内容その①・・・ AIサービス(文書生成、AI QA、動画検索)

今、空前の話題となっている生成AIを活用したサービスをはじめ、様々なAIサービスを展示します。

1. 報告文書の自動作成、文章生成

文書作成AI

当社のAIサービスは、システムに直接入力された報告のインプットデータと、過去の文書データを基に、AIを利用して報告文章を自動的に生成します。 これによって、従来の手作業による文書作成プロセスを大幅に改善できます。 手間と時間をかけてゼロから文書を作成する必要がなくなるだけでなく、人力では見落としがちな報告すべき内容もAIが網羅します。 これにより、全体的に品質の高い報告文書を短時間で作成することが可能となります。

2. 自動QA、過去の類似QA検索

自動QA、過去の類似QA検索

お客様からの問合せや、特定のテーマに関する反復的な質問に対して、過去の事例や類似のQAデータベースから最適な回答をAIが迅速に提供します。 これは、一貫性のある情報提供を実現し、高品質なカスタマーサービスを提供するための重要な手段です。 さらに、回答作業の労力を大幅に軽減することができます。これにより、お客様サポートスタッフはより複雑な問題解決に集中することができ、全体的な業務効率を向上させます。

3. 動画内ピンポイントシーン検索

動画検索AI
当社のAIサービスは大量の動画データから、ユーザーが指定したキーワードに基づいて目的のシーンをピンポイントで迅速に探し出すことが可能です。 これは、今までになかった新しい画像検索方法です。 従来の検索方法では、ユーザーは時間と労力をかけて動画を一つ一つ見ていく必要がありました。 しかし、このサービスにより、ユーザーはキーワードを入力するだけで瞬時に関連シーンを見つけることができます。 これにより、ユーザーは時間と労力を節約し、より効率的に情報を取得できます。


www.youtube.com

展示内容その②・・・ BW AI Gauge - ブリスウェル・AI・ゲージ 〜 アナログメーター読み取り

BW AI Gauge - ブリスウェル・AI・ゲージ

アナログメーター読み取りに革新をもたらすAI技術を搭載したソリューションです。 これにより、目視によるメーター検針作業の課題が解決され、メーター値の自動記録により誤記、記載漏れ、不正を防ぐことが可能となります。

この技術の一番の魅力は、既存のメーター設備をそのまま利用しながら、付属のカメラを取り付けるだけで検針のデジタル化を容易に実現できることです。 これにより、大規模な設備投資や長期的なメンテナンスを必要とせずに、現行のシステムをアップグレードできます。

さらに、WiFiカメラを使ってメーターを読み取ることができます。これにより、検針作業を迅速かつ正確に行うことが可能となり、人間が行う作業に比べて読み取りの誤差を大幅に減らすことができます。

また、このシステムは、複数地点のデータを一箇所で簡単に管理・監視できる機能も提供します。 これは特に、広範囲に分散した設備を持つ企業や施設にとって大きな利点となります。 様々な場所に設置されたメーターの値を一元的に把握できるため、データの分析や管理が大幅に効率化します。

ブリスウェル・AI・ゲージは、業務の効率化だけでなく、データの正確性向上にも寄与します。 これにより、企業はより迅速かつ正確な意思決定を行うことが可能となり、ビジネスの成長を加速させることができます。

展示内容その③・・・ AI画像解析

当社のAI画像解析は、お客様の活用シーンに応じたカスタマイズと、用途に応じたアプリケーションへの組み込みを実現します。 これにより、最小限のデータを使用して効率的に高品質なモデルを構築することが可能となります。 これは、企画段階から導入、そして運用後のフォローまで、お客様を全面的にサポートするという当社のコミットメントの一環です。

危険エリア侵入検知

危険エリア侵入検知

最新の物体検出技術を活用し、リアルタイムで特定エリアを監視して不正侵入を即座に検出します。 不正侵入があれば、当システムは管理者に速やかに通知が可能で、化学工場や建設現場でも確実な安全対策を提供します。

顔検出機能

顔検出機能

最先端のディープラーニング技術を用いて、顔認証や表情認識など、多岐にわたるソリューションをご提案します。 高精度な検出能力とリアルタイムの処理能力により、マーケティングからエンターテイメント分野まで幅広く応用可能です。

外観検査・異常検出

外観検査・異常検出

AI技術を使用し、画像中の異常を高精度で検出します。外部通知機能や監視UIの提供、教師あり・なし学習の両方をサポートします。 お客様のニーズに合わせて最適な手法を選択し、さらに柔軟にカスタマイズも可能です。 これらの機能により、お客様のビジネスの効率化と品質向上を実現します。

展示内容その④・・・ AWS導入コンサルティング

AWSテクノロジーパートナーとして、クラウド上でのシステム構築実績が多数あるため、様々なニーズに対応したAWS導入支援が可能です。

詳細はこちら。 www.briswell.com

展示内容その⑤・・・ 「アイカタ」中小企業・成長企業向け 受発注工程管理システム

「アイカタ」SMB・成長企業向け 受発注工程管理システム

「アイカタ」は、中小企業や成長を志向する企業向けの受発注工程管理システムです。 このシステムはパッケージ形式のシステムであり、低コストで導入が可能なのが最大の特長です。 また、シンプルな操作性を持ちつつも拡張性の高いクラウドサービスとして提供されており、このため様々な外部システムとのデータ連携が容易になっています。

ERP(基幹システム)

  • 生産管理
  • 在庫管理
  • 販売管理
  • 税制対応
  • API連携機能
  • カスタマイズ対応

受発注システム

  • 在庫管理
  • 顧客情報管理
  • 請求書処理
  • 支払い処理
  • 受注処理
  • リアルタイムな情報更新
  • カスタマイズ対応
  • 分析・レポート
  • 顧客サポートと問合せ管理
  • モバイルデバイス対応

詳細はこちら。 ai-cata.com

展示会概要

■ 展示会名 JAPAN IT WEEK 春 【AI・業務自動化展】

■ 開催日時 2024.4.24(水)〜26(金) 10:00~18:00 ※最終日のみ17:00終了

■ 会場 東京ビッグサイト 東ホール Google Map: https://maps.app.goo.gl/muPr3A2C9RWuGNKm8maps.app.goo.gl

東京ビッグサイト公式サイトはこちら www.bigsight.jp

■ ブース番号 17-14

■ 入場方法 来場登録をお願い致します。 https://www.japan-it.jp/spring/ja-jp/register.html?code=0996212775874600-3GV

■ 相談予約 ご相談の予約も可能です。 以下からご予約下さい。 https://bit.ly/4aoc7id

AWS LightsailでWordpressのサーバーを作成してみる

みな様こんにちは。ブリスウェルのSonです。

最近、WordPressを使って案件を開発しました。サーバーへソースコードを速く反映できるサービスがないか調べてみました。AWSのLightsailというサービスを見つけました。

今回はAWSのLightsailを触ってみましたので、わかったことなど書いていきます。

I. AWS Lightsailとは

① 概要

Amazon Lightsail は、数クリックでウェブアプリケーションやウェブサイトを立ち上げられる、使いやすいクラウドリソースを提供します。

Lightsail は、インスタンス、コンテナ、データベース、ストレージなどの簡素化されたサービスを提供します。Lightsail では、WordPress、Joomla、LAMP などの事前に設定されたブループリントを使用して、ウェブサイトやアプリケーションを簡単に立ち上げることができます。

参照先:https://aws.amazon.com/jp/free/compute/lightsail/?nc1=h_ls

② メリット

  • インストール・導入が簡単
  • 料金がシンプルで計画が立てやすい

③ デメリット

  • 大規模な構成には向いていない
    リソース要件が高く大規模なアプリケーションには適していません。
    これは、CPUやRAMの制限があり、EC2などの他のAWSサービスと比較して柔軟性に欠けるためです。
  • セキュリティグループやIAMなどのような複雑なセキュリティを設定できません

II. Wordpressのサイトを作成してみる

① サイトの初期化

下記のリンクでLightsailコンソールにアクセスする。
https://lightsail.aws.amazon.com/ls/webapp/home/instances

サイトを作成する画面に移動するために、[Create instance]のボタンを押下する。

次の画面でWordpress型やサーバー名などを設定して、[Create instance]のボタンを押下する。

下の画像により、[Wordpress-test]のサーバーの作成が完了しました。

② サイトの管理画面にアクセス

サイトの稼働状況を確認する

サイトを作成したら、[Instances]一覧の画面で[Wordpress-test]サイトを押下する。次の画面の[Connect]タブに[Public IPv4 address]の項目がある。このIPは、[Wordpress-test]サイトのIPです。このIPにアクセスしてみます。

下の画像により、ウェブサイトは正常に動作しています。




サイトの管理エリアにアクセスする方法

管理エリアにアクセスために、ユーザー名とパスワードが必要です。
ユーザー名は[Default WordPress admin user name]の値です。

パスワードの取得するために、CloudShellでコマンドを実行する必要がある。
取得方は下の画像と通りです。

認証情報が分かりましたら、下記のリンクにアクセスする。
http://54.xx.xx.xxx/wp-login.php

ユーザーとパスワードを入力し、[Log In]のボタンを押下する。

下の画像により、ログインできました。



③ ローカル環境でサイトのDBにアクセスする方法

DB情報の取得する方法

LightsailのTerminalを起動するために、[Connect]タブで[Connect using SSH]のボタンを押下する。

[wp-config]ファイルを下記のコマンドで開く。

vi /opt/bitnami/wordpress/wp-config.php

上の画像により、DB情報の取得ができました。

ローカル環境でDBにアクセスする方法

ローカル環境からDBにアクセスするために、SSHのキーをダウンロードする必要があります。

次に、DB情報とサーバー情報とSSHのキーを入力する。入力する方法は下の画像と通りです。
入力したら、[Connect]のボタンを押下する。

ローカル環境からDBへアクセスすることが成功しました。

III. 最後に

AWS LightsailでWordPressのサイトを簡単に作成でき、コストも手頃です。
使いやすく信頼性が高い、小規模なプロジェクトに最適なサービスです。

最後まで読んでいただきありがとうございました。
疑問があれば、コメントをしてください。

巡回セールスマン問題

東京はようやく桜が咲きそうです。桜並木の散歩が楽しみです。

今回は、巡回セールスマン問題です。複数の地点を訪れる際の最短経路を見つける問題です。時間の制約があり効率化が求められる現代ではより良い解が必要とされますね。可能であれば、時間を気にせず気の向くままに好きな方向に進んでいきたいですが。

では、巡回セールスマン問題の解法をGoogle Maps APIを利用して確認してみましょう。

設計

画面上で、以下5地点を指定し、Google Maps APIを呼び出して最短経路を取得・確認できるようにします。

  • 出発地
  • 経由地1
  • 経由地2
  • 経由地3
  • 到着地

APIの構築

画面から呼び出すAPIPython(Flaskフレームワークを利用)で構築します。

1. モジュールのインポート
from flask import Flask, render_template, request, jsonify
import googlemaps
from datetime import datetime
import os
2. Google Maps APIを利用
google_maps_api_key = os.environ.get('GOOGLE_MAPS_API_KEY')
gmaps = googlemaps.Client(key=google_maps_api_key)
3. 初期表示用
@app.route('/')
def index():
    return render_template('index.html', google_maps_api_key=google_maps_api_key)
4. 経路計算API

画面からの入力(出発地、経由地、到着地)を受け取り、Google Maps Directions APIを呼び出して最適な経路を計算します。

@app.route('/compare_modes', methods=['GET'])
def compare_modes():
    # 出発地(origin)、到着地(destination)、経由地(waypoints)を取得
    origin = request.args.get('origin')
    destination = request.args.get('destination')
    waypoints = filter(None, [request.args.get(f'waypoint{i}') for i in range(1, 4)])
    optimize = request.args.get('optimize') == 'true'  # 経由地の最適化オプション
    waypoints_str = '|'.join(waypoints) if waypoints else None
    optimize_waypoints = optimize

    try:
        # Google Maps Directions APIを呼び出す
        directions_result = gmaps.directions(origin, # 出発地
                                              destination, # 到着地
                                              mode="driving",  # 移動手段:driving
                                              waypoints=waypoints_str, # 経由地
                                              optimize_waypoints=optimize_waypoints, #最適化オプション
                                              departure_time=datetime.now(),  # 出発時間:現在時刻
                                              traffic_model='best_guess')  # 交通状態を考慮した時間算出方法:正確に予測

        # 所要時間と距離を合計
        total_duration_seconds = sum(leg['duration']['value'] for leg in directions_result[0]['legs'])
        total_distance_meters = sum(leg['distance']['value'] for leg in directions_result[0]['legs'])

        # 所要時間の変換
        total_hours, remainder = divmod(total_duration_seconds, 3600)
        total_minutes = remainder // 60

        # 距離の変換
        total_distance_km = total_distance_meters / 1000

        # 所要時間と距離を整形
        duration_text = f"{total_hours}時間{total_minutes}分" if total_hours else f"{total_minutes}分"
        distance_text = f"{total_distance_km:.2f}km"

        results = {
            "route": [step["html_instructions"] for leg in directions_result[0]["legs"] for step in leg["steps"]],
            "duration": duration_text,
            "distance": distance_text
        }

        # ログファイルを初期化して出力
        with open('app.log', 'w') as f:
            f.write("")

        custom_print(duration=results['duration'], distance=results['distance'])
        
    except Exception as e:
        return jsonify({"error": str(e)}), 500

    return jsonify(results)
5. ログ表示API
@app.route('/logs')
def view_logs():
    with open('app.log', 'r') as f:
        content = f.read()

    return content

画面の構築

FlaskのHTMLテンプレートを構築します。

1. HTML
<body>
    <div id="map"></div> <!-- 地図表示用 -->
    <div class="form-row"> <!-- ユーザー入力用 -->
        <div class="form-group">
            <label for="start_location">出発地:</label>
            <input type="text" id="start_location" name="start_location" required>
        </div>
        <div class="form-group">
            <label for="waypoint1">経由地1:</label>
            <input type="text" id="waypoint1" name="waypoint1" required>
        </div>
        <div class="form-group">
            <label for="waypoint2">経由地2:</label>
            <input type="text" id="waypoint2" name="waypoint2" required>
        </div>
        <div class="form-group">
            <label for="waypoint3">経由地3:</label>
            <input type="text" id="waypoint3" name="waypoint3" required>
        </div>
        <div class="form-group">
            <label for="end_location">到着地:</label>
            <input type="text" id="end_location" name="end_location" required>
        </div>
    </div>
    <div>
        <div class="form-group">
            <label for="optimize_route">ルートを最適化する:</label>
            <input type="checkbox" id="optimize_route" name="optimize_route">
        </div>
        <div class="form-group">
            <button onclick="callEndpoint()">ルート確認</button>
            <button onclick="reloadPage()">初期化</button>
        </div>
    </div>
    <!-- ログ表示セクション -->
    <div id="log-content">
        <pre id="logs"></pre>
    </div>
</body>
2. Style
<style>
        /* 地図表示領域 */
        #map {
            width: 100%;
            height: 400px;
            margin-bottom: 20px;
        }
        /* フォームとボタンの配置 */
        #main-container {
            display: flex;
            flex-direction: row;
            justify-content: space-between;
            align-items: flex-start;
        }
        .form-row {
            display: flex;
            justify-content: space-between;
            flex-grow: 1;
        }
        .form-group {
            margin-bottom: 15px;
        }
        input[type="text"] {
            margin-top: 5px;
            width: 90%;
        }
        button {
            background-color: #4CAF50;
            color: white;
            padding: 10px 15px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            margin-right: 10px;
        }
        button:hover {
            background-color: #45a049;
        }
        #log-content {
            font-size: 16px;
        }
</style>
3. Javascript
<script>
        var map; // Google Mapオブジェクト
        var clickCount = 0; // 地図クリック回数
        var directionsRenderer; // 経路表示を管理

        // 地図の初期設定と表示
        function initMap() {
            var mapProp = {
                center: new google.maps.LatLng(35.6895, 139.6917), // 地図の中心地点
                zoom: 10, // ズームレベル
            };
            map = new google.maps.Map(document.getElementById("map"), mapProp);
            directionsRenderer = new google.maps.DirectionsRenderer();
            directionsRenderer.setMap(map); // 経路を地図上に表示するための設定

            google.maps.event.addListener(map, 'click', function(event) {
                placeMarker(event.latLng);
            });
        }

        // クリックされた地点の座標を入力
        function placeMarker(location) {
            clickCount++;
            var latRounded = location.lat().toFixed(6); // 緯度を小数点以下6桁に丸める
            var lngRounded = location.lng().toFixed(6); // 経度を小数点以下6桁に丸める
            var locationRounded = latRounded + ',' + lngRounded; // 緯度・経度を結合

            // 最初のクリックは出発地、2〜4回目のクリックは経由地、5回目のクリックは到着地に設定
            if (clickCount === 1) {
                document.getElementById('start_location').value = locationRounded;
            } else if (clickCount >= 2 && clickCount <= 4) {
                document.getElementById('waypoint' + (clickCount - 1)).value = locationRounded;
            } else if (clickCount === 5) {
                document.getElementById('end_location').value = locationRounded;
                clickCount = 0;
            }
        }

        // 経路表示
        function displayRoute(origin, destination, waypoints, optimize) {
            directionsRenderer.setDirections({routes: []}); // 前の経路をクリア

            var directionsService = new google.maps.DirectionsService();
            var waypointArray = waypoints.filter(value => !!value).map(location => ({
                location: location,
                stopover: true
            }));

            var request = {
                origin: origin,
                destination: destination,
                waypoints: waypointArray,
                optimizeWaypoints: optimize, // 経路の最適化
                travelMode: 'DRIVING' // 移動手段はDRIVING
            };

            directionsService.route(request, function(result, status) {
                if (status == 'OK') {
                    directionsRenderer.setDirections(result);
                }
            });
        }

        // 経路計算
        function callEndpoint() {
            var startLocation = document.getElementById('start_location').value;
            var endLocation = document.getElementById('end_location').value;
            var waypoint1 = document.getElementById('waypoint1').value;
            var waypoint2 = document.getElementById('waypoint2').value;
            var waypoint3 = document.getElementById('waypoint3').value;
            var optimizeRoute = document.getElementById('optimize_route').checked;

            if (!startLocation || !endLocation || !waypoint1 || !waypoint2 || !waypoint3) {
                alert("すべて入力してください。");
                return;
            }

            // compare_modes API を呼び出す
            fetch(`/compare_modes?origin=${startLocation}&destination=${endLocation}&waypoint1=${waypoint1}&waypoint2=${waypoint2}&waypoint3=${waypoint3}&optimize=${optimizeRoute}`)
                .then(response => response.json())
                .then(data => {
                    loadLogs();
                })
                .catch(error => console.error('Error:', error));
            
            displayRoute(startLocation, endLocation, [waypoint1, waypoint2, waypoint3], optimizeRoute);
        }

        // ログ表示
        function loadLogs() {
            // logs API を呼び出す
            fetch('/logs')
            .then(response => response.text())
            .then(data => {
                document.getElementById('logs').textContent = data;
            })
            .catch(error => console.error('Error:', error));
        }

        // 初期化
        function reloadPage() {
            location.reload();
        }
</script>

デモ

  1. 地図を表示します。
  2. 地図上で「出発地」、「経由地」、「到着地」をクリックして各地点の緯度・経度を入力します。
  3. 「ルート確認」ボタンをクリックすると、指定した経由地順のルートを表示します。
  4. 「ルートを最適化する」チェックを入れると、経由地を効率的に巡る最短ルートを表示します。

最適化するときれいな一本線になりますね!

業務の効率化はとても大事です。
ただ人生は、寄り道してその場所で精一杯がんばり、次の寄り道をするのも大事かと思います。あの時の大失敗、今思い返すといい思い出ですよね。

Middyを使用することで、Lambda向けのMiddlewareの作成を簡易化する。

I. はじめに

こんにちは。
ブリスウェルのSonです。

Lambdaを使ってNode.jsアプリを開発する際、いくつかの困っていることに直面しています。

・複雑なエラー処理の実装
・認証や権限の実装
・安全な環境変数の管理
・パフォーマンス最適化

その改善策としてフレームワークを探していたところ、Middyが良さそうだと感じて、実際に使ってみた。

II. Middyとは

① 概要

AWS LambdaのためのスタイリッシュなNode.jsミドルウェアエンジンです。
Lambdaコードを整理し、重複を外し、ビジネスロジックに集中する。

② メリット

  • すぐに使用できる豊富な公式のミドルウェアとユーティリティと一緒に提供されています。
  • 最小のコアを保持することで、関数のサイズを小さくし、Cold Startをコントロールします。
  • 簡単に拡張できます。

③ 機能紹介

1. warmup

Lambdaのコールドスタートの問題を軽減するために使用されます。

使用例

const middy = require('@middy/core')
const warmup = require('@middy/warmup')

const isWarmingUp = (event) => event.isWarmingUp === true

const lambdaHandler = (event, context, cb) => {
  /* ... */
}

export const handler = middy()
  .use(warmup({ isWarmingUp }))
  .handler(lambdaHandler)

2. do-not-wait-for-empty-event-loop

タイムアウトが発生しないようにするために使用されます。例えば、データベースへの接続する時。

使用例

import middy from '@middy/core'
import doNotWaitForEmptyEventLoop from '@middy/do-not-wait-for-empty-event-loop'

const lambdaHandler = (event, context) => {
  /* ... */
}

export const handler = middy()
  .use(doNotWaitForEmptyEventLoop({ runOnError: true }))
  .handler(lambdaHandler)

3. http-json-body-parser

HTTPリクエストを自動的に解析し、JSON形式の本文をオブジェクトに変換する。

使用例

import middy from '@middy/core'
import httpJsonBodyParser from '@middy/http-json-body-parser'

const lambdaHandler = (event, context) => {
  /* ... */
}

export const handler = middy()
  .use(httpJsonBodyParser())
  .handler(lambdaHandler)

4. validator

自動的に着信イベントと送信レスポンスをカスタムスキーマに対して検証します。

注意のこと:
eventSchemaまたはresponseSchemaの少なくとも1つが必要です。

使用例

import middy from '@middy/core'
import validator from '@middy/validator'
import httpJsonBodyParser from '@middy/http-json-body-parser'
import { transpileSchema } from '@middy/validator/transpile'

const lambdaHandler = (event, context) => {
  /* ... */
}

const eventSchema = {
  type: 'object',
  required: ['body'],
  properties: {
    body: {
      type: 'object',
      required: ['name', 'email'],
      properties: {
        name: { type: 'string' },
        email: { type: 'string', format: 'email' }
      }
    }
  }
}

export const handler = middy()
  .use(httpJsonBodyParser())
  .use(
    validator({
      eventSchema: transpileSchema(eventSchema)
    })
  )
  .handler(lambdaHandler)

5. http-content-encoding

レスポンスのHTTP Content-Encodingヘッダーを設定し、レスポンス本文を圧縮します。

使用例

import middy from '@middy/core'
import httpContentNegotiation from '@middy/http-content-negotiation'
import httpContentEncoding from '@middy/http-content-encoding'
import { constants } from 'node:zlib'

const lambdaHandler = (event, context) => {
  /* ... */
}

export const handler = middy()
  .use(httpContentNegotiation())
  .use(httpContentEncoding({
    br: {
      params: {
        [constants.BROTLI_PARAM_MODE]: constants.BROTLI_MODE_TEXT, // adjusted for UTF-8 text
        [constants.BROTLI_PARAM_QUALITY]: 7
      }
    },
    overridePreferredEncoding: ['br', 'gzip', 'deflate']
  })
  .handler(lambdaHandler)

6. http-cors

Cross-Originリクエストを実行するために必要なAccess-Control-Allow-OriginAccess-Control-Allow-Headers、および Access-Control-Allow-Credentialsを含むHTTP CORSヘッダーをレスポンスオブジェクトに設定するために使用されます。

使用例

import middy from '@middy/core'
import cors from '@middy/http-cors'

const lambdaHandler = (event, context) => {
  /* ... */
}

export const handler = middy()
  .use(cors())
  .handler(lambdaHandler)

7. secrets-manager

AWS Secrets Managerからパラメーターを取得して、関数ハンドラのcontextのオブジェクトにアサインされる。

注意のこと
・Lambdaは、secretsmanager:GetSecretValueのIAM権限を持っている必要があります。

使用例

import middy from '@middy/core'
import secretsManager from '@middy/secrets-manager'

const lambdaHandler = (event, context) => {
  /* ... */
}

export const handler = middy()
  .use(
    secretsManager({
      fetchData: {
        apiToken: 'dev/api_token'
      },
      awsClientOptions: {
        region: 'us-east-1'
      },
      setToContext: true
    })
  )
  .handler(lambdaHandler)

8. ssm

AWS Systems Manager Parameter Storeからパラメータを取得して、関数ハンドラのcontextのオブジェクトにアサインされる。

注意のこと
・Lambdaは、ssm:GetParametersのIAM権限を持っている必要があります。

使用例

import middy from '@middy/core'
import { getInternal } from '@middy/util'
import ssm from '@middy/ssm'

const lambdaHandler = (event, context) => {
  /* ... */
}

let globalDefaults = {}
export const handler = middy()
  .use(
    ssm({
      fetchData: {
        accessToken: '/dev/service_name/access_token',
        dbParams: '/dev/service_name/database/'
      },
      cacheExpiry: 15 * 60 * 1000,
      cacheKey: 'ssm-secrets'
    })
  )
  .before(async (request) => {
    const data = await getInternal(
      ['accessToken', 'dbParams', 'defaults'],
      request
    )
    Object.assign(request.context, data)
  })
  .handler(lambdaHandler)

9. sts

他のAWSサービスに接続する際に使用するSTSAWS Security Token Service)資格情報を取得することのようなシーンで使うことがあります。

注意のこと
sts:AssumeRoleのIAM権限が必要です。

使用例

import middy from '@middy/core'
import sts from '@middy/sts'

const lambdaHandler = (event, context) => {
  /* ... */
}

export const handler = middy()
  .use(
    sts({
      fetchData: {
        assumeRole: {
          RoleArn: '...',
          RoleSessionName: ''
        }
      }
    })
  )
  .handler(lambdaHandler)

III. 終わりに

AWS Lambda開発者が開発プロセスを効率化したいと考えている場合には最適なツールです。

Middyは軽量でモジュール化されたアプローチを採用しており、問題を分離し、重複を減らし、Lambda関数の主要なビジネスロジックに焦点を当てることができます。

AWS Lambdaの開発に強力で柔軟なミドルウェアフレームワークをお探しの場合は、ぜひMiddyをご検討ください。

AWS Textractを利用してみました

みな様こんにちは。
ブリスウェルのSonと申します。
最近、暖かい日が続くようになり、春も近づいてきましたね。

OCRについて調べていたところ、AWSのTextractというサービスを見つけました。
今回はAWSのTextractを触ってみましたので、わかったことなど書いていきます。

I. Textractとは

① 概要

Textractは、印刷されたテキスト、手書きの文字、レイアウト要素、データを、あらゆるドキュメントから自動的に抽出する
https://aws.amazon.com/jp/textract

② メリット

  1. テキスト認識プロセスの自動化
  2. AWSの他のサービスとの柔軟な統合

③ デメリット

  1. 大規模プロジェクトには高いコストがかかる
    https://aws.amazon.com/jp/textract/pricing

  2. 一部の地域のみをサポートしており、日本語がまだサポートされていません

II. 使ってみる

Nodejs、Textractを利用して以下のようなフローを実装してみます。

S3バケットに抽出したいpdfファイルをアップロードする。S3バケットからRunの関数を発火し、その後にTextractを実行する。

もしTextractが実行完了したら、[AWS SNS]からCompleteの関数を発火し、Textractの結果からjsonファイルをS3バケットへ保存する。

① 初期化

リソースやLambdaのソースコードをデプロイするために、今回はServerlessのフレームワークを利用します。
導入方法などは下記のリンクを参照してください。

https://www.serverless.com

記載された処理により、3つのs3、textract、util-utf8のライブラリをインストールする必要がある。

プロジェクトの下にpackage.jsonファイルを以下の内容で作成する。

{
  "name": "textracttest",
  "version": "0.0.1",
  "license": "UNLICENSED",
  "devDependencies": {
    "serverless": "3.38.0"
  },
  "dependencies": {
    "@aws-sdk/client-s3": "^3.529.1",
    "@aws-sdk/client-textract": "^3.529.1",
    "@smithy/util-utf8": "^2.2.0"
  }
}

② serverless.ymlファイルの設定

LambdaとTextractのロールの作成

TextractとLambdaを実行できるように、最初にxxxlambdaDevRole、xxxTextractDevRoleのロールを2つ作成する必要がある。

serverless.ymlファイルで下記の設定を追加する。

service: textract
frameworkVersion: "3"

provider:
  name: aws
  runtime: nodejs20.x
  region: us-east-1
  stage: dev

resources:
  Resources:
    xxxlambdaDevRole:
      Type: AWS::IAM::Role
      Properties:
        RoleName: xxxlambdaDevRole
        AssumeRolePolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
              Action: sts:AssumeRole
        Policies:
          - PolicyName: xxxlambdaDevPolicy
            PolicyDocument:
              Version: '2012-10-17'
              Statement:
                - Effect: Allow
                  Action:
                    - logs:*
                    - s3:*
                    - textract:*
                    - iam:PassRole
                  Resource:
                    - '*'
    xxxTextractDevRole:
      Type: AWS::IAM::Role
      Properties:
        RoleName: xxxTextractDevRole
        AssumeRolePolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - textract.amazonaws.com
              Action: sts:AssumeRole
        ManagedPolicyArns:
          - arn:aws:iam::aws:policy/AmazonSNSFullAccess
          - arn:aws:iam::aws:policy/AmazonTextractFullAccess
          - arn:aws:iam::aws:policy/AWSLambdaExecute

上記の設定部分を書いたら、AWSへ2つのロールをデプロイするために、下記のコマンドを実行する。

serverless deploy

上の画像により、xxxlambdaDevRoleとxxxTextractDevRoleの作成が完了しました。

S3バケットSNSの作成

次に、xxxTextractSnsTopic-devSNSxxxtextract-devのS3バケットを作成する必要があり、serverless.ymlファイルで[resources].[Resources]の下に、下記のソースコードを追加する。

    xxxTextractBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: xxxtextract-dev 
    xxxTextractSns:
      Type: AWS::SNS::Topic
      Properties:
        TopicName: xxxTextractSnsTopic-dev    
        DisplayName: xxxTextractSnsDisplay-dev

SNSとS3バケットを作成するために、下記のコマンドをまた実行する。

serverless deploy

画像により、SNSとS3バケットの作成が完了しました。

Lambdaの環境変数の設定

次に、serverless.ymlファイルのproviderで下記の設定を追加する。

iam:
  role: xxxlambdaDevRole
environment:
  S3_BUCKET_NM: xxxtextract-dev
  SNS_TOPIC_NAME: xxxTextractSnsTopic-dev
  SNS_TOPIC_ARN: !Ref xxxTextractSns
  TEXTRACT_ROLE_ARN: 
    Fn::GetAtt: [xxxTextractDevRole, Arn]

Lambdaのソースコードの作成

次に、プロジェクトの[dist]フォルダーの下に2つのLambdaを作成する必要がある。

* run.jsのファイル

const { TextractClient, StartDocumentTextDetectionCommand } = require("@aws-sdk/client-textract");

exports.handler = async (event) => {
  try {
    const s3Bucket = event.Records[0].s3.bucket.name;
    const s3Key = event.Records[0].s3.object.key;

    const textractCli = new TextractClient();
    await textractCli.send(new StartDocumentTextDetectionCommand({
      DocumentLocation: {
        S3Object: {
          Bucket: s3Bucket,
          Name: s3Key
        }
      },
      NotificationChannel: {
        SNSTopicArn: process.env.SNS_TOPIC_ARN,
        RoleArn: process.env.TEXTRACT_ROLE_ARN
      }
    }));
    
    return true;
  } catch (error) {
    console.error('Error: ', error);
    return false;
  }
};

* complete.jsのファイル

const { TextractClient, GetDocumentTextDetectionCommand } = require("@aws-sdk/client-textract");
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3");
const { fromUtf8 } = require("@smithy/util-utf8");

exports.handler = async (event) => {
  try {
    const message = JSON.parse(event.Records[0].Sns.Message);

    if (message.Status === 'SUCCEEDED' && message.JobId) {
      const textractCli = new TextractClient();
      const result = await textractCli.send(new GetDocumentTextDetectionCommand({
        JobId: message.JobId
      }));

      const texts = result.Blocks
        .filter((block) => block.BlockType === 'LINE')
        .map(block => block.Text);

      const s3Cli = new S3Client();
      await s3Cli.send(new PutObjectCommand({
        Bucket: process.env.S3_BUCKET_NM,
        Key: `result/${message.JobId}.json`,
        Body: fromUtf8(JSON.stringify(texts)),
        ContentType: 'application/json'
      }));

      return true;
    }
    
  } catch (error) {
    console.error('Error: ', error);
    return false;
  }
};

Lambdaのデプロイし方の設定

Lambdaのデプロイするために、serverless.ymlファイルで下記の設定を追加する必要がある。

functions:
  run:
    handler: dist/run.handler
    events:
      - s3:
          bucket: ${self:provider.environment.S3_BUCKET_NM}
          event: s3:ObjectCreated:*
          rules:
            - prefix: input/
            - suffix: .pdf
          existing: true
          forceDeploy: true
  complete:
    handler: dist/complete.handler
    events:
      - sns:
          arn: !Ref xxxTextractSns
          topicName: ${self:provider.environment.SNS_TOPIC_NAME} 

下記のコマンドをまた実行する必要がある。

serverless deploy

③ テスト

こんな内容のようなpdfファイルをテストする。

inputフォルダーでpdfファイルをアップロードする。

resultフォルダーjsonファイルが自動的に作成されました。

抽出した内容を確認は、*.jsonファイルを開きます。

["Adobe Acrobat PDF Files","AdobeR Portable Document Format (PDF) is a universal file format that preserves all","of the fonts, formatting, colours and graphics of any source document, regardless of","the application and platform used to create it.","Adobe PDF is an ideal format for electronic document distribution as it overcomes the","problems commonly encountered with electronic file sharing.","Anyone, anywhere can open a PDF file. All you need is the free Adobe Acrobat","Reader. Recipients of other file formats sometimes can't open files because they","don't have the applications used to create the documents.","PDF files always print correctly on any printing device.","PDF files always display exactly as created, regardless of fonts, software, and","operating systems. Fonts, and graphics are not lost due to platform, software, and","version incompatibilities.","The free Acrobat Reader is easy to download and can be freely distributed by","anyone.","Compact PDF files are smaller than their source files and download a","page at a time for fast display on the Web."]

pdfファイルから文字の抽出が完了しました。

III. 最後に

私のデモでは、思ったより簡単に文字などを抽出できました。
システムの自動化、DX化において、AWS Textractは重要な役割を果たすことができると感じます。

この記事では疑問があれば、コメントをしてください。
最後まで読んでいただきありがとうございました。

ローカル環境でS3とLambdaを実行するために、LocalStackを設定してみた

みな様こんにちは。
BriswellのSonと申します。

最近、以下の図に従ってLambdaをテストして実行するのに興味があります。

AWS環境では、上の処理の実行・テストが簡単ですが、localhostではファイルのアップやLambdaの起動などは難しいです。しかし、LocalStackを使用すると、簡単になります。

I. LocalStackとは

① 概要

  • ローカル環境でAWSのサービスのシミュレートするツールです。
  • AWSのリソースを使用せずにアプリケーションをテスト・開発できます。

参照先: https://docs.localstack.cloud/overview

② メリット

  1. ローカル開発への便利性
    AWSのリソースを実際に使用せずに、ローカル環境でアプリケーションを開発およびテストする環境を提供します。

  2. コスト削減
    AWSの実際のサービスを使用する必要がないため、開発およびテストにかかる費用を削減できます。

  3. 多くのAWSサービスのシミュレート
    S3、LambdaなどのさまざまなAWSサービスをシミュレートできる。これにより、さまざまなサービスを使用してアプリケーションをテストできます。

③ デメリット

  1. シミュレーションの制限
    AWSの一部のサービスを提供しますが、すべての機能が完全にシミュレートされているわけではありません。

  2. パフォーマンスの低下
    シミュレーション環境でのLocalStackのパフォーマンスは、AWSの実際の環境と比較して期待されるほど高くないことがあります。

  3. 複雑な設定
    設定は、新しいユーザーにとって複雑で理解しにくい場合があります。

II. 使ってみる

① イメージ

以下は、LocalStackがソースコードをLambdaに直接マウントする方法を示すイラストです。

イラストにより、ローカル環境でソースコードを実行できるために、LocalStackとDockerを利用する必要があります。

② 環境構築

LocalStackのインストール

macの場合】Terminalで下記のコマンドを実行する必要がある

brew install localstack/tap/localstack-cli
brew install awscli-local

【他のプラットホーム】下記のリンクを参照できます。 https://docs.localstack.cloud/getting-started/installation/

インストールしたら、LocalStack CLIとLocalStack AWS CLIのバージョンを確認するために、下記のコマンドを実行する。

localstack --version
awslocal --version  

Dockerのインストール

docker: https://docs.docker.com/get-docker/
docker-compose (version 1.9.0+): https://docs.docker.com/compose/install/

実装

LocalStackとDockerを導入したら、以下の内容でdocker-compose.ymlファイルを作成します。

version: "3.8"

services:
  localstack:
    container_name: localstack_test
    image: localstack/localstack:latest
    ports:
      - "127.0.0.1:4566:4566"            # LocalStack Gateway
      - "127.0.0.1:4510-4559:4510-4559"  # external services port range
    environment:
      - DEBUG=1
      - LAMBDA_REMOTE_DOCKER=0
      - LAMBDA_DOCKER_FLAGS=-e NODE_OPTIONS=--inspect-brk=0.0.0.0:9229 -p 9229:9229
      - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR-}
      - DOCKER_HOST=unix:///var/run/docker.sock
    volumes:
      - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"

Dockerコンテナを起動するために、次のコマンドを実行します。

docker-compose up -d

下の添付した画像により、ローカル環境の作成が完了しました。

③ ローカル環境の管理

常にAPIのコマンドでサービスを管理できますが、もっと簡単に管理するために、下記のURLでアカウントを登録して、コンソールで管理できます。

https://app.localstack.cloud/dashboard

④ ローカル環境でサービスの登録方法

Lambdaの作成方法

functiontestのLambda関数を作成するために、Terminalで下記のコマンドを実行する。

awslocal lambda create-function \
    --function-name functiontest \
    --region ap-northeast-1 \
    --code S3Bucket="hot-reload",S3Key="$(pwd)/" \
    --handler dist/function.handler \
    --runtime nodejs18.x \
    --timeout 300 \
    --role arn:aws:iam::000000000000:role/lambda-role

上のコマンドにより、ソースコードが[dist]フォルダーの[function]ファイルにあります。

注意:Lambdaなどを作成・実行するために、Dockerを起動する必要があります

Lambdaをチェックしたい時に、下記のリンクで参照できます。 https://app.localstack.cloud/inst/default/resources/lambda/functions

上の添付した画像により、functiontestのLambda関数の作成が完了しました。

S3バケットの作成方法

buckettestのS3バケットを作成するために、Terminalで下記のコマンドを実行する。

awslocal s3api create-bucket --bucket buckettest --create-bucket-configuration LocationConstraint=ap-northeast-1

S3バケットをチェックしたい時に、下記のリンクで参照できます。

https://app.localstack.cloud/inst/default/resources/s3

上の添付した画像により、buckettestのS3バケットの作成が完了しました。

S3からLambda実行を発火の設定方法

buckettestのS3バケットからfunctiontestのLambdaを発火するために、Terminalで下記のコマンドを実行する。

awslocal s3api put-bucket-notification-configuration \
    --bucket buckettest \
    --notification-configuration '{
        "LambdaFunctionConfigurations": [
          {
            "Id": "1", 
            "LambdaFunctionArn": "arn:aws:lambda:ap-northeast-1:000000000000:function:functiontest",
            "Events": ["s3:ObjectCreated:*"]
          }
        ]
    }'

LambdaFunctionArnは、作成したLambdaの[Function Arn]です。

LambdaのDebugするために、[Visual Studio Code]の設定方法

プロジェクトの下に.vscodeのフォルダーを作成して、.vscodeの下に2つのファイルを作成する必要がある。

*launch.jsonファイル

{
  "version": "0.2.0",
  "configurations": [
      {
          "address": "127.0.0.1",
          "localRoot": "${workspaceFolder}",
          "name": "Attach to Remote Node.js",
          "port": 9229,
          "remoteRoot": "/var/task/",
          "request": "attach",
          "type": "node",
          "preLaunchTask": "Wait Remote Debugger Server"
      },
  ]
}

*tasks.jsonファイル

{
  "version": "2.0.0",
  "tasks": [
      {
        "label": "Wait Remote Debugger Server",
        "type": "shell",
        "command": "while [[ -z $(docker ps | grep :9229) ]]; do sleep 1; done; sleep 1;"
      }
  ]
}

ローカル環境でS3とLambdaを実行してみましょう

発火を確認するためにはブレークポイントを設定した方が良いです。

ブレークポイントを設定したら、[Visual Studio Code]の[デバッグの開始(F5)]ボタンを押下する。

LocalStackのコンソールのS3管理画面でファイルをアップロードして、[Visual Studio Code]側でアップしたファイルの情報が表示されます。

上記の画像により、ローカル環境でS3からLambdaの発火できました。

最後まで読んでいただきありがとうございました。
疑問があれば、コメントをしてください。

AWS Transfer FamilyでのSFTP環境を構築してみました

みな様こんにちは。
BriswellのSonと申します。

最近、帳票や画像などのファイルを読む・保存するために、SFTPサーバーの設定してみました。

SFTPサーバーは外部ユーザーがアクセスできるので、承認されていないものが公開されることを避けるために、適切な権限を与える必要があります。

AWSクラウドではSFTPサーバーを簡易的に管理するために、[Transfer Family]というサービスを提供しています。

添付した画像を参照すれば、大体な流れがわかると思いますが、詳しい設定方法を追記します。

I. S3バケットの作成

ファイルを保存するため、S3バケットを1つ作成する必要があります。
バケット名を bw-test-transferfamily と設定します。

上記の画像により、バケットの作成が完了しました。

II. Transfer Familyの作成

①. ポリシーの作成
ユーザーを認証するために、最初にIAMサービス側でポリシーを作成しないといけないです。
作成方法は下記の通りです。

[アクセス許可を指定] 画面で [ポリシーエディタ] 項目でJSONを選び、下記のルール内容を入力する。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:ListBucket",
                "s3:GetBucketLocation"
            ],
            "Resource": [
                "arn:aws:s3:::bw-test-transferfamily"
            ],
            "Effect": "Allow",
            "Sid": "CommonS3Role"
        },
        {
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject",
                "s3:DeleteObjectVersion",
                "s3:GetObjectVersion",
                "s3:GetObjectACL",
                "s3:PutObjectACL"
            ],
            "Resource": [
                "arn:aws:s3:::bw-test-transferfamily/*"
            ],
            "Effect": "Allow",
            "Sid": "ReadWriteS3Role"
        }
    ]
}

次の画面は [確認して作成] です。
[ポリシー名] 項目で bw-user-transferfamily-policy を入力し、[ポリシーの作成] ボタンを押下する。

上記の画像により、ポリシーの作成が完了しました。

②. [Transfer Family] のユーザーのロール作成
ユーザーのアクションを管理するために、ロールを作成する必要があります。
詳しい作成方法は下記の通りです。

[信頼されたエンティティを選択] 画面では [信頼されたエンティティタイプ] を [AWS のサービス] に選択し、[ユースケース] 項目で [Transfer] を選択します。
選択後、[次へ] ボタンを押下する。

[許可を追加] 画面で bw-user-transferfamily-policy のポリシーを検索する。
Hitしたレコードから該当のレコードにチェックを入れて、[次へ] ボタンをクリックする。

[名前、確認、および作成] 画面では、[ロール名] で bw-user-transferfamily-role と入力する。
入力後、[ロールを作成] ボタンをクリックする。

上記の画像により、ロールの作成が完了しました。

③. [Transfer Family] 作成
最初に [AWS Transfer Family] 画面で [サーバーを作成] ボタンを押下します。
押下後、[プロトコルを選択] 画面に移動します。

SFTPメソッドでバケットへファイルをアップロードしたいので、[SFTP (SSH ファイル転送プロトコル) - Secure Shell 経由のファイル転送] を選択し、[次へ] ボタンをクリックする。

[ID プロバイダーを選択] 画面で [サービスマネージド] を選択します。
選択後、[次へ] ボタンを押下する。

[エンドポイントを選択] 画面でパブリックでアクセスしたいので、[パブリックアクセス可能] を選択します。
選択後、[カスタムホスト名]は [なし] に選択してもいいです。[次へ] ボタンを押下する。

[ドメインを選択] 画面でS3へ接続したいので、[Amazon S3] を選択します。
選択後、[次へ] ボタンを押下する。

[追加の詳細を設定] 画面でデフォルトのままにして、[次へ] ボタンを押下します。

確認と作成の画面で詳細な設定を確認し、OKだった場合、[作成] ボタンをクリックします。
注意:作成したら、1 時間あたり 0.30USDの料金がかかってしまうので、調査だけの場合は終わり次第削除したほうがいいです。

上記の画像により、[Transfer Family] の作成が完了しました。

④. [Transfer Family] のユーザー作成
ユーザーを作成するために、[Transfer Familyサーバー管理] 画面で先程作成したサーバーIDを押下する。
[詳細なサーバー] 画面に移動し、[ユーザー] 設定のグループで [ユーザーを追加] ボタンを押下する。

[ユーザーを追加] 画面で [ユーザー名] 項目を bw-user-transferfamily と入力し、[ロール] 項目で bw-user-transferfamily-role を選択し、ホームディレクトリで bw-test-transferfamily を選びます。

bw-test-transferfamily/csv のフォルダにファイルをアップしたいので、[オプションのフォルダ] 項目で csv を入力し、[制限付き] のチェックを入れます。

最後に追加ボタンを押下する。

上記の画像により、ユーザーの作成が完了しました。

⑤. ユーザーの [SSH パブリックキー] 追加
SSHキーを作成しておくために、自分のパソコンのTerminalで下記のコマンドを実行する。

ssh-keygen -t rsa -C "bw-test-transferfamily" -f ~/.ssh/bw-test-transferfamily
cat ~/.ssh/bw-test-transferfamily.pub
  • ssh-keygen -t rsa -C "bw-test-transferfamily" -f ~/.ssh/bw-test-transferfamily:[プライベートキー] と [パブリックキー] を作成するため。
  • cat ~/.ssh/bw-test-transferfamily.pub:[パブリックキー] 内容を取得し、[Transfer Family] のユーザーのSSHキー設計部分に追加するため。

SSHキー作成方法は下記の画像の通りです。

SSHキーの作成が完了したら、先程作成した [Transfer Family] のユーザーを押下して、[明細ユーザー] 画面に移動します。
[SSH パブリックキー] 設定のグループで [SSH パブリックキーを追加] ボタンを押下する。

[キーを追加] 画面の [SSH パブリックキー] 項目で作成したばかりキー(~/.ssh/bw-test-transferfamily.pubファイル内容)を入力後、[キーを追加] ボタンを押下する。

上記の画像により、SSH パブリックキーの追加が完了しました。

III. テスト

今回S3へファイルをアップロードするために、Cyberduckを使います。
設定方法は下記の画像の通りです。

Connection型が、[SFTP]
Sever: Transfer Familyサーバのエンドポイントを参照
Username: Transfer Familyサーバのユーザー名
SSH Private Key: 自分のパソコンでSSHキーを作成した時に、このファイルを作成しました。

必須項目の入力が完了したら、[Connect] ボタンを押下する。
問題がなければS3の bw-test-transferfamily/csvにあるファイルが表示されます。

上記の画像により、CyberduckアプリでS3へファイルをアップロードできました。

最後に

今回は、CyberduckなどからS3にSFTPの方法でアップロードするために、SFTPサーバの構成について共有しました。
最後まで読んでいただきありがとうございました。
疑問があれば、コメントをしてください。

WebRTCを使ってビデオ通話アプリを作ってみました

皆さん、こんにちは! 最近は天気が暖かくなってきましたね。もうすぐ花見の時期になってきて楽しみですね。

皆さん、仕事をする時はビデオ通話のサービスをよく使っていますか?Google MeetsかZoomか、色んなビデオ通話のサービスがないと仕事に困りますね。 自分の方はこれらのサービスはどうすれば作れるかちょっと気になったので、調べて作ってみました。 新しい技術ではないですが、WebRTCという技術が存在します。 WebRTCを使ったら、誰でもビデオ通話アプリを作れます。

WebRTC(英語: Web Real-Time Communication)は、ウェブブラウザやモバイルアプリケーションにシンプルなAPI経由でリアルタイム通信(英語: real-time communication; RTC)を提供する自由かつオープンソースのプロジェクトである。ウェブページ内で直接ピア・ツー・ピア通信を行うことによって、プラグインのインストールやネイティブアプリのダウンロードを行わなくても、ウェブブラウザ間のボイスチャットビデオチャット、ファイル共有などを実装できるようになる。

wikipediaによる


アプリの概要

  • ビデオ通話の時、ビデオORオーディオの通信を自由に切れます。
  • 画面共有はできます。

使う技術:WebRTC + socket.io

処理のフロー

処理のフローはこの下のフローチャートを参考してください。

flowchart
処理のフローチャート

一部のサンプルのソースコード

クライアント側のソースコード

  • navigator.mediaDevices.getUserMediaで自分のPCのビデオとオーディオのデータストリームを取得する:
const localStream = ref(undefined);
...
localStream.value = await navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true,
  });
  • RTCPeerConnectionを作成して、オファーを送る:
// 後の処理に必要
const pcArr: { toSocketId: string; pc: RTCPeerConnection }[] = [];
...
const pc = new RTCPeerConnectionRTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.services.mozilla.com' },
    { urls: 'stun:stun.l.google.com:19302' },
  ],
});
pcArr.push({ toSocketId: user.socketId, pc });

// ビデオを表示するため
const remoteVideoData = reactive({
  socketId: user.socketId,
  mediaStream: undefined as MediaStream | undefined,
  userName: user.name,
});
if (!remoteVideo.value.find((video) => video.socketId === user.socketId)) {
  remoteVideo.value.push(remoteVideoData);
}

pc.ontrack = (event) => {
  if (
    remoteVideoData.mediaStream === undefined ||
    remoteVideoData.mediaStream.id === event.streams[0].id
  ) {
    // ウェブカムからのビデオ
    remoteVideoData.mediaStream = event.streams[0];
  } else {
    // 画面共有からのビデオ
    screenSharingActive.value = true;
    screenSharingStream.value = event.streams[0];
    screenSharingVideo.value!.srcObject = screenSharingStream.value;
  }
};

localStream.value!.getTracks().forEach((track) => {
  pc.addTrack(track, localStream.value!);
});

const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

io.emit('room.offer', user.socketId, pc.localDescription, userName.value);

pc.onicecandidate = (event) => {
  if (event.candidate) {
    io.emit('room.candidate', user.socketId, event.candidate);
  }
};
  • オファー情報を保存する、アンサー情報を作って送る:
io.on('room.offer', async (socketId: string, offer: RTCSessionDescriptionInit, name: string) => {
  const pc = new RTCPeerConnection({
    iceServers: [
      { urls: 'stun:stun.services.mozilla.com' },
      { urls: 'stun:stun.l.google.com:19302' },
    ],
  });

  localStream.value!.getTracks().forEach((track) => {
    pc.addTrack(track, localStream.value!);
  });

  const remoteVideoData = reactive({
    socketId,
    mediaStream: undefined as MediaStream | undefined,
    userName: name,
  });
  if (!remoteVideo.value.find((video) => video.socketId === socketId)) {
    remoteVideo.value.push(remoteVideoData);
  }

  pc.ontrack = (event) => {
    if (
      remoteVideoData.mediaStream === undefined ||
      remoteVideoData.mediaStream.id === event.streams[0].id
    ) {
      // ウェブカムからのビデオ
      remoteVideoData.mediaStream = event.streams[0];
    } else {
      // 画面共有からのビデオ
      screenSharingActive.value = true;
      screenSharingStream.value = event.streams[0];
      screenSharingVideo.value!.srcObject = screenSharingStream.value;
    }
  };

  await pc.setRemoteDescription(new RTCSessionDescription(offer));
  pcArr.push({ toSocketId: socketId, pc });

  const answer = await pc.createAnswer();
  await pc.setLocalDescription(answer);

  io.emit('room.answer', socketId, pc.localDescription);

  pc.onicecandidate = (event) => {
    if (event.candidate) {
      io.emit('room.candidate', socketId, event.candidate);
    }
  };
});
  • オファーとアンサー共通の情報を保存する:
io.on('room.candidate', async (socketId: string, candidate: RTCIceCandidate) => {
  const pc = pcArr.find((pc) => pc.toSocketId === socketId)?.pc;
  if (pc) {
    await pc.addIceCandidate(new RTCIceCandidate(candidate));
  } else {
    alert('RTC not found!');
  }
});
  • アンサーを保存する:
io.on('room.answer', async (socketId: string, answer: RTCSessionDescription) => {
  const pc = pcArr.find((pc) => pc.toSocketId === socketId)?.pc;
  if (pc) {
    await pc.setRemoteDescription(new RTCSessionDescription(answer));
  } else {
    alert('RTC not found!');
  }
});
  • 画面共有:
const screenSharingVideo = ref(null);
const screenSharingStream = ref(undefined);
...
screenSharingStream.value = await navigator.mediaDevices.getDisplayMedia({
  video: { cursor: 'always' } as any,
  audio: false,
});
screenSharingVideo.value!.srcObject = screenSharingStream.value!;
screenSharingStream.value.getVideoTracks()[0].onended = () => {
  stopScreenSharing();
};

for (const pcData of pcArr) {
  screenSharingStream.value.getTracks().forEach((track) => {
    pcData.pc.addTrack(track, screenSharingStream.value!);
  });

  // 再びオファー送る
  const offer = await pcData.pc.createOffer();
  await pcData.pc.setLocalDescription(offer);
  io.emit('room.reoffer', pcData.toSocketId, offer);
}
  • 画面共有のオファーを保存する、新しいアンサーを作って送る:
io.on('room.reoffer', async (socketId: string, offer: RTCSessionDescription) => {
  const pc = pcArr.find((pc) => pc.toSocketId === socketId)?.pc;
  if (pc) {
    await pc.setRemoteDescription(new RTCSessionDescription(offer));
    const answer = await pc.createAnswer();
    await pc.setLocalDescription(answer);
    io.emit('room.answer', socketId, pc.localDescription);
  } else {
    alert('RTC not found!');
  }
});
  • ビデオORオーディオ通信を切る
const toogleVideo = () => {
  const videoTracks = localStream.value.getVideoTracks();
  videoTracks?.forEach((track) => {
    track.enabled = false;
  });
};

const toogleAudio = () => {
  const audioTracks = localStream.value.getAudioTracks();
  audioTracks?.forEach((track) => {
    track.enabled = false;
  });
};

バックエンド側のソースコード

socket.on('room.candidate', (userSocketId: string, candidate: RTCIceCandidate) => {
  socket.to(userSocketId).emit('room.candidate', socket.id, candidate);
});

socket.on('room.offer', (userSocketId: string, offer: RTCSessionDescription, name: string) => {
  socket.to(userSocketId).emit('room.offer', socket.id, offer, name);
});

// 画面共有で使う
socket.on('room.reoffer', (userSocketId: string, offer: RTCSessionDescription) => {
  socket.to(userSocketId).emit('room.reoffer', socket.id, offer);
});

socket.on('room.answer', (userSocketId: string, answer: RTCSessionDescription) => {
  socket.to(userSocketId).emit('room.answer', socket.id, answer);
});

今日の記事はここまでです。 何か質問があれば、遠慮なくコメント欄に投稿してください。

CIからAWSにOIDCの方法でデプロイしてみた

みな様こんにちは。
私はBriswellのSonと申します。

最近、CIでAWSにOIDC方法でデプロイしてみました。

最初に私が作成した図を見ましょう!

添付した画像を参照したら、大体な処理がわかると思いますが、詳しい流れを書きます。

初めに

普通にAWSソースコードをデプロイするために、aws_access_key_idaws_secret_access_keyが必須な情報じゃないでしょうか。 しかし、OIDC方法を利用する際には、上記の2つの情報が先に設定する必要がない、その情報をAWS側で提供します。

基本的に流れは下記の通りです。

I. idPでTokenの発行

最初に、Buildは、CI idPへTokenの作成の命令を送ります。CI idPでもらったパラメーターとPrivateキーを利用し、Tokenを作成します。

Tokenの内容については、例えば:AWS側で案件のIdとユーザのIdで確認したい場合、Buildがパラメーターを渡す情報をTokenに入れる必要があります。

作成が完了した後にBuildに返却します。

II. AWSへTokenの送信

CI idPにTokenを作成してもらったら、BuildでAWS cliassume-role-with-web-identityの関数でrole-arnweb-identity-tokenAWS送信します。送信の目的としてはAWSサービスにアクセスする時に使うためのAccessKeyIdSecretAccessKeySessionTokenを3つ取得したいからです。

 - role-arnは、AWSでロールを作成しておきました。例えば:arn:aws:iam::xxxxxxxxxxx:role/Test-Role-deploy-dev
 - web-identity-tokenは、CI idPでTokenを発行したばかりです。

assume-role-with-web-identityのパラメーターの中にrole-arnweb-identity-tokenの以外、他のパラメーターもあります。
例えば:duration-seconds, role-session-name

III. AWSでIAMの認証

AWS側では、IAMがもらったrole-arnで「ID プロバイダ」を参照します。

上の画像はサンプルのロールです。

上の画像はサンプルの「ID プロバイダ」です。

設定したプロバイダの連絡情報でCI idPに連絡して、Publicのキーを取得します。連絡先は「プロバイダのURL + '.well-known/openid-configuration'」の形です。

Publicのキーを取得したら、IAMがTokenをパラメーターに変換します。

その後に、パラメーターとロールの設定した「信頼されたエンティティ」の比較を行います。ロールにしている場合、一時的な情報(AccessKeyId、SecretAccessKey、SessionToken)を発行し、Buildに返却します。もらったデータが指定していない場合、エラーを返却する。

IV. BuildでAWS認証の設定

最後にBuild側ではAWS認証情報をもらったら、「aws configure」のコマンドで保持します。

AWSにデプロイやアクセスなどするために、この情報を使います。この情報は一時的な情報なので、Buildが完了したら、このキーがなくなり、CI側に保存しないです。

最後に

上記は、CIからAWSにOIDCの方法でデプロイに関する基本的な共有です。
拙い文章でしたが、最後まで読んでいただきありがとうございました。
疑問があれば、コメントをしてください。

よろしくお願いします。

PHPのバージョンアップ影響調査(PHP7.4 to 8.2)

はじめに

PHP案件のバージョンアップを行う機会がありましたので、進め方を紹介します。 表題の通り、PHP7.4から8.2へのアップデートでした。

バージョンアップに向けた心構え

システムの規模に比例してバージョンアップのインパクトも大きくなっていきます。

バージョンアップを行うことで今まで動いていたコードが動かなくなることがあり、この動かなくなる部分 = バージョンアップ向けの改修 がどの程度発生するかの規模感を知るところから始めます。

(闇雲にバージョンアップ作業から始めてもスケジュールを立てられず、苦しいです)

大体は何かしら動かなくなる部分が出てきてしまい、バージョンアップ用の修正が必要となります。

バージョンアップ時の注意点を以下にまとめます。

  • 関数の振る舞いが変わる、廃止される等、互換性がなくなりソースの修正が必要になることがある
    • オフィシャルの変更点一覧など、一通り確認しておくと良い
    • (複数回のメジャーバージョンアップが必要な場合、バージョンアップよりも最新バージョンをベースに作り直したほうがトータルで安くつくこともある)
  • ライブラリもPHPの新しいバージョンに合わせて更新
    • 依存関係の問題で使えなくなる場合もある
    • ライブラリの更新が止まっており、PHPの新しいバージョンに追従できないこともある
  • バージョンアップ作業後は一通りの機能に問題がないか、実際に動かして検証する必要がある
    • 目に見える形でエラーは出ていないがバリデーションのかかり方が以前と異なる、のように実際に動かしてみないとわからないケースもある

また、日々進んでいる改修作業を止められない場合、本流の環境とは別にバージョンアップ環境を準備し、平行で進めていくとよいケースもあります。

  • STEP1:バージョンアップの検証環境にて、バージョンアップ作業を行いとりあえずで動くところまでを目指す
  • STEP2:バージョンアップの検証環境にて、入念に検証を行う
  • STEP2.5:本流の改修で出た差分を取り込む
  • STEP3:2~2.5でエラーや問題が出れば修正する
  • STEP4:2~3を繰り返し、安定した頃合いで本番へリリース
  • ※改修は止まっている、または本番を一定期間止めてもOKということであれば、直接本流の環境で作業を行うほうが対応コストは低いです

調査方法

ソースコードに修正が必要な箇所がどれだけ存在するか確認を行っていきますが、目検でソースコードを一つずつチェックしていくのはしんどいです。

PHPにはバージョンアップ時にも使える便利な静的解析ツールが存在しています。

静的解析ツールだけで100%の検証が保証されるというものではなく、あくまでも最低限確実に変更が必要な箇所を見つけてくれる程度であり、実際に動かしての検証は必ず行うものと考えておくと良いです。

機械的に必要箇所を見つけてくれるだけでも作業はかなり楽になるものです。

今回はPHP8向けの以下のツールを使用させていただきました。 (製作者様、大変感謝🙏 )

odan.github.io

コーディング規約をチェックをするphp_codesnifferをベースに、PHP8の規約に違反していないか、というルールを追加するphp-compatibilityとの組み合わせになります。 インストール手順はリンク先のページを参照ください。

実行例

PHP 8 Compatibility Check
実行してみると上記のように変更が必要な箇所を知らせてくれます。 ちなみに、今回のケースではERRORが230件、WARNが228件の結果です。...なかなかの件数が出てしまっていますね。

ERRORは互換性がなくなる等必ず修正しなくてはいけないもの、WARNは今後非推奨の予定が立っている等どうせなら一緒に修正したほうが良いもの、と理解しています。もしかしたらWARNでも修正しないと動かないというものがあるのかもしれません。

終わりに

今回はPHPのバージョンを8系へアップデートする際にphp_codesniffer+php-compatibilityの組み合わせで静的解析を行う話をまとめさせていただきました。

実行結果が出るとどの程度コードに影響が出るかを知れるだけではなく、実際には修正して再実行してというサイクルをERROR/WARNが0件になるまで繰り返していくという流れになります。

適切なツールを使用することで雪かきのような作業の負荷を軽減し、本来注力したい作業にリソースを向けていけると良いですね。