[Laravel5.4] Proxy環境下でSSL強制

5年ほどROM専だったけど気が向いたので投稿。
最近業務でLaravel使うこと多くなってきた。

前提

  • Laravel 5.4
  • PHP7.1
  • AWS + CloudFront(CDN)
  • Port443空いてる Port80閉じてる

解決したいこと

まーあるあるネタ

  • 普通にLaravel使ってると、リダイレクト時やblade でリンク生成時、
    最終的にUrlGeneratorでURL生成してることがほとんど
  • UrlGeneratorはアクセス元のスキームやホストからフルURLを生成する
  • Proxy環境下ではクライアントからのアクセスはHTTPSでも、エンドポイントから見るとHTTPなこと多し
  • よってUrlGeneratorがクライアント側がHTTPSでもhttpなURLを生成しちゃう

解決方法

ミドルウェアでリクエストを確認して、HTTPSなアクセスだったらスキーマを書き換える

ミドルウェア作成

app/Http/Middleware/SecureAccess.php
<?php
namespace AppHttpMiddleware;

use SymfonyComponentHttpFoundationRequest;

class SecureAccess
{
    public function handle($request, Closure $next, $guard = null)
    {
        $is_secure =  $request->server('HTTPS') === 'on'
            || $request->server('HTTPS') === '1'
            || $request->server('SSL') === 'on'
            || $request->server('HTTP_X_FORWARDED_PROTO') === 'https'
            || $request->server('HTTP_CLOUDFRONT_FORWARDED_PROTO') === 'https'
        ;   // ※後述1

        if (! $is_secure) {
            return $next($request);
        }

        URL::forceScheme('https'); // ※後述2

        Request::setTrustedProxies([
            '0.0.0.0/0'
        ]); // ※後述3

        return $next($request);
    }
}
  1. 大抵のProxy環境下ではHTTP_X_FORWARDED_PROTOでいけるはずだけど、
    CloudFront経由時は更にHTTP_CLOUDFRONT_FORWARDED_PROTOを見ないとダメだった。
    HTTPSかどうかの判定は他にもあるかも。
  2. UrlGenerator::forceScheme() のエイリアス。これでUrlGeneratorが返すURLが(ほぼ)httpsになる。
  3. 上記2の例外の対処で一部のURLを生成する関数は、
    forceScheme関係なくSymfonyのRequestオブジェクトからURLを生成している。
    そやつは $_SERVER['REMOTE_ADDR']とここでセットしたIPを判定しマッチしてればhttps、
    そうでなければ$_SERVER['HTTPS']のみを見てスキーマを返している。
    (正しくIP指定しないとローカル開発時の直HTTPSアクセスでProxy経由と判定されちゃうよ☆)

ミドルウェアの登録

あとは普通にミドルウェア登録。

app/Http/Kernel.php
<?php
namespace AppHttp;

use IlluminateFoundationHttpKernel as HttpKernel;

class Kernel extends HttpKernel
{
    protected $middleware = [
        /* ~~ その他ミドルウェア ~~ */
        AppHttpMiddlewareSecureAccess::class,
        /* ~~ その他ミドルウェア ~~ */
    ];
}

所感

  • forceScheme 強制スキーマ(強制とは言ってない)
  • SymfonyのRequestクラスはホストはX_ORIGINAL_URLやらX_REWRITE_URLetc…見て大分がんばってるのに、
    スキーマだけはこういう判定方法にしてるのはなんか理由あるんかな?

続きを読む

NLBでfluentdのforwardパケットを分散させてみた

AWSの新しいロードバランサであるNLB(Network Load Balancer)を使ってfluentdのforwardパケットを分散してみたので、レポートをまとめておく。

NLB自体については、クラスメソッドのブログ等で紹介されているのでそちらを参照するのが分かり易い。

静的なIPを持つロードバランサーNetwork Load Balancer(NLB)が発表されました!
試してわかった NLB の細かいお作法

ざっくり言うと、TCPプロトコルを対象にしたALBって感じ。
ターゲットグループはポートレベルで設定できるので、コンテナ環境と相性が良い。
ポート違いで複数fluentdが立っていても同じグループとしてまとめて分散できる。

利用までの流れ

  1. ターゲットグループを作成し、TCPレベルでコネクションが貼れるかのヘルスチェックの設定をする
  2. インスタンスもしくは対象IPと、ポートの組をターゲットグループに登録する
  3. NLBを作成し対象となるAZを設定、リスナーポートとターゲットグループを紐付ける
  4. DNSを登録してそのDNS向けにforwardパケットを流す様にする

fluentdで利用する場合

普通、fluentdのforwardパケットはクローズドなネットワークを流れるので、internal NLBとして作成した。
この時、NLBはEIPを紐付けられる様になっているので、必要があればEIPを準備する。その場合AZ毎に必要になる。後からの変更はできないらしい。
EIPを紐付けない場合は自動でプライベートIPが採番される。
ヘルスチェックパケットは、NLBのIPからやってくる。
NLBはセキュリティグループを持つことができないので、ヘルスチェックを通過するにはノード側のセキュリティグループでNLBのIPから対象ポートへの通信を許可する必要がある。
VPCのレベルで通信を許可して問題になることはあんまり無さそうなので、VPCのCIDRレベルで通信を許可しておくのが楽だろう。

エントリポイントとしてDNS名が自動で生成されるので、そこかもしくはEIPを対象にしたDNSのレコードを作っておく。
どうもALIASレコードには対応していない様なので、NLBのDNS名を登録する場合はCNAMEレコードになる。
一回作ろうとして失敗した記憶があるんですが、さっき試してみたらALIASレコード作れました。

実際に通信する時の動きについて

