DRYな備忘録

Don't Repeat Yourself.

Amazon EC2インスタンスにsshが到達しない原因のまとめ(随時追加予定)

当該SubnetのRouteTableに当該InternetGatewayが追加されていない: Operation timed out

問題

  • sshコマンドを打ってもOperationTimeoutになる
    • TCP的に到達していない感じ

原因

  • // TODO(あとでかく): Subnetとは
  • // TODO(あとでかく): RouteTableとは

解決

  1. EC2インスタンス一覧
  2. 当該インスタンスSubnet IDをメモる
  3. VPC→Subnets→当該SubnetのRoute TableのリンクをクリックしてRoute Table設定へ行く
  4. 当該RouteTableのRoutesでEdit
  5. (便宜上とりあえず)Destination0.0.0.0/0として当該VPCにattachされているInternetGatewayを追加

[DONE]

ダウンロードしたKeyPairファイル(.pem)のアクセス権限: Permission denied (publickey).

問題

  • sshがネットワーク的に到達しているが以下のようなエラーが出る
% ssh -i /tmp/my-project.pem ec2-user@X.X.X.X
Permissions 0644 for '/tmp/my-project.pem' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "/tmp/my-project.pem": bad permissions
Permission denied (publickey).

原因

  • 最後の一文だけ見て焦るけど、おちついて全文読みましょう
  • pemファイルのファイルアクセス権限の問題なので、chmodの話

解決

% chmod 600 /tmp/my-project.pem

[DONE]

雑感

  • 何事も、慣れだ

Amazon Web Services実践入門 (WEB+DB PRESS plus)

Amazon Web Services実践入門 (WEB+DB PRESS plus)

(メモ)Go言語のinvalid recursive typeエラー、再帰データ型の実装とイテレーションについて

jsonでいうとこんな感じの

{
    "id": 48,
    "name": "foo",
    "entry": {
        "id": 36,
        "name": "bar",
        "entry": {
            "id": 24,
            "name": "baz",
            "entry": {
                "id": 12,
                "name": "qux"
            }
        }
    }
}

問題

コンパイル時のinvalid recursive typeエラーは、ポインタ型にすれば解決 invalid recursive type in a struct in go - Stack Overflow

実装

package main

import (
    "encoding/json"
    "fmt"
    "strings"
)

const data = `{
  "id": 48,
  "name": "foo",
  "entry": {
      "id": 36,
      "name": "bar",
      "entry": {
          "id": 24,
          "name": "baz",
          "entry": {
              "id": 12,
              "name": "qux"
          }
      }
  }
}`

type Entry struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Child *Entry `json:"entry"`
}

func main() {

    root := new(Entry)
    if err := json.NewDecoder(strings.NewReader(data)).Decode(root); err != nil {
        panic(err)
    }

    for entry := root; entry.Child != nil; entry = entry.Child {
        fmt.Printf("%s has a child: %+v\n", entry.Name, entry.Child)
    }
}

https://play.golang.org/p/vBNfDuhLHB

参考

雑感

Go言語によるWebアプリケーション開発

Go言語によるWebアプリケーション開発

(メモ)docker/machineのテストが落ちる問題

github.com

