Blynk local server を使うとAlexaが答えてくれない

published_with_changes 更新日: event_note 公開日:

labelamazon alexa labelBlynk labelIoT labelRaspberry pi

「アレクサ、部屋の温度を教えて」を実現のための第7歩になります。

Alexaと対話できるようになったのですが、Blynkをローカルサーバーに変えたら使えなくなりました。原因はインターネットからBlynk local serverが見えていなかったことと、Blynkを使う上でのポート設定でした。

しっかりDocumentを読めば書いてあるのですが、そこを飛ばして、見よう見まねとコピペでやっているので、なかなかうまく回りません。

状況

クラウドサーバのBlynkでは、問題なくAlexaが温度をアナウンスしてくれたのですが、BlynkのTokenをlocal server発行のものに変えたら、「スキルがリクエストに正しく応答できませんでした。」となりうまくいきません。

192.168.0.22:9443はBlynk local serverのLAN内のアドレスとポートです。ブラウザで、
https://192.168.0.22:9443/"ローカルのblynkAuthToken"/get/v0
などとやると、温度センサ(BME280)の値は読めています。

LAN内ではOKなので、外からBlynk local serverが見えていないようです。Blynkがそういうことをやってくれていると勝手に思っていたのですが、勝手な思い込みでした。

対策1:Raspberry Piに外部ネットワークからアクセス

このブログを読んでやり方を学びました。
https://camo.qiitausercontent.com/84a041f26da6f1478f6df3f9ee01a78779f865ab/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f39333736332f32643463336436372d623165372d353731342d306331312d3632336632653163346435362e706e67

Raspberry Piに外部ネットワークからアクセスできる様にして携帯でペットを遠隔監視する方法 - Qiita

はじめに 前回、赤外線カメラモジュールを繋げて「mjpg-streamer」を使い、家内のネットワークのブラウザから動画を見れる様にしました。 【Raspberry Piで赤外線カメラモジュールを使ってみる】 今回はその続きで...


1. Raspberry PiのプライベートIPアドレスの固定

Rasbian上でソフト的に固定するのですが、これは実施済みです。

2. 外部にサーバを公開

1)ルータにグローバルIPアドレスを設定する

プロバイダから自動的に割り当てられるグローバルIPアドレスは、ルータを再起動すると変わります。ルータのグローバルIPアドレスはルータ自身が持つので、あえて設定できるものではありません。

2)ポートマッピングの設定 (ルーターのポートを解放する)

外部からRaspberryPiアクセスできる様にルーターのポートを解放し、解放したポートを先ほど固定したRaspberryPiのプライベートIPアドレスと紐づけます。

ブラウザから、192.168.0.1 などとややって、ルータにログインします。


左列の「詳細設定」から「IPv4ポートマッピング設定」を選びます。次の上の画面は既に設定された画面です。新規に設定するときは、右下の「追加」をタップします。

2番目の画面になり、ここで設定をします。


  • LAN側ホスト:Blynk local serverのIPアドレスを指定します。たとえば、192.168.0.22
  • プロトコル:TCPを指定します。
  • ポート番号 :8080 です。8080だけを開けたいので、左・右の空白に8080と入力します。
ポート番号 :8080についてはGithubのblynkkk/blynk-serverに、つぎのような記載がありました。
Run the server on default 'hardware port 8080' and default 'application port 9443' (SSL port) 
  • 優先度:1~50の間で入力します。一番初めにポート開放するなら1です。次は2,3,4….となります。

以上で、ポート開放は完了です。右下の「設定」をタップします。

3)IPパケットフィルタリングの設定(確認のみ)

外部からサーバへのアクセスを許可する設定を行います。また、外部からサーバへの不要なパケットのアクセスをブロックする設定を行います。


4)ルータでも、Raspberry PiのプライベートIPアドレスを固定

Raspberry PiのMACアドレスとRaspberry PiのIPv4アドレスを紐づけ(固定化)します。

コマンドプロンプトで「arp -a」とやるとリストが出てきます。b8-27で始まるのがRaspberry pi のMACアドレスで、その行の左がRaspberry pi のIPアドレスです。

ルーターに戻って、「詳細設定」のなかの「DHCPv4固定割当設定」をクリックして、右下の「追加」をクリックします。