forwardの送信元になるノードはNLBのIPに対してTCP接続している様に見える。
forwardを受信するノードは送信元のノードのIPからTCP接続されている様に見える。
コネクションは長時間に渡って維持できるので、どちらかの要求で再接続されない限りは同じコネクションをずっと使い回すことができる。
動作を見るにProxyはせずに、IPの送信先だけを書き換えてパケットフローをコントロールしているっぽい。
しかし、これでは送信側と受信側でIPが噛み合わないので、戻りパケットがどうやって戻ってきているのか良く分からん。
送信側と受信側でtcpdumpを使ってパケットをキャプチャした結果、送信側はNLBとやり取りしている様に見えてるし、受信側は各個別のノード宛にパケットを送っている。
受信側のルーティングテーブルを弄ることなく繋がっているので、VPC内のスイッチで何らかの魔法が行われていないと戻りパケットがNLBに戻らないと思うのだが、この理解で合ってんのかな。
まあ、VPCとはいえ内部では色々とスイッチレイヤーを経てインスタンス同士が通信しているだろうし、そういう事は出来そうだけどちょっと気持ち悪いw

health checkについて

NLBからのhealth checkはTCPで疎通できれば通過するので、実際にfowardパケットが届くかどうかまでは分からない。
out_forwardプラグインのhealth checkは0.14系からはtransportモードがデフォルトになっていて実際に通信を行うコネクションをそのまま利用してheartbeatを送っている。
デフォルトだと基本的にはTCPのコネクションをそのまま利用するはずなので、ちゃんとheartbeatの疎通が通る。
もしノードが死んだ場合、即座にheartbeatパケットが反応してconnectionが切断される。(この辺からもTCPレベルでは直結してる様に見える)
再接続する際は、実際にout_forwardのheartbeatパケットが届く所に対して接続されるので、すぐに生きてるノードに対してのコネクションが復活する。
LBを噛ました時の動作としては望ましい感じがする。

パフォーマンスについて

今のところ最大で40Mbpsぐらいの流量でパケットを受け取っている。
すごい大規模なサービスではないので、そんなに大した量ではない。
アクティブなコネクション数は、2箇所のAZを合計して200前後。
LBのCapacityUnitは大体0.3ぐらいを推移している。
パケットの分散具合も安定している。
当分は使っていけそう。
面倒だったので実際のレイテンシまでは測定してない。まあうちぐらいだと問題になることは無さそう。
弊社の流量では限界があるので、もっと大規模なトラフィックが流れている環境でのレポートがあると良さそう。

続きを読む

serverless frameworkを使って本格的なAPIサーバーを構築(Express編)

serverless frameworkを使って本格的なAPIサーバーを構築(Express編)

最近、推しメンの serverless framework の記事の第3弾です。
保守メンテが楽になりつつも、実戦で速攻で構築ができます。

目次

framework_repo.png

前回まで、serverless frameworkのハンズオンの記事まで書きました。
今回は、serverless frameworkで 「 lambda + APIGateway + DynamoDB 」 の構成ですが、Expressを導入しローカルで実行できるようにします。

この記事でできるようになること

  • Lambda上でExpressを動かす
  • ローカルでAPIを試す環境を構築する

Expressを使う旨味

  • サーバレスでサービス全体を作るのはしんどい
  • ローカルで開発+テストできるようになる
  • Expressは、RubyのSinatraのようなシンプルなMVCフレームワークなのでとっつきやすい
  • packege.jsonをfunctionごとに用意しなくて良くなる!!

前準備

  • expressをinstallしておく
  • Lambda上でExpressを動かすツールとして、「aws-serverless-express(公式)」と「express-on-serverless」というものがあります。本来公式のものを使うべきですが、今回はexpress-on-serverlessを使用しました。
$ yarn add express
$ yarn add express-on-serverless

まずは、単純にJSONを返してみる(Express)

  • serverless.ymlを編集します

    • /配下をAnyですべてExpressに渡します
serverless.yml
functions:
  app:
    handler: functions/api/api.handler
    events:
      - http:
          path: /{proxy+}
          method: any
          cors: ${self:custom.cors_enabled.${opt:stage, self:provider.stage}}
  • api.jsでは、/hello がきたらJSONを返すようにしておきます。
functions/api/api.js
'use strict';

const express = require('express');
const app = express();
const bodyParser = require('body-parser');

app.use(bodyParser.urlencoded({
  extended: true
}));
app.use(bodyParser.json());

const response = {
  statusCode: 200,
  body: JSON.stringify({
    message: 'Go Serverless v1.0! Your function executed successfully!'
  }),
};

app.get("/hello", function(req, res, next){
  res.json(response);
});
exports.handler = require('express-on-serverless')(app);

ローカルで動かしてみる

  • 先ほどの、シンプルなアプリケーションをローカルで動かして見ましょう
  • functions/api/api.jsに以下を追記してください。
functions/api/api.js
// 追記する
if (process.env.NODE_ENV === 'dev') {
  app.listen(3000,  () => {
    console.log("Node.js is listening to PORT: 3000");
  });
}

そしてサーバーを起動します $ node functions/api/api.js

すると、http://localhost:3000/hello で先ほどのAPIにアクセスすることができます!!

  • これで、deployしても、前回と同じようなAPIを作ることができます!!
$ sls deploy -v -s dev

デプロイが成功し、APIにアクセスすると
前回Expressを使わずに作ったAPIと同じレスポンスになると思いますが、ローカルで開発できるようになったので、開発スピードは5000兆倍になります!!

まとめ

  • serverlessにExpressを乗せて使うと、開発スピードが5000兆倍になりました!!
  • 今まで通り、serverlessの旨味を生かしたイベントのフックでの発動+APIの構築ができるようになり利益ありありです。

次回

  • serverless frameworkを使って本格的なAPIサーバーを構築(テストコード編)です!!

続きを読む

AWS Greengrassを利用する際の「センサー制御部」と「新しいLambda関数のデプロイ」について

9/26のJAWS-UG IoT専門支部主催「AWS Greengrass Handson」を受講し終わったばかりの松下です

