katekichiのゆるブログ

普段の作業メモや日常の出来事とか

【自由研究③】react-reduxのconnectについて調査してみた話

Reduxで実装する場合にReact部とRedux部の繋ぎ込みに react-redux が有効ということは、分かったのですがどうもconnect() に渡している引数や、戻り値として何が生成されるのか分からず 結果として、なんでconnect()使うと幸せになれるのか分からなかったため 調査してみました。

github.com


なぜ分からないのか?

なぜ分からないのか考えてみました。

  1. チュートリアル等で、手を動かしただけでドキュメントの読み込みが足りない
  2. そもそもReduxの理解が不足している

ドキュメントを再度読んでみる。

Usage with React · Redux を読んでまずは まとめてみることにしました。


Presentational / Container componentについて

再利用性を考慮して、Presentational(プレゼンテーショナル) / Container(コンテナ) component の2つに分離されるようです。 2つのコンポーネントの役割を簡単にまとめてみましたが、ざっくりいうと

  • 見た目(表示)担当のPresentational component
  • ロジック担当のContainer component

って感じのようです。

medium.com

また、調べていたら良い記事を既に書いていらっしゃる方がいたので参考にさせて 頂きました。

qiita.com

Presentational Componentsとは

  • 役割:見た目の実装
  • データ取得時:propsより取得
  • データ更新時:propsのcallback関数
  • コンポーネントの作成:自分で作る

実際のイメージとしては、こんな感じです(こちらからの抜粋です)。

import React from 'react'

export default React.createClass({
    render() {
        const {
            text,
            onButtonClick,
        } = this.props

        return (
            <div>
                <p>{text}</p>
                <button onClick={onButtonClick}>Click!</button>
            </div>
        )
    }
})
  • {text}の表示は、props経由
  • ボタンクリック時の{onButtonClick}の実装もprops経由

Container Componentsとは

  • 役割:データフェッチやStateの更新
  • データ取得時:reduxのstateにより取得
  • データ更新時:reduxのactionにより更新
  • コンポーネントの作成:react-reduxが生成

実際のイメージとしては、こんな感じです(こちらからの抜粋です)。

import { connect } from 'react-redux'

import SomePresentationalComponent from 'some-presentational-compnent'

const mapStateToProps = (state, ownProps) => {
    return {
        // blah blah blah
    }
}

const mapDispatchToProps = dispatch => {
    return {
        // blah blah blah
    }
}

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(SomePresentationalComponent)

はい、出てきましたー。connect() この辺りから、分からなくなってきます・・・。

こちらに以下のようなコメントが ありました。

mapStateToProps(state, ownProps)は,store.getState()の結果を第一引数に,このContainer component
へ渡されたpropsを第二引数にして呼び出される関数で,これらのstateとpropsを使って子のPresentational
 componentにpropsとして渡す値を生成します.

mapDispatchToProps(dispatch)は,store.dispatchを第一引数にして呼び出される関数で,
子のPresentational componentにpropsとして渡す値(というか,コールバック関数)を生成します.
通常,このコールバック関数では,引数として渡されているdispatchを呼び出し,子のPresentational Componentが
StoreへActionを送信できるようにしておきます.
ボタン押下時に呼ばれる処理などもコールバック関数として書いてPresentational componentにpropsとして
渡すと言いましたが,このコールバック関数を作るためのものと思っておけばいいと思います.

すごく丁寧な解説をして頂いているのですが、そもそも以下が良く分かっていないことが判明しました。 その結果として、connect()の引数から何が生成されるのかが分かっていないのだという結論に至りました。

  • Store.getState()
  • Store.dispatch()

以下のドキュメントで各メソッドの仕様を調査しました http://redux.js.org/docs/api/Store.html

getState()

  • 要訳すると、現状のStoreが持つ最新のState郡を返す。

dispatch()

  • 要訳すると、dispatchは、唯一Stateを変更する手段。
  • 引数として、actionを渡す。actionとは、UIイベント、 networkのcallback等を指す。 actionのfieldには、"type"が必要

これを踏まえて

上記を踏まえて、connect()の解説を読むとより理解が深まりました。 要は、Presentational componentに対して、propsとして、stateと stateを更新するためのCallback関数として渡してるだけのようです。

変に、connect()の第1引数と、第2引数がなにかしらの対応づけがされているのでは と推察していたことがそもそもの間違いでした・・

ちゃんとドキュメント読めよと・・