設定画面がでるので、先ほど調べたRaspberry PiのMACアドレスとRaspberry PiのIPv4アドレスを入力します。最後に「設定」をクリックして記録します。

ルーターの設定を変えたときは、最終的に左上の「保存」(オレンジに変わっている)をクリックします。さらにルーターの再起動を行います。

3. ファイアウォール設定

ここを参考にして設定しました。

【パクろう】ラズパイでファイアーウォールを設定する方法

【パクろう】ラズパイでファイアーウォールを設定する方法

みなさんラズパイでファイアウォールの設定をしていますか?ここではその設定方法や、そのまま使えるお勧め設定を載せました。ラズパイのファイアウォール設定はこれを見れば完璧です。

開けたポートは
SSH:ポート22
VNC:ポート5900
Blynk:ポート8080, 9443

Blynk側のセキュリティは、次のように記されています。

Blynkサーバーには、さまざまなセキュリティレベル用に開いている5つのポートがあります。
  • 80-ハードウェアのプレーンTCP接続(セキュリティなし)
  • 8080-ハードウェア用のプレーンTCP接続(セキュリティなし)
  • 443-モバイルアプリとSSLを使用したハードウェアのSSL / TLS接続
  • 9443-モバイルアプリとSSLを使用したハードウェアのSSL / TLS接続
ハードウェアは、その機能に応じて、443(9443)または80(8080)への接続を選択する場合があります。アプリとサーバー間の接続は常にSSL / TLSを介して行われるため、常に保護されています。ハードウェアとサーバー間の接続は、ハードウェアの機能によって異なります。引用元:https://docs.blynk.cc/Security

対策2:AWS Lambda関数の変更

クラウドのBlynkサーバー用に作ったLambda関数をBlynk local server用に変更します。変更箇所はクラウドサーバーをローカルサーバーに変え、加えてポートを変更します。

変更前

http.get({
  host: 'blynk-cloud.com',
  path: '/' + blynkAuthToken + '/get/' + blynkPin,
  port: '80'
}

 変更後

http.get({
  host: globalIP, // globalIPはルータのグローバルIPアドレス
  path: '/' + blynkAuthToken + '/get/' + blynkPin,
  port: '8080'
}

 portについては、上の方にも書きましたがGitHubにつぎのようにあるので'8080'にしたらうまくいきました。

 Run the server on default 'hardware port 8080' and default 'application port 9443' (SSL port)


Alexa Skill ローカルサーバー用スクリプト

対話モデルのjsonコード

alexa developer consoleで作る対話モデルのjsonコードです。
taiwa_skill.json
{
    "interactionModel": {
        "languageModel": {
            "invocationName": "ローカル",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                },
                {
                    "name": "HelloWorldIntent",
                    "slots": [],
                    "samples": [
                        "hello",
                        "ハロー",
                        "こんにちは"
                    ]
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                },
                {
                    "name": "GetMeasurement",
                    "slots": [
                        {
                            "name": "Location",
                            "type": "LIST_OF_LOCATIONS"
                        },
                        {
                            "name": "Measurement",
                            "type": "LIST_OF_MEASUREMENTS"
                        }
                    ],
                    "samples": [
                        "GetMeasurement {Location} ",
                        "GetMeasurement {Location} は",
                        "GetMeasurement {Location} はいくつ",
                        "GetMeasurement {Location} を教えて",
                        "GetMeasurement {Location}  の  {Measurement} はどんだけ",
                        "GetMeasurement {Measurement} はどんだけ",
                        "GetMeasurement {Location} は  {Measurement}",
                        "GetMeasurement {Location} の  {Measurement} を教えて",
                        "GetMeasurement {Location}  の  {Measurement} はいくつ",
                        "GetMeasurement {Location}  の {Measurement} は",
                        "GetMeasurement {Location} の  {Measurement}",
                        "GetMeasurement {Measurement} を教えて",
                        "GetMeasurement {Measurement} はいくつ",
                        "GetMeasurement {Measurement} は",
                        "GetMeasurement {Measurement}"
                    ]
                }
            ],
            "types": [
                {
                    "name": "LIST_OF_LOCATIONS",
                    "values": [
                        {
                            "name": {
                                "value": "屋根裏",
                                "synonyms": [
                                    "天井裏"
                                ]
                            }
                        },
                        {
                            "name": {
                                "value": "仏間",
                                "synonyms": [
                                    "和室"
                                ]
                            }
                        },
                        {
                            "name": {
                                "value": "二階"
                            }
                        }
                    ]
                },
                {
                    "name": "LIST_OF_MEASUREMENTS",
                    "values": [
                        {
                            "name": {
                                "value": "熱中度指数",
                                "synonyms": [
                                    "暑さ度",
                                    "熱中度",
                                    "暑さ指数"
                                ]
                            }
                        },
                        {
                            "name": {
                                "value": "気圧",
                                "synonyms": [
                                    "大気圧"
                                ]
                            }
                        },
                        {
                            "name": {
                                "value": "湿度",
                                "synonyms": [
                                    "湿気"
                                ]
                            }
                        },
                        {
                            "name": {
                                "value": "気温",
                                "synonyms": [
                                    "何度",
                                    "室温",
                                    "温度"
                                ]
                            }
                        }
                    ]
                }
            ]
        }
    }
}