受講目的

  1. AWS Greengrass Coreからセンサー制御ってどうするの?
  2. Lambda関数の更新時の手順は?

この2点が前から気になっていました。運用するのに必須だからさ。

結論

AWS Greengrass についてわかった事 (たぶん)

AWS Greengrass Core (エッジデバイス上で動くコンテナプロセス、以下ggc) は、AWS IoT に対するMQTT Proxyサーバ的役割と、Lambda実行環境を持っている。

そのため

  • ggcを起動すると 8883 ポートでMQTT接続を待ち受ける
  • オフラインとなったとしても、センサー制御部プログラム側から見ると、AWS IoTに直接接続しているかの如く動作し続けることができる

AWS Greengrass Coreとセンサー制御部の関係

ggcやその上で動くLambdaからOSが持っているローカルリソース、例えば /dev/ttyUSB0 をRead/Writeすることはできない。(と推測される)

  • そのため、「センサーからReadする→AWS Greengrass SDKを使って)ggcにデータを投げる」というプログラムが必要。また、このプログラムが動くようにする環境が必須
  • また、上記プログラムの活動制御はggcは行ってくれないので、systemdのUnitを作る※、そのプログラム自体の更新の仕組みを考える、等が必須
    • ※ggc上のLambdaを更新する際、ggcのMQTT接続が切れる模様。そうなると、センサー制御部のプログラムは再接続の仕組みを持たせるか、supervisor的なところから再起動してもらう必要がある

Lambda関数の更新時の手順

ggc上で動くLambdaのイメージ

ハンズオンでの構成を再現すると

ThingShadowSensor.py
  (publishするように書く)
     ↓
(8883:sensing/data/Sensor-01)
     ↑
  (subscribeするように設定)
Lambda:cpuUsageChecker-01:VERSION
  (publishするように書く)
     ↓
(8883:$aws/things/Alert-01/shadow/#)
     ↑
  (subscribeするように書く)
ThingAlertSensor.py

わかりづらくてゴメン。何が言いたいかというと、Lambda関数だ~とか言わず、結局のところMQTTのトピックを介してパイプ処理をしている。そして、ggc内はスクリプトじゃなくてLambda関数を実行できるよ、という解釈で大丈夫。(たぶん)

そのため、この行先(=サブスクリプション)に、たとえば Lambda:cpuUsageChecker-01:NEW-VERSION が含まれるエントリー(トピックの組み合わせ)を作ってあげれば NEW-VERSION な Lambda関数が起動する。

手順は以下の通り;

  1. AWS Lambda関数を更新、発行する (新しいバージョンができる)
  2. クラウド側AWS Greengrassの「Lambda」メニューで、新しいバージョンのLambda関数を「追加」する (複数のバージョンが存在することになる)
  3. クラウド側AWS Greengrassの「サブスクリプション」メニューで、新しいバージョンのLambda関数を「ソース」や「ターゲット」とするエントリーを作成し、古いバージョンのLambda関数を利用しているエントリーを消す
    • 消さなくても良い。その代わり、トピックを調整する必要がある
  4. クラウド側AWS Greengrassの「グループ」メニューから、デプロイする

まだわからないこと

  • ggcに対してすっぴんのMQTTクライアント接続が可能なのか? (AWS Greengrass SDKはデカすぎる。センサー制御プログラムは極力小さくしたいはずだ!)

あとがき

市川さんはじめ、全国のJAWS-UG IoT 専門支部の方々、お疲れ様でしたー!!!

続きを読む

node.js + Expressでリクエスト単位でプロキシするにはrequestをpipeするだけで良い

概要

“express proxy” で普通に検索すると、express-http-proxyとか、express-request-proxyとか言うパッケージが見つかると思います。ただ、どちらもあるサイトをまるごと別のサイトにプロキシするという目的のために作られていて、キャッシュとかインジェクションの仕組みがあって便利なものの、「あるリクエストをプロキシしたい」というシンプルな用途には向いていません。また、いずれも、URLのエンコードにバグがあって、それで苦労させられたりしました。

最初、上記のパッケージをフォークしていろいろコードをいじっていたのですが、コードを見ていたら、単にリクエスト単位でプロキシをするだけなら、Expressの機能だけで、ものすごく簡単にできるということが分かりました。要するに、requestの結果をresにパイプするだけで、プロキシできるのです。まぁ、言われてみれば、Expressのresponseは、WritableStreamなので、そりゃそうか・・・という感じですが、これを知ったときは、拍子抜けしました。

当然、生のHTTPがそのまま返されるだけなので、レスポンスの加工には向いていませんが、レスポンスの加工が必要ないのなら、これで十分です(その気になれば、テキストレベルでのヘッダーの微調整くらいなら簡単にできる思いますが・・・・)。

サンプルコード

request側は、基本的には、req.headersをそのまま渡せば良いのですが、hostだけは削除しないといけないようです。セキュリティも考慮すると、 ‘authorization’と’cookie’も削除するのが妥当でしょう。用途によってはx-forwarded-* の変更(あるいは削除)が必要かもしれません。

const request = require('request');

function (req, res, next) {
  const proxyRequestHeaders = Object.assign({}, req.headers);
  for(key of ['host', 'authorization', 'cookie']){
    delete proxyRequestHeaders.headers[key]
  }
  const proxyUrl = 'http://calculated.proxy.url/';
  request({
    url: proxyUrl,
    method: req.method,
    headers: proxyRequestHeaders,
  }).pipe(res);
}

用途

ちなみに、なんでこんなことをやろうとしたかというと、AWS S3のsignedURLをプロキシで返したかったのです。基本的には、302 redirectで問題ないのですが、後方互換性のためproxyする必要がありました。proxyだと、S3側で、if-none-matchヘッダとかrangeヘッダの処理とか細かいことを全部やってくれるので便利です。

続きを読む

serverless frameworkを使って本格的なAPIサーバーを構築(ハンズオン編)

serverless frameworkを使って本格的なAPIサーバーを構築(ハンズオン編)

目次

  • serverless frameworkを使って本格的なAPIサーバーを構築(魅力編)
  • serverless frameworkを使って本格的なAPIサーバーを構築(ハンズオン編)← 今ここ
  • serverless frameworkを使って本格的なAPIサーバーを構築(Express編)
  • serverless frameworkを使って本格的なAPIサーバーを構築(MVC編)

framework_repo.png

前回、serverless frameworkの魅力の記事を書きました。
今回は、serverless frameworkで 「 lambda + APIGateway + DynamoDB 」 の構成で簡単なサンプルアプリを作成します。

この記事でできるようになること

  • REST FULLなAPIを構築する
  • DynamoDBと連携できる
  • スケージューリングで実行する
  • DynamoStreamが使えるようになる

前準備

  • serverlessをinstallしておく
$ npm install -g serverless
  • プロジェクトを生成する
$ serverless create --template aws-nodejs --path my-service

すると、以下のファイルが出来ているはずです。

$ ls
serverless.yml
handler.js

また、サービス用のテンプレートですが、以下の言語用のテンプレートが用意されています。

使える言語

aws-nodejs
aws-python
aws-java-maven
aws-java-gradle
aws-scala-sbt

まずは、単純にJSONを返してみる

  • 最初はHelloWorld!をJSONで返すAPIで表示してみます。
  • serverless create をした時点でhandler.jsはこのようになっているので、messageを hello world など適当に変えれば良いです。
handler.js
'use strict';

module.exports.hello = (event, context, callback) => {
  const response = {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Go Serverless v1.0! Your function executed successfully!',
      input: event,
    }),
  };

  callback(null, response);

  // Use this code if you don't use the http event with the LAMBDA-PROXY integration
  // callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event });
};

  • 最初のserverless.ymlを少し編集します

    • regionがdefaultでは us-east-1 なので ap-northeast-1 にする
    • getでアクセスできるように eventsを追加する