以前作成した、以下のコードを「React Developer Tools」で確認(スクショ参照)すると、それが良く分かりました。CalculatorContainerコンテナ(Presentational component)に対して、propsとして引数に渡していることが 分かります。

import * as actions from '../actions';

const mapState = (state, ownProps) => ({
    calculator: state.calculator,
});

function mapDispatch(dispatch) {
    return {
        actions: bindActionCreators(actions, dispatch)
    }
}

export default connect(mapState, mapDispatch)(CalculatorContainer);

f:id:katekichi:20170815185231p:plain

(上記のスクショが見えづらかったため、Reactコードを転記しました。)

<Provider store={dispatch: dispatch() subscribe: subscribe() getState: getState() replaceReducer: replaceReducer()}>
 <Connect(CalculatorContainer)>
   <CalculatorContainer calculator={inputValue: 0, resultValue: 0,showingResult: false} actions={onNumClick:fn(), onPlusClickfn():, onMinusClick:fn()}/>
 </Connect(CalculatorContainer)>
</Provider>

そして再びconnet()へ

ここまでで、connet()の挙動に関しては、理解できました。 最後に謎なのが・・。

export default  connect(mapState, mapDispatch)(CalculatorContainer);

という呼び出し時の記述です。connect(mapState, mapDispatch)の戻り値になんらかの関数が 戻ってきて、それに対してコンテナ(Presentational component)に引数に渡して、Class生成している ことが予想されます。

connect()の戻り値をドキュメントで確認すると

Returns

A higher-order React component class that passes state and action creators into your component derived from the supplied arguments. This is created by connectAdvanced, and details of this higher-order component are covered there.

とありますが、higher-order React component classというなんとも説明しづらい記述があったため 実際にreact-reduxの実装を見てみることにしました。

connect.jsを読む

src/connect/connect.jsが対象です。 ソース内のconnectHOC(selectorFactory)が ポイントのようです!。 connectAdvanced()の実装を確認します。

connectAdvanced.jsを読む

src/components/connectAdvanced.jsが対象です。 ソース を確認すると、Connectコンポーネントを作成し、それの子コンポーネントとして引数のコンテナ(Presentational component) を使用しているようです。

なので、connect()の戻りは、

「higher-order React component class」 == 「Connectコンポーネント

ということになります。


まとめ

長々と調査してきましたが、自分なりの結論としては以下に至りました。

connect()とは

  • 引数には、stateとstateを更新するCallback関数を渡す(引数のPresentational componentはpropsでのみ扱える)。
  • Connectコンポーネントを返す(引数のPresentational componentを子コンポーネントして持つ)。

connect()のへの疑問から様々な箇所まで波及しましたが、そもそもReduxの理解不足という一言につきました・・。 ライブラリの内部実装を確認することは、いつも時間の無さにかまけて出来てなかったのですが 今回のconnect()の実装が腑に落ちなく、別のエンジニアに説明する材料が乏しかったため今回の確認作業を実施しました。

中々、ヘビーな作業でしたが、ライブラリのメリデメの理解は断然高まりますので、まとまった時間がある際に再度トライ してみたいです。

最後に

これを読まれている方で、ちょっと認識違うんじゃないかとか、この辺が分かりづらいというご意見ありましたら 是非、コメント頂ければと思います!

【自由研究②:react native】ExpoでDebug環境を整える

前回に引き続き、夏休み自由研究の第2段です! 今回は、前回導入したExpoのDebug環境を整えます。 (環境は、macを対象としています)

目次:


シミュレータ/エミュレータを使用したDebug

まずは、シミュレータ/エミュレータを使用したDebug環境 を試してみました。

Expoの公式サイトのままやっています。 docs.expo.io


Android (Genymotionを使用した場合)

※Genymotionの導入については、別途まとめます。

Either press “Menu” button in Genymotion’s toolbar, or just hit Cmd-m. とあるので、エミュレータ上で、Cmd + mをします。

f:id:katekichi:20170813112911p:plain

メニュー表示されるので、「Debug JS Remotely」を選択します。


Chromeが起動して以下のメッセージが表示されるので、それに従います。

f:id:katekichi:20170813114127p:plain

こんな感じで、developer toolsが使用できるようになり、Debug出来るように なり、ログDebug等が可能になりましたが、JS記述のためやはり、ReactのDebug 環境としてはまだ厳しい感じです。


iOS