AWS Lambda

index.js;

node.js.2.2xに対応するように書き換え

'use strict';

const http = require('http');

// --------------- Helpers that build all of the responses -----------------------

function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
    return {
        outputSpeech: {
            type: 'PlainText',
            text: output,
        },
        card: {
            type: 'Simple',
            title: `SessionSpeechlet - ${title}`,
            content: `SessionSpeechlet - ${output}`,
        },
        reprompt: {
            outputSpeech: {
                type: 'PlainText',
                text: repromptText,
            },
        },
        shouldEndSession,
    };
}

function buildResponse(sessionAttributes, speechletResponse) {
    return {
        version: '1.0',
        sessionAttributes,
        response: speechletResponse,
    };
}

// --------------- Functions that control the skill's behavior -----------------------

function getWelcomeResponse(callback) {
    const sessionAttributes = {};
    const cardTitle = 'Welcome';
    const speechOutput = "二階の温度を教えてなどと問いかけると、それを答えます。";
    const repromptText = "私はこの部屋を見てるわよっ!何が知りたいのっ!   ってかっ";
    const shouldEndSession = false;

    callback(sessionAttributes,
        buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}

function handleSessionEndRequest(callback) {
    const cardTitle = 'Session Ended';
    const speechOutput = 'Thank you for trying the Alexa Skills Kit sample. Have a nice day!';
    const shouldEndSession = true;

    callback({}, buildSpeechletResponse(cardTitle, speechOutput, null, shouldEndSession));
}

function createLocationAttributes(location) {
    return { location };
}

async function readTemperatureInSession(intent, session, callback) {
    const cardTitle = intent.name;
    const LocationSlot = intent.slots.Location;
    const MeasurementSlot = intent.slots.Measurement;
    let repromptText = '';
    let sessionAttributes = {};
    const shouldEndSession = false;
    let speechOutput = '';
    let body = '';
    const globalIP = '***.***.***.***';
    const blynkport = '8080';
    let blynkAuthToken;
    let blynkPin;

    let location = LocationSlot.value || (session.attributes && session.attributes.location) || '二階';
    switch (location) {
        case '二階':
            blynkAuthToken = '********************************';
            break;
        case '仏間':
            blynkAuthToken = '********************************';
            break;
        case '屋根裏':
            blynkAuthToken = '********************************';
            break;
    }

    let measurement = MeasurementSlot.value || '温度';
    switch (measurement) {
        case '何度':
        case '気温':
        case '室温':
        case '温度':
            blynkPin = 'V0';
            break;
        case '湿気':
        case '湿度':
            blynkPin = 'V1';
            break;
        case '大気圧':
        case '気圧':
            blynkPin = 'V2';
            break;
        case '熱中度指数':
        case '熱中度':
        case '暑さ指数':
        case '暑さ度':
        case '暑さ':
            blynkPin = 'V4';
            break;
    }

    try {
        const httpPromise = new Promise((resolve, reject) => {
            http.get({
                host: globalIP,
                path: `/${blynkAuthToken}/get/${blynkPin}`,
                port: blynkport,
            }, (response) => {
                response.on('data', (d) => {
                    body += d;
                });
                response.on('end', () => {
                    resolve('Done Sending');
                });
            });
        });

        await httpPromise;

        const info = parseFloat(JSON.parse(body));
        speechOutput = location + 'の ' + measurement + 'は ';
        repromptText = speechOutput;
        switch (measurement) {
            default:
            case '何度':
            case '気温':
            case '室温':
            case '温度':
                speechOutput += `${info.toFixed(1)}度 です。`;
                repromptText += `${info.toFixed(1)}℃です。`;
                break;
            case '湿気':
            case '湿度':
                speechOutput += `${info.toFixed(1)}パーセント です。`;
                repromptText += `${info.toFixed(1)}%です。`;
                break;
            case '大気圧':
            case '気圧':
                speechOutput += `${info.toFixed(0)}ヘクトパスカル です。`;
                repromptText += `${info.toFixed(0)}hPaです。`;
                break;
            case '熱中度指数':
            case '熱中度':
            case '暑さ指数':
            case '暑さ度':
            case '暑さ':
                let annotation = '';
                if (info >= 31) {
                    annotation = '危険! ブラックです。';
                } else if (info >= 28) {
                    annotation = '厳重警戒! レッドです。';
                } else if (info >= 25) {
                    annotation = '警戒! オレンジです。';
                } else if (info >= 21) {
                    annotation = '注意です。';
                }
                speechOutput += `${info.toFixed(0)}度 です。${annotation}`;
                repromptText += `${info.toFixed(0)}℃です。${annotation}`;
                break;
        }
        sessionAttributes = createLocationAttributes(location);
        callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
    } catch (error) {
        console.error(`Error: ${error}`);
        throw new Error('Error in reading temperature');
    }
}

// --------------- Events -----------------------

function onSessionStarted(sessionStartedRequest, session) {
    console.log(`onSessionStarted requestId=${sessionStartedRequest.requestId}, sessionId=${session.sessionId}`);
}

function onLaunch(launchRequest, session, callback) {
    console.log(`onLaunch requestId=${launchRequest.requestId}, sessionId=${session.sessionId}`);
    getWelcomeResponse((sessionAttributes, speechletResponse) => {
        callback(sessionAttributes, speechletResponse);
    });
}

function onIntent(intentRequest, session, callback) {
    console.log(`onIntent requestId=${intentRequest.requestId}, sessionId=${session.sessionId}`);
    const intent = intentRequest.intent;
    const intentName = intentRequest.intent.name;

    if (intentName === 'GetMeasurement') {
        readTemperatureInSession(intent, session, callback);
    } else if (intentName === 'AMAZON.HelpIntent') {
        getWelcomeResponse((sessionAttributes, speechletResponse) => {
            callback(sessionAttributes, speechletResponse);
        });
    } else if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') {
        handleSessionEndRequest((sessionAttributes, speechletResponse) => {
            callback(sessionAttributes, speechletResponse);
        });
    } else {
        throw new Error('Invalid intent');
    }
}

