西新プラリバのオープンでいろいろ思ったこと

家の最寄り駅に、プラリバっていう小規模なショッピングモールがオープンした。

praliva.jp

そこまで期待はしてなかったんだけど、とりあえず行ってみた感じ、悪くはなさそうだなと思った。

オープニング

7/26 10:00オープンだが、内装は実は7/24の朝から見えていた。

f:id:YusukeIwaki:20190726232533p:plain

なんて小綺麗な。リバレインモールみたいに閑散としている様子が似合う感じ?と思ってしまうくらい、なんか上品だ。

ところが、今朝の様子が

f:id:YusukeIwaki:20190726232445p:plain

うわ、なんだこれ!

関東に住んでいたときに経験した武蔵小杉のごちゃごちゃショッピングモールを彷彿させる、ありえん人の多さだった。藤原紀香を見たくて並んでた人も多いんだろうけど。

なんというか、こんなに人があふれるのって西新地区では見たことがない風景なので、結構衝撃的でした。というか、通勤するときにめちゃくちゃ通行の妨げになっていて邪魔でしたww 広い地下があるのに、なんで狭い狭い1階をオープニングの待ちに選んだんだろ...謎。たぶん「地下は市の持ち物だから・・・」とかそういう系の理由なんだろうけども、正直とても迷惑だったので猛烈に反省して欲しいと思う。

"地下直結!"なのになぜか地下から入れない

これは確実に設計ミスじゃないかって思った。

地下直結!と言っているのに地下から入れない。暑いなか、階段で地下2階から地上1階まで上がり、そこから階段で地下2階へどうぞ、という謎のナビゲーション。(あとから分かったんだけど、このプラリバ、地下2階のフードウェイっていうスーパーの構造にかなりの難がある。)

お年寄りの方が、地下1階から入ろうとして警備員に止められている(地上まで上がって地下に降りて、という説明を受けて非常に困惑している)のを見て、なんかそれはちょっと違うんじゃないかって思わずにいられなかった。

迷路のようなスーパーマーケット

フードウェイという、福岡では比較的イケイケなスーパーが地下2階フロアの大半を占める形で入っている。

私の勝手なスーパーの王道ルートは f:id:YusukeIwaki:20190726235600p:plain

こんなかんじなんだけど、フードウェイ西新店は(うろ覚えだけど)

f:id:YusukeIwaki:20190727000925p:plain たしかこんな構造をしていた気がする。なので、慣れている買い物の仕方をしようとすると

f:id:YusukeIwaki:20190727001237p:plain

ああ悲惨・・・。

慣れなんだろうな。

お惣菜が入り口にあるっていうのは、たぶん成城石井あたりを真似したんじゃないかなっておもう。なんだけど、それにしては中途半端で、お惣菜のクオリティが低くノイズが多いかなと思った。

近所の西鉄レガネットストアよりも営業時間が長く、珍しいものも売ってそうなので、今後に期待かな。

待望の本屋さん

西新は修猷館高校もあることだし、勉強のためには本屋が必要だ。なんだけど、今まで中規模以上の本屋は無かった。

プラリバに新たにくまざわ書店っていうのができていて、小さいながら割といろいろな本がバランス良く置かれている印象。

いままで、六本松か天神まで出ないと本屋が無かったので、これは便利に使わせてもらうことになりそう。

あとのお店は・・・

天神VIOROで飲んでファンになったハニー珈琲をはじめ、多くの店にはまだ行けていない。

全体的に人が多すぎて、「また今度かな」ってなってしまったのが正直なところ。

ホークスタウンにあるマークイズ福岡に比べると、便利だし、リピート率が高そう。でも早く落ち着いてほしいなー。

自分用Cloud9をSSL化して、oauth2_proxyで自分だけの認証を追加した

この記事では、SSH使うから認証は後回しで、って書いてたんだけど、それじゃあChromebookで気軽にお遊びができないことに気づいたので、結局SSL化と認証を追加した。

ドメイン取得&SSL