iOSシミュレータは、試せていませんが以下で可能なようです。

Hit Ctrl-Cmd-Z on a Mac in the emulator to simulate the shake gesture, or press Cmd+D.


react-native-debuggerのインストール

react-native-debuggerを使用すると、ReactのDebugが可能なようなので 導入してみます。また、reducerに手を加えることによってReduxのDebugも可能なようです。

github.com

$ brew update && brew cask install react-native-debugger

react-native-debugger-open のインストール

ドキュメントを確認すると、react-native-debugger-open も必要なようなので、インストールします。

https://github.com/jhen0409/react-native-debugger/tree/master/npm-package

インストール

$ npm i --save-dev react-native-debugger-open 

package.jsonに追記

"scripts": {
  "postinstall": "rndebugger-open"
}

npm run postinstall を実行し、getDevToolsMiddleware.js をreplaceする

$ npm run postinstall

react-native-debuggerの起動

前述しました、Androidエミュレータ場合、Cmd + m後に「Debug JS Remotely」 を選択すると、react-native-debuggerが起動します。

f:id:katekichi:20170813235744p:plain


まとめ

react-native-debuggerは使用すると、react NativeのDebugが捗りそうです。 ただ、Androidエミュレータは「Genymotion」を使用することが推奨な感じです。 (SDK版では試していませんが、公式ドキュメントに記述がありませんでした) ReduxのDebugをする場合は、redux-devtools-extensionが別途必要そうですが こちらは、次回以降で検証したいと思います。

【自由研究①:react native】Expoを使って、react native環境を作ってみる

夏休み中ですが、休暇明けに「react native」 +「redux」の案件に携わるので 自由研究な感じで諸々やってみたメモをまとめてみようと思います。 今日は、Expoを触ってみた感想をまとめました。


目次:


Expo.io

ちょっと前までは、「react native」はreact-native-cliを使用してアプリ構築する手段 しななかったようですが、xcodeAndroid Stdioでビルドする必要があったりして、結構手間なようです。 また、実機テストが通常のネイティブアプリ開発のようにUSB接続が必要があります。

今回、Expo.ioというサービスを使用すると幸せになれるらしいということが分かったので こちらを使用してみることにします。react nativeの公式ページを見てもこれ使うサンプルが あるのでこちらを使用する方向になりそうです。


create-react-native-appのインストール

Expoを使用するために、create-react-native-appを使用してreact-nativeプロジェクトを 作成する必要があります。

npm install -g create-react-native-app

プロジェクト作成

プロジェクトを作成します。

create-react-native-app hogefuga

XDEのインストール

Desktop Development Tool: XDEをインストールします。

以下を参照しました。 Installation | Expo v15.0.0 documentation


Expoアプリのインストール

各デバイス向けのExpoアプリをGoogle Play/App Storeよりインストールします。 自分はとりあえず、Androidアプリをインストールしました。

以下を参照しました。 Installation | Expo v15.0.0 documentation


XDEでプロジェクトを読み込み

「Project」-「Open Project」でプロジェクトを読み込みます。 f:id:katekichi:20170812231609p:plain

「Share」でQRコードを表示します。 f:id:katekichi:20170812231602p:plain

QRコードをExpoアプリでスキャンする

Expoアプリ(今回は、Android)を起動して、「Scan QR Code」を選択し QRコードを読み込みます。 f:id:katekichi:20170812231551p:plain

しばらくすると、作成したサンプルプロジェクトが起動します。 f:id:katekichi:20170812231541p:plain


まとめ

Expoを使用したreact native環境は、非常にお手軽に実機テストまで出来て良い感じでした。 ビルド待ちが少し気になりましたが・・。

がっつりネイティブ依存の機能を作り込む場合は、「react-native-cli」+「各IDE」を使用して開発するのかもしれ ませんが現状は「Expo」+「create-react-native-app」で充分に思いました。

今日はここまで。


Reactビギナーズガイド ―コンポーネントベースのフロントエンド開発入門

Reactビギナーズガイド ―コンポーネントベースのフロントエンド開発入門

入門 React ―コンポーネントベースのWebフロントエンド開発

入門 React ―コンポーネントベースのWebフロントエンド開発

zappaが想定しないAWSアカウントにデプロイされて焦った話し

世間は、最近サーバレスが盛んで業務でlambdaを使用するメンバも増えてきましたが自分は なかなか使う機会がなかったのですが、PyConJPで弊社ブースを出すに当たってなにか展示物を作る 必要があったので、ZappaGitHub - Miserlou/Zappa: Serverless Python Web Services)を使ってみようと思いました。