serverless.yml
service: my-service

provider:
  name: aws
  runtime: nodejs6.10

  stage: dev
  region: ap-northeast-1

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          path: hello
          method: get
  • これでOKです!! さっそく、deployしてみましょう!!!!!!
$ sls deploy -v -s dev
  • -v は進捗を表示
  • -s はステージの名前を指示します(defaultはdev)
  • APIGatewayでは、プロジェクトからステージごとにデプロイしていましたが、serverlessではステージごとにプロジェクトが作られます。
Serverless: Stack update finished...
Service Information
service: XXXXXXXX
stage: dev
region: ap-northeast-1
api keys:
  None
endpoints:
  GET - https://XXXXXXXX.ap-northeast-1.amazonaws.com/dev/hello
functions:
  hello: XXXXXXXX
Stack Outputs

Serverless: Removing old service versions...

デプロイが成功すると、 APIGateWayには設定済みのプロジェクトと、endpoints を教えてくれるのでそこにアクセスしてみてhelloとJSONが表示されればOKです!!!

スクリーンショット 2017-09-23 15.28.55.png

スクリーンショット 2017-09-23 15.28.38.png

このように、GETでアクセスをするとさっき作成したJSONを返していることがわかります。
完璧だー 🍙🍙🍙

REST APIを作ってみる

  • 次は、DynamoDBと連携 して、get post put deleteを実装してみます。

  • serverless.ymlを編集します

  • REST APIのサンプルは、aws-node-rest-api-with-dynamodbにあります。

serverless.yml

serverless.yml
service: serverless-rest-api-with-dynamodb

frameworkVersion: ">=1.1.0 <2.0.0"

provider:
  name: aws
  runtime: nodejs4.3
  environment:
    DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:Scan
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"

functions:
  create:
    handler: todos/create.create
    events:
      - http:
          path: todos
          method: post
          cors: true

  list:
    handler: todos/list.list
    events:
      - http:
          path: todos
          method: get
          cors: true

  get:
    handler: todos/get.get
    events:
      - http:
          path: todos/{id}
          method: get
          cors: true

  update:
    handler: todos/update.update
    events:
      - http:
          path: todos/{id}
          method: put
          cors: true

  delete:
    handler: todos/delete.delete
    events:
      - http:
          path: todos/{id}
          method: delete
          cors: true

resources:
  Resources:
    TodosDynamoDbTable:
      Type: 'AWS::DynamoDB::Table'
      DeletionPolicy: Retain
      Properties:
        AttributeDefinitions:
          -
            AttributeName: id
            AttributeType: S
        KeySchema:
          -
            AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: ${self:provider.environment.DYNAMODB_TABLE}
  • serverless.ymlのポイント

    • environmentで、どのLambdaからでも DYNAMODB_TABLE を参照することができます。
    • todosというフォルダの中に create.js get.js list.js delete.js update.jsを置いておきます。
    • resourcesでDynamoDBを生成しています。

次に、それぞれのLambda関数を作成します

create.js

todos/create.js
'use strict';

const uuid = require('uuid');
const AWS = require('aws-sdk'); // eslint-disable-line import/no-extraneous-dependencies

const dynamoDb = new AWS.DynamoDB.DocumentClient();

module.exports.create = (event, context, callback) => {
  const timestamp = new Date().getTime();
  const data = JSON.parse(event.body);
  if (typeof data.text !== 'string') {
    console.error('Validation Failed');
    callback(null, {
      statusCode: 400,
      headers: { 'Content-Type': 'text/plain' },
      body: 'Couldn't create the todo item.',
    });
    return;
  }

  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Item: {
      id: uuid.v1(),
      text: data.text,
      checked: false,
      createdAt: timestamp,
      updatedAt: timestamp,
    },
  };

  // write the todo to the database
  dynamoDb.put(params, (error) => {
    // handle potential errors
    if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { 'Content-Type': 'text/plain' },
        body: 'Couldn't create the todo item.',
      });
      return;
    }

    // create a response
    const response = {
      statusCode: 200,
      body: JSON.stringify(params.Item),
    };
    callback(null, response);
  });
};

