NETFLIXの最強人事戦略 自由と責任の文化を築く

 

スタートアップから急成長を遂げ、現在進行形で成長を続けているNETFLIXの元人事担当役員だった方がその人事戦略について説明している書籍。現在はシリコンバレーでも指折りのテックカンパニーなわけだけど、初期の事業はレンタルDVD+デリバリーでそこから何度か大きく事業転換して今の動画ストリーミング事業となったらしい。


自分も今の所属組織でソフトウェアサービスの製造部門マネージャーをやらせてもらってるけど、テーマアップするならば「変化に適応し続けるチーム・組織体制をどう構築するか」って勝手に考えてて、特に各メンバーのマインドセットをどう持ってもらうか、カルチャーをどう定義し浸透させるかっていうことをいつも考えてる。そういったことに対するヒントが語られていた書籍だったと思う。


NETFLIXが他の組織よりも秀でている特徴として「適応力」と「ハイパフォーマンス」が挙げられる。またその文化を支えるいくつかの柱・規律について語られていた。メモしたキーワードは以下。


「徹底的に正直であれ」

「自由と責任」

「事実に基づいて議論する」

「顧客中心主義」


従業員はもうすでに力を持っていて与えるものではない。力を発揮できる環境を整えることが会社の務めで、そのために会社全体が直面している課題や挑戦をオープンに、根気強く伝え続ける必要がある(と自分は解釈した)。


従業員のモチベーションはインセンティブを与えたところで引き出されるものではない(低いとやる気は削ぐけどね、とは思った)。それよりも優秀な同僚と、明確な目的意識を持って、困難だがやりがいのある挑戦に取り組むことで引き出される。


ハイパフォーマーはそうだろうなと共感する。それ俺だ!というほどハイパフォーマーではない(ので日々勉強...)が、モチベーションと感じる部分は共感を覚える部分が多い。


全体を通して共感できる部分が多くあり、とても考えさせられる内容だった。ただ「報酬」と「解雇」の話は面白くはあったが典型的日本企業のいちマネージャーが関与できる話ではないので如何ともしがたい。自分のマネージメントの振り返りで数ヶ月後にまた目を通したいと思った。

language-detectをワンライナーで

そのソースファイルのプログラミング言語は何?を判別する実装がnpmにある。 https://www.npmjs.com/package/language-detect

これをワンライナーで適用する。事前にnpmモジュールはインストールして、

$ npm install language-detect

以下な感じで。

$ TARGET=$FILE node -e 'require("language-detect")(process.env.TARGET,(_,lang)=>{console.log(lang)})'

nodeって標準入力を扱うのが面倒くさいので環境変数経由で渡してる。

freeCodeCampを魔改造してSlackでOAuth2認証を可能にする

f821a35be730fb6cf73d75de5ff76ed0f75d9d6a 時点の staging ブランチを魔改造したよっていう話。

passport-slack のインストール

$ npm install --save passport-slack 

Slack でアプリ作成

https://api.slack.com/apps にアクセスして新規アプリを作成する。 Redirect URI がアプリをサービスする URL に対して作成する必要あり。ローカル用では以下を登録する。

http://localhost:3000/auth/slack/callback

App Credentials から Client IDClient Secret をコピって .env に記述する。

diff

以下の通り。どこかのタイミングで github でしか OAuth 認証をしないようにハードコーディングされているので、 slack にハードコーディング上書きしている。プロフィール画像も決め打ち実装。