Zappaとは

Python/flaskベースのAWSの Serverless frameworkでAPI Gateway + lambda + S3を使用するようです。

テストアプリを作ってみた

zappa で hello world するまで - Qiitaを参考にさせてもらいながら もりもり作成して、デプロイしたわけです。

こんな感じで設定ファイルを書いて

{
    "dev": {
        "app_function": "test.app",
        "aws_region": "ap-northeast-1",
        "profile_name": "default",
        "s3_bucket": "zappa-xyzxyzxyzxyz"
    }
}

そしてデプロイしたわけです。

$ zappa deploy dev
Calling deploy for stage dev..
Creating demo-zappa-dev-ZappaLambdaExecutionRole IAM Role..
Creating zappa-permissions policy on demo-zappa-dev-ZappaLambdaExecutionRole IAM Role.
Warning! Your project and virtualenv have the same name! You may want to re-create your venv with a new name, or explicitly define a 'project_name', as this may cause errors.
Downloading and installing dependencies..
Packaging project as zip..
Uploading demo-zappa-dev-1498726098.zip (7.6MiB)..
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 7.94M/7.94M [00:01<00:00, 4.31MB/s]
Scheduling..
Scheduled demo-zappa-dev-zappa-keep-warm-handler.keep_warm_callback with expression rate(4 minutes)!
Uploading demo-zappa-dev-template-1498726129.json (1.6KiB)..
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1.64K/1.64K [00:00<00:00, 5.09KB/s]
Waiting for stack demo-zappa-dev to create (this can take a bit)..
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 4/4 [00:15<00:00,  5.71s/res]
Deploying API Gateway..
Deployment complete!: https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev

すげーデプロイできた!!。そして、実行。

$ curl -l https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev
hello from Flask!

おお、楽すぎるーと感動しつつ・・。さて、マネージメントコンソールでどんな感じなのかなと 見てみました・・・。

何故か、該当するFunctionが見当たらない・・。

profileを確認してみる。

zappa_settings.jsonでは、defaultをしていました。

$ cat ~/.aws/config

[default]
region = ap-northeast-1

[profile xxxxxx]
output = json
region = ap-northeast-1

[profile yyyyyy]
region = ap-northeast-1
output = json

# @@@@は伏せ字
$ cat ~/.aws/credentials 

[xxxxxx]
aws_access_key_id = @@@@
aws_secret_access_key = @@@@
[yyyyyy]
aws_access_key_id = @@@@
aws_secret_access_key = @@@@

???。defaultaccess_keysecret_access_key が定義されていないのに、なぜかデプロイされちゃってます・・・。

調査する。

aws cliを駆使して調査しました。

$ aws lambda list-functions
{
    "Functions": [
        {
            "FunctionName": "demo-zappa-dev",
            "FunctionArn": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXX:function:demo-zappa-dev",
            "Runtime": "python3.6",
            "Role": "arn:aws:iam::[デプロイ先のAWSアカウント]:role/demo-zappa-dev-ZappaLambdaExecutionRole",
            "Handler": "handler.lambda_handler",
            "CodeSize": 7939825,
            "Description": "Zappa Deployment",
            "Timeout": 30,
            "MemorySize": 512,
            "LastModified": "2017-06-29T09:17:27.435+0000",
            "CodeSha256": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
            "Version": "$LATEST"
        }
    ]
}

デプロイされたlambda functionのAWSアカウントが分かりました。 しかし、いまいち覚えがないアカウント・・・。

$ aws sts get-caller-identity
{
    "UserId": [デプロイ先のAWSアカウント],
    "Account": [デプロイ先のAWSアカウント],
    "Arn": "arn:aws:iam::[デプロイ先のAWSアカウント]:root"
}

うーん、これも同じ。~/.aws/configは使用していない模様。

$ aws configure list
      Name                    Value             Type    Location
      ----                    -----             ----    --------
     profile                <not set>             None    None
     access_key     ****************XXXX     boto-config    
     secret_key     ****************4YT1     boto-config    
      region           ap-northeast-1      config-file    ~/.aws/config

configの一覧が確認できるようなので、試すとboto-config というキーワードがあります。それで思い出しました。以前、botoを使用するために /etc/boto.cfgを作成したことを・・

