LaravelからDynamoDBのデータ取得|Lambda×API Gateway経由

標題にある通り、LaravelからDynamoDBのデータを取得します。
その際に、AWSサービスの「Lambda」と「API Gateway」経由して取得します。

今回、かなり参考にさせて頂いた記事がこちらです。
AWS API GatewayとLambdaでDynamoDB操作

API Gateway → Lambda → DynamoDB までの部分はこちらを参考にさせて頂きつつ、
・途中でエラーで詰まった部分の紹介
・API GatewayとLaravelとの連携部分についての紹介(curlを利用します。)
をしていきます。

■前提

・Laravelインストール済み
・AWSアカウント取得済み

LaravelからDynamoDBのデータ取得の流れ

①:DynamoDBのテーブル、レコード作成
②:Lambda作成
③:API Gateway作成
④:API Gatewayテスト実施
⑤:API Gateway公開
⑥:ローカル環境(mac / ターミナル)から、API Gatewayへリクエスト
⑦:LaravelからAPI Gatewayへリクエスト
おまけ:LaravelからAPI Gatewayの通信に認証を追加

①〜⑤は、こちらの記事を参考に進めて頂けたらと想います。 AWS API GatewayとLambdaでDynamoDB操作 

⑥:ローカル環境(mac / ターミナル)から、API Gatewayへリクエスト

①~⑤で、API Gatewayテストが完了し、DynamoDBのデータが正しくResponseされているでしょうか。

■SCANを実施したい場合のコマンド

$ curl -X POST 'https://XXXXXX.execute-api.ap-northeast-1.amazonaws.com/APItest/dynamodbctrl' -d '{"OperationType":"SCAN"}' | jq

ここで自分が見誤った部分が、リクエストする際のURL部分です。
ステージエディターのURLを利用すれば問題ないのですが、階層によってURLが違うので注意して下さい。
こちらの方と同じ勘違いをして、「Missing Authentication Token」となってしまいました。 【AWS】APIGatewayのURLにアクセスするとエラー「Missing Authentication Token」が表示される

⑦:LaravelからAPI Gatewayへリクエスト

ローカル環境から、curlを利用してデータが返ってきましたか。

curlを利用して、LaravelからAPI Gatewayへリクエストして、DynamoDBのデータを取得していきます。

public function index()
{
    
    $header = [
        'Content-Type: application/json',
    ];

        $params = [
        "OperationType" => "SCAN"
    ];

        
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, env('AWS_APIGATEWAY_URL'));
    curl_setopt($curl, CURLOPT_POST, TRUE);
    curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($params));
    curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
    
    $output= curl_exec($curl);
    $result = json_decode($output);
    curl_close($curl);

    dd($result);
    }

curlはPHPに標準で含まれているので、importをする必要はありません。

env(‘AWS_APIGATEWAY_URL’)

ステージの呼び出しURLを指定して下さい。

curl_setopt($curl,CURLOPT_POSTFIELDS, json_encode($params));

連想配列から、jsonへ変換しています。

これで、dd($result);で、ターミナルからcurlしたデータと同じデータが返ってくると思います。

LaravelからAPI Gatewayの通信に認証を追加

下記にてまとめております。

Laravel,balde「@auth・Auth::user()」が反応しない件

今回、非常にピンポイントなシチュエーションによる問題です。

■前提条件

・マルチログイン認証機能で、guard: users,adminを2つ作成
・guardのdefaultをusers指定

■問題発生状況

・adminでログイン状態
・baldeにて、「@auth」や「Auth::user()」がnullで返ってくる

■原因

原因は、guardのdefault指定のようです。

config > auth.php

の中で、defaultを指定を下記でしておりました。

    'defaults' => [
        'guard' => 'users',
        'passwords' => 'users',
    ],

「@auth」や「Auth::user()」「Auth::login()」 等で呼び出すのは auth.php でdefault に指定したguardのようです。

adminでログイン状態でも、defaultがusersだったのでnullが返ってきたようです。

■解決策

defalut以外のguard を呼び出すには Auth::guard(‘admin’) のように guard 名を指定する必要があります。