function onSessionEnded(sessionEndedRequest, session) {
    console.log(`onSessionEnded requestId=${sessionEndedRequest.requestId}, sessionId=${session.sessionId}`);
}

exports.handler = (event, context) => {
    try {
        console.log(`event.session.application.applicationId=${event.session.application.applicationId}`);

        if (event.session.new) {
            onSessionStarted({ requestId: event.request.requestId }, event.session);
        }

        if (event.request.type === 'LaunchRequest') {
            onLaunch(event.request, event.session, (sessionAttributes, speechletResponse) => {
                context.succeed(buildResponse(sessionAttributes, speechletResponse));
            });
        } else if (event.request.type === 'IntentRequest') {
            onIntent(event.request, event.session, (sessionAttributes, speechletResponse) => {
                context.succeed(buildResponse(sessionAttributes, speechletResponse));
            });
        } else if (event.request.type === 'SessionEndedRequest') {
            onSessionEnded(event.request, event.session);
            context.succeed();
        }
    } catch (e) {
        console.error(`Exception: ${e}`);
        context.fail(`Exception: ${e}`);
    }
};
 

グローバルIPアドレスの自動取得:失敗

自動で取得はできない。理由は、

  1. アドレス取得をLambda関数に実装 (http.get('http://checkip.amazonaws.com')) すると、 LambdaのパブリックIP を取得してしまうため、ユーザーの実際のグローバルIPを取得できない。
  2. AWS Lambda関数が外部サイトからグローバルIPアドレスを取得できない理由は、Lambda関数が固定のIPアドレスを持たないかららしい。Lambda関数は、実行されるたびに異なるIPアドレスが割り当てられるため、外部サイトから見ると常に変わることになる。
  3. 「ユーザーのグローバルIP」を取得したい場合は、クライアント(ユーザーのデバイスやブラウザ)でIPを取得し、それをLambdaにプッシュ送信する設計にする必要がある。



