woshidan's blog

あいとゆうきとITと、とっておきの話。

OPTIONSメソッドが気になったのでCORSとプリフライトリクエストについて少し調べました

Amazon API GatewayでCORSを有効にするとOPTIONSメソッドが追加されるのが気になったので、CORSについて少し調べました。

CORSとは

オリジン間リソース共有Cross-Origin Resource Sharing (CORS) は、追加の HTTP ヘッダーを使用して、あるオリジン (ドメイン) で動作しているウェブアプリケーションに、異なるオリジンのサーバーにある選択されたリソースへのアクセスを許可することができる仕組みです。ウェブアプリケーションは、自分のオリジンとは異なるオリジン (ドメインプロトコル、ポート番号) からリソースをリクエストするとき、オリジン間 HTTP リクエストを発行します。*1

クライアントサイドの実装は、最近ではIEのごく一部のブラウザ以外で実装されていて、JavaScriptで異なるドメインのサーバへアクセスしようとした時によくエラーになっているあれです。

CORSが必要な理由

CORSのことは一旦置いておいて、Webサーバにリクエストを送った時のことを考えましょう。基本的に、Webサーバはリクエストを送ってきたクライアントのIPにレスポンスを返します。

f:id:woshidan:20181126210159p:plain

たとえば自社でAPIとWebページを開発しているときは以下のような感じになりますね。

f:id:woshidan:20181126210321p:plain

ここでもし、自社API(図中の api.example.com)に他者のホームページからリクエストが

f:id:woshidan:20181126210754p:plain

のように送信されていたらどうなるでしょうか。不正なリクエストを送ってサーバに不具合を起こされたり、顧客に本物のWebサイトと偽って表示して顧客に表示するためのコンテンツを盗まれたり、といったことは起こるかもしれません。

CORSはこのような場合に役に立つもので、CORSを用いると、Webサイトのものとは異なるドメインにリクエストを送る場合、そのリクエストが許可されていることをサーバに一度確認してからでないとPOSTなどのHTTPリクエストが送ることができなくなります*2

f:id:woshidan:20181126211356p:plain

f:id:woshidan:20181126211528p:plain

CORSの仕組み

CORSを用いると、Webサイトのものとは異なるドメインにリクエストを送る場合、そのリクエストが許可されていることをサーバに一度確認してからでないとPOSTなどのHTTPリクエストが送ることができなくなります と書きましたが、具体的にどのようにやっているのか、基本的なパターンについてさらっておきます*3

プリフライトリクエス

Content-Typetext/plain でそれ以外にめぼしいリクエストヘッダを含まないGETリクエストなど一部の単純なもの以外*4、 Webページ上のJavaScriptが外部ドメインにリクエストを送ろうとした際、Webブラウザは元のHTTPリクエストを送るのではなく、 プリフライトリクエスト というものを送信します。

この プリフライトリクエスト には

  • パスは元のリクエストのまま
  • メソッドは OPTIONS
  • リクエストヘッダには
    • リクエスト元のドメインを示す Origin
    • 元々送ろうとしていたリクエストが入る Access-Control-Request-Method
    • 元々送ろうとしていたリクエストのリクエストヘッダが入る Access-Control-Request-Headers

が含まれ、この プリフライトリクエスト を受け取ったサーバは、

  • サーバがリクエストを許可する外部サーバのドメイン一覧である Access-Control-Allow-Origin
  • サーバがリクエストを許可するリクエストメソッドの一覧である Access-Control-Allow-Methods
  • サーバがリクエストを許可するリクエストヘッダの一覧である Access-Control-Allow-Headers
  • プリフライトリクエストをしばらく有効にしておく期限を表す Access-Control-Max-Age

が含まれるレスポンスを返します。Webブラウザはこのレスポンスを見て、自分が送ろうとしていたリクエストが外部サーバから許可されているかを確認し、許可されているのを確認できたら元々送信したかったリクエストを送信します。

サーバから通信を許可されていないドメインであったり、許可されていないメソッドでリクエストしようとしている場合は、Webブラウザはリクエストの送信を断念します。

参考

*1:https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

*2:GETのみなど一部確認用のリクエスト(プリフライトリクエスト)なしに送ることも可能です(単純なリクエスト)。

*3:リクエストそのものだけでなくクッキーを扱えるかどうかなどもCORSを用いて制御することができます https://qiita.com/tomoyukilabs/items/81698edd5812ff6acb34#cookie%E3%82%82%E8%A8%B1%E5%8F%AF%E3%81%97%E3%81%9F%E3%81%84%E5%A0%B4%E5%90%88

*4:詳しくは https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#Simple_requests

AWSによるサーバーレスアーキテクチャを読みました

3連休はサーバレスをやるぞ! と決めていたので、2ヶ月くらい積ん読してた気がする本をようやく読みました。

AWSによるサーバーレスアーキテクチャ

AWSによるサーバーレスアーキテクチャ

最初の方、思ったよりLambda関数を作成してはロールを付与して確認して、の流れが慣れなくて思ったより時間がかかりました。。

また感想がてらいくつかの話題についてまとめておきます。

サンプルのLambda関数を動かしてみて