Laravel認証後,認証ユーザーである情報をどこで保持するのか.

Laravelの認証後に、認証ユーザーであることをどこで判別しているのか。
疑問を調べてみました。

認証の設定方法によって違いますが、
laravelのdefaultでは、Sessionに設定されているかと思います。

app > config > auth.php

下記のようにdriverがsessionで指定されていると思います。

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],   
    ],

sessionではどのような情報として保持されているのか?

実際にどのような情報として保持しているのかを確認してみます。

・確認用のControllerの作成

<?php
 namespace App\Http\Controllers;
 use Illuminate\Http\Request;
 class TestSessionController extends Controller
 {
     //
     public function index(Request $request)
     {
     $data = $request->session()->all();     dd($data);  }
 }

・確認用のRouteの作成

Route::get('test', [TestSessionController::class, 'index'])->name('index');

ログイン完了後.  /testへ遷移してみる。

dd()によって、下記の情報が確認できると思います。

下記の部分がAuthのguardのsession部分です。
{guard} 部分は、guardで設定し、ログインに利用したguardの文字列が含まれます。

array:4 [▼
   "login_{guard}_XXXXXXXXXXXXXXXXXXXXXXXXX" => 1
 ]

例えば、マルチログイン対応で、adminのguaradを作成し、adminでログインしている場合(認証されている場合)には、adminが入ります。

array:4 [▼
   "login_admin_XXXXXXXXXXXXXXXXXXXXXXXXX" => 1
 ]

User / Admin 複数のGuardでログインしている時

では、User、Admin複数のGuardでログインしている場合はどうなっているでしょうか。

同じく、/testへ遷移してみます。

"login_users_XXXXXXXXXXXXXXXXXXXXXXXXX" => 1
"login_admin_XXXXXXXXXXXXXXXXXXXXXXXXX" => 1

両方のguard, session情報が含まれていることがわかります。

ACM,ALB,Route53を活用してSSL化する.httpsのリダイレクトも.| AWS

標題の通りです。ご自身で取得したドメインをSSL化したい場合の一連の流れと作業を解説していきます。ちなみに、自分はお名前ドットコムでドメイン取得をしております。

■前提

・httpによるアクセスが出来ている環境であること.
・AWSを利用して、サーバー構築していること.

0:ACM,ALB,Route53各サービスの役割

初めに、各サービスの相互役割を解説して、全体の流れを解説していきたいと思います。

Route53の役割:

・ドメインのDNSレコードの作成
・ルーティング先をALBへ指定(ドメインアクセス後にALBへ)

ACMの役割:

・SSL化に必要な証明証の発行

ALBの役割:

・httpsアクセスに対して、ACMで発行した証明証を基にSSL化
・Target(今回はEC2インスタンス)へ、転送
・httpアクセスに対して、httpsへリダイレクト

①:ドメインの取得

ご自身でドメイン取得をして下さい。自分はお名前ドットコムでドメイン取得しております。

②:ACMで証明証発行

AWS Certificate Manager > 証明書 @ リクエスト

・「パブリック証明証をリクエスト」を選択
・完全修飾ドメイン名 (FQDN): {{ご自身で取得したドメインを指定}}

これで、証明証を発行します。

③:Route53レコードの設定

③-1:ホストゾーンの作成

Route 53 > ホストゾーン @ ホストゾーンの作成

・ドメイン名: {{ご自身で取得したドメインを指定}}
・パブリックホストゾーンを選択

これで、ホストゾーンを作成します。

③-2:NSレコードを、ドメイン購入サイトに反映

下記サイトなどで手順を参考に。

https://dev.classmethod.jp/articles/route53-domain-onamae/
https://blog.i-tale.jp/2020/04/13_02/

③-3:CNAMEレコードを、ACMからから作成

③-3-1:CNAMEレコードとは.

CNAMEは、ドメイン名と別ドメイン名をつけることができる。
つまり違うドメイン名で、同じ動きをしてくれます。

③-3-2:ACMから、Route53のCNAMEを作成

AWS Certificate Manager > 証明書 > ②で作成した証明証へ