'use strict';

const http = require('http');
const https = require('https');

// --------------- Helpers that build all of the responses -----------------------

function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
    return {
        outputSpeech: {
            type: 'PlainText',
            text: output,
        },
        card: {
            type: 'Simple',
            title: `SessionSpeechlet - ${title}`,
            content: `SessionSpeechlet - ${output}`,
        },
        reprompt: {
            outputSpeech: {
                type: 'PlainText',
                text: repromptText,
            },
        },
        shouldEndSession,
    };
}

function buildResponse(sessionAttributes, speechletResponse) {
    return {
        version: '1.0',
        sessionAttributes,
        response: speechletResponse,
    };
}

// --------------- Functions that control the skill's behavior -----------------------

function getWelcomeResponse(callback) {
    const sessionAttributes = {};
    const cardTitle = 'Welcome';
    const speechOutput = "二階の温度を教えてなどと問いかけると、それを答えます。";
    const repromptText = "私はこの部屋を見てるわよっ!何が知りたいのっ!   ってかっ";
    const shouldEndSession = false;

    callback(sessionAttributes,
        buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}

function handleSessionEndRequest(callback) {
    const cardTitle = 'Session Ended';
    const speechOutput = 'Thank you for trying the Alexa Skills Kit sample. Have a nice day!';
    const shouldEndSession = true;

    callback({}, buildSpeechletResponse(cardTitle, speechOutput, null, shouldEndSession));
}

function createLocationAttributes(location) {
    return { location };
}

async function getGlobalIP() {
    return new Promise((resolve, reject) => {
        http.get('https://checkip.amazonaws.com', (resp) => {
            let data = '';
            resp.on('data', (chunk) => { data += chunk; });
            resp.on('end', () => {
                try {
                    const ip = data.trim(); // JSON.parse は不要
                    console.log(`Global IP: ${ip}`);
                    resolve(ip);
                } catch (e) {
                    reject(`Error parsing IP: ${e.message}`);
                }
            });
        }).on("error", (err) => {
            reject(`Error: ${err.message}`);
        });
    });
}


async function readTemperatureInSession(intent, session, callback) {
    const cardTitle = intent.name;
    const LocationSlot = intent.slots.Location;
    const MeasurementSlot = intent.slots.Measurement;
    let repromptText = '';
    let sessionAttributes = {};
    const shouldEndSession = false;
    let speechOutput = '';
    let body = '';
    const blynkport = '8080';
    let blynkAuthToken;
    let blynkPin;

    let location = LocationSlot.value || (session.attributes && session.attributes.location) || '二階';
    switch (location) {
        case '二階':
            blynkAuthToken = '********************************';
            break;
        case '仏間':
            blynkAuthToken = '********************************';
            break;
        case '屋根裏':
            blynkAuthToken = '********************************';
            break;
    }

    let measurement = MeasurementSlot.value || '温度';
    switch (measurement) {
        case '何度':
        case '気温':
        case '室温':
        case '温度':
            blynkPin = 'V0';
            break;
        case '湿気':
        case '湿度':
            blynkPin = 'V1';
            break;
        case '大気圧':
        case '気圧':
            blynkPin = 'V2';
            break;
        case '熱中度指数':
        case '熱中度':
        case '暑さ指数':
        case '暑さ度':
        case '暑さ':
            blynkPin = 'V4';
            break;
    }

    try {
        const globalIP = await getGlobalIP();
        console.log(`Global IP used: ${globalIP}`); // デバッグ用のログ出力
        const httpPromise = new Promise((resolve, reject) => {
            http.get({
                host: globalIP,
                path: `/${blynkAuthToken}/get/${blynkPin}`,
                port: blynkport,
            }, (response) => {
                response.on('data', (d) => {
                    body += d;
                });
                response.on('end', () => {
                    resolve('Done Sending');
                });
            }).on("error", (err) => {
                reject(`Error: ${err.message}`);
            });
        });

        await httpPromise;

        const info = parseFloat(JSON.parse(body));
        speechOutput = location + 'の ' + measurement + 'は ';
        repromptText = speechOutput;
        switch (measurement) {
            default:
            case '何度':
            case '気温':
            case '室温':
            case '温度':
                speechOutput += `${info.toFixed(1)}度 です。`;
                repromptText += `${info.toFixed(1)}℃です。`;
                break;
            case '湿気':
            case '湿度':
                speechOutput += `${info.toFixed(1)}パーセント です。`;
                repromptText += `${info.toFixed(1)}%です。`;
                break;
            case '大気圧':
            case '気圧':
                speechOutput += `${info.toFixed(0)}ヘクトパスカル です。`;
                repromptText += `${info.toFixed(0)}hPaです。`;
                break;
            case '熱中度指数':
            case '熱中度':
            case '暑さ指数':
            case '暑さ度':
            case '暑さ':
                let annotation = '';
                if (info >= 31) {
                    annotation = '危険! ブラックです。';
                } else if (info >= 28) {
                    annotation = '厳重警戒! レッドです。';
                } else if (info >= 25) {
                    annotation = '警戒! オレンジです。';
                } else if (info >= 21) {
                    annotation = '注意です。';
                }
                speechOutput += `${info.toFixed(0)}度 です。${annotation}`;
                repromptText += `${info.toFixed(0)}℃です。${annotation}`;
                break;
        }
        sessionAttributes = createLocationAttributes(location);
        callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
    } catch (error) {
        console.error(`Error: ${error}`);
        throw new Error('Error in reading temperature');
    }
}

// --------------- Events -----------------------

function onSessionStarted(sessionStartedRequest, session) {
    console.log(`onSessionStarted requestId=${sessionStartedRequest.requestId}, sessionId=${session.sessionId}`);
}

function onLaunch(launchRequest, session, callback) {
    console.log(`onLaunch requestId=${launchRequest.requestId}, sessionId=${session.sessionId}`);
    getWelcomeResponse((sessionAttributes, speechletResponse) => {
        callback(sessionAttributes, speechletResponse);
    });
}

function onIntent(intentRequest, session, callback) {
    console.log(`onIntent requestId=${intentRequest.requestId}, sessionId=${session.sessionId}`);
    const intent = intentRequest.intent;
    const intentName = intentRequest.intent.name;

    if (intentName === 'GetMeasurement') {
        readTemperatureInSession(intent, session, callback);
    } else if (intentName === 'AMAZON.HelpIntent') {
        getWelcomeResponse((sessionAttributes, speechletResponse) => {
            callback(sessionAttributes, speechletResponse);
        });
    } else if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') {
        handleSessionEndRequest((sessionAttributes, speechletResponse) => {
            callback(sessionAttributes, speechletResponse);
        });
    } else {
        throw new Error('Invalid intent');
    }
}