bash-4.4$ DRIVER=virtualbox make test-integration test/integration/core/core-commands.bats
test/integration/run-bats.sh test/integration/core/core-commands.bats
INFO: Run the tests with B2D_CACHE=1 to avoid downloading the boot2docker iso each time.
=> test/integration/core/core-commands.bats
 ✓ virtualbox: machine should not exist
 ✓ virtualbox: appears with ls
 ✓ virtualbox: has status 'started' appearing in ls
 ✓ virtualbox: create with same name again fails
 ✓ virtualbox: run busybox container
 ✓ virtualbox: url
 ✓ virtualbox: ip
 ✓ virtualbox: ssh
 ✓ virtualbox: version
 ✓ virtualbox: docker commands with the socket should work
 ✓ virtualbox: stop
 ✓ virtualbox: machine should show stopped after stop
 ✓ virtualbox: url should show an error when machine is stopped
 ✓ virtualbox: env should show an error when machine is stopped
 ✓ virtualbox: version should show an error when machine is stopped
 ✗ virtualbox: machine should not allow upgrade when stopped
   (in test file test/integration/core/core-commands.bats, line 109)
     `[[ "$status" -eq 1 ]]` failed
   Starting machine so machine can be upgraded... Starting "bats-virtualbox-test-shared-1498198301"... (bats-virtualbox-test-shared-1498198301) Check network to re-create if needed... (bats-virtualbox-test-shared-1498198301) Waiting for an IP... Machine "bats-virtualbox-test-shared-1498198301" was started. Waiting for SSH to be available... Detecting the provisioner... Waiting for SSH to be available... Detecting the provisioner... Upgrading docker... Stopping machine to do the upgrade... Upgrading machine "bats-virtualbox-test-shared-1498198301"... Copying /tmp/machine-bats-test-virtualbox/cache/boot2docker.iso to /tmp/machine-bats-test-virtualbox/machines/bats-virtualbox-test-shared-1498198301/boot2docker.iso... Starting machine back up... (bats-virtualbox-test-shared-1498198301) Check network to re-create if needed... (bats-virtualbox-test-shared-1498198301) Waiting for an IP... Restarting docker...
 ✗ virtualbox: start
   (in test file test/integration/core/core-commands.bats, line 115)
     `[ "$status" -eq 0  ]` failed
   Starting "bats-virtualbox-test-shared-1498198301"... Machine "bats-virtualbox-test-shared-1498198301" is already running.
 ✓ virtualbox: machine should show running after start
 ✓ virtualbox: kill
 ✓ virtualbox: machine should show stopped after kill
 ✓ virtualbox: restart
 ✓ virtualbox: machine should show running after restart
 ✓ virtualbox: status

23 tests, 2 failures

make: *** [test-integration] Error 1
bash-4.4$

Code Reading

まず、(in test file test/integration/core/core-commands.bats, line 109) とあるので、その行へ行く

@test "$DRIVER: machine should not allow upgrade when stopped" {
  run machine upgrade $NAME
  echo ${output}
  [[ "$status" -eq 1 ]]
}

run machine upgradeがstatus 1で終わるべきだが、status 0で成功している。

次に、run machine upgradeの内容としてcommands/upgrade.goを見る

package commands

import "github.com/docker/machine/libmachine"

func cmdUpgrade(c CommandLine, api libmachine.API) error {
    return runAction("upgrade", c, api)
}

commands/commands.go

runAction抜粋

   if errs := runActionForeachMachine(actionName, hosts); len(errs) > 0 {
        return consolidateErrs(errs)
    }

runActionForeachMachine抜粋

   for _, machine := range machines {
        numConcurrentActions++
        go machineCommand(actionName, machine, errorChan)
    }

machineCommand

// machineCommand maps the command name to the corresponding machine command.
// We run commands concurrently and communicate back an error if there was one.
func machineCommand(actionName string, host *host.Host, errorChan chan<- error) {
    // TODO: These actions should have their own type.
    commands := map[string](func() error){
        "configureAuth": host.ConfigureAuth,
        "start":         host.Start,
        "stop":          host.Stop,
        "restart":       host.Restart,
        "kill":          host.Kill,
        "upgrade":       host.Upgrade,
        "ip":            printIP(host),
        "provision":     host.Provision,
    }

    log.Debugf("command=%s machine=%s", actionName, host.Name)

    errorChan <- commands[actionName]()
}

host.Upgradeが呼ばれている

libmachine/host/host.go

Upgrade冒頭抜粋