delete.js

todos/delete.js
'use strict';

const AWS = require('aws-sdk'); // eslint-disable-line import/no-extraneous-dependencies

const dynamoDb = new AWS.DynamoDB.DocumentClient();

module.exports.delete = (event, context, callback) => {
  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Key: {
      id: event.pathParameters.id,
    },
  };

  // delete the todo from the database
  dynamoDb.delete(params, (error) => {
    // handle potential errors
    if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { 'Content-Type': 'text/plain' },
        body: 'Couldn't remove the todo item.',
      });
      return;
    }

    // create a response
    const response = {
      statusCode: 200,
      body: JSON.stringify({}),
    };
    callback(null, response);
  });
};

get.js

todos/get.js
'use strict';

const AWS = require('aws-sdk'); // eslint-disable-line import/no-extraneous-dependencies

const dynamoDb = new AWS.DynamoDB.DocumentClient();

module.exports.get = (event, context, callback) => {
  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Key: {
      id: event.pathParameters.id,
    },
  };

  // fetch todo from the database
  dynamoDb.get(params, (error, result) => {
    // handle potential errors
    if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { 'Content-Type': 'text/plain' },
        body: 'Couldn't fetch the todo item.',
      });
      return;
    }

    // create a response
    const response = {
      statusCode: 200,
      body: JSON.stringify(result.Item),
    };
    callback(null, response);
  });
};

list.js

todos/list.js
'use strict';

const AWS = require('aws-sdk'); // eslint-disable-line import/no-extraneous-dependencies

const dynamoDb = new AWS.DynamoDB.DocumentClient();
const params = {
  TableName: process.env.DYNAMODB_TABLE,
};

module.exports.list = (event, context, callback) => {
  // fetch all todos from the database
  dynamoDb.scan(params, (error, result) => {
    // handle potential errors
    if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { 'Content-Type': 'text/plain' },
        body: 'Couldn't fetch the todos.',
      });
      return;
    }

    // create a response
    const response = {
      statusCode: 200,
      body: JSON.stringify(result.Items),
    };
    callback(null, response);
  });
};

update.js

todos/update.js
'use strict';

const AWS = require('aws-sdk'); // eslint-disable-line import/no-extraneous-dependencies

const dynamoDb = new AWS.DynamoDB.DocumentClient();

module.exports.update = (event, context, callback) => {
  const timestamp = new Date().getTime();
  const data = JSON.parse(event.body);

  // validation
  if (typeof data.text !== 'string' || typeof data.checked !== 'boolean') {
    console.error('Validation Failed');
    callback(null, {
      statusCode: 400,
      headers: { 'Content-Type': 'text/plain' },
      body: 'Couldn't update the todo item.',
    });
    return;
  }

  const params = {
    TableName: process.env.DYNAMODB_TABLE,
    Key: {
      id: event.pathParameters.id,
    },
    ExpressionAttributeNames: {
      '#todo_text': 'text',
    },
    ExpressionAttributeValues: {
      ':text': data.text,
      ':checked': data.checked,
      ':updatedAt': timestamp,
    },
    UpdateExpression: 'SET #todo_text = :text, checked = :checked, updatedAt = :updatedAt',
    ReturnValues: 'ALL_NEW',
  };

  // update the todo in the database
  dynamoDb.update(params, (error, result) => {
    // handle potential errors
    if (error) {
      console.error(error);
      callback(null, {
        statusCode: error.statusCode || 501,
        headers: { 'Content-Type': 'text/plain' },
        body: 'Couldn't fetch the todo item.',
      });
      return;
    }

    // create a response
    const response = {
      statusCode: 200,
      body: JSON.stringify(result.Attributes),
    };
    callback(null, response);
  });
};

これもデプロイをしてみてください!!

postmanなどで GET POST PUT DELETE すると確認ができるはずです。

スケジュールを設定して定期実行してみる

serverless.yml
  hoge:
    handler: functions/aggregate/index.handler
    events:
      - schedule:
          rate: cron(0 1 * * ? *)

serverless.yml
  hoge:
    handler: functions/aggregate/index.handler
    events:
      - schedule:
          rate: rate(5 minutes)

DynamoStreamを使ってputされたときに何か実行してみる

  • DynamoStreamとは、DynamoDBにputやupdateがあった場合に、そのイベントをトリガーにLambdaでまた処理させることができます。

  • DynamoのResourcesにStreamViewType を追加

serverless.yml
    HogeDynamoDbTable:
      Type: 'AWS::DynamoDB::Table'
      DeletionPolicy: Retain
      Properties:
        AttributeDefinitions:
          -
            AttributeName: column
            AttributeType: S
        KeySchema:
          -
            AttributeName: column
            KeyType: HASH

        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: "hoge-${self:provider.stage}"
        StreamSpecification:
          StreamViewType: KEYS_ONLY

そして、Streamでどの関数が呼ばれるかを指定します
以下のようにすると、二つのDynamoDBに変更がある場合に1つのLambda関数が呼ばれます。

serverless.yml
  hoge:
    handler: functions/hoge/index.handler
    events:
      - stream:
          type: dynamodb
          arn:
            Fn::GetAtt:
              - HogeDynamoDbTable
              - StreamArn
          batchSize: 1
      - stream:
          type: dynamodb
          arn:
            Fn::GetAtt:
              - HogeDynamoDbTable2
              - StreamArn
          batchSize: 1

batchSizeは1度にどれだけ項目がほしいか設定できます。

ちなみに、StreamViewTypeには

KEYS_ONLY => HASHキーのみ関数で取得できる
NEW_IMAGE =>  新しいものだけ取得できます
OLD_IMAGE => 古いものだけ取得できます
NEW_AND_OLD_IMAGES => 新旧のデータが取得できます