certbot(Let's encrypt)を使うには、TXTレコードやCNAMEレコードをイジる必要があるので、しぶしぶ c9work.net というドメインを購入。(胡散臭いキャンペーンとか嫌いなので、AWS経由で購入した)

証明書の取得は

letsencrypt.org

にあるように、certbotで行う。

  • Cloud9 IDEを表示するURL(hogehoge.ide.c9work.net)
  • 成果物を誰かに見せる用のURL(hogehoge.preview.c9work.net)
  • 認証用その他もろもろのページのURL(auth.c9work.net, page.c9work.net, ...)

の3つを想定していたので、雑にワイルドカード証明書を3つ。

sudo certbot certonly --manual --manual-public-ip-logging-ok -d *.preview.c9work.net -m ore_ore@yahoo.co.jp
sudo certbot certonly --manual --manual-public-ip-logging-ok -d *.ide.c9work.net -m ore_ore@yahoo.co.jp
sudo certbot certonly --manual --manual-public-ip-logging-ok -d *.c9work.net -m ore_ore@yahoo.co.jp

みたいな感じで。

都度、TXTレコードに確認用文字列を入れる、みたいなことはやる必要があったけど、そこはRoute53なりAzure DNSなりで管理してたらなんの問題もなくいける。

前段にGitHub認証を入れる

これが結構つまった。(ひとえに、nginxのAuth Requestを知らなかったからなんだけどw)

SSL化をした時点で、

f:id:YusukeIwaki:20190722171647p:plain

なんとなくこんな感じに、nginx二段構えにしていた。なので、認証はとりあえずSSLオフロードをやってる前段のnginxに何かしらやればいいんだろうなーと。Cloud9 SDK自身も、なんとなく認証っぽい仕組みは内部で持っていそう( core/auth.js at master · c9/core · GitHub )だったんだけど、SSLオフロードした先に認証機構があるのはちょっと筋が悪い気がした、なのでnginxかその前段に持たせようかなと。

んで、CW社の偉大なる同期が昔書いていた記事も参考に、

qiita.com

これはとりあえずoauth2_proxyをAuth Requestと組み合わせて使えばラクかなと。

認証エンドポイントと認証した先に戻るURLでドメインが違うのをなんとかする

手っ取り早くウェブアプリケーションにOAuth2認証を導入する - その手の平は尻もつかめるさ あたりを参考にするとわかりやすいんだけど、oauth2_proxyは /oauth2/start?rd=/return_back_to/this/address のようにパスを渡すと、認証完了後に /return_back_to/this/address に帰ってきてくれる。

今回の場合は、

と想像していた。が、現実はそうはいかず。 https://auth.c9work.net/ にリダイレクトされてしまった。

oauth2_proxyのソースをみたら、「あーー」ってなんたんだけど

https://github.com/pusher/oauth2_proxy/blob/863539154366f456d8ba57142b3a66b4544fda82/oauthproxy.go#L477-L483

redirect = req.Form.Get("rd")
if !p.IsValidRedirect(redirect) {
    redirect = req.URL.Path
    if strings.HasPrefix(redirect, p.ProxyPrefix) {
        redirect = "/"
    }
}

をチェックして、NGだったら強制的に / に書き換えられてしまっているではありませんかー!

OAUTH2_PROXY_EMAIL_DOMAINS に .ide.c9work.net を設定すると解決。

GitHubユーザをYusukeIwakiに限定する

これも結構ハマった。

本当は前段の認証側でどうにかしたかったんだけど、nginx.confの記述方法わからなすぎたので、

  • 前段の認証ではGitHubユーザだったらとりあえず通すようにする
    • バックのnginxにリクエストを渡すときに X-Remote-Username というカスタムヘッダにGitHubユーザ名を渡す
  • バックに居るコンテナ振り分けをやるnginxのほうで、GitHubユーザ名が YusukeIwaki じゃなかったら強制的に403を返すようにする

という雑な方法で実現。

        location / {
            auth_request /oauth2/auth;
            error_page 401 = /oauth2/start?rd=https:///$host$request_url;

            proxy_set_header X-Forwarded-Scheme $http_x_forwarded_proto;
            proxy_set_header Host $host;
            auth_request_set $user $upstream_http_x_auth_request_user;  # ←認証完了時に、oauth_proxy2 がX-Auth-Request-UserヘッダにGitHubユーザ名をセットしてくれるのを拾う。
            proxy_set_header X-Remote-Username $user;  # ←nginx_backにリクエストをプロキシするときに、 X-Remote-Usernameというカスタムヘッダを付ける
            proxy_pass http://nginx_back:8888;
        }
    server {
        listen             8888;
          (中略)

        if ($http_x_remote_username != 'YusukeIwaki') {
            return 403;
        }

たぶんもっといいやり方はあるんだろうなぁ...。

そんなわけで

Chromebookでも開発をできるようになった。

うぇいうぇい

Cloud9 PRO終了のお知らせにカッとなって、自分用のCloud9を作った

まえおき

f:id:YusukeIwaki:20190713151336p:plain

Cloud9AWS Cloud9という似て非なるサービスに変わった。

自分はCloud9 PROというサービスをとても熱心に(課金して)使っていたので、「これからはAWS Cloud9を使ってね♪」って気軽に言われてもな、そうはいかないんだ。

  • AWSGitHubじゃログインできないし
  • Cloud9 PROだと Runボタンをポチッとおしたらhttpsで即座に外から見れた のが、AWS Cloud9はできないし
  • そもそも開発環境なのに従量課金なんてありえないだろ!!!!!!

ということで、ものすごくフラストレーションが溜まっていた。

Cloud9 PROが良かった理由

他のクラウドIDEは使えなさすぎた

Cloud9終了のお知らせを受けて、 Alternatives to Cloud9 とかでGoogle検索して色々探したよ。有名ドコロだと

などなど。

ただ、実際に使ってみて使い物にならなかった。Paiza Cloudは謎のGUIがとても邪魔くさかったし、CodeAnywhereはエディタの品質が悪すぎ&コンテナごとに画面が分けられていないのがしっくりこなかった。

あと、両者とも致命的にダメだったのが、コンソールからエディタを起動できないこと。

Cloud9は c9 ってコマンドを使うとコンソールからエディタが立ち上がるんだけど、

f:id:YusukeIwaki:20190713013551g:plain

それに相当する機能がPaiza CloudにもCodeAnywhereにもなかった。

これエンジニアだったら必須機能だと思うんだけど、PaizaやCodeAnywhereはユーザの使い込み試験やってないの?

料金体系がエンジニアフレンドリーだったCloud9 PRO

成果物をhttpsでササッと公開する、というだけならCloud9じゃなくても他のクラウドIDEでもできるんだけど、無料でそこまで太っ腹なことをやっているところは無かった。完全に蛇足だけど、いろいろ話題を読んでいるプログラミングスクール とかでも、昔は↑を最大限に活用したカリキュラムを組んでいた(と、人づてに聞いた)。今はどうしてるんだろう。

個人的には課金はしていたので、無料であることは求めていなかったんだけど、それでも

  • 「あ、これやってみたい」と思ったものは基本的に新規に作業用コンテナ作って始めたい
    • 作成可能なコンテナ数に上限があるとか論外
  • せめて1つくらいは常時(外部から)接続可能なコンテナはあってほしい
  • 料金は10ドル/月 くらいであってほしい

という、エンジニアの平均的な要望を満たしてくれるIDEはCloud9 PRO以外には無かったのだ。

無いなら作るしかない

Cloud9 はエディタとしては100% ではないんだけど、ちょうどいい具合に整っていた。Cloud9 PROが終了してもAWS Cloud9は使いたくないし、他のクラウドIDEも使いものにならないので、もう自分で作る以外の選択肢が現状では無い。

Cloud9は親切にも、SDKを公開してくれている。

github.com

あとは、クラウドワ ークス って会社で働いていたときに

qiita.com

こんな素晴らしいことをやってるメンバーがいて、これにCloud9 SDKを組み込むと理想的な使い捨て開発環境ができることはお試し済みだった。

なので、サーバーの月額費用だけなんとかなれば、自分用のCloud9 PROはできるという確信はあった。

ちょうどタイミングよく、今の会社でMicrosoft Azureの優待利用(1ヶ月あたり○○円までは無料でずっと使える、的なやつ)があったのでそれも相まって、作ることにした。

自分用のCloud9 PROっぽい環境の作る前に・・・

Qiita記事にするか迷ったけど、面倒なのでここに書く。

前述の通り、ほぼほぼ Docker swarmで作る社内heroku: yadockeri - Qiita のパクリである。

用意するもの

制限事項

自分専用なので

  • 認証は特にやらない
  • SSL化はいったんあきらめる

ということにする。

SSHのポートだけ開放して、SSHポートフォワーディングで使う想定。

自分用のCloud9 PROっぽい環境を作る

さて作るぞ。

とりあえずEC2のインスタンスが3台あると仮定してすすめる。

同じVPC(Azureだったらリソースグループ)上に配置して、プライベートIPで相互に通信できるようにしておく。

dockerを入れる

個々のサーバーにSSHで入り、

curl https://raw.githubusercontent.com/YusukeIwaki/cloud-pine/190e8454b1dbae0521f035f7f37ceb62f8f126f4/install-docker-ce.sh | bash

する。

Docker Swarmクラスタを組む

c9-masterで docker swarm init する。

swarm initすると、「ワーカーを追加するには docker swarm join ….. —token=xxxxxx を打ってね」みたいなのが出てくるので、そのコマンドをコピって、c9-slave1, c9-slave2 で実行する。

ちなみに最近のDockerはSSH経由でリモートのDockerホストに接続できる(参考: Docker CE 18.09からssh経由でリモートのdockerデーモンに接続できるようになるってよ - Qiita )ので、c9-masterから

DOCKER_HOST=ssh://c9-slave1 docker swarm join …….. —token=xxxxxx

のように実行すれば、いちいちc9-slave1やc9-slave2にSSHしにいかなくても済む。

c9-masterで docker node ls を打って

$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
eeshee1yai5Moh5saGu0Ueje *   c9-master   Ready               Active              Leader              18.09.7
ahm3iexaeth5co2eir0Hahno     c9-slave1   Ready               Active                                  18.09.7
eangie6ohDigh1Zu4epung2I     c9-slave2   Ready               Active                                  18.09.7

みたいな出力が得られたらOK。

ワークスペース用のコンテナをつくる

Cloud9 PROはUbubtu 14.04ベースで、 RubyPythonもNodeもgitもheroku-cliaws-cliも(バージョンは古いけど)入っている状態だった。

とりあえず今はそこまでは求めないので、Cloud9 SDKだけ入れたDebianにする。(本気で使い始めたら、RubyPythonくらいは入れようと思う) cloud-pine/Dockerfile at df0962b100241db3eb3fd40219d6cc22e4195ea8 · YusukeIwaki/cloud-pine · GitHub

docker buildして、DockerHubにプッシュする。 https://hub.docker.com/r/yusukeiwaki/cloud-pine-workspace

docker-composeを↓みたいに作り、 docker-compose pull で正常にワークスペースが取得できる状態になればOK。 cloud-pine/docker-compose.yml at 190e8454b1dbae0521f035f7f37ceb62f8f126f4 · YusukeIwaki/cloud-pine · GitHub

ワークスペースを試しにデプロイしてみる

c9-masterにSSHで入り

$ wget https://raw.githubusercontent.com/YusukeIwaki/cloud-pine/190e8454b1dbae0521f035f7f37ceb62f8f126f4/docker-compose.yml
$ docker stack deploy --compose-file docker-compose.yml playground1

をする。docker-compose.yml は1つ前の手順で作成したものを指定する。

Creating network playground1_default
Creating service playground1_cloud9

こんな感じで、オーバーレイネットワークとサービスが作られたらOK。

$ docker service ps playground1_cloud9
ID                  NAME                   IMAGE                                     NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
prviwbnxxu3l        playground1_cloud9.1   yusukeiwaki/cloud-pine-workspace:latest   c9-slave1   Running             Running 43 seconds ago       

みたいな感じで、無事にc9-slave1 に配置された。(空いているDockerノードにいい感じに配置されるので、c9-masterやc9-slave2に配置されることもある)

ただ、これ単体では外からアクセスはできない。

外からアクセスできるようにする

グローバルIPを持っているc9-masterにnginx(リバースプロキシ)サーバーを立てるわけだが、ネイティブにリバースプロキシサーバを立てると、

$ ping playground1_cloud9
ping: playground1_cloud9: 名前またはサービスが不明です

のように、playground1_cloud9コンテナへのルーティングがうまくできない。docker inspectを使ってIPアドレスを特定して、・・・とかをやるのは茨の道だ。

ということで、nginxはネイティブで立てるのではなくdockerサービスとして立てる。そんで、playground1_cloud9とかと同じオーバーレイネットワークに入れれば、

$ docker exec -it 4936af085963 ping playground1_cloud9
PING playground1_cloud9 (10.0.6.88): 56 data bytes
64 bytes from 10.0.6.88: seq=0 ttl=64 time=0.058 ms
64 bytes from 10.0.6.88: seq=1 ttl=64 time=0.125 ms
64 bytes from 10.0.6.88: seq=2 ttl=64 time=0.076 ms
64 bytes from 10.0.6.88: seq=3 ttl=64 time=0.067 ms

のようにnginxコンテナからplayground1_cloud9が見えるようになる。

この辺の仕組みは Docker swarmで作る社内heroku: yadockeri - Qiita あたりで言及されているやつ丸パクリです。

nginxコンテナをつくる

nginx.confを

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    server {
      listen             80;

      set_real_ip_from   '10.0.0.0/8';
      real_ip_header     X-Forwarded-For;
      proxy_set_header   X-Forwarded-Scheme $http_x_forwarded_proto;
      proxy_set_header   Host $host;
      resolver           127.0.0.11 valid=5s; # nginx:alpineコンテナの/etc/resolve.conf にべた書きされているnameserverの値をそのまま転記

      if ($host ~* ([a-z0-9][a-z0-9_-]*)\.ide\.mydomain\.com$) {
        set $subdomain $1;
        set $dst_port 8888;
      }

      if ($host ~* ([a-z0-9][a-z0-9_-]*)\.preview\.mydomain\.com$) {
        set $subdomain $1;
        set $dst_port 8080;
      }

      location / {
        proxy_pass http://${subdomain}_cloud9:${dst_port};
      }
    }
}

こんな感じ。 mydomain.net のところは自分のドメインにする。

あとは、c9-masterにSSHで入って

$ docker network create --driver overlay --internal c9-overlay-network
$ docker service create --constraint 'node.role==manager' --mount type=bind,source=$(pwd)/nginx.conf,destination=/etc/nginx/nginx.conf,readonly --name nginx --network c9-overlay-network -p 80:80 nginx:stable-alpine

のようにDockerサービスを立ち上げれば、リバースプロキシが立つ。

ドメインの向き先をc9-masterにする

AWS Route53側の設定。

*.preview.mydomain.com*.ide.mydomain.com のAレコードをc9-masterのグローバルIPにする。(ちなみにAWS EC2じゃなくてAzure VMだったら、AレコードでIP指定するのではなく、CNAMEレコードで hogehoge.japanease.cloudapp.azure.com を指定する)

nginxからCloud9を見えるようにする

先の手順までで、 http://playground1.preview.mydomain.com/ とか http://playground1.ide.mydomain.com/ とかを指定すると、nginxが502エラーを返す状態にまでなるはず。

f:id:YusukeIwaki:20190713145639p:plain

これはnginxまでは到達できているけど、その先(Cloud9コンテナ)にリクエストを転送できていない状態。

なんでエラーになるかと言うと、この時点では先に立ち上げたplayground1サービスのCloud9コンテナが c9-overlay-network ネットワークに参加していないので、nginxからplayground1_cloud9の名前解決ができないのだ。じゃあどうすればできるかというと、とてもシンプルで

$ docker service update --network-add c9-overlay-network playground1_cloud9
playground1_cloud9
overall progress: 1 out of 1 tasks 
1/1: running   [==================================================>] 
verify: Service converged 

のように、ネットワークに参加させたらよい。リロードしたら・・・

f:id:YusukeIwaki:20190713145951p:plain

キタ━━━━(゚∀゚)━━━━!!

f:id:YusukeIwaki:20190713150017p:plain

キタ━━━━━━━━━━(゚∀゚)━━━━━━━━━━!!

となるだろう。

Cloud9 PROもどきの運用方法

構築手順はいろいろあったが、一旦作ってしまえばあとは簡単で、

新規のワークスペース立ち上げたいなーって思ったら

c9-masterに入って

docker stack deploy --compose-file docker-compose.yml nanika_new_idea
docker service update --network-add c9-overlay-network nanika_new_idea_cloud9

nanika_new_idea のところは都度変える)

と2つコマンドを打てば、1分もたたないうちに

  • http://nanika_new_idea.ide.mydomain.com/ でCloud9 IDEにアクセスできる
  • Cloud9上で8080番ポートでHTTPサーバーを立ち上げれば http://nanika_new_idea.preview.mydomain.com/ で見れる

ようになる。

ワークスペースを捨てたいなーって思ったら

c9-masterに入って

$ docker service ps playground1_cloud9 | grep Running | awk '{print $4}'
c9-slave1

のように、Cloud9を実行しているノードを特定してから、

$ docker stack rm playground1
Removing service playground1_cloud9
Removing network playground1_default
$ DOCKER_HOST=ssh://c9-slave1 docker volume rm  playground1_workspace-data
playground1_workspace-data

って感じで、2つコマンドを打てば瞬時に消せる。

まとめ

ほぼほぼ Docker swarmで作る社内heroku: yadockeri - Qiita なので、何も新しいことはやっていない。

とりあえず自分用Cloud9ができたので、しばらくは楽しくエンジニアやっていけそう。

追記

この構成だとChromebookでサクサク使えないことがわかったので続き・・・

yusukeiwaki.hatenablog.com