function onSessionEnded(sessionEndedRequest, session) {
    console.log(`onSessionEnded requestId=${sessionEndedRequest.requestId}, sessionId=${session.sessionId}`);
}

exports.handler = (event, context) => {
    try {
        console.log(`event.session.application.applicationId=${event.session.application.applicationId}`);

        if (event.session.new) {
            onSessionStarted({ requestId: event.request.requestId }, event.session);
        }

        if (event.request.type === 'LaunchRequest') {
            onLaunch(event.request, event.session, (sessionAttributes, speechletResponse) => {
                context.succeed(buildResponse(sessionAttributes, speechletResponse));
            });
        } else if (event.request.type === 'IntentRequest') {
            onIntent(event.request, event.session, (sessionAttributes, speechletResponse) => {
                context.succeed(buildResponse(sessionAttributes, speechletResponse));
            });
        } else if (event.request.type === 'SessionEndedRequest') {
            onSessionEnded(event.request, event.session);
            context.succeed();
        }
    } catch (e) {
        console.error(`Exception: ${e}`);
        context.fail(`Exception: ${e}`);
    }
};

 
  ;

Lambda関数のIPアドレスを固定化する

AWS Lambda関数が外部サイトからグローバルIPアドレスを取得できない理由は、Lambda関数が固定のIPアドレスを持たないからです。Lambda関数は、実行されるたびに異なるIPアドレスが割り当てられるため、外部サイトから見ると常に変わることになる。 ならば、Lambda関数が固定のIPアドレスをもつようにすれば解決できるのではないかと考えるが、この点についてどう考えますか?