Route53でレコード作成

③で作成している、Route53を選択して下さい。

④:ALBでSSL化 / ターゲットグループへの転送(EC2) / httpからのリダイレクト

④-1:ロードバランサーの作成

EC2 > ロードバランサー @ ロードバランサーの作成

Create Application Load Balancer

Basic configuration

名前を自由を設定にして、basic 情報を設定。

VPCを選択します。

今回のターゲットグループとなる、EC2インスタンスが設置してあるVPCを選択します。
※少なくとも2つ以上のアベイラビリティゾーンを追加して下さいとのことです。

リスナーを設定

・HTTPSプロトコル、443ポートで受けます。
・Forward to にターゲットグループを設定します。今回の場合は、EC2インスタンスを含まれるターゲットグループを指定
・secure listenerを From ACMで「②で作成したACMを指定」

セキュリティグループを設定

今回作成するセキュリティグループは、ALBへのセキュリティグループです。HTTPS・HTTPどちらもインバウンドを許可します。
※HTTPに対するリダイレクト処理は下記で紹介。

④-2:httpからのアクセスをhttpsへリダイレクト

設定方法を下記記事にて解説しています。

⑤:Route53で、Aレコードの設定

④で作成したALBを、Route53でAレコードをしていきます。

⑤-1:Aレコードとは.

ドメインをIPアドレスへ変換します。
ドメインへのリクエストが合った際のルート先を指定することができます。
今回はリクエストが合った際に、ALBを指定することで、SSL化を実行しています。

⑤-2:設定

・レコードタイプ:A
・エイリアス:オン
・ルーティング先:ALB >> ap-northeast-1(ご自身のリージョン)>>{{④で作成した、ALBを指定}}

[AWS|ALB]http→httpsへリダイレクトの設定

SSL化した後に、httpでのアクセスが可能 OR httpではアクセスが不可の状態が発生することで、httpへのアクセスを全てhttpsへリダイレクトしたい機会があると思います。 AWSのALBを利用して、設定していきます。

■前提

AWSのALBにて、ロードバランサー経由でWebサーバーへ接続をしている場合に限る。

ロードバランサーの設定

EC2 >> ロードバランサー >> リスナー

「リスナーの追加」をします。

すると下記の、追加設定画面が出てきますので。
・httpを80ポートで受けて
・リダイレクトをfull URLで設定

これで、htttp → https化へリダイレクトします。

Laravelリーレション先のデータ個数を集計 / hasMany

Laravelのeloquentは強力なORMです。

例えば、hasManyでリレーション先につながってデータの個数を集計して、オブジェクトへ返してくれる方法があります。

Modelでリレーション記述
    public function drinkNumber()
    {
        return $this->hasMany(Join::class, 'drink_id', 'id');
    }
modelからリレーション先を自動集計したデータをオブジェクトへ
    $drinks = Drink::withCount('drinkNumber')->where('status', 1)->get();

これで、$drinksの中には、Drink Modelのオブジェクトに加えて、集計したデータも一緒に含まれます。

カラム名は下記のルールで、名称付けられます。

リレーション名_count

ngrokを利用して、Line Messaging APIのWebhook検証を「成功」

LINE公式アカウントで、チャットボットを作成する際に必要になるWebhook。開発中でも、httpsでのレスポンスが求められるため開発中断してしまう方もいらっしゃるかもしれません。

前提:
今回は、Laravelを利用してwebhookの処理を実験していきます。
LaravelのInstallや、処理内容については説明を省略します。
pcはmacを利用しています。

ngrokを利用すれば、開発環境(ローカルPC)からでも、Webhookをhttpsで受け付けるエンドポイントを設定することが可能です。また、ランダムURLで問題なければ無料で利用でいることも魅力です。

①:ngrokとは

ngrokとは、ngrokで生成したURLにきたリクエストを、別のエンドポイントへ流してくれるサービスです。
今回の場合でいうと、ngrokで指定したURLにリクエストが合った場合に、ローカルホストのエンドポイントへ流してくれます。

ngrokのサービス: https://ngrok.com/

②:ngrokの設定

