AWS Lambda で Laravel を動かしてみた
会社のブログにも書きましたが、個人ブログにも書いておきます。
はじめに
AWS Lambda の Custom Runtime が来て、PHPが動くようになったので、
せっかくなら最近よく触っている Laravel を動かしてみようと思い立ちました。
Lambda Layer を作る
まずは、こちらのリポジトリを参考に、Laravelが動作する Lambda Layer を作成していきます。
Laravel の 動作に必要な extension の追加
上のリポジトリのままでは、Laravel を動作させるための拡張がいくつか不足しているので、build.sh
を書き換えて追加の拡張をLayerに追加します。
#!/bin/bash yum install -y php71-cli php71-mbstring php71-mysqlnd php71-opcache php71-pdo php71-pgsql zip mkdir /tmp/layer cd /tmp/layer cp /opt/layer/bootstrap . cp /opt/layer/php.ini . mkdir bin cp /usr/bin/php bin/ mkdir lib for lib in libncurses.so.5 libtinfo.so.5 libpcre.so.0; do cp "/lib64/${lib}" lib/ done cp /usr/lib64/libedit.so.0 lib/ mkdir php-7.1.d/ cp -a /etc/php-7.1.d/* php-7.1.d/ cp -a /etc/php-7.1.ini php-7.1.d/php.ini cp -a /usr/lib64/php lib/ zip -r /opt/layer/php71.zip .
bootstrap の修正
起動時に実行される bootstrap
の下記の部分を、
exec("PHP_INI_SCAN_DIR=/opt/etc/php-7.1.d/:/var/task/php-7.1.d/ php -S localhost:8000 -c /var/task/php.ini -d extension_dir=/opt/lib/php/7.1/modules '$HANDLER'");
各種 ini ファイルが読み込まれるように、PHP_INI_SCAN_DIR
を変更しておきます。
exec("PHP_INI_SCAN_DIR=/opt/etc/php-7.1.d/:/var/task/php-7.1.d/:/opt/php-7.1.d/ php -S localhost:8000 -c /var/task/php-7.1.d/php.ini -d extension_dir=/opt/lib/php/7.1/modules/ '$HANDLER'");
修正が済んだら、 make
コマンドを実行することで、Layer の動作に必要な zip ファイルが出来上がります。
Lambda Layer の 公開
zip ファイルを、Lambda Layer に登録します。
upload.sh や publish.sh に記載されたバケット名やレイヤー名は自身の環境に合わせて適宜変更してください。
変更が正しく行われていれば、 make upload
, make publish
で Layer が登録されます。
注意点として、awscli
が最新でないと、 aws lambda
コマンドに Layer 周りのコマンドが存在しないためデプロイが失敗します。
必ず実行前に、awscli
を最新のものにしておきましょう。
SAM テンプレートを作成する
サンプルの SAM テンプレートを下記のように編集します。
AWSTemplateFormatVersion: 2010-09-09 Description: My PHP Application Transform: AWS::Serverless-2016-10-31 Resources: phpserver: Type: AWS::Serverless::Function Properties: FunctionName: !Sub ${AWS::StackName}-Laravel Description: Laravel on Lambda CodeUri: src/laravel Runtime: provided Handler: server.php MemorySize: 1028 Timeout: 30 Tracing: Active Layers: - !Sub arn:aws:lambda:${AWS::Region}:<アカウントID>:layer:<レイヤー名>:<レイヤーバージョン> Events: api: Type: Api Properties: Path: /{proxy+} Method: ANY Environment: Variables: TZ: Asia/Tokyo
Layers
には先ほど作成した Layer の ARN を設定してください。
Lambda 関数を作る
Laravel プロジェクトの作成
template.yml
の設置してあるディレクトリ配下に、src
ディレクトリを作成し、Laravel プロジェクトを作成します。
composer
はインストール済みの想定です。
mkdir src cd src php composer.phar create-project --prefer-dist laravel/laravel laravel "5.7.*"
各種キャッシュを書き込めるようにする
基本的に Lambda のファイルシステムは読み込み専用のため、キャッシュの書き込み等が実行できず、このままでは Laravel が動作しません。
キャッシュ用のリソースを用意するのは面倒だったので、今回は全てを Lambda 上で唯一書き込みが許可されている /tmp
に書き込むようにします。
bootstrap/app.php
の
return $app;
の前に、下記の行を追加して storage のルートディレクトリを /tmp
に上書きします。
$app->useStoragePath('/tmp');
また、 config/cache.php
と config/view.php
で存在しないディレクトリを指定しているとエラーが起きるので、それぞれ /tmp
直下に書き込むように変更します。
// config/cache.php 'file' => [ 'driver' => 'file', 'path' => storage_path(''), ],
// config/view.php
'compiled' => env(
'VIEW_COMPILED_PATH',
realpath(storage_path(''))
),
セッションストレージを DynamoDBに逃がす
手前味噌ですが、今回はこちらの記事のように、DynamoDBをセッションストレージに変更しました。
セッションをファイルに残すこともできますが、その場合は、前述したように、 /tmp
配下に書き込むように設定してください。
これで最低限の設定が完了しました。
Lambda Function のデプロイ
sam
コマンドでデプロイを実行します。
sam package \ --template-file template.yml \ --output-template-file serverless-laravel.yml \ --s3-bucket <デプロイ用バケット名> sam deploy \ --template-file serverless-laravel.yml \ --stack-name serverless-laravel \ --capabilities CAPABILITY_IAM
※ DynamoDB の作成や、DynamoDB に Lambda からアクセスするための IAM の設定などは今回の記事の本筋とは外れるため、各自適切に設定してください。
動作確認
問題なくデプロイが完了したら、API Gateway 経由でアクセスしてみましょう。
https://<API Gateway のエンドポイント>Prod/
見慣れた画面が出てきました。
もし、{ "message": "Missing Authentication Token" }
と表示される場合は、API Gateway のルートエンドポイントに対する GET リクエストを`先ほど作成した Lambda に proxy するように変更しましょう。
http://<API Gateway のエンドポイント>Prod/notfound
など、ルーティングを設定されていないエンドポイントにアクセスすると、きちんと 404 ページが表示されます。
ただし、svg/404.svg
などがうまく処理できておらず、読み込めませんでした。
この辺は CloudFront 等をうまく使って静的ファイルを Lambda の外に逃がしてやる必要がありそうです。
今回作成したコードは、下記のリポジトリにテンプレートとして置いているので、興味のある方は使ってみてください。
なお、下記サンプルでは DynamoDB セッションストレージ化はしておらず、セッションはリクエスト毎に消えてしまいます。
最後に
今回は、Lambda の Custom Runtime を駆使して、Lambda 上で Laravel を動かしてみました。
まだベータ版ですが、Auroraサーバーレス のHTTPSエンドポイントも利用できるようになっていますので、
うまく実装すれば、Laravel on Lambda で使用するデータベースをAuroraにすることができる可能性があります。
セッションはDynamoに、ファイルはS3に、DBはAuroraにうまく逃がすことで、
真の意味で Laravel を Lambda で動作させることができると思っているので、今度挑戦してみたいと思っています。
Laravel のセッションストレージにDynamoDBを利用する
yoshitake_1201 がスルーしているので代わりに書いちゃいます。
この記事は、 Fusic Advent Calendar 2018 5日目の記事です。
はじめに
インフラのメイン担当として関わっている案件で、 ECS を使った構成で Laravel を動かすことになりました。
認証を含むWEBアプリケーションが複数のコンテナで動作するので、セッション管理が問題になってきます。
アプリケーション担当者と議論して、結果としてDynamoDBを使う運びになりました。
しかし、Laravel にはデフォルトで DynamoDB をセッションストレージとして使う機構は用意されていないので、今回はチーム内で実装することにしました。
やってみる
Laravel の公式ドキュメントを確認しながら進めます。
HTTPセッション 5.7 Laravel
ドライバの実装
カスタムセッションドライバでは、SessionHandlerInterfaceを実装してください。 このインターフェイスには実装する必要のある、シンプルなメソッドが数個含まれています。 MongoDBの実装をスタブしてみると、次のようになります。 <?php namespace App\Extensions; class MongoSessionHandler implements \SessionHandlerInterface { public function open($savePath, $sessionName) {} public function close() {} public function read($sessionId) {} public function write($sessionId, $data) {} public function destroy($sessionId) {} public function gc($lifetime) {} }
独自のセッションドライバを利用するためには、SessionHandlerInterface を実装する必要があるようです。
と言うことで、DynamoSessionHandler.php を作成します。
<?php namespace App\Extensions; class DynamoSessionHandler implements implements \SessionHandlerInterface { public function open($savePath, $sessionName) {} public function close() {} public function read($sessionId) {} public function write($sessionId, $data) {} public function destroy($sessionId) {} public function gc($lifetime) {} }
DynamoDB SessionHandler のドキュメントを見ながら、実装を追加
Class Aws\DynamoDb\SessionHandler | AWS SDK for PHP 3.x
DynamoDbClient と SessionHandler が必要そうです。
<?php use Aws\DynamoDb\DynamoDbClient; use Aws\DynamoDb\SessionHandler;
SessionHandler の初期化時にDynamoDbSessionHandler も初期化します。
<?php class DynamoSessionHandler implements implements \SessionHandlerInterface { protected $client; protected $handler; public function __construct(DynamoDbClient $client, array $config) { $this->client = $client; $this->handler = \Aws\DynamoDb\SessionHandler::fromClient($client, $config); } ... }
open()
, read()
など、実際に DynamoDB をセッションハンドラーとして利用するための実装は AWS SDK の SessionHandler が実装しているので、ドライバからSDKにそのまま横流しすればOKです。
<?php class DynamoSessionHandler implements implements \SessionHandlerInterface { ... public function open($save_path, $session_id) { return $this->handler->open($save_path, $session_id); } ... }
ドライバの登録
再度 Laravel の公式ドキュメントを確認します。
ドライバを実装したら、フレームワークへ登録する準備が整いました。 Laravelのセッションバックエンドへドライバを追加するには、Sessionファサードのextendメソッドを呼び出します。 サービスプロバイダのbootメソッドから、extendメソッドを呼び出してください。 既存のAppServiceProviderか真新しく作成し、呼び出してください。 <?php namespace App\Providers; use App\Extensions\MongoSessionHandler; use Illuminate\Support\Facades\Session; use Illuminate\Support\ServiceProvider; class SessionServiceProvider extends ServiceProvider { /** * サービス起動処理の事前登録 * * @return void */ public function boot() { Session::extend('mongo', function ($app) { // SessionHandlerInterfaceの実装を返す… return new MongoSessionHandler; }); } /** * コンテナへ結合を登録する * * @return void */ public function register() { // } } セッションドライバを登録したら、config/session.php設定ファイルでmongoドライバが使用できます。
ドライバを Laravel に認識させる必要があるようですので、公式ドキュメントにしたがって登録します。
まずは、 DynamoSessionServiceProvider.php を作成します。
<?php namespace App\Providers; use App\Extensions\DynamoSessionHandler; use Aws\DynamoDb\DynamoDbClient; use Illuminate\Support\Facades\Session; use Illuminate\Support\ServiceProvider; class DynamoSessionServiceProvider extends ServiceProvider { const DEFAULT_REGION = 'ap-northeast-1'; public function boot() { Session::extend('dynamo', function ($app) { $cfg = $app['config']->get('session'); $dynamoDbClient = DynamoDbClient::factory([ 'region' => (isset($cfg['region']) ? $cfg['region'] : self::DEFAULT_REGION), "endpoint" => (isset($cfg['endpoint']) ? $cfg['endpoint'] : 'https://dynamodb.'.self::DEFAULT_REGION.'.amazonaws.com'), 'version' => 'latest', ]); $config = [ 'table_name' => $cfg['table'], 'hash_key' => 'id', 'session_lifetime' => 60 * $cfg['lifetime'], 'consistent_read' => true, 'locking_strategy' => null, 'automatic_gc' => true, 'gc_batch_size' => 25, 'max_lock_wait_time' => 10, 'min_lock_retry_microtime' => 10000, 'max_lock_retry_microtime' => 50000 ]; // SessionHandlerInterfaceの実装を返す… return new DynamoSessionHandler($dynamoDbClient, $config); }); } // 今回は直接実装しているので register() は利用しない public function register() {} }
DynamoDbClient を作成する時に、リージョンやエンドポイントを環境変数から指定できるようにしました。
これで、 Laravel アプリケーションで DynamoSessionHandler が使用できるようになりました。
セッションストレージとして使う
いよいよ、DynamoDB をセッションストレージとして使用する準備が整いました。
セッション管理に使用する DynamoDB は session-table
としましょう。
ハッシュキーは、コード上で id
としているので間違えないように作成します。
$ aws dynamodb create-table \ --table-name session-table \ --attribute-definitions '[{"AttributeName":"id","AttributeType": "S"}]' \ --key-schema '[{"AttributeName":"id","KeyType": "HASH"}]' \ --provisioned-throughput '{"ReadCapacityUnits": 5,"WriteCapacityUnits": 5}'
あとは、config/session.php
に必要な情報を記載するだけです。
<?php return [ /* |-------------------------------------------------------------------------- | Default Session Driver |-------------------------------------------------------------------------- | | This option controls the default session "driver" that will be used on | requests. By default, we will use the lightweight native driver but | you may specify any of the other wonderful drivers provided here. | | Supported: "file", "cookie", "database", "apc", | "memcached", "redis", "array" | */ 'driver' => 'dynamo', 'region' => 'ap-northeast-1', 'table' => 'session-table', ...
無事に Laravel で DynamoDB をセッションストレージとして使用することができました!! (画面上は何の変化もない)
最後に
(^_^;)
でも、エンドポイントの指定ができるように作ったから、こっちは dynamodb-local が使えるもんね(強がり
SNSトピックへのメッセージをGoランタイムのLambdaでSlack通知する
前置き
AWSのリソースに関するイベントをSlackに通知したい欲があり 少し前にLamdaのGo言語サポートが実装されたことを思い出したので Goランタイムで動くLambda関数を作成しました。
実装コード
package main import ( "bytes" "context" "fmt" "net/http" "os" "github.com/antonholmquist/jason" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" ) type Slack struct { WebhookURL string Channel string Name string Emoji string Message string } func send(s Slack) { jsonStr := `{"channel":"` + s.Channel + `","username":"` + s.Name + `","icon_emoji":"` + s.Emoji + `","text":"` + s.Message + `"}` req, err := http.NewRequest( "POST", s.WebhookURL, bytes.NewBuffer([]byte(jsonStr)), ) if err != nil { fmt.Print(err) } req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { fmt.Print(err) } fmt.Print(resp) defer resp.Body.Close() } func createMessage(message string) string { json, err := jason.NewObjectFromBytes([]byte(message)) if err != nil { panic(err) } text := "<!channel>\n```\n" for k, v := range json.Map() { s, sErr := v.String() if sErr == nil { text += fmt.Sprintf("%s\t:%s\n", k, s) } else { text += fmt.Sprintf("%s\t:%s\n", k, sErr) } } text += "```" return text } func slackNotice(ctx context.Context, snsEvent events.SNSEvent) { for _, record := range snsEvent.Records { snsRecord := record.SNS var s = Slack{os.Getenv("WEBHOOK_URL"), os.Getenv("SLACK_CHANNEL"), os.Getenv("SLACK_NAME"), os.Getenv("SLACK_EMOJI"), createMessage(snsRecord.Message)} send(s) } } func main() { lambda.Start(slackNotice) }
Slackに通知するためのPost処理の部分は、
crossbridge-lab.hatenablog.com
こちらの記事を参考にさせていただきました。
SNSのイベント情報を取得する
今回の本命処理は、以下の部分です。 (本命と言うほどでもない)
func slackNotice(ctx context.Context, snsEvent events.SNSEvent) { for _, record := range snsEvent.Records { snsRecord := record.SNS var s = Slack{os.Getenv("WEBHOOK_URL"), os.Getenv("SLACK_CHANNEL"), os.Getenv("SLACK_NAME"), os.Getenv("SLACK_EMOJI"), createMessage(snsRecord.Message)} send(s) } }
今回は、SNSトピックに色々なイベントを集約して同一のSlackチャンネルに通知する方針です。
そのため、snsEvent events.SNSEvent
でトリガーとなったSNSのイベント情報を取得しています。
また、Slack通知関係の定数(WebhookURLなど)は、Lambda関数の環境変数をos.Getenv
で取ってくるようにしているため、別のアカウントでも流用できる形となっています。
まとめ
Go言語は初学者中の初学者なのですが、割と直感的に書けていい感じの言語だと思いました。 また、フォーマッティングまで言語がサポートしてくれるのはとても嬉しいですね。
もっと複雑な処理もサーバーレスに処理できるようにLambdaGoの知見を貯めて行きたいと思います。
TerraformでAutoScalingGroupを作るときのメモ
起動設定周りで少しハマった。
公式サイトにサンプルコードがあって、見れば一目瞭然なんだけど、
日本語の情報がヒットしないので自分用にメモしておく。
コード
# 起動設定 resource aws_launch_configuration lc { name_prefix = "${var.common["app_name"]}-lc-${terraform.workspace}-" image_id = "${var.launch_configuration["ami-id"]}" instance_type = "${var.launch_configuration["${terraform.workspace}.instance_type"]}" security_groups = ["${var.security_group["ec2_web_id"]}"] key_name = "${var.launch_configuration["key_name"]}-${terraform.workspace}" user_data = "${file("${path.module}/userdata.sh")}" lifecycle { create_before_destroy = true } depends_on = [ "aws_launch_configuration.lc", ] } # グループ resource aws_autoscaling_group asg { name = "${var.common["app_name"]}-asg-${terraform.workspace}" availability_zones = [ "${var.autoscaling_group["availability_zone1"]}", "${var.autoscaling_group["availability_zone2"]}", ] vpc_zone_identifier = [ "${var.public_subnet_ids[0]}", "${var.public_subnet_ids[1]}", ] launch_configuration = "${aws_launch_configuration.lc.name}" min_size = "${var.autoscaling_group["${terraform.workspace}.min_size"]}" max_size = "${var.autoscaling_group["${terraform.workspace}.max_size"]}" target_group_arns = ["${var.lb_target_group}"] lifecycle { create_before_destroy = true } depends_on = [ "aws_launch_configuration.lc", ] }
大事なところは
name_prefix = "${var.common["app_name"]}-lc-${terraform.workspace}-"
と
lifecycle { create_before_destroy = true }
name_prefix
を指定すると、prefixの後ろに自動で日付を基にした文字列をつけてくれるので、
「launch config の名前が重複している」エラーが出なくなる。
create_before_destroy
で、先に更新したlaunch configを作成してAutoScalingGroupにアタッチしてくれるので、
「launch configが消せない」エラーが出なくなる。
Nuxt.jsで作成したページにログイン機構を作る
クリスマスなんて関係ない!!
この記事はFusic Advent Calendar 2017 25日目の記事です。
Fusicでは二ヶ月に一回くらいの頻度で「エンジニア開発合宿」と称して丸1日泊まり込みで自分の興味のある技術やガジェットに関する開発を行うイベントがあります(2017年12月時点)。
自分は、フロントエンドに苦手意識があるので(というか苦手)、javascriptのフレームワークを使ってアプリを作成したいと思い、
今回はNuxt.jsでログイン認証ができるところまでを実装しました。
プロジェクトの作成
1. vue-cliをインストール
Vue.jsを使う環境をいい感じに構築してくれるコマンドラインインタフェースをインストールします。
$ npm install -g vue-cli
2. vue init
プロジェクトの初期化を行います。
$ vue init nuxt-community/starter-template hello_nuxt ? Project name hello_nuxt ? Project description Nuxt.js project ? Author k-masatany <masatani@fusic.co.jp> vue-cli · Generated "hello_nuxt". To get started: cd hello_nuxt npm install # Or yarn npm run dev
作成されたプロジェクトに必要なnodeモジュールをインストールします。
$ cd hello_nuxt/ $ yarn
Hello, Nuxt.js
$ yarn dev
を実行して、 http://localhost:3000 にアクセスします。 下記のようなページが表示されるはずです。
認証機構のベースを作成する
基本的な部分は公式ページを参考にして、認証機構を構築していきます。 (細かい説明は公式ページに書いてあるので、作業内容を箇条書きしています。)
1. 依存パッケージをインストール
$ yarn add express express-session body-parser whatwg-fetch
2. server.jsファイルを作成
プロジェクトのルートディレクトリにserver.js
を作成します。
const { Nuxt, Builder } = require('nuxt') const bodyParser = require('body-parser') const session = require('express-session') const app = require('express')() // req.body へアクセスするために body-parser を使う app.use(bodyParser.json()) // req.session を作成します app.use(session({ secret: 'super-secret-key', resave: false, saveUninitialized: false, cookie: { maxAge: 60000 } })) // POST /api/login してログインし、認証されたユーザーを req.session.authUser に追加 app.post('/api/login', function (req, res) { if (req.body.username === 'k-masatany' && req.body.password === 'demo') { req.session.authUser = { username: 'k-masatany' } return res.json({ username: 'k-masatany' }) } res.status(401).json({ error: 'Bad credentials' }) }) // POST /api/logout してログアウトし、ログアウトしたユーザーを req.session から削除 app.post('/api/logout', function (req, res) { delete req.session.authUser res.json({ ok: true }) }) // オプションとともに Nuxt.js をインスタンス化 const isProd = process.env.NODE_ENV === 'production' const nuxt = new Nuxt({ dev: !isProd }) // プロダクション環境ではビルドしない if (!isProd) { const builder = new Builder(nuxt) builder.build() } app.use(nuxt.render) app.listen(3000) console.log('Server is listening on http://localhost:3000')
このサンプルコードではk-masatany/demo
でしかログインできません。
3. package.jsonを更新
先ほど作成したserver.js
を読み込むようにします。
{ "name": "hello_nuxt", "version": "1.0.0", "description": "Nuxt.js project", "author": "k-masatany <masatani@fusic.co.jp>", "private": true, "scripts": { "dev": "node server.js", // ここと "build": "nuxt build", // ここと "start": "cross-env NODE_ENV=production node server.js", // ここ "generate": "nuxt generate", "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", "precommit": "npm run lint" }, "dependencies": { "body-parser": "^1.18.2", "express": "^4.16.2", "express-session": "^1.15.6", "nuxt": "^1.0.0-rc11", "whatwg-fetch": "^2.0.3" }, "devDependencies": { "babel-eslint": "^7.2.3", "eslint": "^4.3.0", "eslint-config-standard": "^10.2.1", "eslint-loader": "^1.9.0", "eslint-plugin-html": "^3.1.1", "eslint-plugin-import": "^2.7.0", "eslint-plugin-node": "^5.1.1", "eslint-plugin-promise": "^3.5.0", "eslint-plugin-standard": "^3.0.1" } }
その後、
$ yarn add cross-env $ yarn dev
を実行してデバッグ環境を再起動します。
4. ストアを作成する
ユーザーの情報を保持するためのstore/user.js
を作成します。
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) // window.fetch() のためのポリフィル require('whatwg-fetch') const store = () => new Vuex.Store({ state: { authUser: null }, mutations: { SET_USER: function (state, user) { state.authUser = user } }, actions: { nuxtServerInit ({ commit }, { req }) { if (req.session && req.session.authUser) { commit('SET_USER', req.session.authUser) } }, login ({ commit }, { username, password }) { return fetch('/api/login', { // クライアントのクッキーをサーバーに送信 credentials: 'same-origin', method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }) .then((res) => { if (res.status === 401) { throw new Error('Bad credentials') } else { return res.json() } }) .then((authUser) => { commit('SET_USER', authUser) }) }, logout ({ commit }) { return fetch('/api/logout', { // クライアントのクッキーをサーバーに送信 credentials: 'same-origin', method: 'POST' }) .then(() => { commit('SET_USER', null) }) } } }) export default store
5. 認証が必要なページを作成
/hello
ルートを作成するため、pages/hello.vue
を作成します。
今回はindex.vueをコピーして、文面だけ変えています。
<template> <!-- 中身は自由に作成 --> <section class="container"> <div> <h2 class="subtitle"> Hello!! </h2> </div> </section> </template> <script> export default { // データをこのコンポーネントにセットする必要がないため fetch() を使う fetch ({ store, redirect }) { if (!store.state.authUser) { return redirect('/auth') } } }
デフォルトのindex.vue
に手を加えて、/hello
へのリンクを作成します。
<template> <section class="container"> <div> <logo/> <h1 class="title"> hello_nuxt </h1> <h2 class="subtitle"> Nuxt.js project </h2> <div class="links"> <a href="/hello" class="button--green"> go to hello page</a> // ここ </div> </div> </section> </template>
今はまだ認証がおこなわれていないので、ボタンをクリックすると/auth
にリダイレクトされます。
/auth
はまだ作成されていないので、404になります。
6. ログインページを作成
認証用の/auth
ページを作成するために、pages/auth.vue
を作成します。
CSSなどは適当に当ててください。
<template> <section class="container"> <div> <h2 class="subtitle"> Login </h2> <form v-if="!$store.state.authUser" @submit.prevent="login"> <p class="error" v-if="formError">{{ formError }}</p> <div> <input type="text" class="form-control" v-model="formUsername" name="username" placeholder="Username" /> <input type="password" class="form-control" v-model="formPassword" name="password" placeholder="Password" /> <button type="submit" class="button--green block">Login</button> </div> </form> <div v-else> <h2>Hello {{ $store.state.authUser.username }}!</h2> <div class="links"> <a href="/hello" class="button--green">go to hello page</a> <button class="button--grey" @click="logout">Logout</button> </div> </div> </div> </section> </template> <script> export default { data() { return { formError: null, formUsername: '', formPassword: '' } }, methods: { async login() { try { await this.$store.dispatch('login', { username: this.formUsername, password: this.formPassword }) this.formUsername = '' this.formPassword = '' this.formError = null } catch (e) { this.formError = e.message } }, async logout() { try { await this.$store.dispatch('logout') } catch (e) { this.formError = e.message } } } } </script>
ログインしていない状態では、フォームが表示され、認証が通ったらユーザー名と/hello
へのリンクが表示されます。
ログインしてみる
k-masatany/demo
でログインできるので、入力します。
きちんとk-masatany
と表示されています。
それでは、改めて/hello
へ移動してみます。ログイン後のボタンをクリックしてみましょう。
※画像はいらすとや様よりお借りしました。
無事に表示されました。(hello.vueのコードは書き換えました)
この状態であれば、/
に戻って/hello
へのリンクをクリックしても/auth
へ飛ばされることはありません。
終わりに
今回は公式ページを参考にして、Nuxt.jsにログイン機構を作成しました。
今後は、AWS Cognitoなどを使ったログインの仕組みをつけたいと思います。
nc(netcat)コマンドでHTTPサーバーの気持ちを知る
この記事はFusic Advent Calendar 2017 12日目の記事です。
WEBエンジニアとしていつもお世話になっているApacheやNginxと言った「HTTPサーバーの気持ち」を知ろうと思い立ったので、ncコマンドを使ってHTTPリクエストを受けてHTTPレスポンスを返す一連の流れを体験してみました。
まがいなりにもネットワークスペシャリストなのでネットワーク関連の記事を書こうと思っていましたが、良いネタが思いつかなかったので、今日はレイヤー7で遊ぼうと思います。
ncコマンドで簡易サーバーを立てる
何はともあれ、HTTPを受けるためのサーバーが必要です。 8000番ポートで待ち受けるTCPサーバーをncコマンドで構築します。
while : ; do nc -l 127.0.0.1 8000; done
これでオレオレHTTPサーバ(文字通り)の構築は終わりです。簡単ですね。 もちろんこのサーバーは、同時に1つのリクエストしか処理できません。
ncコマンドはコネクションがクローズすると、プロセスが終了してしまいます。 一々コマンドを再実行してもいいですが、面倒臭いのでwhileで永続化しています。
HTTPリクエストを受けてみる
おもむろにブラウザ(Chromeがおすすめ)を立ち上げて、http://localhost:8000にアクセスしてみます。 おそらく、下記のようなHTTPリクエストが飛んでくるはずです。
$ while : ; do nc -l 127.0.0.1 8000; done GET / HTTP/1.1 Host: localhost:8000 Connection: keep-alive User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36 Upgrade-Insecure-Requests: 1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Accept-Encoding: gzip, deflate, br Accept-Language: ja,en-US;q=0.9,en;q=0.8 Cookie: username-localhost-50080="2|1:0|10:1511168650|24:username-localhost-50080|44:Nzc3OWJmNjE0NTI5NDEzZDk2Y2I2MzU5ZmEwMmE1MDQ=|4279df9a2de706e91212b77f6ef85216f0b7f2f9fb8211e6430c53b5f283b952"
ブラウザにはまだ何も表示されません。 レスポンスを返していないのだから当然ですね。
HTTPレスポンスを返す
いつまでもブラウザを待たせているのもかわいそうなので、そろそろレスポンスを返してあげます。 コンソールに、下記の内容のレスポンスヘッダを返します。
HTTP/1.1 200 OK Server: k-masatany 28.5 (nightly) Content-Type: text/html <!DOCTYPE html> <html> <title>nc server</title> </head> <body>
ここまで入力すると、Chromeを使っていれば、開いているページが真っ白になり、ページタイトルが「nc server」になっているはずです。 Safariだと、Ctrl+DでEOFを送信するまでレンダリングは行われませんでした。
bodyの内容も送る
真っ白なページでは面白くないのでbodyの内容を送ってみましょう。 先ほどの<body>に続いて、下記の内容を書いて行きます。
<div>Hello, HTTP World!</div> <h1>H1 Message</h1> <h2 style="color: red">Red Color Message<h2> <h3>hogehoge</h3>
Chromeでは、タグを閉じるたび、レンダリングが行われます。
404 NotFoundを返してみる
http://localhost:8000/notfoundにアクセスしてみます。 ここにはコンテンツがないので、404を返してみます。 (オレオレHTTPサーバにはそもそもコンテンツがないので全部404なのですが・・・)
HTTP/1.1 404 Not Found Server: k-masatany 28.5 (nightly) Content-Type: text/html <!DOCTYPE html> <html> <title>nc server</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous"> </head> <body> <h1 class="display-1">404 Not Found</h1> <p class="h2">ご指定のページは見つかりませんでした<p>
おっと、文字化けしてますね(しれっとBootstrap適用)。
ページをリロードして、metaタグを追加したレスポンスbodyを再送信します。
HTTP/1.1 404 Not Found Server: k-masatany 28.5 (nightly) Content-Type: text/html <!DOCTYPE html> <html> <meta charset="utf-8"> <title>nc server</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous"> </head> <body> <h1 class="display-1">404 Not Found</h1> <p class="h2">ご指定のページは見つかりませんでした<p>
日本語も無事表示されました。
404ステータスコードを返しても、bodyにコンテンツを入れていると、描画されてしまうので、 レスポンスボディを消してみます。
HTTP/1.1 404 Not Found Server: k-masatany 28.5 (nightly) Content-Type: text/html ^D # Ctrl+D
ChromeのHTTP ERROR 404ページになりました。 きちんとステータスコードが影響しています。
HTTPで遊んでみて
色々HTTPで遊んでみましたが、各種HTTPヘッダに対する応答をはじめ、オレオレおHTTPサーバーでは様々な機能が使えません。
こんな面倒なことをいつも一瞬でやってくれているHTTPサーバーに感謝を感じる遊びでした。
最後に
忙しくてレスポンスが返せない時は、きちんと503エラーを返しましょう。
HTTP/1.1 503 Service Unavailable Server: k-masatany 28.5 (nightly) Content-Type: text/html ^D # Ctrl+D
YAPC::Fukuoka 2017に行ってきました
ブログを書くまでがYAPCです。
というわけで、「弊社の参加者はブログを書かないのかしら」と社内Slackで実行委員長さんに突っつかれたので、久々の更新です。
そもそも、YAPCに限らず、こういったイベントに参加すること自体が初めて(だと思う)なので、期待半分、不安半分でした。
拝見したセッション一覧(講演順)
- レガシーPerlと「今」を組み合わせ、開発を継続し続ける方法(山下 和彦さん)
- 稼働中の Web サービスの Perl 処理系バージョンアップをしていく話(astjさん)
- ウェブセキュリティの最近の話題早分かり(徳丸 浩さん)
- コンテナを「守る」仕組みから、中身を理解しよう!(近藤 宇智朗さん)
- システム障害をめぐる冒険 (タケタニヒロトさん)
- Web application good error messages and bad error messages(moznionさん)
- cpm(鍛治 匠一さん)
- 本編スペシャルセッション: 福岡のIT企業、開発現場の未来
- Inside Evalpark - the evolution of sandboxing(Dan Kogaiさん)
- 巨大Perlプロジェクトに、Dockerが出会った(acidlemonさん)
- Lightning Talk
- Sponsor Session
- 福岡からニューヨークへ転勤になったエンジニアの話(山本 竜三さん)
印象に残ってるセッション
コンテナを「守る」仕組みから、中身を理解しよう!
このセッションが見たくて参加したこともあり、とても勉強になりました。
コンテナと呼ばれる技術の基本的な考え方が理解でき、好奇心から、帰ってすぐにコンテナ風プロセスを実装して遊びました。
ウェブセキュリティの最近の話題早分かり
「時間がないのでサクッと攻撃しちゃいますね」というパワーワードが今でも頭から離れない。そんなセッション。
登壇用に用意されたサイトだとしても、徳丸さんがいとも簡単に脆弱性をついている様子を見て、「今までよりももっとセキュリティに意識を向けないといけない」と強く感じました。
割と嬉しかったこと
帰宅後、@udzuraさんのセッションで出てきたコンテナ(風)プロセスを、スライドを見ながらC言語で実装してみました。
(とりあえずchrootとか試すだけのクソコードなのでコードは割愛します)
思っていたよりずっと簡単に、コンテナみたいなプロセスができてしまいました。
↑の話を呟いたら、@udzuraさん本人がいいねしてくれたのが地味に嬉しかったです。
まとめ
前は割と「イベントってスライドも大体ネットに上がるし、行くほどでもないかな」と思って参加はあまりしていませんでした。
今回も、社内の参加予定の方が仕事の都合で行けなくなったため、チケットを譲っていただき参加しています。
ただ、今回の参加をきっかけに、少し考えを改めようと思いました。
行かないと得られない情報もありますし、何よりあの空気感は向上心を刺激してくれるので、今後も定期的にイベントには参加したいと思いました。
運営の皆様、登壇者の皆様、ありがとうございました。