その考え方は正しいです。AWS Lambda関数が外部サイトからのIP制限を受けることなくグローバルIPを取得するには、固定のIPアドレスを持たせる方法を検討する必要があります。いくつかの方法がありますが、それぞれメリット・デメリットがあります。


Lambda関数に固定IPを持たせる方法

① VPC + NAT Gateway を利用

LambdaをVPC(仮想プライベートクラウド)内で実行し、NAT Gateway を経由してインターネットへ接続させることで、固定IPアドレスを割り当てる方法です。

メリット

  • 完全に固定のIPアドレスを使用可能
  • 他のAWSリソース(RDS, EC2など)と安全に連携可能
  • AWS公式の方法であり、サポートが手厚い

デメリット

  • VPCのセットアップが必要(サブネット、NAT Gateway、ルートテーブル設定)
  • 追加コストが発生(NAT Gatewayの使用料金がかかる)

設定方法

  1. LambdaをVPC内のプライベートサブネットに配置
  2. NAT Gateway を作成
    • Elastic IP を関連付ける(これが固定のIPアドレスとなる)
  3. VPCルートテーブルを設定
    • プライベートサブネットからインターネットアクセスがNAT Gatewayを経由するように設定
  4. LambdaのVPC設定でプライベートサブネットを選択
  5. Lambda関数を実行し、外部サイトへアクセス
    • NAT Gateway経由でインターネットへアクセスするため、固定IPが適用される

② AWS Global Accelerator を利用

AWS Global Acceleratorを使用して、Lambdaが経由するIPアドレスを固定する方法です。

メリット

  • VPCなしでも使用可能
  • 複数のリージョンで利用可能
  • レイテンシ最適化

デメリット

  • 追加コストが発生
  • セットアップが複雑
  • すべての外部APIに対応するわけではない

→ NAT Gatewayを使う方がシンプルで一般的


③ API Gateway + Static IP Proxy

API Gateway と EC2 インスタンスを組み合わせ、Lambdaの外部通信を固定IPを持つプロキシサーバー経由にする方法です。

メリット

  • Lambdaの外部アクセスを制御しやすい
  • EC2に複数のIPを割り当てることが可能

デメリット

  • EC2の運用管理が必要
  • プロキシのメンテナンスが必要

→ 基本的にはNAT Gatewayの方が推奨される


結論

  • 確実に固定IPを使いたいならLambdaをVPC + NAT Gateway に配置
  • コストを抑えつつ高速化もしたいならAWS Global Accelerator
  • 独自のプロキシを運用したいならEC2を利用したAPI Gateway Proxy

AWS Lambdaはデフォルトでは固定IPを持たない仕様ですが、VPC + NAT Gateway を使うことで固定IPを実現可能です。コストはかかりますが、運用上の安定性やセキュリティを考えるとVPC + NAT Gateway の方法が最も一般的で推奨されるでしょう。

Alexa Skills Kitでテスト

local serverでも、クラウドサーバと同じようにちゃんと対話できるようになりました。


課題

(1) オリジナルスキルは「ローカルを開いて」とかの呼び出し名から始めなければならず。2回のやり取りが必要です。温度を知りたいだけならば、Alexaに聞くのは面倒です。Blynkアプリを開く方が簡単です。

(2) ルーターのグローバルIPアドレスはルーターが再起動すると変わります。私の環境では何かの理由でルーターが再起動することがよくあり、Alexaが答えなくなります。そのたびに、AWS Lambdaを開いて、index.jsでグローバルIPアドレスを書き換える必要があります。

(3) Lambdaの問題かもしれませんが、グローバルIPアドレスが変わって新しい温度を読み込んでいなくても、Alexaは覚えていた温度を答えます。

以上の理由から、「アレクサ、部屋の温度を教えて」をほとんど使わなくなりました。グローバルIPアドレスを都度見に行くようなLambda関数を作れれば問題解決なのですが、そのような能力はありません。


Powered by Blogger | Designed by QooQ

keyboard_double_arrow_down

keyboard_double_arrow_down