diff --git a/common/models/User-Identity.js b/common/models/User-Identity.js
index 962fe0d..6cad516 100644
--- a/common/models/User-Identity.js
+++ b/common/models/User-Identity.js
@@ -72,7 +72,7 @@ export default function(UserIdent) {
           loopback.getModelByType(loopback.User);

         const userObj = options.profileToUser(provider, profile, options);
-        if (getSocialProvider(provider) !== 'github') {
+        if (getSocialProvider(provider) !== 'slack') {
           const err = new Error(createAccountMessage);
           err.userMessage = createAccountMessage;
           err.messageType = 'info';
@@ -130,7 +130,8 @@ export default function(UserIdent) {
       }

       const { profile, provider } = userIdent;
-      const picture = getFirstImageFromProfile(profile);
+      // const picture = getFirstImageFromProfile(profile);
+      const picture = profile && profile.user && profile.user.image_72;

       debug('picture', picture, user.picture);
       // check if picture was found
diff --git a/package.json b/package.json
index 2123144..a352f92 100644
--- a/package.json
+++ b/package.json
@@ -95,6 +95,7 @@
     "passport-linkedin-oauth2": "^1.2.1",
     "passport-local": "^1.0.0",
     "passport-oauth": "^1.0.0",
+    "passport-slack": "0.0.7",
     "passport-twitter": "^1.0.3",
     "pmx": "~0.6.2",
     "react": "~15.4.2",
diff --git a/server/passport-providers.js b/server/passport-providers.js
index 2b8aebf..cc8c796 100644
--- a/server/passport-providers.js
+++ b/server/passport-providers.js
@@ -162,5 +162,32 @@ export default {
     successFlash: [ 'We\'ve updated your profile based ',
                     'on your your GitHub account.'
                   ].join('')
+  },
+  'slack-login': {
+    provider: 'slack',
+    module: 'passport-slack',
+    clientID: process.env.SLACK_ID,
+    clientSecret: process.env.SLACK_SECRET,
+    authPath: '/auth/slack',
+    callbackURL: '/auth/slack/callback',
+    callbackPath: '/auth/slack/callback',
+    successRedirect: successRedirect,
+    failureRedirect: failureRedirect,
+    scope: ['identity.basic', 'identity.email', 'identity.avatar', 'identity.team'],
+    failureFlash: true
+  },
+  'slack-link': {
+    provider: 'slack',
+    module: 'passport-slack',
+    clientID: process.env.SLACK_ID,
+    clientSecret: process.env.SLACK_SECRET,
+    authPath: '/link/slack',
+    callbackURL: '/link/slack/callback',
+    callbackPath: '/link/slack/callback',
+    successRedirect: successRedirect,
+    failureRedirect: linkFailureRedirect,
+    scope: ['identity.basic', 'identity.email', 'identity.avatar', 'identity.team'],
+    link: true,
+    failureFlash: true
   }
 };

.env 追記

SLACK_IDSLACK_SECRET を追記している。

Slack 認証リンク

View のどこかに以下を記述すれば OK

        a.btn.btn-lg.btn-block.btn-social.btn-facebook(href='/auth/slack')
            i.fa.fa-slack
            | Sign in with Slack

LoopBack 入門

門を叩きます。

LoopBack - Node.js framework

インストール

$ npm install -g loopback-cli

アプリケーションの作成

$ lb

     _-----_     
    |       |    ╭──────────────────────────╮
    |--(o)--|    │         LoopBack         │
   `---------´   │ アプリケーションを作成しましょう。 │
    ( _´U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |     
   __'.___.'__   
 ´   `  |° ´ Y ` 

なんか出たw

? アプリケーションの名前は何ですか? hello_loopback
? プロジェクトを格納するディレクトリーの名前を入力してください: hello_loopback
   create hello_loopback/
     info 作業ディレクトリーを hello_loopback に変更します

? どのバージョンの LoopBack を使用しますか? 3.x (current)
? どのようなタイプのアプリケーションにしますか? hello-world (A project containing a controller, including a single vanilla Message and a single remote method)
.yo-rc.json の生成中

メニューによっては上下キーで選択もできる。すごい。

いろいろモジュールがインストールされたら最後に以下の出力。

次のステップ:

  アプリケーションのディレクトリーに移動します
    $ cd hello_loopback

  アプリケーションでモデルを作成します
    $ lb model

  アプリケーションを実行します
    $ node .

The API Connect team at IBM happily continues to develop,
support and maintain LoopBack, which is at the core of
API Connect. When your APIs need robust management and
security options, please check out http://ibm.biz/tryAPIC

インストラクションに従ってモデルを作成する。

$ cd hello_loopback
$ lb model
? モデル名を入力します: person
? person を付加するデータ・ソースを選択します: db (memory)
? モデルの基本クラスを選択します PersistedModel
? REST API を介して person を公開しますか? Yes
? カスタム複数形 (REST URL の作成に使用します): people
? 共通モデルですか、あるいはサーバー専用ですか? 共通
では、person プロパティーをいくつか追加しましょう。

完了したら、空のプロパティー名を入力してください。
? プロパティー名: 

モデルのプロパティを入力していく。

? プロパティー名: name
   invoke   loopback:property
? プロパティー・タイプ: string
? 必須 Yes
? デフォルト値 [なしの場合は空白のまま]: 

別の person プロパティーを追加しましょう。
完了したら、空のプロパティー名を入力してください。
? プロパティー名: age
   invoke   loopback:property
? プロパティー・タイプ: number
? 必須 Yes
? デフォルト値 [なしの場合は空白のまま]: 20

別の person プロパティーを追加しましょう。
完了したら、空のプロパティー名を入力してください。
? プロパティー名: 

モデルができたらアプリケーションを起動(!)

$ node .

起動したら http://0.0.0.0:3000/explorer/ にアクセス。

作成したモデルの POST を開いて、適当なデータをPOST。

f:id:kozy4324:20170223181006p:plain

内部で curl とか叩いてくれているっぽい。

f:id:kozy4324:20170223181158p:plain

GET を開いてリクエストすると作成した値が返ってきた。

f:id:kozy4324:20170223181412p:plain

うん、コード書かずに簡単な RESTful API ができてる。なお DB は設定していないのでアプリ再起動でデータは消える。DB設定すれば簡単に永続化もできそう。

モデル作成時のファイル差分

On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   common/models/message.json
    modified:   server/middleware.json
    modified:   server/model-config.json

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    common/models/person.js
    common/models/person.json

no changes added to commit (use "git add" and/or "git commit -a")

common/models/ 以下にモデル定義とモデルクラスのソースが出力される。

server/model-config.json に列挙されたモデルの情報が入る。

$ git diff server/model-config.json 
diff --git a/server/model-config.json b/server/model-config.json
index a432664..6b901f0 100644
--- a/server/model-config.json
+++ b/server/model-config.json
@@ -37,5 +37,9 @@
   },
   "Message": {
     "dataSource": null
+  },
+  "person": {
+    "dataSource": "db",
+    "public": true
   }
 }

server/datasources.json で DB の接続情報が書けそうだ。

$ cat server/datasources.json 
{
  "db": {
    "name": "db",
    "connector": "memory"
  }
}

server/server.js がエントリーポイントだろう。また server/boot/root.js で middleware 設定してた。これは loopback-boot 関連なのかな。

$ cat server/boot/root.js 
'use strict';

module.exports = function(server) {
  // Install a `/` route that returns server status
  var router = server.loopback.Router();
  router.get('/', server.loopback.status());
  server.use(router);
};

server/component-config.json/explorer の定義もあるな。

$ cat server/component-config.json 
{
  "loopback-component-explorer": {
    "mountPath": "/explorer"
  }
}

全体のディレクトリ構成

$ ls
client      common      node_modules    package.json    server
  • client にはフロントエンドリソースを格納せよと書いてあった
  • common はモデル情報入っている
  • server にはWebアプリ実装が入っている

いまさらながら Express 入門

こいつを調べようと思って、

LoopBack - Node.js framework

Express ベースらしいので、最初はそっちから戯れる。

Hello Express!

$  mkdir hello_express
$  cd hello_express
$  npm init
$  npm install express --save
$ cat app.js 
const express = require('express')
const app = express()

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(3000, () => {
  console.log('Example app listening on port 3000!')
})

reqres は Node 標準 API の HTTP のやつと同じ。

$ node app.js
$ curl http://localhost:3000/

Routing 基本

基本形。

app.METHOD(PATH, HANDLER)

GETとPOSTのルーティング。

app.get('/', (req, res) => {
  res.send('Hello World!')
})
app.post('/', (req, res) => {
  res.send('Got a POST request')
})

Static ファイルの配信

ビルドインの express.static middleware を使う。

app.use(express.static('public'))

特定URLでマッピングするなら、 app.use() の第1引数にパスを指定する。

app.use('/static', express.static('public'))

ファイルの探索はプログラム起動したディレクトリからの相対パスなので、絶対パスで指定するのがよい。

app.use('/static', express.static(path.join(__dirname, 'public')))

404 のハンドリング方法

全ての middleware と route を実行した結果、どれもレスポンスを返せなかった場合に 404 となる。なので一番最後に以下 middleware を定義すればよい。

app.use((req, res, next) => {
  res.status(404).send("Sorry can't find that!")
})

エラーハンドリング

引数4つの middleware でエラーハンドリングとなる。

app.use((err, req, res, next) => {
  console.error(err.stack)
  res.status(500).send('Something broke!')
})

Routing

この基本形。

app.METHOD(PATH, HANDLER)

METHOD は HTTP メソッドと対応してて、めっちゃ定義されている。

get, post, put, head, delete, options, trace, copy, lock, mkcol,  move, purge, propfind, proppatch, unlock, report, mkactivity, checkout, merge, m-search, notify, subscribe, unsubscribe, patch, search, and connect

app.all() という HTTP メソッドと対応していないやつがあり、HTTP メソッド関係ない。特定パスに対してのみ middleware を使うのに等しいのかな。

app.all('/secret', (req, res, next) => {
  console.log('Accessing the secret section ...')
  next() // pass control to the next handler
})

PATH にはパターンや正規表現が使える。 ?, +, *, (, ) はパターンで処理される。

app.get('/ab?cd', (req, res) => {}) // `/abcd`, `/acd`
app.get('/ab+cd', (req, res) => {}) // `/abcd`, `/abbcd`, `/abbbcd`
app.get('/ab*cd', (req, res) => {}) // `/abcd`, `/abxcd`, `/abRANDOMcd`, `/ab123cd`
app.get('/ab(cd)?e', (req, res) => {}) // `/abe`, `/abcde`
app.get('/ab(cd)?e', (req, res) => {}) // `/abe`, `/abcde`
app.get(/a/, (req, res) => {}) // `a` 含むもの全て

: でURLの部分文字列がパラメータにマッピングされる。

app.get('/users/:userId/books/:bookId', (req, res) => {
  console.log(req.params) // /users/1/books/23:title] => { userId: '1', bookId: '23' }
})

-, . はその文字のまま取り扱われる。なので、以下定義ではそれぞれ from, to および genus, species がキャプチャ対象。

/flights/:from-:to
/plantae/:genus.:species

HANDLER は複数指定できる。next() を呼ばないと次のハンドラには行かない。

app.get('/example/b', (req, res, next) => {
  console.log('the response will be sent by the next function ...')
  next()
}, (req, res) => {
  res.send('Hello from B!')
})

Arrayでもよい。

app.get('/example/c', [cb0, cb1, cb2])

express.Router というやつを使うと、いくつかの route や middleware 利用をまとめてモジュールとできる。

var express = require('express')
var router = express.Router()

// middleware that is specific to this router
router.use(function timeLog (req, res, next) {
  console.log('Time: ', Date.now())
  next()
})
// define the home page route
router.get('/', function (req, res) {
  res.send('Birds home page')
})
// define the about route
router.get('/about', function (req, res) {
  res.send('About birds')
})

module.exports = router
var birds = require('./birds')
app.use('/birds', birds)

middleware と route の実行順

定義した順。next() を呼ぶ限り、次の middleware or route が実行される。

next 仮引数

次の middleware 関数ってことらしい。

next('route') とすると、次の middleware へ飛ぶ(stack を辿らない)。以下は 1-12 が出力される。

app.get('/', [
  (req, res, next) => {
    console.log('1-1')
    next('route')
  },
  (req, res) => {
    console.log('1-2')
  }
])
app.get('/', (req, res) => {
  console.log('2')
})

テンプレートエンジン

テンプレートエンジンは I/F が準拠してるものは全て使える。

  • app.set('views', './views') でテンプレートファイルがあるディレクトリを指定する
  • app.set('view engine', 'pug') でテンプレートエンジンを指定する

上記が設定されていれば、 res.render() を呼び出しすればよい。

res.render('index', { title: 'Hey', message: 'Hello there!' })

エラーハンドラする middleware を複数

app.use(logErrors)
app.use(clientErrorHandler)
app.use(errorHandler)

logErrors では next(err) として、次のエラーハンドラに err オブジェクトを渡してあげる必要がある。つまり next() に引数を渡すと次のエラーハンドラが実行されるってことらしい。

function logErrors (err, req, res, next) {
  console.error(err.stack)
  next(err)
}

デバッグ

環境変数で出力有無を制御できる。

$ DEBUG=express:* node index.js

値の指定方法は debug モジュールってやつを利用してて、名前空間的なアプローチっぽい。

app.set()

設定値的なやつを設定すると middleware をまたいで値の共有ができる。

freeCodeCampのサーバ起動スクリプトを追いかける

アプリケーションサーバ起動方法がいくつかあるように見受けられるので、まずはそこらへんの整理から。

追いかけたブランチ

master 50a388d

gulp

ドキュメントに記載ある実行方法gulpfileの中を覗いてみたところnodemon使ってたりしてるのでローカル開発用かと。エントリーポイントはserver/server.js なのを確認

npm start

実態は babel-node server/server.js

npm start-production

名前的に production 実行用だろう。実態は node pm2Startpm2Start.jsが動いて、server/production-start.jsがキックされる。

server/production-start.jsではDB接続を確認してからserver/server.jsを動かしているってことみたい。

server/server.js

というわけで、こいつがエントリーポイント。

利用されてるライブラリとかフレームワーク

以下らへんを別途調べていく。

loopback がメインな Web-App フレームワークっぽい。ルーティングとかどうやって解決されるか謎いので、ちょっとそっちを調べよう。

freeCodeCampのmasterブランチをローカルで動かす

こいつをカスタマイズしてローカルで動かしたくて弄っているログです。

master ブランチに docker-compose.yml が存在するので簡単にイケるんじゃね?と思ったのがスタートで、OSS Gate東京ミートアップ2017-02-20に参加した時にテーマとして選択してみた。ちなみにそのイベントでは動かせていない

暫定版の起動方法

ごにょごにょした結果、以下の手順を踏めば起動まで持っていけた。

ちなみに staging ブランチが本家で動いているものを差異があるようだったので master ブランチで動かしているが、 docker-compose.yml は staging ブランチにしかコミットされてないのでそっちから持ってきている。

git clone https://github.com/freeCodeCamp/freeCodeCamp.git
cd freeCodeCamp
git checkout -b master-20170222 50a388d

# `loopback` `debug` `normalizr` は固定しないとダメ
perl -pi -e 's/"loopback": "\^2.22.0"/"loopback": "~2.22.0"/' package.json
perl -pi -e 's/"debug": "\^2.2.0"/"debug": "~2.2.0"/' package.json
perl -pi -e 's/"normalizr": "\^2.0.0"/"normalizr": "~2.0.0"/' package.json

npm install
bower install
npm run build
cp sample.env .env
echo NODE_ENV=production >> .env

git checkout staging -- docker-compose.yml
docker-compose up

これで一旦 Web サーバーと MongoDB の 2コンテナが起動するので、別セッションで以下を実行すればチャレンジ(問題)がロードされる。

# データベースへロード
docker exec $(docker ps -f name=freecodecamp_server_1 -q) node seed

DBの内容は起動時に読み込まれるっぽいので、一度 docker-compose を終了させて再起動すれば反映される。

docker-compose up

カスタマイズしたいポイント

  • 言語切替 + 翻訳して日本語化
  • OAuth認証の向き先変更
  • ヘルプフォーラムの向き先変更

お前は何がしたいんだっていう話ですが、社内の研修リソースとしてうまいこと活用できないかなと目論んでいるわけです。本家にも貢献できればいいけれども、それは出来たらベースということで。