があります。

その他できること

まとめ

  • serverlessを使うとかんたんにAPIが作成できました!!
  • イベントの作成やオプションもまったく困らないと思います。
  • サンプルやプラグインが豊富で、ベストプラクティスには迷いません!

余談

  • 前回serverlessの魅了の紹介で、serverlessを使わないほうがいいときはないのかという質問を受けました。

serverlessを使わないほうがいいとき

  • 複雑なクエリを要するアプリを作るとき

    • 複雑なクエリを要するアプリを作るときは、DynamoDBでも設計を入念に行えばいいかと思いますが、whereなどのクエリが使えないので注意が必要です
    • MySQLクライアントもLambdaで使用することができますが、若干使いにくいようです。
  • Dynamoに書き込む容量が大きいとき

次回

  • serverless frameworkを使って本格的なAPIサーバーを構築(Express編)です!!

続きを読む

AWS CloudWatch で、Amazon Linux のパフォーマンスとログの監視をしてみる

0.はじめに

Amazon Linuxを利用していますが、
パフォーマンス監視は Zabbix を使って、
ログ監視は特に何も、
という感じでした。

CloudWatch のメトリクスの保存期間も長くなったみたいですし、
運用の手間やリスク、コスト削減も考慮して、
パフォーマンス監視を CloudWatch、
ログ監視を CloudWatch Logs、
にしようかと思います。

1.IAM Role へのポリシーのアタッチ

  1. 以下の IAM ポリシーを作成します。

    • ポリシー名 : GSCloudWatchWriteOnlyAccess ※ 任意
    • 説明 : ※ 任意
    • ポリシードキュメント :
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "Stmt1459146265000",
                "Effect": "Allow",
                "Action": [
                    "cloudwatch:PutMetricData"
                ],
                "Resource": [
                    "*"
                ]
            },
            {
                "Sid": "Stmt1459146665000",
                "Effect": "Allow",
                "Action": [
                    "logs:CreateLogGroup",
                    "logs:CreateLogStream",
                    "logs:PutLogEvents"
                ],
                "Resource": [
                    "arn:aws:logs:*:*:*"
                ]
            }
        ]
    }
    

  2. 作成したポリシーを EC2 インスタンスに割り当てられている IAM Role に付与します。

1.CloudWatch へのメトリクスデータの送信

  1. 色々調べたところ、collectd と その CloudWatch 用プラグインを利用するのが一般的みたいなので、今回はその手順で進めていきます。

  2. collectd をインストールします。

    $ sudo yum -y install collectd
    

  3. collectd の CloudWatch 用プラグインをインストールします。

    $ git clone https://github.com/awslabs/collectd-cloudwatch.git
    $ cd collectd-cloudwatch/src
    $ sudo ./setup.py
    
    Installing dependencies ... OK
    Installing python dependencies ... OK
    Downloading plugin ... OK
    Extracting plugin ... OK
    Moving to collectd plugins directory ... OK
    Copying CloudWatch plugin include file ... OK
    
    Choose AWS region for published metrics:
      1. Automatic [ap-northeast-1]
      2. Custom
    Enter choice [1]: 
    
    Choose hostname for published metrics:
      1. EC2 instance id [i-00484bb5ac67e244d]
      2. Custom
    Enter choice [1]: 
    
    Choose authentication method:
      1. IAM Role [testuekamawindowsserver]
      2. IAM User
    Enter choice [1]: 
    
    Enter proxy server name:
      1. None
      2. Custom
    Enter choice [1]: 
    
    Enter proxy server port:
      1. None
      2. Custom
    Enter choice [1]: 
    
    Include the Auto-Scaling Group name as a metric dimension:
      1. No
      2. Yes
    Enter choice [1]: 
    
    Include the FixedDimension as a metric dimension:
      1. No
      2. Yes
    Enter choice [1]: 
    
    Enable high resolution:
      1. Yes
      2. No
    Enter choice [2]: 
    
    Enter flush internal:
      1. Default 60s
      2. Custom
    Enter choice [1]: 
    
    Choose how to install CloudWatch plugin in collectd:
      1. Do not modify existing collectd configuration
      2. Add plugin to the existing configuration
      3. Use CloudWatch recommended configuration (4 metrics)
    Enter choice [3]: 
    Plugin configuration written successfully.
    Creating backup of the original configuration ... OK
    Replacing collectd configuration ... OK
    Replacing whitelist configuration ... OK
    Stopping collectd process ... NOT OK
    Starting collectd process ... NOT OK
    Installation cancelled due to an error.
    Executed command: '/usr/sbin/collectd'.
    Error output: 'Error: Reading the config file failed!
    Read the syslog for details.'.
    

  4. collectd の起動に失敗しています。collectd の python 用ライブラリが足りないみたいなので、インストールします。

    $ sudo yum -y install collectd-python
    

  5. collectd を起動します。

    $ sudo service collectd start
    

  6. collectd の自動起動の設定をします。

    $ sudo chkconfig collectd on
    $ sudo chkconfig --list | grep collectd
    
    collectd           0:off    1:off    2:on    3:on    4:on    5:on    6:off
    

  7. /etc/collectd.conf の設定を変更します。

    $ sudo cp -frp /etc/collectd.conf /etc/collectd.conf.ORG
    $ sudo vi /etc/collectd.conf
    
    • cpu :

      • LoadPlugin cpu をコメント解除し、以下の設定を行う。
      <Plugin cpu>
              ReportByCpu false
              ReportByState true
              ValuesPercentage true
      </Plugin>
      
    • df :

      • LoadPlugin df をコメント解除し、以下の設定を行う。
      <Plugin df>
      #       Device "/dev/hda1" 
      #       Device "192.168.0.2:/mnt/nfs" 
      #       MountPoint "/home" 
      #       FSType "ext3" 
      #       IgnoreSelected false
              ReportByDevice false
              ReportInodes false
              ValuesAbsolute true
              ValuesPercentage true
      </Plugin>
      
    • load :

      • LoadPlugin load をコメント解除し、以下の設定を行う。
      <Plugin load>
              ReportRelative true
      </Plugin>
      
    • memory :

      • LoadPlugin memory をコメント解除し、以下の設定を行う。
      <Plugin memory>
              ValuesAbsolute true
              ValuesPercentage true
      </Plugin>
      
    • swap :

      • LoadPlugin swap をコメント解除し、以下の設定を行う。
      <Plugin swap>
              ReportByDevice false
              ReportBytes false
              ValuesAbsolute false
              ValuesPercentage true
      </Plugin>
      

  8. /opt/collectd-plugins/cloudwatch/config/whitelist.conf の設定を変更します。以下のメトリクスの中で不要なものがあれば、適当に削除して下さい。

    $ cd /opt/collectd-plugins/cloudwatch/config/
    $ sudo cp -frp whitelist.conf whitelist.conf.ORG
    $ sudo vi whitelist.conf
    
    cpu-.*
    df-root-df_complex-free
    df-root-df_complex-reserved
    df-root-df_complex-used
    df-root-percent_bytes-free
    df-root-percent_bytes-reserved
    df-root-percent_bytes-used
    load--load-relative
    memory--percent-free
    memory--percent-used
    memory--memory-free
    memory--memory-used
    swap--percent-cached
    swap--percent-free
    swap--percent-used
    

  9. collectd を再起動します。

    $ sudo service collectd restart
    

  10. CloudWatch のマネジメントコンソールの左側ペインから、「メトリクス」を選択します。カスタム名前空間の「collectd」→「Host, PluginInstance」→ EC2 インスタンスの ID でフィルタをかけて、設定したメトリクスのデータがあるか確認します。

    • FireShot Capture 165 - CloudWatch Management Console_ - https___ap-northeast-1.console.aws.png

    • FireShot Capture 166 - CloudWatch Management Console_ - https___ap-northeast-1.console.aws.png

    • FireShot Capture 171 - CloudWatch Management Console_ - https___ap-northeast-1.console.aws.png