試しにboto.cfgを削除して、デプロイすると見事にエラーとなりました。

$ zappa deploy dev
Calling deploy for stage dev..
Oh no! An error occurred! :(

==============

Traceback (most recent call last):
 ・・・
==============

Need help? Found a bug? Let us know! :D
File bug reports on GitHub here: https://github.com/Miserlou/Zappa
And join our Slack channel here: https://slack.zappa.io
Love!,
 ~ Team Zappa!

まとめ

とりあえず、原因は分かりましたが、zappaのデプロイ時にzappa_settings.json に該当するprofileが無かった場合はせめてエラーメッセージ出力してデプロイしないで欲しいなあとも思ったのですが botoの挙動なのかもしれないので、ドキュメントを確認しました。

これ見る限り、 botoの仕様ぽいですね。良い勉強になりました。

The mechanism in which boto3 looks for credentials is to search through a list of possible locations and stop as soon as it finds credentials. The order in which Boto3 searches for credentials is:

Passing credentials as parameters in the boto.client() method
Passing credentials as parameters when creating a Session object
Environment variables
Shared credential file (~/.aws/credentials)
AWS config file (~/.aws/config)
Assume Role provider
Boto2 config file (/etc/boto.cfg and ~/.boto)
Instance metadata service on an Amazon EC2 instance that has an IAM role configured.

Amazon Web Services クラウドネイティブ・アプリケーション開発技法 一番大切な知識と技術が身につく

Amazon Web Services クラウドネイティブ・アプリケーション開発技法 一番大切な知識と技術が身につく

docker for mac + nginxのログをrsyslogコンテナで出力してみた

docker for mac + nginx + オレオレ証明書でローカルSSL環境を作ったメモ - katekichiのゆるブログ

前回の続きで、ログの集中管理する的なことをやってみたかったので rsyslogコンテナに吐き出してみることにしました。

ログの集中管理というと、「fluentd」(http://www.fluentd.org/)が主流だと思いますが 今回は、基本的なところを抑えたかったのでrsyslogにしました。


rsyslogサーバーを立てる

http://d.hatena.ne.jp/tmatsuu/20140603/1401811893 の記事を参考に立てました。

Dockerfile作成

FROM centos:latest
RUN yum install -y rsyslog
ADD remote.conf /etc/rsyslog.d/
EXPOSE 514
CMD ["/sbin/rsyslogd", "-n"]

remote.conf作成(rsyslog.conf)

$ModLoad imudp
$UDPServerRun 514
$AllowedSender UDP, 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16

local1.* /var/log/nginx_access_log
local2.* /var/log/nginx_error_log

イメージビルド

$ docker build --tag simple-rsyslog .

コンテナ起動

$ docker run -d -p 514:514/udp -h "logserver" simple-rsyslog

テストしてみる

loggerコマンドを使用して、以前構築したnginxコンテナからログを送信してみます。

rsyslogコンテナのIPは、以下で調べました。

$ docker inspect -format="{{ .NetworkSettings.IPAddress }}"  コンテナID

nginxコンテナからログ送信

$ docker exec -it [nginxのコンテナID] /bin/bash  # コンテナに入る

# logger -n[コンテナのIP] -p local1.info testtesttest
# logger -n[コンテナのIP] -p local2.error errorerrorerrorerror

rsyslogコンテナでログの確認

$ docker exec -it [rsyslogのコンテナID] /bin/bash # コンテナに入る

# tail -f /var/log/nginx_access_log 
Jun 27 10:07:16 172.17.0.3 <someone>: testtesttest

# tail -f /var/log/nginx_error_log  
Jun 27 10:12:01 172.17.0.3 <someone>: errorerror12345678

ログフォーマット等の調整は必要そうですが、出力することはできました。

nginxイメージの修正

nginx.confのログ設定を変更します。

  access_log    /var/log/nginx/access.log;
  error_log     /var/log/nginx/error.log;
  ↓
  access_log syslog:server=logserver,facility=local1 main;
  error_log syslog:server=logserver,facility=local2 notice;

イメージビルドと起動(--add-hostで「logserver」を指定)

$ docker build --tag webapp-syslog .
$ docker run -d --name webapp-syslogp -p 443:443 -p 80:80 --add-host logserver:[rsyslogのIP] webapp-syslog

rsyslogコンテナでログ確認

$ docker exec -it [rsyslogのコンテナID] /bin/bash # コンテナに入る

# tail -f /var/log/nginx_access_log 
Jun 27 11:08:12 f4809369ff7e nginx: 172.17.0.1 - - [27/Jun/2017:11:08:12 +0000] "GET / HTTP/1.1" 200 132 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" "-"
Jun 27 11:08:22 f4809369ff7e nginx: 172.17.0.1 - - [27/Jun/2017:11:08:22 +0000] "GET / HTTP/1.1" 200 132 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" "-"
Jun 27 11:08:24 f4809369ff7e nginx: 172.17.0.1 - - [27/Jun/2017:11:08:24 +0000] "GET / HTTP/1.1" 200 132 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" "-"

まとめ

思いつきでやってみましたが、rsyslogの知見がそもそも無いのに、Docker上で構築するってのが 少し敷居が高かったです(そもそもDockerのオペレーションも怪しい・・)。

何事もステップ・バイ・ステップですね。良い勉強になりました。

記事読まれている方で、この辺り詳しい方、是非アドバイス頂けると嬉しいです。


rsyslog 実践ログ管理入門

rsyslog 実践ログ管理入門

docker for mac + nginx + オレオレ証明書でローカルSSL環境を作ったメモ

nginxのローカルSSL環境が必要になり、docker for macオレオレ証明書で作ったので備忘録としてまとめました。

以下の記事を参考に構築してみました。

docker の nginx イメージの設定ファイルを眺めながら、独自ページを表示します。 - Qiita


nginxのdokcerイメージを取得

Docker Hubから最新のnginxイメージを取得する

詳細は、以下の記事を参照

$ docker pull nginx:latest

docker コマンド 基本のキ(nginx のコンテナを実行してみる) - Qiita

環境構築

$ mkdir nginx; cd $ _
$ touch Dockerfile
$ mkdir html; cd $_
$ touch index.html

$ tree
.
├── Dockerfile
└── html
    └── index.html

index.html

<html>
  <head>
     <meta charset="UTF-8">
     <title>SSL Test</title>
  </head>
  <body>
    <h1>SSL Test</h1>
  </body>
</html>

DockerFile作成

FROM nginx
COPY ./html /usr/share/nginx/html

Dockerイメージの作成とコンテナ立ち上げ

$ docker build --tag ssl-nginx .
Sending build context to Docker daemon 4.096 kB
Step 1/2 : FROM nginx
 ---> cc1b61406712
Step 2/2 : COPY ./html /usr/share/nginx/html
 ---> Using cache
 ---> 2cc22a506a12
Successfully built 2cc22a506a12

$ docker run -d --name ssl-nginx-container -p 80:80 ssl-nginx
651eb9b5642d13274aed51f535d7014754bb001bdcfdf90d7e194bcd5c188b74

ブラウザで確認(http://localhost/


SSL対応するためにオレオレ証明書の作成をします。 この辺りは、こちらを参考にしました。 DockerでNginxのコンテナを作成し、https化してWebページやサービスを公開する方法 | Black Everyday Company

$ brew list openssl 

$ mkdir keys; cd $ _
### 秘密鍵を暗号化してしまうと、コンテナ起動時(Nginx起動時)に
「Enter PEM pass phrase」とパスワードが求められ起動できないようなので暗号化はしない。###
$ openssl genrsa 2048 > server.key

### XXXXは適当に入力 ###
$ openssl req -new -key server.key > server.csr

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:XXXX
Locality Name (eg, city) []:XXXX
Organization Name (eg, company) [Internet Widgits Pty Ltd]:XXXX
Organizational Unit Name (eg, section) []:XXXX
Common Name (e.g. server FQDN or YOUR name) []:example.com
Email Address []:hoge@example.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []: <--- パスワードは空白でOK
An optional company name []:

# 有効期限はざっくり100日で
$ openssl x509 -in server.csr -days 100 -req -signkey server.key > server.crt

# 現状の構成
$ tree
.
├── Dockerfile
├── html
│   └── index.html
└── keys
    ├── server.crt
    ├── server.csr
    └── server.key

nginx.confの設定

HTTP(ポート80)で受けたリクエストはHTTPS(ポート443)にリダイレクトする設定にしておきます。

server {
  listen      80;
  server_name localhost;

  return 301 https://$host$request_uri;
}

server {
  server_tokens off;

  access_log    /var/logs/access.log;
  error_log     /var/logs/error.log;

  listen      443;
  server_name localhost;

  ssl                  on;
  ssl_protocols        TLSv1 TLSv1.1 TLSv1.2;
  ssl_certificate      /etc/nginx/server.crt;
  ssl_certificate_key  /etc/nginx/server.key;

  location / {
    alias /var/www/;
  }
}

DockerFileの変更

FROM nginx

ADD ./html /var/www/html
ADD ./app.conf /etc/nginx/conf.d/app.conf
ADD ./keys/server.crt /etc/nginx/server.crt
ADD ./keys/server.key /etc/nginx/server.key

RUN chmod 755 -R /var
RUN chmod 400 /etc/nginx/server.key

EXPOSE 443
CMD ["nginx", "-g", "daemon off;"]

Dockerイメージの作成とコンテナ立ち上げ

$ docker build --tag webapp .
Sending build context to Docker daemon 12.29 kB
Step 1/9 : FROM nginx
 ---> cc1b61406712
Step 2/9 : ADD ./html /var/www/html
 ---> Using cache
 ---> 102243bc267d
Step 3/9 : ADD ./app.conf /etc/nginx/conf.d/app.conf
 ---> Using cache
 ---> 00668c5641b7
Step 4/9 : ADD ./keys/server.crt /etc/nginx/server.crt
 ---> Using cache
 ---> 85ee3c0da89a
Step 5/9 : ADD ./keys/server.key /etc/nginx/server.key
 ---> Using cache
 ---> f3095cadf8a3
Step 6/9 : RUN chmod 755 -R /var
 ---> Running in 8eab56aefa1c
 ---> 565d1f1e9f07
Removing intermediate container 8eab56aefa1c
Step 7/9 : RUN chmod 400 /etc/nginx/server.key
 ---> Running in fe5c00f05409
 ---> 02c65246caec
Removing intermediate container fe5c00f05409
Step 8/9 : EXPOSE 443
 ---> Running in 1e67a2a92994
 ---> d95cc7a21aa0
Removing intermediate container 1e67a2a92994
Step 9/9 : CMD nginx -g daemon off;
 ---> Running in 0efbccec8be8
 ---> 437f35b60db0
Removing intermediate container 0efbccec8be8
Successfully built 437f35b60db0

$ docker run -d --name webapp -p 443:443 -p 80:80 webapp
2e389797c84eb2162c0f67ab13eb865966ea70ecead1077a61055eadd869e952

ブラウザで確認(https://localhost/

f:id:katekichi:20170614162035p:plain

とりあえずできました。


最後にログは、コンテナ外に出したいのでその辺を試します。

ホストのディレクトリにマウントする

以下のように-v でホストのディレクトリにマウントするのが まずは、手っ取り早い方法かなと思いした。

$ docker run -d -p 443:443 -p 80:80 -v /tmp/sslnginx-log:/var/log/nginx webapp

Syslogで吐き出す方法もありますが、こちらは別の機会に試そうと思います。 nginx-1.7.1でsyslogにログを飛ばせるようになったので試してみた - Dマイナー志向


まとめ

最近、手を動かすことが少なくなってきているように感じたのでローカルの SSLテスト環境を作ろうと思ってやってみましたが、Dockerの理解がまだ浅かったので 良い勉強になりました。

あと普段の業務では、SSL証明書AWSのELBに配置してしまっているため、直接nginxに 設定する良い機会になりました。

今回作成した環境は、githubにあります。 GitHub - y-nakazawa/docker-sslnginx

nginx実践入門 (WEB+DB PRESS plus)

nginx実践入門 (WEB+DB PRESS plus)

nginx実践ガイド impress top gearシリーズ

nginx実践ガイド impress top gearシリーズ

ハイパフォーマンスHTTPサーバ Nginx入門

ハイパフォーマンスHTTPサーバ Nginx入門

マスタリングNginx

マスタリングNginx

Nginx ポケットリファレンス

Nginx ポケットリファレンス

「Electronではじめるアプリ開発」を写経してみた ⑥

またかなり間が空いてしまったのと、写経したものの期待通りの動きではなく調査しましたが 以下の理由からかなりハマってしまっていました。

  1. Electron上でのReactのDebug方法を確立できていなかった。

  2. ES2016と、Promiseの理解不足から原因の特定に時間が掛かった。

  3. そもそもFirebaseのお作法が分かってなく、原因の特定に時間が掛かった。

※1.に関しては、以下を参考にしてDebug環境の構築をしたいと思います。 qiita.com

※2. 3.の課題については別途調査してまとめようと思います。

今回は、チャットルーム一覧の実装から

src/renderer/Rooms.jsx

import React from "react";
import { hashHistory } from "react-router";
import RoomItem from "./RoomItem";
import firebase from "firebase/firebase-browser";

const ICON_CHAT_STYLE = {
    fontSize: 120,
    color: "#DDD"
};

const FORM_STYLE = {
    display: "flex"
};

const BUTTON_STYLE = {
    marginLeft: 10
};

export default class Rooms extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            roomName: "",
            rooms: []
        };
        this.db = firebase.database();
        this.handleOnChangeRootName = this.handleOnChangeRootName.bind(this);
        this.handleOnSubmit = this.handleOnSubmit.bind(this);        
    }

    componentDidMount() {
        this.fetchRooms();
    }


    handleOnChangeRootName(e) {
        this.setState({ roomName: e.target.value });
    }

    handleOnSubmit(e) {
        const { roomName } = this.state;
        e.preventDefault();

        if (!roomName.length) {
            return;
        }

        // Make new Chatroom for a Firebase 
        const newRoomRef = this.db.ref("/chatrooms").push();
        const newRoom = {
            description: roomName
        };

        newRoomRef.update(newRoom).then(() => {
            this.setState({ roomName: "" });
            return this.fetchRooms().then(() => {
                hashHistory.push(`/rooms/${newRoomRef.key}`);            
            });
        });
    }

    fetchRooms() {
        return this.db.ref("/chatrooms").limitToLast(20).once("value").then(snapshot => {
            const rooms = [];
            snapshot.forEach(item => {
                rooms.push(Object.assign({ key: item.key }, item.val()));
            }); 

            this.setState({ rooms });  
        });
    }

    // Drawing the left pane 
    renderRoomList() {
        const { roomId } = this.props.params;
        const { rooms, roomName } = this.state;
        return (
            <div className="list-group">
                { rooms.map(r => <RoomItem room={r} key={r.key} selected={r.key == roomId} />)}
                <div className="list-group-header">
                    <form style={FORM_STYLE} onSubmit={this.handleOnSubmit} >

                        <input 
                            type="test"
                            className="form-control"
                            placeholder="New room"
                            value={roomName}
                            onChange={this.handleOnChangeRootName}
                        />

                        <button className="btn btn-default" style={BUTTON_STYLE}>
                            <span className="icon icon-plus" />
                        </button>
                    </form>
                </div>
            </div>
        );
    }

    // Drawing the right pane 
    renderRoom() {
        if (this.props.children) {
            return this.props.children;        
        } else {
            return (
                <div className="text-center">
                    <div style={ICON_CHAT_STYLE}>
                        <span className="icon icon-chat" />
                    </div>
                    <p>
                        Join a chat room from the sidebar or create your chat room.
                    </p>
                </div>                                                
            );
        }
    }

    render() {
        return (
            <div className="pane-group">
                <div className="pane-sm slider">{this.renderRoomList()}</div>
                <div className="pane">{this.renderRoom()}</div>                
            </div>
        );
    }
}

以下は、チャットルーム一覧の個々のアイテムの実装

src/renderer/RoomItem.jsx

import React from "react";
import { Link } from "react-router";

const LINK_STYLE = {
    color: "inherit",
    textDecoration: "none"
};

export default function RoomItem(props) {
    const { selected } = props;
    const { description, key } = props.room;
            
    return(
        <div className={selected ? "list-group-item selected" : "list-group-item"}>
            <Link to={`/rooms/${key}`} style={LINK_STYLE}>
                <div className="media-body">
                    <strong>{description}</strong>
                </div>
            </Link>
        </div>
    );
}

ルームを追加すると一覧に登録されることが確認できました。 f:id:katekichi:20170614001823p:plain f:id:katekichi:20170614001827p:plain

Firebaseのchatroomsにも登録されました。 f:id:katekichi:20170614001831p:plain f:id:katekichi:20170614001834p:plain

今回はここまで!!

Reactビギナーズガイド ―コンポーネントベースのフロントエンド開発入門

Reactビギナーズガイド ―コンポーネントベースのフロントエンド開発入門

入門 React ―コンポーネントベースのWebフロントエンド開発

入門 React ―コンポーネントベースのWebフロントエンド開発

JavaScript フレームワーク入門

JavaScript フレームワーク入門