func (h *Host) Upgrade() error {
    machineState, err := h.Driver.GetState()
    if err != nil {
        return err
    }

    if machineState != state.Running {
        log.Info("Starting machine so machine can be upgraded...")
        if err := h.Start(); err != nil {
            return err
        }
    }

テストのエラー文言にもあったように、明らかにUpgradeするためにStartを試みている。この実装をした日時と最後にテストが編集された日時が欲しい。blameしてみる。

f:id:otiai10:20170623163828p:plain

このコミットは、Start machine if needed for upgrade · docker/machine@e55dfb2 · GitHub

2017/02/23。対して、test/integration/core/core-commands.batsは、History for test/integration/core/core-commands.bats - docker/machine · GitHub この時点で2016/01/08なので、明らかにテストのほうが追従していない。

試しに、host.Upgradeの中で、machineが動いて無くてもStartしないように変更を加えると、

func (h *Host) Upgrade() error {
-  machineState, err := h.Driver.GetState()
-  if err != nil {
-      return err
-  }
-
-  if machineState != state.Running {
-      log.Info("Starting machine so machine can be upgraded...")
-      if err := h.Start(); err != nil {
-          return err
-      }
-  }

    provisioner, err := provision.DetectProvisioner(h.Driver)
    if err != nil {
        return err
    }

testそのものは

bash-4.4$ DRIVER=virtualbox make test-integration test/integration/core/core-commands.bats
test/integration/run-bats.sh test/integration/core/core-commands.bats
INFO: Run the tests with B2D_CACHE=1 to avoid downloading the boot2docker iso each time.
=> test/integration/core/core-commands.bats
 ✓ virtualbox: machine should not exist
 ✓ virtualbox: appears with ls
 ✓ virtualbox: has status 'started' appearing in ls
 ✓ virtualbox: create with same name again fails
 ✓ virtualbox: run busybox container
 ✓ virtualbox: url
 ✓ virtualbox: ip
 ✓ virtualbox: ssh
 ✓ virtualbox: version
 ✓ virtualbox: docker commands with the socket should work
 ✓ virtualbox: stop
 ✓ virtualbox: machine should show stopped after stop
 ✓ virtualbox: url should show an error when machine is stopped
 ✓ virtualbox: env should show an error when machine is stopped
 ✓ virtualbox: version should show an error when machine is stopped
 ✓ virtualbox: machine should not allow upgrade when stopped
 ✓ virtualbox: start
 ✓ virtualbox: machine should show running after start
 ✓ virtualbox: kill
 ✓ virtualbox: machine should show stopped after kill
 ✓ virtualbox: restart
 ✓ virtualbox: machine should show running after restart
 ✓ virtualbox: status

23 tests, 0 failures

make: Nothing to be done for `test/integration/core/core-commands.bats`.
bash-4.4$

通る。

結論

以下のどちらか

  1. host.Upgradeの仕様を戻す
  2. core-commands.batsのシナリオをちゃんと更新する

まあ2かな。

DRY


追記 2017/06/23 17:40

issue立てた

github.com

Firebase Cloud Messaging(FCM)でより簡単にWebブラウザにPush通知を送るサンプル

前回、下記のエントリでProgressive Web Appにおけるそこそこ生のWeb-Push-Protocolについておおまかな仕組みを触れました

otiai10.hatenablog.com

ので、今回はFirebase Cloud Messagingを使って、もっと手軽にWebブラウザにPush通知を送るのをやってみた備忘録です。

公式ドキュメント

必要なもの

  • GCM Sender ID
    • ブラウザ(デバイス)側で、Subscriptionを発行するときに、このアプリケーションの所属を明らかにするために必要
    • WebPushProtocolにおけるApplicationServerPublicKeyをFirebaseがラップしている要素だと思えばよい
  • FCM Server Key
    • 自前サーバ側で、FCMを叩くときに、アプリケーションの所属と妥当性を明らかにするために必要
    • WebPushProtocolにおけるApplicationServerPrivateKeyをFirebaseがラップしている要素だと思えばよい

どっちもFirebase consoleで手に入る。

  1. Firebase consoleに行き、[プロジェクトを追加]する(図)
  2. [ウェブアプリにFirebaseを追加]を選択(3つ並んだ◯の右の赤いやつ)
  3. messagingSenderIdというのがあるので、これがGCM_SENDER_IDです
  4. つづいて、左上、[Overview]の隣の設定アイコンから[プロジェクトの設定]を選択
  5. 上部のタブのうち[クラウドメッセージング]を選択 6.サーバーキーというのがあるので、これがFCM_SERVER_KEYです

f:id:otiai10:20170622010233p:plain

やること

  • ブラウザのタブで動くmain.js
    • firebaseアプリケーションの初期化
    • messagingモジュールの初期化
      • 自動で/firebase-messaging-sw.jsをServiceWorkerとしてregisterしようとする
    • Notificationの許可と、DeviceToken取得できたら自前サーバにそれを送る
  • バックグラウンドで動き続けるfirebase-messaging-sw.js
    • ブラウザタブがactiveでないときにPush通知が来たときのハンドラを設定できる
  • 自前のサーバserver.js
    • ブラウザから送りつけられてきた各ユーザのDeviceTokenの保存
    • 何らかのきっかけでFCMのエンドポイントを叩く
      • payloadのtoというフィールドでPush通知を投げたい相手のDeviceTokenを指定する
    • もちろんjsである必要はない全く無い

うるせえ動くコード見せろ

ウッス

github.com

% git clone git@github.com:otiai10/firebase-messaging-sample.git
% cd firebase-messaging-sample
% GCM_SENDER_ID={上記の} \
FCM_SERVER_KEY={上記の(けっこう長い)} \
node server.js

こんな感じでうごく

f:id:otiai10:20170622022753p:plain

注意点・ハマったポイント

manifest.jsonのgcm_sender_idは"103953800507"

  • 問題: ブラウザのコンソールで「Messaging: Please change your web app manifest’s ‘gcm_sender_id’ value to ‘103953800507’ to use Firebase messaging. (messaging/incorrect-gcm-sender-id).」と叱られる
  • 原因: これは、実際にPush通知を送る主体はFirebaseのサーバなので、FirebaseのアプリケーションIDを登録する必要があるため
  • 解決: ハードコードで103953800507を指定する manifest.json

setBackgroundMessageHandlerで設定したハンドラが呼ばれてない

  • 問題: firebase-messaging-sw.jsにおいて、setBackgroundMessageHandlerを使ってハンドラを登録しているのにもかかわらず、ぜんぜん呼ばれてる気配が無い
  • 原因:
    • その1: ブラウザのタブがactiveな時は必ず、ブラウザタブのmain.jsにおけるmessaging.onMessageが呼ばれるので、ServiceWorkerには入らない
    • その2: これが上記で「癖がある」って言ったやつなんですけど、自前サーバ側でFirebase叩くときに、そのペイロード中にnotificationというフィールドのデータが入っていると、そのnotificationで指定したtitle,body,iconでNotificationが生成され、ServiceWorkerには入らない。ふしぎだね。
  • 解決: まずブラウザのタブを閉じる。そんで、サーバからのFCMへのリクエストのトップにnotificationというフィールドを含めない

雑感

  • 暗号化その他もろもろFirebaseがやってくれるので、やっぱり素でWeb-Push-Protocolに乗るより断然お手軽だった
  • 癖があるところとか、ほげほげKEYが多すぎて混乱するところとかはあるけども
  • 酒が弱くなってる。よいことだ。

DRYな備忘録として

Using Google App Engine

Using Google App Engine

webブラウザにPush通知送るサーバとjsのサンプル

このドキュメントは

以下の2つのドキュメントをよりプリミティブに理解するためのDRYな備忘録です。

  1. Adding Push Notifications to a Web App  |  Web  |  Google Developers
  2. The Web Push Protocol  |  Web  |  Google Developers

f:id:otiai10:20170619113329p:plain

背景

かつて サーバからブラウザにプッシュ通知を送りたい(非WebSocket、非ロングポーリング) - DRYな備忘録 これ書いたけど、改めてProgressive Web Appのドキュメント行ったらアホみたいに分かりづらくなってて不条理を感じたので書きます。

サーバからPush通知がブラウザに対して送れて、ブラウザのjsが閉じててもServiceWorkerが生きてるからNotificationが出る、っていうやつです。最近だとウェブのGmailとかFacebook Messanger若干うざいやつとかでよく見る気がします。

この公式ドキュメント、パッとサンプル落としてきてハイ動くでしょ!っていうタイプなのか最小限の要素を理解させつつ動かすタイプなのか、完全にどっち付かずのドキュメントになっていて、若干わかりにくいです。

今回は、WebPushProtocolにおける暗号化されたbodyを送る、というところを除き、クライアントのコードとサーバのコードを書いたので、そのログを備忘録として晒します。

概念の確認

サービスとしては、3つです

  1. ブラウザ
    • ネイティブアプリ開発における「デバイス」だと理解して問題ないです
    • ブラウザではふたつのことをします
      • ServiceWorkerを登録し、登録したServiceWorkerにプッシュをsubscribeさせ、PushSubscriptionモデルを取得します
        • これが、ApplePushNotificationにおけるDevice Token的なものです
      • ServiceWorker内部では、タブのJavaScriptが死んでもbackgroundで動き続けるイベントリスナーを登録します
        • pushが来たとき、というイベントリスナーもここで定義します
  2. 自前サーバ
    • ブラウザで登録・取得されたPushSubscriptionモデルのデータを、自作アプリケーションにおけるユーザなどと紐付けて保存しておく必要があります
    • 何らかのトリガーで(たとえばユーザによる他ユーザへの呼びかけなど)下記の「Pushサービス」に「このsubscription endpointへこの内容でプッシュ通知送ってちょんまげ」というリクエストをする役割があります
  3. Pushサービス
    • 各デバイスに対してPushを実際に送るサービスです
    • Web-Push-Protocolのインターフェースを満たすサービスである必要があります
      • Chromeではfcm.googleapis.com
      • Firefoxではupdates.push.services.mozilla.com
      • が、それぞれPushサービスとして採用されます
    • 僕らは自前サーバからこのエンドポイント叩くだけなのでその違いを意識する必要は無い ← ここだいじ!

うるせえ動くコード見せろ

ウッス

github.com

こんな感じに動きます

f:id:otiai10:20170619200505p:plain

実際の処理

  1. ブラウザに登録されるServiceWorker
    • ソースコード: service-worker.js
    • やること
      • 自分自身に何かイベントがあったらそれをハンドリングする
        • install, activate, push
    • 必要なもの
      • 無い。ぶっちゃけ登録されるServiceWorkerがやるべき責務は小さい。
  2. ウェブページのmain
    • ソースコード: main.js
    • やること
      • ServiceWorkerをブラウザにregisterする
      • そしたらregistration.pushManager.subscribe()というのを呼ぶ
      • 上記で得られたsubscriptionモデルを自作サーバへ送りつける
        • このsubscriptionというのが、Pushを提供するGCMサーバのエンドポイントと、このデバイス(ブラウザ)のDeviceToken的なものを持っている
    • 必要なもの
      • ApplicationServerPublicKeyが必要
        • サーバのみが知りうるApplicationServerPrivateKeyに対をなすもの
  3. Pushをトリガーする自前サーバ
    • ソースコード: server.js(もちろんjsで書く必要は無い)
    • やること
      • ウェブページのjsから送られてきたsubscriptionの保存
      • 任意のトリガーでそのsubscriptionへPushを送る
        • 厳密にはPushを"送る"のはgcmなりMozilaPushServiceなので、「Pushサービスが提供したエンドポイントを叩く」のほうが正確
        • また、Pushサービスを叩くときのリクエストはWeb-Push-Protocolで定められた方法で暗号化されている必要があるが、今回はこれはweb-pushライブラリにすべて任せている
    • 必要なもの
      • ApplicationServerPrivateKeyが必要

注意点・ハマったところ

ServiceWorkerの更新ができない

  • 問題: ページをリロードしても編集したServiceWorkerが適用されていない
  • 原因: ServiceWorkerのライフサイクルは「ブラウザ自体」と「ウェブページのドメイン」によって定められるのであって、「ウェブページのタブ(いわゆるいつものブラウザのjsランタイム)」ではないので、ページをリロードしても更新されない
  • 解決: getRegistraionからのunregisterを呼ぶ。あるいはchrome://serviceworker-internals/に行き該当ウェブページに登録されているServiceWorkerを[Unregister]ボタンで削除するなりする必要がある

Subscriptionの更新ができない

InvalidStateError: Registration failed - A subscription with a different applicationServerKey (or gcm_sender_id) already exists; to change the applicationServerKey, unsubscribe then resubscribe.
  • 問題: registration.pushManager.subscribe()を呼ぶと上記のようなエラーが出ることがある。
  • 原因: エラーメッセージでは「別のapplicationServerKeyを使ったsubscriptionがすでにあるので、applicationServerKeyを変えてunsubscribeしてからもっかいsubscribeしてね」と言っている。基本的にはApplicationServerPublicKeyApplicationServerPrivateKeyの組み合わせは、1つのプロジェクトについてただ1つだけなので、このようなエラーは起きないと思うが、開発中とかだと起きるかもしれませんねハイ。上記のサンプルでは、npmのpost-installで1度だけVAPIDを作るようにしてます
  • 解決: エラーメッセージにおとなしく従う

ApplicationServerKeyとは

InvalidAccessError: Failed to execute 'subscribe' on 'PushManager': The provided applicationServerKey is not valid.
Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.
  • 問題: subscribeするときに、上記のようなエラーが出ることがある。どうやらapplicationServerKeyがよくないようだが、そもそもApplicationServerKeyって何なのよ。
  • 原因: 今回いちばんハマったところなんですが、ApplicationServerKeyっていうのは、WebPushProtocolの文脈でのただの言い方で、その他いろいろな文脈とは関係ない、ただの、定められた方法で生成されたPublicKeyとPrivateKeyのペアというだけです。ここにGCM_API_KEYとかFCM_SERVER_KEYとか突っ込んで困る、というケースが多そう。まあ僕もなんですけど。RSAとかも無関係、名前似てるけど。
  • 解決: ほんで、この「Keyのペア」は、今回はweb-pushライブラリのgenerateVAPIDKeysに全部まかせています。PushサーバとしてGCMやMozilaPushServiceを叩くことこそあれど、今回のサンプルはGCM, FCM, MPSを関知しない、Web-Push-Protocolを用いたアプリケーションの構成のサンプルという位置づけです。

雑感

DRYな備忘録として

Building Progressive Web Apps: Bringing the Power of Native to the Browser

Building Progressive Web Apps: Bringing the Power of Native to the Browser

はじめてのNode.js -サーバーサイドJavaScriptでWebアプリを開発する-

はじめてのNode.js -サーバーサイドJavaScriptでWebアプリを開発する-

Goのコードから複数の異なるDockerホストに対してコンテナの起動を実装する

2017/06/19 追記

  • エラーハンドリングにバグがあったので修正しました
    • channelに何か流したとき、channelから取り出されないと流し込んだ側をブロックするのを忘れていました
- errored <- err
+ go func() {
+   errored <- err
+ }()

前回までで以下のことをやったので

今回は、Goのソースコードが動いているマシンじゃないマシンに対してDockerコンテナの起動を指示する実装をしてみたいなと思いました。

準備

  • 複数のDockerホストを再現するためにdocker-machineを入手しとく
  • ドライバとしてVirtualBoxをインストールしとく
  • 前回までで使っていたclient.NewEnvClientclient.NewClient実装の違いを見とく
  • docker-machine create --driver virtualbox hoge
    • docker-machine env hoge で得られるDOCKER_HOSTとかDOCKER_CERT_PATHとかあとで使う
  • docker-machine create --driver virtualbox fuga
    • docker-machine env fuga で得られるDOCKER_HOSTとかDOCKER_CERT_PATHとかあとで使う

動くコード

package main

import (
    "bufio"
    "context"
    "fmt"
    "net/http"
    "path/filepath"

    "github.com/docker/docker/api/types"
    "github.com/docker/docker/api/types/container"
    "github.com/docker/docker/api/types/mount"
    "github.com/docker/docker/api/types/network"
    "github.com/docker/docker/client"
    "github.com/docker/go-connections/tlsconfig"
)

// WorkerMachine is a host on which any container runs,
// This shoule also have file mout source path.
type WorkerMachine struct {
    Host        string
    CertPath    string
    MountSource string
}

// NewClient provides Docker client for **THIS** host machine.
func (machine WorkerMachine) NewClient() (*client.Client, error) {
    options := tlsconfig.Options{
        CAFile:             filepath.Join(machine.CertPath, "ca.pem"),
        CertFile:           filepath.Join(machine.CertPath, "cert.pem"),
        KeyFile:            filepath.Join(machine.CertPath, "key.pem"),
        InsecureSkipVerify: false,
    }
    tlsc, err := tlsconfig.Client(options)
    if err != nil {
        return nil, err
    }
    httpClient := &http.Client{
        Transport: &http.Transport{
            TLSClientConfig: tlsc,
        },
    }
    headers := map[string]string{}
    return client.NewClient(machine.Host, "1.30", httpClient, headers)
}

// This is the definition of docker host machines
// on which any tasks would be executed.
var machines = []WorkerMachine{
    WorkerMachine{
        Host:        "tcp://192.168.99.100:2376",
        CertPath:    "/Users/otiai10/.docker/machine/machines/hoge",
        MountSource: "/Users/otiai10/tmp/hoge",
    },
    WorkerMachine{
        Host:        "tcp://192.168.99.101:2376",
        CertPath:    "/Users/otiai10/.docker/machine/machines/fuga",
        MountSource: "/Users/otiai10/tmp/fuga",
    },
}

// task function defines what to do on that machine.
// Because hijacking stdout of container returns io.Reader
// and also because the first out put would be lost when container starts BEFORE hijacking,
// this function should return channel which can message error,
func task(index int, machine WorkerMachine) (errored chan error) {

    errored = make(chan error)

    c, err := machine.NewClient()
    if err != nil {
        go func() {
            errored <- err
        }()
        return
    }
    defer c.Close()
    ctx := context.Background()

    // Ensure Target Image to exist inside this machine
    r, err := c.ImagePull(ctx, "otiai10/foo", types.ImagePullOptions{})
    if err != nil {
        go func() {
            errored <- err
        }()
        return
    }
    defer r.Close()
    fmt.Printf("[%d][IMAGE PULL][START]", index)
    for scanner := bufio.NewScanner(r); scanner.Scan(); {
        fmt.Printf(".")
    }
    fmt.Printf("\n[%d][IMAGE PULL][FINISH]\n", index)

    // Create container in this machine
    containerconfig := &container.Config{Image: "otiai10/foo"}
    hostconfig := &container.HostConfig{
        Mounts: []mount.Mount{
            mount.Mount{
                Type:   mount.TypeBind,
                Source: machine.MountSource,
                Target: "/var/data",
            },
        },
        AutoRemove: true,
    }
    networkingconfig := &network.NetworkingConfig{}
    body, err := c.ContainerCreate(
        ctx,
        containerconfig, hostconfig, networkingconfig,
        fmt.Sprintf("work-%d", index),
    )
    if err != nil {
        go func() {
            errored <- err
        }()
        return
    }

    // Hijack container's Stdout, and it should be hijacked BEFORE container starts
    hijacked, err := c.ContainerAttach(ctx, body.ID, types.ContainerAttachOptions{
        Stream: true,
        Stdout: true,
    })
    if err != nil {
        go func() {
            errored <- err
        }()
        return
    }
    go func() {
        for scanner := bufio.NewScanner(hijacked.Reader); scanner.Scan(); {
            fmt.Printf("[%d][CONTAINER STDOUT]\t%s\n", index, scanner.Text())
        }
        hijacked.Close()
        fmt.Printf("[%[1]d][COMPLETED] Congrats! `work-%[1]d` is completed without errors!\n", index)
        errored <- nil
    }()

    // OK, now it's time to start the container!
    if err := c.ContainerStart(ctx, body.ID, types.ContainerStartOptions{}); err != nil {
        go func() {
            errored <- err
        }()
        return
    }

    return
}

func main() {
    for i, machine := range machines {
        errored := task(i, machine)
        if err := <-errored; err != nil {
            panic(err)
        }
        close(errored)
    }
}

これを実行すると、

f:id:otiai10:20170607171124p:plain

となり、複数の異なるホストマシンで、異なるContainerのプロセスが、同期的に逐次実行されていることがわかる。

なお、二回目からは、各ホストマシンにimageがすでにあるので

f:id:otiai10:20170607171538p:plain

こういう感じになるし、あえて

% eval $(docker-machine env fuga)
% docker images -a
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
otiai10/foo         latest              92af8256ff46        39 minutes ago      193 MB
% docker rmi otiai10/foo
%

このようにfugaの方だけでotiai10/fooimageを消しとくと、

f:id:otiai10:20170607172406p:plain

fugaでのみdocker pullでまるっとimageを落としてくる必要があり、hogefugaがDockerホストマシンとして全く別物であることが確認できる。

雑感

  • Dockerちょっとずつわかってきた
  • 去年ずっとJSとSwiftだったので、ひさしぶりにGo触ってて超たのしい
  • GoたのしすぎてJSのタスクがぜんぜん進まない…

DRYな備忘録として

Docker

Docker

Infrastructure as Code ―クラウドにおけるサーバ管理の原則とプラクティス

Infrastructure as Code ―クラウドにおけるサーバ管理の原則とプラクティス

マイクロサービスアーキテクチャ

マイクロサービスアーキテクチャ

【GAE/Go】goappで "There are too many files in your application" と怒られる

追記

gcloud components updateするとgoogle-cloud-sdk以下のファイルが更新されて魔改造が吹っ飛ぶ。かなしみ。

問題

% goapp serve
INFO     2017-06-07 01:58:24,616 devappserver2.py:692] Skipping SDK update check.
INFO     2017-06-07 01:58:24,646 api_server.py:272] Starting API server at: http://localhost:52623
INFO     2017-06-07 01:58:24,648 dispatcher.py:205] Starting module "default" running at: http://localhost:8080
INFO     2017-06-07 01:58:24,650 admin_server.py:116] Starting admin server at: http://localhost:8000
/Users/otiai10/.google-cloud-sdk/platform/google_appengine/google/appengine/tools/devappserver2/mtime_file_watcher.py:157: UserWarning: There are too many files in your application for changes in all of them to be monitored. You may have to restart the development server to see some changes to your files.
  'There are too many files in your application for '