2.CloudWatch Logs へのログデータの送信

  1. awslogs をインストールします。

    $ sudo yum -y install awslogs
    

  2. /etc/awslogs/awscli.conf の設定を変更します。

    $ sudo cp -frp /etc/awslogs/awscli.conf /etc/awslogs/awscli.conf.ORG
    $ sudo vi /etc/awslogs/awscli.conf
    
    [plugins]
    cwlogs = cwlogs
    [default]
    region = ap-northeast-1
    

  3. /etc/awslogs/awslogs.conf の設定を変更します。

    • apache や nginx の設定もしています。不要であれば、削除して下さい。
    $ sudo cp -frp /etc/awslogs/awslogs.conf /etc/awslogs/awslogs.conf.ORG
    $ sudo vi /etc/awslogs/awslogs.conf
    
    [/var/log/messages]
    datetime_format = %b %d %H:%M:%S
    file = /var/log/messages
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/var/log/messages
    
    [/var/log/cron]
    datetime_format = %b %d %H:%M:%S
    file = /var/log/cron
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/var/log/cron
    
    [/etc/httpd/logs/access_log]
    datetime_format = [%a %b %d %H:%M:%S %Y]
    file = /etc/httpd/logs/access_log
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/etc/httpd/logs/access_log
    
    [/etc/httpd/logs/error_log]
    datetime_format = [%a %b %d %H:%M:%S %Y]
    file = /etc/httpd/logs/error_log
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/etc/httpd/logs/error_log
    
    [/etc/httpd/logs/ssl_access_log]
    datetime_format = [%a %b %d %H:%M:%S %Y]
    file = /etc/httpd/logs/ssl_access_log
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/etc/httpd/logs/ssl_access_log
    
    [/etc/httpd/logs/ssl_error_log]
    datetime_format = [%a %b %d %H:%M:%S %Y]
    file = /etc/httpd/logs/ssl_error_log
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/etc/httpd/logs/ssl_error_log
    
    [/etc/httpd/logs/ssl_request_log]
    datetime_format = [%a %b %d %H:%M:%S %Y]
    file = /etc/httpd/logs/ssl_request_log
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/etc/httpd/logs/ssl_request_log
    
    [/var/log/nginx/access.log]
    datetime_format = %Y/%m/%d %H:%M:%S
    file = /var/log/nginx/access.log
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/var/log/nginx/access.log
    
    [/var/log/nginx/backend.access.log]
    datetime_format = %Y/%m/%d %H:%M:%S
    file = /var/log/nginx/access.log
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/var/log/nginx/backend.access.log
    
    [/var/log/nginx/badactor.log]
    datetime_format = %Y/%m/%d %H:%M:%S
    file = /var/log/nginx/badactor.log
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/var/log/nginx/badactor.log
    
    [/var/log/nginx/error.log]
    datetime_format = %Y/%m/%d %H:%M:%S
    file = /var/log/nginx/error.log
    buffer_duration = 5000
    log_stream_name = {instance_id}
    initial_position = start_of_file
    log_group_name = AmazonLinux/var/log/nginx/error.log
    

  4. awslogs を起動します。

    $ sudo service awslogs start
    

  5. awslogs の自動起動の設定をします。

    $ sudo chkconfig awslogs on
    $ sudo chkconfig --list | grep awslogs
    
    awslogs            0:off    1:off    2:on    3:on    4:on    5:on    6:off
    