2年以上前の本だったのでAuth0などのライブラリが古くて動かないところもあったのですが、そもそもLambda関数を作ってイベントから起動させたり、API Gatewayから呼び出したり、というのに不慣れだったため、7章くらいまでは一部妥協しながらも手を動かしながら読み進めていました。

Lambdaで実装しようとするときの一連の流れは

  • Lambda関数用のロールとロール用ポリシー作る
  • 上を関数のロールに指定してLambda関数を作る
  • Nodeなど関数を書く
  • 書いた関数をzipでまとめてデプロイ
  • その後、S3などイベントを発行するサービスから連携の設定をする

という感じです。

ポリシーの作り方について、Lambda関数の実行に必要最低限のポリシーを用意するのに管理ポリシーの AWSLambdaBasicExecutionRole (CloudWatchへのログ書き込みに必要な権限のみ)というのがあり、これに対し関数で行う内容によってさらに必要な権限を付与していきました。

特に参考になったのが、テスト手法の紹介で、lamdbaと同様の処理*1がローカルで実行できる run-local-lambda の紹介や、Lambda関数のコンソールにあるテストの設定を作る際に利用出来るイベントテンプレートがすごくよかったです。

イベントテンプレートはAWSの各種サービスから受け取るイベントの内容を下のgifのように1クリックで確認できて、すごい楽になりました。

f:id:woshidan:20181125135514g:plain

あと、地味にLambda関数のデフォルト実行時間の3秒が短くて、サンプルの動画をダウンロードしてメタデータを読み取る関数がエラーになりまくるなどしました。

バックエンドで実行すべき処理の例

サーバレスアプリケーションでは、基本的にフロントエンド*2から直接リクエストできる処理はなるべく間にバックエンドのサーバーなどを挟まない方がよいとされているのですが、それでもバックエンドから呼び出した方がよい処理はあります。

たとえば、

  • フロントエンドのコードというのはユーザーのブラウザにロードされる情報なので、ユーザーのブラウザに読み込まれてはいけないような 機密情報 を利用する処理であったり。
  • ブラウザというのはいつ閉じられてもおかしくないのに、中断されたら困るような処理をブラウザ上で実行したり。

というのはバックエンドで行った方がよい処理です。

フロントエンドから直接サービスを呼ぶために、HTTPリクエストをAWSのサービスに合わせたクエリに変換する

上の節にもサーバレスアプリケーションでは、基本的にフロントエンドから直接リクエストできる処理はなるべく間にバックエンドのサーバーなどを挟まない方がよい、と書いたのですが、たとえば、

  • Firebaseをフロントエンドから利用する
  • Auth0などの認証・認可のサービスをフロントエンドから利用する

といったサーバレスを意識して作られているだろうサービスを利用するのは、これらのサービスのSDKやサンプルコードも公開されているしイメージがつきやすいです。

しかし、それ以外のサービス、たとえばAWSに用意したRDSへ接続して SELECT 文を発行するような場合、単純なHTTPリクエストでもHTTPリクエストの内容をクエリに変換するようなコードを動かすバックエンド(LambdaやEC2上で動くWebアプリケーション)を用意する必要があるのではないでしょうか?

たとえば GET /users/2 というHTTPリクエストを受け取ったとき、それを SELECT * FROM users where id = 2; のクエリとその発行に変換するためにLambda関数を用意するといった具合に。

そういう疑問があったのですが、単純なクエリであればAmazon API GatewayAWSサービス統合を利用すれば、バックエンドを用意せずに実装することが可能です。

リクエストのパラメータとクエリに使う値、レスポンスの形式をテンプレートを書いて指定するのですが、DynamoDBやKinesis Firehoseの例は以下の記事が参考になります。

dev.classmethod.jp

dev.classmethod.jp

Lambda関数を書いていく上で重要な要素について

Lambdaの関数を書いてく上で重要な要素

  • 関数ハンドラ
  • イベントオブジェクト
  • コンテキストオブジェクト
  • コールバック関数
  • ログ

についてメモします。

まず、関数ハンドラは、Lambdaランタイムが書いた関数を実行するために呼び出しているもの(=エントリポイント)です。関数ハンドラの外に書いたコードはウォームスタート時は実行されないので、変数の初期化などは関数ハンドラの外で行うとよさそうです。

関数ハンドラの構文は