非常に簡単な設定で利用を開始できます。

ngrokにsign upして下さい。

https://dashboard.ngrok.com/get-started/setup

macの場合

②-1:Download ngrok

ローカルPCへ

②-2:Unzip to install

zipファイルを回答します。

unzip /path/to/ngrok.zip
②-3:Connect your account

authtokenを利用して、権限認証します。

ngrok authtoken {//自分のauthtoken}
②-4:Fire it up

ngrokの起動です。

ngrok http 80

これで、ターミナル上にngrokが立ち上がります。

②-5:ngrokでアクセス

Forwardingになっているアドレスが、利用可能なアドレスです。

③:webhookでレスポンスを返す処理を作成

Laravelで、ルーティングと、Postの処理を記述していきます。

③-1:ルーティング設定 web.php

// LINE メッセージ受信
Route::post('line/webhook', [LineWebhookController::class, 'webhook'])->name('line.webhook');

③-2:Controllerを設定 web.php

class LineWebhookController extends Controller
{
    public function webhook(Request $request) {
        return 'ok';
    }
}

Webhookが正常に起動するかを確認するための実験なので、Postの処理は一番簡単なレスポンスを記述

④:LineのWebhook設定にURLを適用

https://0a62-39-110-209-109.ngrok.io/line/webhook

上記のように設定します。

「検証」ボタンをおして、成功が返ってくれば成功です。

ngrokを利用して、Webhookのテストをすることが出来ました。

⑤:Line Webhookをngrokが利用出来ない??

https://www.line-community.me/en/question/5f07c0fb851f74ab9c18e8dc/ngrok%E3%81%A7%E5%A4%96%E9%83%A8%E5%85%AC%E9%96%8B%E3%81%97%E3%81%9Furl%E3%81%A7webhook%E6%A4%9C%E8%A8%BC%E3%82%92%E6%88%90%E5%8A%9F%E3%81%95%E3%81%9B%E3%81%9F%E3%81%84?sortby=newest

上記のようなコメントがありましたが、2022年4月時点で利用は出来ました!

dockerコンテナ内で生成したファイルを、ホスト(PC)へDL(連携)する方法

dockerコンテナ内でpythonのライブラリをinstallし、そのライブラリをzip化。 そして、そのzipファイルをホスト環境(ローカルPC)へ連携することを実現します。

①:phpのライブラリをinstallするdockerコンテナ環境を構築

ファイル構成

/ 
 L Dockerfile
 L entrypoint.sh
Dockerfile
FROM python:3.7

RUN apt-get update && apt-get install -y zip unzip

#pipでinstallしたいディレクトリ$$パッケージを指定&&インストール
RUN pip install --target ./python google-api-python-client google-auth requests-oauthlib

ADD entrypoint.sh /
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

・imageのベースはphp 3.7をpullしてきています。
・pip install で installしたいライブラリを。※今回zipファイルで連携したいライブラリ郡です。
・entrypoint.shをコンテナ上に追加・コンテナでの操作権限を付与して、コンテナで上で、コンテナ上でコマンド実行可能な状態を作る。(コマンドの具体は、entrypoint.shに記載)
・ENTRYPOINTに /entrypoint.shを指定して、コンテナ起動後の実行を指定

②:ライブラリをZip化 / entrypoint.shにてコードで実行

entrypoint.sh
!/bin/bash -eu
 SRC=/python
 DIST=/dist/layer.zip
 zip -q -r ${DIST} ${SRC}
 /bin/bash

・コンテナ上の/python ディレクトリをzip化して、 DISTで指定したディレクトリ位置へ
・コンテナ内を確認するために、 /bin/bash を起動させる。

・DIST=/dist/layer.zip というdistディレクトリが突然現れました。これは、後に、run する際にバインドマウントする際に連携するディレクトリです。※詳細は下記

③:バインドマウントをして、dockerコンテナとホストを連携

docker build でimageを作成

docker build . -t {imageのnameを指定}

docker run で コンテナ起動 &&

docker run --rm -v $(pwd):/dist -it {imageのname}

rm:終了後、コンテナ削除

今回の処理の一番重要な部分は、

-v $(pwd):/dist 

ここです。

ここのバインドマウントの処理によって、
$(pwd):ホストの現在のディレクトリ

/dist :コンテナ上の distディレクトリ
を連携しています。
※FROM python:3.7にはdistは存在しないので、このタイミングで自動生成される。

この後、
>>②:ライブラリをZip化 / entrypoint.shにてコードで実行

>> zip -q -r ${DIST} ${SRC} の処理によって
dist へLayer.zipが生成されています。

④:ホスト環境 / dockerコンテナ環境で確認

④-1:ホスト環境を確認

ファイル構成

/ 
 L Dockerfile
 L entrypoint.sh
 L Layer.zip

上記のようになってると思います。

④-2:dockerコンテナ環境を確認

/bin/bashでコンテナを起動していると思いますので、コンテナの中身を確認していきましょう。

distディレクトリに移動して、その中で空のディレクトリを生成してみて下さい。

cd dist && mkdir gogo

そうすると、コンテナ上の/dist/gogoディレクトリが生成されます。

それと同時に、ホスト環境でも下記のようになっていると思います。

/ 
 L Dockerfile
 L entrypoint.sh
 L Layer.zip
 L gogo

⑤:バインドマウント

https://matsuand.github.io/docs.docker.jp.onthefly/storage/

バインドマウントを利用する際の副作用として、これが良いことか悪いことかはわかりませんが、コンテナー 内に動作するプロセスを通じて ホスト のファイルシステムに変更がかけられるということです。 たとえばシステムにとって重要なファイル、ディレクトリを生成、編集、削除ができてしまいます。 セキュリティに影響を及ぼしかねない強力な能力がここにあって、ホストシステム上の Docker 以外のプロセスへも影響します。

Lambdaでselenium実行 / 空のHTMLがreturn|

Lambda(AWS) ✕ seleniumを利用して、下記の関数コードでスクレイピングを実行した。(Lambdaの設定詳細は割愛)

from selenium import webdriver
 import os
 os.environ['HOME'] = '/opt/'
 def lambda_handler(event, context):
     options = webdriver.ChromeOptions()
     options.add_argument("--headless")
     options.add_argument("--disable-gpu")
     options.add_argument("--hide-scrollbars")
     options.add_argument("--single-process")
     options.add_argument("--ignore-certificate-errors")
     options.add_argument("--window-size=880x996")
     options.add_argument("--no-sandbox")
     options.add_argument("--homedir=/tmp")
     options.binary_location = "/opt/headless/python/bin/headless-chromium"
     #headless-chromiumへのPATHは、ご自身の環境に応じて設定して下さい。

    browser = webdriver.Chrome("/opt/headless/python/bin/chromedriver", options=options)
    #choromedriverへのPATHは、ご自身の環境に応じて設定して下さい。

    browser.get("https://www.google.com/")
    title = browser.title
   
    print(title)
    print(browser.page_source)    
   
    browser.close()
    return 'ok'

すると、printしたかったtitleが空白。
page_sourceが下記のように空っぽのHTMLで返ってきた。

<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body></body></html>

解決方法

optionに下記を追加して下さい。

options.add_argument('--disable-dev-shm-usage')

どうやら、Chromeがメモリ不足でクラッシュしてしまっているようです。(Lambdaの設定でメモリは十分に高くしていても関係ないようです。)

メモリ保持場所を/dev/shmの代わりに/tmpディレクトリを使うことができる設定です。

参考;

https://qiita.com/yoshi10321/items/8b7e6ed2c2c15c3344c6

追加したコード

from selenium import webdriver
 import os
 os.environ['HOME'] = '/opt/'
 def lambda_handler(event, context):
     options = webdriver.ChromeOptions()
     options.add_argument("--headless")
     options.add_argument("--disable-gpu")
     options.add_argument("--hide-scrollbars")
     options.add_argument("--single-process")
     options.add_argument("--ignore-certificate-errors")
     options.add_argument("--window-size=880x996")
     options.add_argument("--no-sandbox")
     options.add_argument("--homedir=/tmp")
     options.add_argument('--disable-dev-shm-usage')
     options.binary_location = "/opt/headless/python/bin/headless-chromium"
     #headless-chromiumへのPATHは、ご自身の環境に応じて設定して下さい。

    browser = webdriver.Chrome("/opt/headless/python/bin/chromedriver", options=options)
    #choromedriverへのPATHは、ご自身の環境に応じて設定して下さい。

    browser.get("https://www.google.com/")
    title = browser.title
   
    print(title)
    print(browser.page_source)    
   
    browser.close()
    return 'ok'

【最小労力】AWS Lambdaの関数で、S3へファイルアップロードする方法

Lambda → S3 へファイルアップロードをする方法をコンパクトにお伝えします。 連携できるを確認したい、通信方法を理解したい、といった場合には最小労力での動かし方を把握しましょう。

S3、Lambdaそれぞれのサービスを設定していきましょう。

①:S3で、ファイルアップロード先のbucketを作成

S3のbucket作成しましょう。
バケット名、AWSリージョンを設定して → bucket作成

S3のbucket設定

②-1:Lambdaの関数を作成

関数の作成 >> 1から作成 を選択します。

実行ロールの部分が重要です。この後、S3にアップロードする際にS3へのアクセス権限が必要です。

赤枠で囲んだ部分が、ロールです。
後ほど、このロールにS3へのファイルアップロード可能なアクセス権限を付与していきます。

②-2:Lambdaの関数を作成:ファイルアップロードを実行するコードソース

関数のコードソースに書きを反映します。
下記部分は環境に応じて変更して下さい。
bucket = '{S3で作成したアップロード先のbucket名}'

import json import urllib.parse import boto3 import datetime def lambda_handler(event, context): try: # Get the object from the event and show its content type s3 = boto3.resource(‘s3’) bucket = ‘{S3で作成したアップロード先のbucket名}’ key = ‘test_{}.txt’.format(datetime.datetime.now().strftime(‘%Y-%m-%d-%H-%M-%S’)) file_contents = ‘S3へファイルアップロード’ obj = s3.Object(bucket,key) obj.put( Body=file_contents ) except Exception as e: print(e) raise e

②-3:Lambdaのテストイベント設定

イベント JSON部分に下記で指定した、形式を反映して下さい。

JSON イベント形式

{ “Records”: [ { “eventVersion”: “2.0”, “eventSource”: “aws:s3”, “awsRegion”: “us-east-1”, “eventTime”: “1970-01-01T00:00:00.000Z”, “eventName”: “ObjectCreated:Put”, “userIdentity”: { “principalId”: “EXAMPLE” }, “requestParameters”: { “sourceIPAddress”: “127.0.0.1” }, “responseElements”: { “x-amz-request-id”: “EXAMPLE123456789”, “x-amz-id-2”: “EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH” }, “s3”: { “s3SchemaVersion”: “1.0”, “configurationId”: “testConfigRule”, “bucket”: { “name”: “example-bucket”, “ownerIdentity”: { “principalId”: “EXAMPLE” }, “arn”: “arn:aws:s3:::example-bucket” }, “object”: { “key”: “test%2Fkey”, “size”: 1024, “eTag”: “0123456789abcdef0123456789abcdef”, “sequencer”: “0A1B2C3D4E5F678901” } } } ] }

③:Lambdaの実行ロールにS3へのアクセス権限を付与

Lambdaの設定 >> 実行ロール へ。

先程作成した「実行ロール」が記載されています。ここからリンク遷移をして下さい。

IAM Mangerへ飛びます。

Lambdaの実行ロールとなっているロール(リンク遷移でこればそのままでOK)に対して、
アクセス許可を追加 >> ポリシーをアタッチ
から
AmazonS3FullAccess をアタッチして下さい。

④:テストで確認

Deploy → テスト を実行。
S3の指定したbucketへファイルがアップロードされています。

以上です。

今回紹介した、S3へのファイルアップロード方法だけでは、あまり使いみちがありませんが、トリガーなども組み合わせると、サーバーレスで、S3にファイル格納をすることが可能です。