99.ハマりポイント

  • 今回は、凡ミスばかりで本当に自分が嫌になりました…。もう、毎回、何やってんだ…。

  • CloudWatch へのメトリクスデータの送信では、 CloudWatch のカスタム名前空間の「collectd」ではなく、AWS の EC2 のフィルタに表示されると勘違いして、全然ログが出てこないと悩んでいました…。もう、本当馬鹿…。
  • 後、/etc/collectd.conf の設定も結構悩みました。
  • CloudWatch Logs へのログデータの送信では、/etc/awslogs/awscli.conf の設定を /etc/awslogs/awslogs.conf にすると勘違いして、本当に無駄な時間を浪費しました…。

XX.まとめ

以下、参考にさせて頂いたサイトです。
ありがとうございました。

続きを読む

AWSのS3サービスにMavenリポジトリを構築

始めに

Apache Mavenを利用する場合、インハウスリポジトリを構築すると便利なので、これまでWebDAVが使えるWebサーバに環境構築していました。
AWSではS3に構築することが出来るので、費用面からしても断然有利なので、今回はこれを使ってみましょう。

準備環境

準備した環境は以下の通り。
Windows7 Pro
Java 1.8
Maven 3.3.3
Eclipse4.6
Spring Boot 1.5.6.RELEASE(デモ用)

AWS作業

AWSは既存のアカウントでも良いですが、今回はセキュリティ対策のため、S3にアクセス可能なアカウントを用意しました。
アクセスポリシーは、こんな感じで良いと思います。
your-bucketのところは、各自で書き換えてください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListAllMyBuckets"
            ],
            "Resource": "arn:aws:s3:::*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "arn:aws:s3:::your-bucket",
                "arn:aws:s3:::your-bucket/*"
            ]
        }
    ]
}

Java&Eclipse作業

適当なMavenプロジェクトを作成します。
私はSpring Bootプロジェクトで試しました。
以下は抜粋ですので、必要に応じて書き換えてください。

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <!-- (省略) -->

  <repositories>
    <repository>
      <id>s3-repos</id>
      <name>AWS S3 Repository</name>
      <url>s3://your-bucket/</url>
    </repository>
  </repositories>

  <distributionManagement>
    <repository>
      <id>aws-snapshot</id>
      <name>AWS S3 Repository</name>
      <url>s3://your-bucket/snapshot</url>
    </repository>
  </distributionManagement>

  <dependencies>
    <dependency>
  <!-- (省略) -->
    </dependency>
  </dependencies>


  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <!-- Windows環境におけるJunit実行時の文字化け対応 -->
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
          <junitArtifactName>junit:junit</junitArtifactName>
          <encoding>UTF-8</encoding>
          <inputEncoding>UTF-8</inputEncoding>
          <outputEncoding>UTF-8</outputEncoding>
          <argLine>-Dfile.encoding=UTF-8</argLine>
          <!-- <skipTests>true</skipTests> -->
        </configuration>
      </plugin>
    </plugins>
    <extensions>
      <extension>
        <groupId>org.springframework.build</groupId>
        <artifactId>aws-maven</artifactId>
        <version>5.0.0.RELEASE</version>
      </extension>
    </extensions>
  </build>
</project>

あとは、Mavenコマンドで使用するユーザ設定を行います。
Eclipseの場合は、ウィンドウ→設定→Maven→ユーザー設定か、Maven実行時にユーザ設定を指定します。
Maven実行時のパラメータ設定でも良いかと思います。
S3へのアクセスにプロキシ設定が必要な場合は、プロキシサーバの設定も適宜追加します。

setting.xml
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
  <proxies>
    <proxy>
      <id>http_proxy</id>
      <active>true</active>
      <protocol>http</protocol>
      <host>xx.xx.xx.xx</host>
      <port>xx</port>
    </proxy>
    <proxy>
      <id>https_proxy</id>
      <active>true</active>
      <protocol>https</protocol>
      <host>xx.xx.xx.xx</host>
      <port>xx</port>
    </proxy>
    <proxy>
      <id>s3_proxy</id>
      <active>true</active>
      <protocol>s3</protocol>
      <host>xx.xx.xx.xx</host>
      <port>xx</port>
    </proxy>
  </proxies>
  <servers>
    <server>
      <id>aws-release</id>
      <username>アクセスキーID</username>
      <password>シークレットアクセスキー</password>
    </server>
    <server>
      <id>aws-snapshot</id>
      <username>アクセスキーID</username>
      <password>シークレットアクセスキー</password>
    </server>
  </servers>
</settings>

実行結果

mvn deplyコマンドを実行して、S3に登録を行います。
snapshotかreleaseかは、アクセスするリポジトリ名を切り替えればOKかと思います。
他にいいやり方があるとは思いますが、とりあえずこれで。

[INFO] --- maven-deploy-plugin:2.8.2:deploy (default-deploy) @ ews ---
[INFO] Uploading: s3://your-bucket/snapshot/com/example/1.0.0/sample-1.0.0.jar
[INFO] Configuring Proxy. Proxy Host: xx.xx.xx.xx Proxy Port: xx
[INFO] Uploaded: s3://your-bucket/snapshot/com/example/1.0.0/sample-1.0.0.jar (2000 KB at 1000.0 KB/sec)
[INFO] Uploading: s3://your-bucket/snapshot/com/example/1.0.0/sample-1.0.0.pom
[INFO] Uploaded: s3://your-bucket/snapshot/com/example/1.0.0/sample-1.0.0.pom (5 KB at 1.0 KB/sec)
[INFO] Downloading: s3://your-bucket/snapshot/com/example/maven-metadata.xml
[INFO] Uploading: s3://your-bucket/snapshot/com/example/maven-metadata.xml
[INFO] Uploaded: s3://your-bucket/snapshot/com/example/maven-metadata.xml (300 B at 0.1 KB/sec)

考察

インターネットで調べたものの、手順がよく分からなかったので、実際にやってみました。
他のプロジェクトからうまく引っ張れるかどうか試していませんが、jarファイルを一般公開したくないなぁって言う場合には、かなり使えるんじゃ無いでしょうか。

続きを読む