exports.handler = fucntion(event, context, callback) { // コード }

となっていますが、それぞれの引数は

  • 第1引数 ... イベントデータ
  • 第2引数 ... コンテキストオブジェクト
  • 第3引数 ... コールバックオブジェクト

と呼びます。コールバックオブジェクトは、関数の呼び出し元に情報を返したい時やエラーのログを残したい時に使います。

ちなみに、LambdaではS3へのファイルアップロードなどのイベント駆動で動作する関数は呼び出し元に情報を返す必要はなく、これらの実装(情報を返したりログを残す)はLambda関数を作成する上ではオプションです(やった方がいいです)。

イベントオブジェクトは、Lambda関数を呼び出した側がLambda関数に渡す情報が入ったオブジェクトで、S3のファイルアップロードによるイベントで起動される関数なら、ファイル名やバケット名が入ったイベントオブジェクトを受け取ります。

単純にリクエストパラメータとイベントオブジェクトの値をJSONとして対応させたいならAPI GatewayでLambda統合を利用すればよく、認証情報などがリクエストヘッダに含まれるのをイベントオブジェクトから取り出せるようにしたい、などカスタマイズする場合はマッピング*3を行います。

コンテキストオブジェクトはLambdaランタイムについての情報がわかる便利なプロパティやメソッドが生えています。たとえば、getRemaininTimeInMillis() メソッドで実行中のLambda関数のタイムアウトまでの残り時間が分かります*4

コールバック関数は、ハンドラ関数のオプションの第3引数で、Amazon API Gateway経由で呼び出される関数のように、関数の呼び出し元に情報を返すためのものです。

callback(Error error, Object result) のように第1引数がエラーを表すオブジェクトとなっていて、処理に成功した場合の第一引数はnullとします。

ログを出力したい場合は consle.log("message") でCloudWatchにログを書き込めます。 console.warn()console.error() なども利用できますが、CloudWatch側としては実質的な処理に違いはありません*5

もう一冊読みたいな、と思ってたんだけど一冊目だけで思ったよりわたわたしてしまいました。

現場からは以上です。

*1:イベントのJSONが渡されてハンドラ関数が実行されるという意味で

*2:具体的にはWebブラウザで動作するJavaScriptですね

*3:https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html など

*4:詳しくは http://amzn.to/1UK9eib

*5:ただ、運用としてはアラートレベルやログオブジェクトはしっかり管理した方がよいでしょう。

試して理解 Linuxのしくみを読みました

有名な本を読みました。感想がてら特に面白かったいくつかのトピックについてまとめてしめます。

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

ファイルシステムの不整合について

本で出てきた例では

  • ファイルシステム上ではファイルはディレクトリに紐づいていて、階層構造になっている
  • そういった階層構造をmvなどで移動させる際、リンクの書き換えと元のリンクの削除の二つの操作が存在する
  • その2つ以上のアトミックに行われていてほしい処理が、途中で電源が切れるなどが原因で一部行われていないがために、ディレクトリの階層構造が壊れてファイルシステムをマウントしたりできなくなる

というような状態をファイルシステム不整合、という。ファイルシステムごとの対応策のあとの最終手段として、 fsck コマンドが存在するが、ファイルシステムの正しい状態がわからずにとりあえず整合性が取れた状態にしようとしたり、失敗に終わることが多かったりするのでファイルシステムが期待しているように利用できる状態になるというものではない。

ファイルシステムの違いとは何を指すのか、特定の目的のためのファイルシステム

ファイルシステム上のファイルやディレクトリをファイルやディレクトリとして扱うことができるのは、ファイルシステムごとに所定の方式でメタデータと本文のデータがストレージ上に配置されているため。

パーティションというのはハードディスク上で論理的に1つのデバイスのように扱える単位で*1、このパーティションという単位に対してファイルシステムを作成、マウントする*2

ファイルシステムでは、データへのアクセス方法やファイルとして扱うデータの形式によってさまざまなファイルシステムがあり、本の中では

他が紹介されていた。

コピーオンライトのライトにどうやって気づくか

fork() システムコールで新しいプロセスを生成する際、親プロセスのメモリを全て子プロセスにコピーするのではなく、ページテーブルだけをコピーする。

この際、親と子の両プロセスのページテーブルの各エントリに書き込み禁止フラグをつける。そうすると、書き込みが発生した時には、CPU上でページフォールトが発生するので、ページフォールトが発生したページだけ書き込み禁止を解除して子用に新しい物理メモリを割り当てるようにする。

コピーオンライトの「ライト」がなされたことを発見する方法が権限違反のエラーというのが面白かった。

HDDのアクセス特性と I/O スケジューラ、SSDの特性について

HDDは磁気ディスク(円盤)で、データ読み取りの際は

  • 半径方向へはアームを機械的に動かして
  • 円周方向へはディスクの回転でデータが書いてある位置へと

ディスクを動かす。そのため、HDDからデータを読み出す際は、複数のデータ読み出しがある場合は同じ半径の位置のデータはまとめて読み出すなどの工夫をすると機械的操作(時間がかかる)を減らせて高速化できる。アームの移動や回転が効率的に行えるように、複数のI/O要求の内容を並べ替えたりまとめたりするのがI/Oスケジューラ。

アームをどう動かすか、といったことはHDDによるが、HDDヘのI/O要求の内容自体はHDDごとに差はないのでデバイスドライバに実装するのではなく、ブロックデバイス層というファイルシステムやデバイスファイルより下のレイヤーにて実装されている。

HDDはシーケンシャルアクセスでは一定の値(HDDの性能限界)までは1回あたりのI/Oサイズが大きくなるほど性能が上がる。また、先読みやI/OスケジューラといったI/O支援機能によって1回あたりのI/Oサイズが小さい場合も性能を上げることができる。

一方、SSD機械的操作がないのでランダムでもシーケンシャルアクセスでもHDDほどは性能の差がなく、I/O支援機能を利用するとI/O要求を貯める待ち時間が無視できず性能が上がらない場合も。

現場からは以上です。

*1:https://users.miraclelinux.com/technet/document/linux/training/1_3_2.html

*2:ファイルシステムの作成、あるいはフォーマットで特定のファイルシステムで扱えるようにメタデータと本文の配置方法などを設定し、マウントでアクセスできるようにする? http://www.atmarkit.co.jp/ait/articles/1802/15/news035.html など

AWS DevDay Tokyo 2018 LT大会に「AWSを用いたWebサービスの負荷試験のTips」というテーマで参加してきました

9月にLTのCFPの公募があり、これはいい勉強になるだろうということで応募したら通ったのでわたわたしながら準備していました。

speakerdeck.com

以下、とっちらかっていますが、自分用のメモ兼補足です。

  • JMeterが同時に1000スレッドを立ち上げたらフリーズした件
    • JMeterJava製の負荷試験用ツールなんですが、2~3GHzの1つのクライアントだと同時に300~600くらいまででWebサーバの場合は例外*1とのことですが、約3GHzの仕事用MBPだとこの辺で止まりました。メモリじゃなくてクロック数書いた方がよかったかもです
    • 実際のテストケースでは、一度に立ち上げるスレッド数を制限して、かわりに短いテストシナリオを短時間でたくさんループして1分あたりのスレッド数*2を増やしています
  • 負荷用クラスタの構成あたりの話について
    • 3/10の量のリクエストが送れるかどうかのテストをしていた際に心配していたこと
      • これで3/10のリクエスト数が送れなかったらAWSアカウントで利用できるEC2のインスタンスタイプごとの制限台数*3にひっかかるからインスタンスタイプあげなきゃ...
      • マスターとスレーブの間の通信でマスターが詰まらないか
        • JMeterのスレーブはテストの最中、制御に必要な情報(グラフリスナーなどが必要とする情報も含むっぽい)をマスターに送っているので、それによってマスター/スレーブ間が詰まらないか*4
        • 結論からいうとCUIモードならだいたい大丈夫ではと思います
      • スレーブから対象クラスタへの通信でネットワーク帯域が詰まらないか
    • なぜEC2を利用しているのか
      • 並列数が欲しいだけならLambdaでもいいのでは、という感じなのですが、負荷試験で調べたいことにElastiCache, RDSとEC2のクラスタとの間のネットワーク転送量があり、ある程度キャッシュが効いた状態の様子を見たかったので
        • キャッシュが効くまでに5分くらい暖機数分をとりたかったことと
        • 上記の暖機をした上で本番に近い形でキャッシュが効くようなデータセットを続けて利用することを考えると、5分の稼働時間のLamdbaで設定やテスト用データの用意をするのはややこしそうです。EC2なら動かしっぱなしでいいので
        • ランダムだったり固定データだったらLambdaでもよさそう
  • 負荷試験用環境のセットアップについて*5
    • ストレージ容量が4桁GiBになってくるとバックトラック(弊社の試験時にうまくいかず。。)がうまくいかないこともあるみたいですね
    • 書いていない工夫としてはバッチの結果が入っているRedisがあって、そのデータを用意するため、バッチの実行時間のAuroraのスナップショットとバッチ実行後のRedisのスナップショットを用意していました
      • 試験当時はAuroraが停止できなかったのでクローンでインスタンス立ち上げた後停止させるわけにはいかなかったので。。

現場からは以上です。

あわせて読みたい

dev.classmethod.jp

*1:https://jmeter.apache.org/usermanual/jmeter_distributed_testing_step_by_step.pdf

*2:JMeter(に限らないかもですが)での同時に動かすスレッド数はだいたい同時にアクセスしているユーザ数みたいな感じ

*3:https://aws.amazon.com/jp/ec2/faqs/#How_many_instances_can_I_run_in_Amazon_EC2

*4:https://jmeter.apache.org/usermanual/jmeter_distributed_testing_step_by_step.pdf

*5:質問されたのですが、試験環境のセットアップも自分でやってました。だいなみっく!

JAWS-UG 初心者支部#14「AWS Night school & LT」 LT大会 に「 IAMロールの使用と必要なポリシー」というテーマで参加してきました

結構前なのですが、仕事ともう一個のLTでバタバタしていていまさらになりますががが。

speakerdeck.com

自分がはじめてIAMさわったとき、IAMのポリシーってたくさんあってどれがどれだかわからないぞって気持ちが強くて。。

IAMのポリシーにつく形容詞には

  • ロールの利用に関するもの(信頼ポリシーなど、スライド参照)
  • 誰が作成しているか(管理ポリシーとか)
  • 何にアタッチするか(サービスのリソース vs プリンシパルエンティティ)

の軸があって、そのうち1つだけでもさらっておくと一度に覚える事が減って後でIAMのドキュメント見るとき読みやすくなりそう、と思って今回のLTで話す範囲を決めました。

なお、スライド中で言っているSTSが出てくるドキュメントは以下となります。

docs.aws.amazon.com

かんたんですが、他に参考になりそうなドキュメントをまとめて現場からは以上とします。

参考

Hadoop3.1.1でs3a://ファイルシステムを利用する場合の環境構築をしてみる

今日はHadoopファイルシステム(特に、s3を利用するもの)についてすこしおさらいしてから表題の環境構築をしてみます。

Hadoopファイルシステム

Hadoopは抽象化されたファイルシステムの概念を持っていて、その実装に

など、さまざまなストレージをバックエンドとした実装があり、今日扱うS3をバックエンドとするファイルシステムもこの中に含まれます。

ただ、Hadoopで扱えるファイルシステムのうち、S3をバックエンドとするものにはいくつか種類があり、ややこしいので動かすための設定を見ていく前にかんたんに整理しておきます。

Hadoopのs3をバックエンドとしたファイルシステム

詳しい歴史的経緯はこちらのドキュメントの冒頭やこちらの記事が参考になりますが、Hadoopのs3用のファイルシステムを表にすると以下のようになります。

ファイルシステムURIスキーマ*1 備考
s3 最初に作成されたs3をバックエンドとするファイルシステム。このファイルシステムで作成されたファイルはHadoopのs3ファイルシステムを通してしか読み書きできなかった。現在はもうApacheによる開発は終了しており、ソースコードにも含まれていない。下の方で述べるEMRのs3ファイルシステムとは別物。
s3n 上述のs3ファイルシステムの問題点を解決し、アプリケーションのコードとHadoopの間でS3のオブジェクトを共用できるようにしたもの。Hadoopの2.x系で利用することができた
s3a s3nより、より大きいファイルをより高いパフォーマンスで扱えることを目的に開発された第三世代。改善内容は、たとえば、部分的なシークの追加など。Hadoop 3.x系で利用できる
s3(Amazon版) s3nが開発されている頃からAmazonが独自にEMR用に改良したファイルシステム。現在s3のファイルシステムで開発されているものはこちらであり、Hadoopコミュニティが開発していたs3ファイルシステムとは別物

それでは、今日はs3aプロトコルを利用する場合の環境構築をしていきます。

Hadoopでs3a://ファイルシステムをPseudo-Distributed Modeで動かしてみる場合の環境構築について

s3にHadoop用のファイルを置くバケットを作成

f:id:woshidan:20180920082701p:plain

なお、バケットには管理者のみがアクセスできるようにします。

バケットを作成したら、テスト用のCSVもアップロードしておきます。

そのCSVの中身は以下です。

dog
dog
dog
cat
cat
dog
dog
cat

Hadoopの設定

Hadoopの認証情報とファイルシステムの実装クラスを設定する

次にHadoop側の設定をしていきます。まず、 core-site.xml の変更内容ですが以下になります。

<!-- etc/hadoop/core-site.xml -->
<property>
  <name>fs.s3a.access.key</name>
  <value>MY_ACCESS_KEY</value>
</property>

<property>
  <name>fs.s3a.secret.key</name>
  <value>MY_SECRET_KEY</value>
</property>

etc/hadoop/core-site.xml の変更はずいぶん少ないですが、これは今回s3a系のファイルシステムを利用する際の設定のうち、

についてHadoopのデフォルトの設定を利用しているためです。

まず、Hadoopのデフォルトの設定については、 https://hadoop.apache.org/docs/r3.1.1/hadoop-project-dist/hadoop-common/core-default.xml に記載されていて、fs.s3a.impl の値は org.apache.hadoop.fs.s3a.S3AFileSystem になっています。

また、 fs.s3a.aws.credentials.provider の値の設定に関しては https://hadoop.apache.org/docs/r3.1.1/hadoop-project-dist/hadoop-common/core-default.xml にも記載されていますが、s3のファイルシステムの設定について説明しているHadoop-AWS module: Integration with Amazon Web Servicesのページの方がわかりやすいです。

Hadoop-AWS module: Integration with Amazon Web Servicesによると、s3a://ファイルシステムを利用する場合、

  • 認証方法の指定は credential provider クラスの指定という形で行い、複数指定することもできる
  • 特に認証方法を指定しない場合、デフォルトで指定されている認証方法が用いられる
    • デフォルトで指定されている credential provider は以下の順で試される
      • org.apache.hadoop.fs.s3a.BasicAWSCredentialsProvider ... core-site.xml に記載した fs.s3a.access.keyfs.s3a.secret.key を利用して認証を行う
      • com.amazonaws.auth.EnvironmentVariableCredentialsProvider ... 環境変数 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY を利用
      • com.amazonaws.auth.InstanceProfileCredentialsProvider ... EC2のインスタンスプロファイルの認証情報を利用

となっています。

そして、今回はデフォルトの認証方法のうち、org.apache.hadoop.fs.s3a.BasicAWSCredentialsProvider を利用する前提の設定となっているので、 core-site.xmlfs.s3a.access.keyfs.s3a.secret.key だけ記載しています。

HadoopからAWSを利用するためのクラスのパスをクラスパスに追加

hadoop から s3 を利用するためには $HADOOP_HOME/share/hadoop/tools/lib/ 以下にある hadoop-aws-3.1.1.jar が必要なので、 etc/hadoop/hadoop-env.sh に以下を追記します。

# etc/hadoop/hadoop-env.sh
export HADOOP_CLASSPATH=$HADOOP_CLASSPATH:$HADOOP_HOME/share/hadoop/tools/lib/*

その他

また、 s3a:// プロトコルを利用することとは関係ないですが、以下の設定もしていました。

<!-- etc/hadoop/hdfs-site.xml -->
<!-- 今回もおためしなのでレプリケーションは作りません -->
<configuration>
  <property>
    <name>dfs.replication</name>
    <value>1</value>
  </property>
</configuration>

コマンドを実行してみる

$ $HADOOP_HOME/bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.1.jar wordcount s3a://woshidan-hadoop-test/word_count_test.csv wordcount-output-s3
$ $HADOOP_HOME/bin/hdfs dfs -cat wordcount-output-s3/*
cat 3
dog 5

よさそうです。

トラブルシューティング

bin/hadoop fs -ls s3a://bucket_name を実行したら ls: `s3a://bucket_name': No such file or directory というエラーメッセージが返ってきた

https://hortonworks.github.io/hdp-aws/s3-s3aclient/index.html のコマンドを眺めていたら s3a://bucket_name/ のように末尾に / が必要なようでした。

[ec2-user@ip-172-31-16-244 hadoop]$ bin/hadoop fs -ls s3a://woshidan-hadoop-test
2018-09-20 23:14:46,289 INFO impl.MetricsConfig: loaded properties from hadoop-metrics2.properties
2018-09-20 23:14:46,384 INFO impl.MetricsSystemImpl: Scheduled Metric snapshot period at 10 second(s).
2018-09-20 23:14:46,384 INFO impl.MetricsSystemImpl: s3a-file-system metrics system started
2018-09-20 23:14:48,278 INFO Configuration.deprecation: fs.s3a.server-side-encryption-key is deprecated. Instead, use fs.s3a.server-side-encryption.key
ls: `s3a://woshidan-hadoop-test': No such file or directory
2018-09-20 23:14:48,543 INFO impl.MetricsSystemImpl: Stopping s3a-file-system metrics system...
2018-09-20 23:14:48,544 INFO impl.MetricsSystemImpl: s3a-file-system metrics system stopped.
2018-09-20 23:14:48,544 INFO impl.MetricsSystemImpl: s3a-file-system metrics system shutdown complete.

[ec2-user@ip-172-31-16-244 hadoop]$ bin/hadoop fs -ls s3a://woshidan-hadoop-test/
2018-09-20 23:20:24,680 INFO impl.MetricsConfig: loaded properties from hadoop-metrics2.properties
2018-09-20 23:20:24,798 INFO impl.MetricsSystemImpl: Scheduled Metric snapshot period at 10 second(s).
2018-09-20 23:20:24,798 INFO impl.MetricsSystemImpl: s3a-file-system metrics system started
2018-09-20 23:20:26,691 INFO Configuration.deprecation: fs.s3a.server-side-encryption-key is deprecated. Instead, use fs.s3a.server-side-encryption.key
Found 1 items
-rw-rw-rw-   1 ec2-user ec2-user         31 2018-09-19 23:26 s3a://woshidan-hadoop-test/word_count_test.csv
2018-09-20 23:20:26,924 INFO impl.MetricsSystemImpl: Stopping s3a-file-system metrics system...
2018-09-20 23:20:26,925 INFO impl.MetricsSystemImpl: s3a-file-system metrics system stopped.
2018-09-20 23:20:26,925 INFO impl.MetricsSystemImpl: s3a-file-system metrics system shutdown complete.

org.apache.hadoop.fs.s3a.BasicAWSCredentialsProvider を fs.s3a.aws.credentials.provider に指定して fs -ls したらエラーになった

[ec2-user@ip-172-31-16-244 hadoop]$ bin/hadoop fs -ls s3a://woshidan-hadoop-test
2018-09-20 23:05:52,547 INFO impl.MetricsConfig: loaded properties from hadoop-metrics2.properties
2018-09-20 23:05:52,670 INFO impl.MetricsSystemImpl: Scheduled Metric snapshot period at 10 second(s).
2018-09-20 23:05:52,670 INFO impl.MetricsSystemImpl: s3a-file-system metrics system started
ls: org.apache.hadoop.fs.s3a.BasicAWSCredentialsProvider constructor exception.  A class specified in fs.s3a.aws.credentials.provider must provide a public constructor accepting Configuration, or a public factory method named getInstance that accepts no arguments, or a public default constructor.

fs.s3a.aws.credentials.provider に指定できる credential provider のクラスは

  • org.apache.hadoop.conf.Configurationインスタンスを引数にとるpublicコンストラクタを持つ
  • 何も引数を取らないgetInstance()メソッドを持つ
  • publicなデフォルトコンストラクタを持つ

という条件を満たす必要があります。

たとえば、他の credential provider のクラスを見てみると TemporaryAWSCredentialsProvider では

https://github.com/apache/hadoop/blob/da9a39eed138210de29b59b90c449b28da1c04f9/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/TemporaryAWSCredentialsProvider.java#L55-L57

  public TemporaryAWSCredentialsProvider(Configuration conf) {
    this(null, conf);
  }

のように、 Configurationインスタンスを引数にとるpublicコンストラクタを持っており、 AnonymousAWSCredentialsProvider では

https://github.com/apache/hadoop/blob/da9a39eed138210de29b59b90c449b28da1c04f9/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/AnonymousAWSCredentialsProvider.java#L40-L56

public class AnonymousAWSCredentialsProvider implements AWSCredentialsProvider {

  public static final String NAME
      = "org.apache.hadoop.fs.s3a.AnonymousAWSCredentialsProvider";

  public AWSCredentials getCredentials() {
    return new AnonymousAWSCredentials();
  }

  public void refresh() {}

  @Override
  public String toString() {
    return getClass().getSimpleName();
  }

コンストラクタが宣言されていないので、publicなデフォルトコンストラクタを使えます。一方、 BasicAWSCredentialsProvider クラスは

https://github.com/apache/hadoop/blob/da9a39eed138210de29b59b90c449b28da1c04f9/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/BasicAWSCredentialsProvider.java#L42-L45

  public BasicAWSCredentialsProvider(String accessKey, String secretKey) {
    this.accessKey = accessKey;
    this.secretKey = secretKey;
  }

のようにコンストラクタが定義されていますが、これは fs.s3a.aws.credentials.provider に指定できる credential provider のクラスの条件

  • org.apache.hadoop.conf.Configurationインスタンスを引数にとるpublicコンストラクタを持つ
  • 何も引数を取らないgetInstance()メソッドを持つ
  • publicなデフォルトコンストラクタを持つ

を満たしていないので、Basic と接頭辞がついているので試しに使ってみたい感じなのですが、 fs.s3a.aws.credentials.provider に指定することはできません。ですが、 fs.s3a.aws.credentials.provider に何も指定しなければ、デフォルトで利用されるため、今回はそうしています。

現場からは以上です。

*1:HadoopではUIスキーマで利用するファイルシステムの実装を指定する

Hadoop 3.1.1をインストールしてPseudo-Distributed Modeで動かしてみる

Java環境変数のセットアップ

$ sudo yum install java-1.8.0-openjdk

[ec2-user@ip-172-31-16-244 ~]$ sudo update-alternatives --config java

There are 2 programs which provide 'java'.

  Selection    Command
-----------------------------------------------
*+ 1           /usr/lib/jvm/jre-1.7.0-openjdk.x86_64/bin/java
   2           /usr/lib/jvm/jre-1.8.0-openjdk.x86_64/bin/java

$ java -version
openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)
# .bash_profile

# Get the aliases and functions
if [ -f ~/.bashrc ]; then
        . ~/.bashrc
fi

# User specific environment and startup programs

PATH=$PATH:$HOME/.local/bin:$HOME/bin

export PATH
export PATH=/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/opt/aws/bin:/home/ec2-user/.local/bin:/home/ec2-user/bin:/usr/java/latest/bin
# sudo update-alternatives --config java で出てきたパスに合わせてみる
export JAVA_HOME=/usr/lib/jvm/jre-1.8.0-openjdk.x86_64
export HADOOP_HOME=/home/ec2-user/hadoop

Hadoopのインストール

$ wget http://ftp.jaist.ac.jp/pub/apache/hadoop/common/hadoop-3.1.1/hadoop-3.1.1.tar.gz
$ tar xvfz hadoop-3.1.1.tar.gz
$ ln -s hadoop-3.1.1 hadoop

$ cd hadoop
[ec2-user@ip-172-31-16-244 hadoop]$  ls
bin  etc  include  lib  libexec  LICENSE.txt  NOTICE.txt  README.txt  sbin  share
[ec2-user@ip-172-31-16-244 hadoop]$ bin/hadoop
Usage: hadoop [OPTIONS] SUBCOMMAND [SUBCOMMAND OPTIONS]
 or    hadoop [OPTIONS] CLASSNAME [CLASSNAME OPTIONS]
  where CLASSNAME is a user-provided Java class

Pseudo-Distributed モードで動かすための設定

<!-- etc/hadoop/core-site.xml -->
<!-- fs.defaultFS には マスターノードの設定を行う
<!-- マスターノードがどのファイルシステムを使うのかをスキーマで指定したり、データノードがハートビートを送る先をIP + portで指定したりするらしい
<!-- https://stackoverflow.com/questions/36072890/what-is-the-exactly-use-of-defaultfs https://community.hortonworks.com/questions/185804/difference-between-fsdefaultfs-and-dfsnamenodehttp.html
<configuration>
+  <property>
+    <name>fs.defaultFS</name>
+    <value>hdfs://localhost:9000</value>
+  </property>
</configuration>
<!-- etc/hadoop/hdfs-site.xml -->
<!-- HadoopでHDFSを利用する場合、一つのファイル中のシークに時間がかからないようにデータをそれなりに分けて保存していて、
<!-- そういうクラスタに投入されたデータを保存しておく単位をブロックという。
<!-- そして、HDFSは耐障害性を高めるために、ブロック単位でレプリケーションを行うんですが、今回はテスト用だしレプリケーションいらないよね、という設定 -->
<!-- https://hadoop.apache.org/docs/r3.1.1/hadoop-project-dist/hadoop-hdfs/hdfs-default.xml -->
<configuration>
+  <property>
+    <name>dfs.replication</name>
+    <value>1</value>
+  </property>
</configuration>

ローカルでネームノード、データノードを立ち上げるので、これらのssh通信用の設定

$ ssh localhost # localhostにsshで通信できないことを確認
The authenticity of host 'localhost (127.0.0.1)' can't be established.
ECDSA key fingerprint is SHA256:0STL9JFnI81GzdikYqN2DLu4BkdtY1Bb3A7o2zzkbpw.
ECDSA key fingerprint is MD5:c5:c0:cc:69:96:23:27:90:fa:d6:6b:13:1d:cf:16:b9.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'localhost' (ECDSA) to the list of known hosts.
Permission denied (publickey).

$ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys

chmod 0600 ~/.ssh/authorized_keys

ためしにジョブを実行するための準備

$ bin/hdfs namenode -format

$ sbin/start-dfs.sh
Starting namenodes on [localhost]
Starting datanodes
Starting secondary namenodes [ip-172-31-16-244]
ip-172-31-16-244: Warning: Permanently added 'ip-172-31-16-244,172.31.16.244' (ECDSA) to the list of known hosts.

$ bin/hdfs dfs -mkdir /user
$ bin/hdfs dfs -mkdir /user/ec2-user
$ bin/hdfs dfs -put etc/hadoop input
2018-09-18 22:34:53,634 WARN hdfs.DataStreamer: Caught exception
java.lang.InterruptedException
    at java.lang.Object.wait(Native Method)
    at java.lang.Thread.join(Thread.java:1252)
    at java.lang.Thread.join(Thread.java:1326)
    at org.apache.hadoop.hdfs.DataStreamer.closeResponder(DataStreamer.java:986)
    at org.apache.hadoop.hdfs.DataStreamer.endBlock(DataStreamer.java:640)
    at org.apache.hadoop.hdfs.DataStreamer.run(DataStreamer.java:810)
    ... # きになる...

exampleのGrep Jobがうまくいかなかった

$ bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.1.jar grep input output 'dfs[a-z.]+'
$ bin/hdfs dfs -cat output/*
$ bin/hdfs dfs -ls output/*
-rw-r--r--   1 ec2-user supergroup          0 2018-09-18 22:36 output/_SUCCESS
-rw-r--r--   1 ec2-user supergroup          0 2018-09-18 22:36 output/part-r-00000
$ bin/hdfs dfs -cat output/_SUCCESS
$ bin/hdfs dfs -cat output/part-r-00000
# 空

Hadoopを操作するユーザーの設定がよくわかってないせいかもしれないです。

exampleの円周率を計算するジョブはうまくいった

$ bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.1.jar pi 2 100
Job Finished in 2.099 seconds
Estimated value of Pi is 3.12000000000000000000

もともとやりたかったことはS3をバックエンドにしてHiveの素振りなので深追いはやめます*1

bin/hdfs dfs コマンドについて

refs: https://hadoop.apache.org/docs/r3.1.1/hadoop-project-dist/hadoop-hdfs/HDFSCommands.html

bin/hdfs スクリプトを利用して、HDFSコマンドを実行することができます。

bin/hdfs dfs -mkdir などで作成しているディレクトリはHDFS上のもので、通常のシェルコマンドでは検索したり開いたりできませんが、 HDFS コマンドで -ls みたいに打ち込むとHDFS上のファイルを操作することができます。

$ bin/hdfs dfs -ls output/*
-rw-r--r--   1 ec2-user supergroup          0 2018-09-18 22:36 output/_SUCCESS
-rw-r--r--   1 ec2-user supergroup          0 2018-09-18 22:36 output/part-r-00000

word-countのexampleは成功した

[ec2-user@ip-172-31-16-244 hadoop]$ bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-3.1.1.jar wordcount input/hadoop-env.sh wordcount-output

[ec2-user@ip-172-31-16-244 hadoop]$ bin/hdfs dfs -ls wordcount-output
Found 2 items
-rw-r--r--   1 ec2-user supergroup          0 2018-09-18 23:10 wordcount-output/_SUCCESS
-rw-r--r--   1 ec2-user supergroup      10073 2018-09-18 23:10 wordcount-output/part-r-00000
[ec2-user@ip-172-31-16-244 hadoop]$ bin/hdfs dfs -cat wordcount-output/*
"  3
"AS  1
"License");    1
"log   1
# 310
##    12
###   28
#export   1
$USER 1
${HADOOP_HOME}/logs   1
${HADOOP_OS_TYPE} 1
${HOME}/.hadooprc 1
...
wants 2
way   1
when  13
where 1
which 3
who   1
why....   1
wildcards 1
will  29
with  6
work  1
workers.sh,   1
writing   1
writing,  1
xxx-env.sh.   1
yarn  1
yet/still,    1
you   2
{YARN_xyz|HDFS_xyz}   1
{yarn-env.sh|hdfs-env.sh} 1

動いているHadoop関連のプロセスを止める

$ sbin/stop-all.sh
WARNING: Stopping all Apache Hadoop daemons as ec2-user in 10 seconds.
WARNING: Use CTRL-C to abort.
Stopping namenodes on [localhost]
Stopping datanodes
Stopping secondary namenodes [ip-172-31-16-244]
Stopping nodemanagers
Stopping resourcemanager

現場からは以上です。

*1:2.5.0で Fully-Distributed モードでGrep Jobが動かなかったらしいのでそういうこともあるかもしれないです... http://hadoop.apache.org/docs/r3.1.1/hadoop-project-dist/hadoop-common/release/2.7.0/CHANGES.2.7.0.html