となって、編集検知のオートビルドされなくてちょっと困る。

調査

なんかdevappserverのコード魔改造するしかなさそう。今んとこ。

解決

appengine/tools/devappserver2/mtime_file_watcher.pyを以下のように編集。まずはわかりやすいように。

diff --git a/watcher_common.py b/watcher_common.py
index fe47078..6541de6 100755
--- a/watcher_common.py
+++ b/watcher_common.py
@@ -47,6 +47,11 @@ def ignore_file(filename, skip_files_re=None):
   Returns:
     Boolean value, True if the filename can be ignored.
   """
+  if not filename.startswith(os.getcwd()):
+    return True
+  if filename.find('/node_modules/') is not -1:
+    return True
+
   if skip_files_re and skip_files_re.match(filename):
     return True
   filename = os.path.basename(filename)
  1. カレントディレクトリ以下のファイル以外は監視対象から除外
  2. node_modulesは監視対象から除外

適宜

適宜、たとえば

diff --git a/watcher_common.py b/watcher_common.py
index fe47078..ad1f641 100755
--- a/watcher_common.py
+++ b/watcher_common.py
@@ -47,6 +47,11 @@ def ignore_file(filename, skip_files_re=None):
   Returns:
     Boolean value, True if the filename can be ignored.
   """
+
+  # Watch ONLY current directory if env "GOAPP_WATCH_CWD" is found
+  if os.getenv('GOAPP_WATCH_CWD') and not filename.startswith(os.getcwd()):
+    return True
+
   if skip_files_re and skip_files_re.match(filename):
     return True
   filename = os.path.basename(filename)
@@ -67,7 +72,11 @@ def _remove_pred(lst, pred):

 def ignore_dir(dirpath, dirname, skip_files_re):
   """Report whether a directory should not be watched."""
+
+  # List of directory names to be ignored; e.g. "node_modules", "vendor" or so
+  _IGNORED_DIRS = ('node_modules')
   return (dirname.startswith(_IGNORED_PREFIX) or
+         (dirname in _IGNORED_DIRS) or
           skip_files_re and skip_files_re.match(os.path.join(dirpath, dirname)))

のようにしたらオシャレなのではないか。魔改造には変わりないけど。

DRYな備忘録として

はじめてのGoogle App Engine Go言語編 (I・O BOOKS)

はじめてのGoogle App Engine Go言語編 (I・O BOOKS)

Go言語プログラミング入門on Google App Engine

Go言語プログラミング入門on Google App Engine

プログラミング Google App Engine

プログラミング Google App Engine