isyumi_netブログ

isyumi_netがプログラミングのこととかを書くブログ

Alt SQLを作ることにした。

取りかかれるのはだいぶ先だと思うけど。



現行のSQLは重大な問題をいくつも抱えている。

だから、新しい文法のSQLが必要だ。

ちょうどDartが新しい文法でJSの世界を救ったように、SQLにも救世主が必要だ。



今の所SQLRDBを操作する唯一の言語だ。

RDBの背景には集合論を土台にした素晴らしいセマンティクスがある。

しかし、SQLの文法が悪いせいでRDBが扱いにくいものだと思われている。

よくあるイメージとして、これは間違った考えだが、KVSはRDBよりかんたんに扱えると思っている人がいる。

この状況を放置すれば人類は安直にKVSに流されてしまう。

 

また、RDBは速度に限界があると言われている。

いくつか話題になりやすい理由を上げる

  1. SQLのパースが遅い
  2. JOINが遅い
  3. ロックが遅い
  4. 挿入時の整合性確認が遅い

1はバイナリフォーマットを定義するとことで回避するべきだ。

2〜4はシステムの安全性とトレードオフだ。

RDBが正規化・トランザクション・整合性確認(参照整合性+チェック制約)という機能を持っているから安全なアプリケーションが作れる。

しかし、速度のためこの機能をあえて使わない選択がされてしまうことがある。

もっと速度を出しやすい文法にするべきだ。

 

速度を出しやすい文法とはどういうことだろうか。

RDBの低レベルのAPIを露出させてプログラマが最適化すれば速くなる。

しかし、それはやめたほうがいい。

逆に、プログラマはより抽象度の高いクエリを書くべきだ。

RDBMSによりたくさんの意図を伝えることで、RDBMS側が自動で最適化できるようにするべきだ。

 

実現方法として、トランスパイルすると標準のSQLに変換できる文法を新しく設計する。

 

解決されるべき課題

TABLEに名前空間がない

RDBではグローバル名前空間に全てのテーブル名がぶら下がっている。

しかし、別のテーブルとJOINせずにSELECTすることはほぼないと言い切れるテーブルもある。

実際のシステムでは半数近くがこのような二級市民テーブルだろう。

僕はMySQL WorkbenchやIntelliJSQL編集機能といったSQL専用エディタで開発をする。

専用エディタは入力中にコード補完をしてくれる。

FROM句やJOINキーワードを書こうとすると候補を上げてくれるのだ。

ところが、大きなシステムでは候補が大量に出てきてあまり意味がないことがある。

「このTABLEとJOINする可能性があるのはこのTABLEに決まっているのではないか」「トップレベルのFROM句にこのTABLEが出てくることはないのではないか」と思うことがある。

そこで、大量のテーブルを分類して名前空間を分けることができるべきだ。

そしてprivate句とpublic句を定義できるようにしたい。

 

フィールドに名前空間がない

UserテーブルやProductテーブルに、ものすごくたくさん列が存在するシステムに覚えはないだろうか。

 

フィールドを分類して垂直分割をすることも考える。

しかし、それはパフォーマンスの問題につながる。

そもそも垂直分割はアンチパターンということになっていると思う。

もっと簡単に列をネストできるべきだ。

 

例えばGo言語で

 

type User struct {

fieldA int

fieldB int

fieldC int

fieldD int

...

}

 

 

と長くなってきてしまったら

 

type User struct{

 subTypeA SubTypeA

 subTypeB SubTypeB

}



type SubTypeA struct {

 fieldA int

 fieldB int

}

 

type SubTypeB struct {

 fieldC int

 fieldD int

}

 

 

 

のように分割する。 同じようなことができてほしい。

これをRDBで扱う時はのような意味に変換するか?

もちろん、「構造体を保存できるようになりました」という話ではない。

実際は並列にカラムが並んでいるが、その中で名前空間を表現できるシンタックスシュガー、という意味に解釈する。

各フィールドは木構造を直列に変換して表現する。

オブジェクトの階層の区切り文字「.」を別の文字に置き換える。

 

// 保存したい値

USER.BODY.HEIGHT = 170

USER.BODY.WEIGHT = 60

USER.MENTAL.COURAGE = 5

USER.MENTAL.CALMNESS = 10

 

// 新SQL

CREATE TABLE USER(

 BODY (

   HEIGHT INT,

   WEIGHT INT,

 ),

 MENTAL (

   COURAGE INT,

   CALMNESS INT,

 ),

);



// トランスパイルされたSQL

CREATE TABLE USER (

 BODY__HEIGHT INT,

 BODY__WEIGHT INT,

 MENTAL__COURAGE INT,

 MENTAL__CALMNESS INT,

);



// 取り出し

SELECT BODY.* FROM USERS;

 

 



親子関係を直接表現する方法がない

一般にTABLEの親子関係を作るにはCREATE TABLE文内でFOREIGN KEYを使い、クエリ時にJOINする。

これをもっと簡易に書きたい。

それができないせいで配列を保存できるようなRDBが出てきてしまうと思う。

 

親TABLEが一つの場合

一旦複数の親を持つTABLEのことは置いておこう。複雑だ。

自分が考えたのはこのような文法だ。

 

CREATE TABLE USERS (

 ID INT,

 NAME VARCHAR

);

 

CREATE TABLE USERS.FAVORITE_FOODS (USERS.ID AS USER_ID) (

 USER_ID,

 FOOD_NAME VARCHAR

);

 

 

まずCREATE TABLE USERS.FAVORITE_FOODSで、FAVORITE_FOODSはUSERSにぶら下がっているということを明示している。

さらに(USERS.ID AS USER_ID)でUSERS.IDはFAVORITE_FOODSのUSER_IDと同じだと宣言している。

これによりFOREIGN KEY文よりわかりやすく親子関係を表現している。

 

クエリはこのようになる。

カレーが好きな人一覧なら

SELECT * FROM USERS WHERE USERS.FAVORITE_FOODS IN "カレー";

のような感じになるだろう。

このあたりは、相関クエリのためにサブクエリを省略できる文法として設計するか、

集計関数のためにFROM・WHERE句を省略できる文法として設計するかちょっと悩んでいる。

 

親TABLEが2つある場合

親TABLEが2つある場合、上記の文法では対応できない。

この場合、そもそも2つのテーブルをJOINした結果のTABLEを定義できるようにし、

そのTABLEに上の文法で子供を作れるようにする。

ユーザー一覧のUSERSと食べ物一覧のFOODSがあり、各ユーザーの好きな食べ物をFAVORITE_FOODSに保存する。

 

CREATE JOINED TABLE USERS, FOODS AS USERS_FOODS;

 

CREATE TABLE USERS_FOODS.FAVORITE_FOODS (USERS_FOODS.USER.ID as USER_ID , USERS_FOODS.FOODS.ID as FOOD_ID) (

 USER_ID,

 FOOD_ID,

);

 

 

一見ただのCREATE VIEW文と変わらないが、あえてCREATE JOINED文を定義することに意味がある。

CREATE VIEW文はあらゆることに使えるのでなんのために作ったViewかわからない。

それに対してわざわざCREATE JOINED TABLE文を使っていたら、

この組み合わせに対して子TABLEをぶら下げるためにこの中間テーブルを作ったというメッセージが伝わりやすい。

 

ダイヤモンドになると最悪

親子関係がダイヤモンド型になる時に、外部キーを正確に書くのは非常に難しい。

 

ライトテーブルに夕食のメニューテーブルと乗客テーブルがぶら下がり、

その両方にぶら下がる乗客の食事メニューテーブルのことを考えよう。

 

CREATE TABLE PASSENGERS_FOODS (

 PASSENGER_ID int,

 FOOD_ID int,

 FOREIGN KEY PASSENGER_ID REFERENCES PASSENGERS (PASSENGER_ID),

 FOREIGN KEY FOODS_ID REFERENCES FOODS (FOODS_ID)

);

 

 

これではだめだ。

このようにうっかりするとフライトAに乗る乗客にフライトBのメニューを設定できるテーブルが出来上がる。

 

正確にはこうだ。

CREATE TABLE PASSENGERS_FOODS (

 FLIGHT_ID int,

 PASSENGER_ID int,

 FOOD_ID int,

 FOREIGN KEY (FLIGHT_ID, PASSENGER_ID) REFERENCES PASSENGERS (FLIGHT_ID, PASSENGER_ID),

 FOREIGN KEY (FLIGHT_ID, FOODS_ID) REFERENCES FOODS (FLIGHT_ID, FOODS_ID),

);

 

この場合CREATE JOIND TABLE文をカスタマイズすればいい。

 

CREATE JOINED TABLE PASSENGERS, FOODS AS PASSENGERS_FOODS

 REPRESENTATIVE KEY (PASSENGERS.FLIGHT_ID, FOODS.FLIGHT_ID) as FLIGHT_ID;

 

 

このようにPASSENGERSとFOODSのデカルト積からFLIGHT_IDが一致しない行を取り除いている。

おかげで、このJOINED TABLEに子テーブルをぶら下げれば、必ずFLIGHT_IDの掛け違いが起こらないようになった。

 

CREATE TABLE PASSENGERS_FOODS.PASSENGERS_FOODS

 (PASSENGERS_FOODS.PASSENGERS.PASSENGER_ID as PASSENGER_ID,

  PASSENGERS_FOODS.FOODS.FOODS_ID as FOOD_ID) (

 PASSENGER_ID,

 FOOD_ID,

);

 

 

似たようなテーブルを複数作るのがめんどくさい

よく、RDBには継承がないと言われる。

しかし、そのような大掛かりな機能を導入するのは難しい。

欲しいのは他のテーブルのCREATE TABLE文をコピペする構文だ。

JSのスプレッド演算子のようなものがあればいいと思う。

 

CREATE ABSTRACT TABLE PERSON (

 NAME VARCHAR,

 AGE INT,

);

 

CREATE TABLE CUSTOMER (

 ID int,

 ...PERSON,

 PHONE VARCHAR,

);

 

CREATE TABLE EMPLOYEE (

ID int,

...PERSON,

JOB VARCHAR

);

 

 

こんな感じだ。

 

3値論理が複雑

やはりOptional型は必要だと思う。

よく「NULLとFALSEを比較したらどうなるか」などのまとめ表がSQLの教科書に載っている。

ありがたいと思うが、そもそもNULLABLEとそれ以外の値を演算できるのがおかしい。

NULLABLEをunwrapせずに演算しているところをすべてコンパイルエラーにするべきだ。

JavaPHP等のRDBMSドライバもNULLABLEのintはintではなくOptional<Integer>で返すべきだ。

新しく追加されたcase文でこれまでに比べれば便利になったが、しかし依然として文字の量が多いので、

もっと便利な構文がほしい。

RDBにもOptional型を追加し、get(), orelse(), is_empty(), is_present()等の関数を追加するべきだ。

 

ユーザー定義型

また、ユーザー定義型も欲しい。

しかし、RDBにおいてスカラ値ではなくベクトル値を保存できてしまうのは良くない。

必要なのは、TABLEでは複数の列を使って値を保存するが、SQL上は一つの構造体として扱えるものだろう。

似たようなものを挙げるとC++だ。

C++のクラスはオブジェクトをメモリ上にどのように表現するかプログラマが責任を持って決めないといけない。

同じようにRDBも構造体を保存できる機能を追加するのではなく、SQLでは構造体として扱われる物をTABLE上でどのように表現するかプログラマが責任を持って決めるべきだ。

例えば何型の列が何個いるか、どんなチェック制約が必要かについて決める必要がある。

 

ついでに代数的データ型もほしい。

 

WITH句にクエリではなく関数を書きたい

自分はJavaScriptなどで高階関数を書くとき、その式が長くなりすぎるならローカル関数を定義し高階関数の中を簡略化することがある。

例えば

function filterUserByGender(users, gender) {

 

 function genderEqual(user) {

    // これならここの式がどれだけ長くなってもストレスがない。

    return user.gender == gender;

 }

 

 return users.filter( u => genderEqual(u));

}

 

SQLも同じように書きたい。

 

複雑な相関クエリをCREATE TEMPORARY TABLE文で簡単にするのはよくあるパターンだ。

クエリによっては大きな効果が得られる。

TwitterでWITH句を使うように教えていただきました)

しかし、相関の仕方が複雑なとき意味をなさないことがある。

これはある会社の各課で誰が一番若いかを集計するクエリだ。

 

SELECT

ID , NAME

FROM

EMPLOYEES

WHERE AGE = (SELECT MIN(AGE) FROM EMPLOYEES as E2 WHERE DIVISION = E2.DIVISION);

 

 

これを一時テーブルで簡略化してみよう。

 

CREATE TEMPORARY TABLE DIVISION_MIN_AGE AS

(SELECT DIVISION, MIN(AGE) AS AGE FROM EMPLOYEES GROUP BY DIVISION);

 

SELECT

ID , NAME

FROM

EMPLOYEES

WHERE AGE = (SELECT AGE FROM DIVISION_MIN_AGE WHERE DIVISION_MIN_AGE.DIVISION = EMPLOYEES.DIVISION);

 

 

このように全く意味がない。

なぜ意味がないのか。

このクエリで一番冗長な箇所はサブクエリのWHERE句だ。

「DIVISION_MIN_AGE.DIVISION = EMPLOYEES.DIVISION」

この部分を外に出す方法がないから意味がないのだ。

 

そこで、WITH句に関数を宣言できるようにしたい。

 

WITH FUNCTION IS_MIN_AGE(VARCHAR(10) DIVISION, INT AGE) AS (

 (SELECT MIN(AGE) FROM EMPLOYEES WHERE EMPLOYEE.DIVISION = DIVISION) == AGE

)

SELECT

ID , NAME

FROM

EMPLOYEES

WHERE AGE = IS_MIN_AGE(ID, AGE);

 

大変綺麗になったと思う。

 

木構造でResultが取りたい

これはSQLというよりドライバの問題かもしれない。

あるクエリの結果の行をもとに別のクエリを投げる、ということがある。

アプリケーション側が一回目のクエリの結果を使わないなら、WHERE INを使えば解決する。

しかし、アプリケーション側が一回目のクエリの結果を使うならば、少なくとも二回に分けてクエリを投げなければいけない。

下の表を見てほしい。

 

 

f:id:isyumi-net:20180810190449p:plain

 



この表を出力するにはアプリケーション側で下のような2重For文を書かなければいけない。

先に都道府県一覧を取得し、その結果をもとに市町村一覧を取得する。

疑似コードだがこんな感じであろう。

 

foreach($pref_row as $connection->fetchAll("SELECT * FROM PREFECTURES")) {

 foreach($city_row as $connection->fetchAll("SELECT * FROM CITIES WHERE PREFECTURE_ID = ${pref_row->id}"){

   print $pref_row->name, $city_row->name, $city_row->population;

 }

}

 

 

少し悔しい。

SELECT文が木構造に対応してくれたらいいのだ。

 

SELECT

PREFECTURE_ID ,

PREFECTURE_NAME,

VISITOR (SELECT * FROM CITIES WHERE PREFECTURES.PREFECTURE_ID = CITIES.PREFECTURE_ID) AS CITIES

FROM PREFECUTRES;



foreach($pref_row as $connection->fetchAll($sql)) {

 foreach($city_row as $pref_row->cities){

   print $pref_row->name, $city_row->name, $city_row->population;

 }

}

 

 

綺麗に書けた。

冒頭でAlt SQLSQLに変換して使うと書いたが、もしRDBMSがこの構文に対応してくれるならパフォーマンスが向上するであろう。

上のコードの場合RDBMSは1回目のクエリ結果をもとに二回目のクエリがやってくるということを知らない。

しかし下のコードであれば、HDDからRAMに読み込んだデータやカーソルを意図的に使いまわすことが可能になる。




コード書く工程は末端作業か?

お断り
  1. 今起きている何らかの喧嘩は関係ありません。未来に向けて書き残しておこうと思ったから書いています。
  2. 「そうやって現場を大事にしないから日本はだめになったんだ」的な話も、今は関係ありません。それはそれ、これはこれ。あくまでソフトウェア産業の話をします。

 

まず、僕の現状認識を説明する。これは自分が体験したことではない。また、統計など信用のおける証拠があるわけでもない。あくまで、伝聞で得たイメージである。

ソフトウェア産業では10年以上前からプログラミングが儲からない仕事になっていくといわれていた。商品開発とアフターサービスの付加価値が高まり、逆にプログラミングの付加価値が下がるであろう。コーディングはどんどん発展途上国に外注されていく。だから日本人は生き残るために別の道を選ぶべきだ。いまプログラマーとして働いている人は設計や管理職などのキャリアへ進むべきだ。というのが定説だった。

しかし、その変化は起こらなかった。プログラミングの価値はますます増大し、プログラマーは稼げる仕事になっていった。多くの先進的な企業が差別化のために能力の高いプログラマーを高報酬で雇い始めた。

ところが、日本企業の多くがその変化に気づいていない。個人の感覚としてもプログラミングを軽視する感覚が根強い。また、企業の採用・人事制度でもプログラマーは立場が低く扱われている。

というのが僕の認識だ。確かめたわけではないがそこそこ正しいだろう。

 

なぜ、このようなことが起こったのか。この先は完全にただの憶測だ。

まず、プログラミングが儲からなくなっていくと思われていたのはなぜか。それは製造業の影響だろう。製造業にはスマイルカーブという概念がある。企画→設計→製造→流通→マーケティング→アフターサービスという各工程の付加価値はお盆のような形をしており、両端の企画とアフターサービスが一番高く、次いで設計とマーケティングが高く、製造と流通は最も低い。この考え方をソフトウェアに当てはめるとプログラミングは製造工程に当たる気がしてしまうのだろう。ここから得られる教訓は製造業とソフトウェア産業は全然別物であるということだ。少しフォローするなら、これも伝聞だが、プログラム言語がアセンブラCOBOLなどしかなかった時代のソフトウェア産業人海戦術の域を出ておらず、製造業と大差なかったのだろう。そもそもソフトウェア産業などという概念自体がなく、それは製造業の一種と考えられていたのなら、そういう勘違いが発生するのも無理はない。

また、なぜプログラミングの価値が落ちなかったのか。これは大変興味深い謎である。僕がTLで見たのはこんな意見だ。

  1. 優秀な人とそうでない人の差が大きすぎるから
  2. 設計とコーディングが別工程だと思われていたが、実は不可分だったから
  3. ソフトウェア開発が高度化しているから
  4. プログラミング可能なものが増えたから
  5. プログラマー文化の本流はヒッピー文化でもあり、反権威的なプログラマーたちがそういう流れを作ったから
  6. シリコンバレーを中心とする経済圏が過熱しすぎていて相場を押し上げているから
  7. プログラミングは人間の限界を拡張する手段であり(エンハンスメントとかオーギュメントとか難しい言葉を使ってた)プログラミングを覚えた人間は一段上のステージに行くから

どれもあり得そうな話だ。僕は4を推しておく。

最後に、なぜ日本人はその変化に気づけなかったのか。この疑問を考えるには、そもそもアメリカでも気づかれていない可能性を考えなければいけない。今をときめく映画評論家町山智浩さんによればアメリカ人の半分はニューヨークの場所を知らないらしいので、Googleプログラマーが高給であることを7割くらいの人が知らない可能性は高い。であれば、日本だけが遅れているという前提すら危うい。まあしかし、いったんそこは置いておいて考える。まず企業の中では、多くの日本企業の昇進制度上、会社を動かしているのは10年以上前に活躍していた人が多いはずだ。つまり、そういう考え方でうまく回っていた時代の人だ。そうであればプログラミングの重要さを前提とした経営判断がし辛いのは想像できる。個人の問題としては、各国の経済成長の動向が大きいと思う。日本はこの数十年の間に途上国に次々と追い越された。日本が追いかける側の国であればアメリカでプログラミングがもうかっているらしいという情報が入った瞬間にみんながまねしたであろう。しかし、一度製造業で世界を制した成功体験があるだけになかなかそこから抜け出せない考えの人も多いと思う。

 

では、今僕たちはどうするべきか。お断りにも書いたが「現場を大事にするべきだ」みたいなわかりやすいスローガンに回収しないほうがいいと思う。もちろん、それはそうなのだが、残念ながら付加価値の多寡というのは必ずある。職業に貴賎はないが給料は違う。個人戦略としてどこが儲かるかを判断する必要はあるし、企業としてもどこで差別化するかを見極めて、誰がやっても同じな業務は安く抑え、勝因になりえる業務に強い人を雇うのは当然のことだと思う。少なくとも僕らはプログラマーとして能力を伸ばすべきだ。管理職やマーケ職の方が付加価値が高くなる可能性よりもプログラマーの付加価値が伸び続けるほうに張りつつ、しかし、それが逆転する兆候があればすぐに身を翻す機敏さも持ち合わせなければいけない。

ポリテックと型システム

金融とテクノロジーフィンテックで行政とテクノロジーがゴブテックなら、政治とテクノロジーの融合はポリテックだそうだ。

政治をよくできそうな技術として、どうも政治家さんにはAIがモテモテのようだが、型システムの方が味がいい気がする。

例えばこの意見を見てどう思うか?

最近少年犯罪が増えているので、若者は全員自衛隊に入って根性を鍛えるべきだ。

そういう考え方の是非はともかく、前提知識として少年犯罪自体は減っている。だから、最初からこういう意見は取り合わなくていい。

また、これはどうだろうか?

 自民党に投票しなかったということは民主党を支持するってことだな?

 これは必要条件と十分条件の区別がついていない人だ。

黙秘するということは犯行を認めるということですね?

これは三値論理と言う。三値論理的にこの主張は間違っている。

 

型システムを使えばこういう無駄な議論に時間を使わなくてよくなる。「わかりきった事実誤認に基づいた意見」「真偽や善悪以前に理屈が通っていない理屈」をあらかじめ排除できる。

どこか一箇所に型定義リポジトリを作りそこに自明の知識をプールしておけばいい。

「少年犯罪は減っている」「東京は出生率が低い」「女子高生が産婦人科に行く理由」「日大と日本体育大は別」など、間違えそうなことは事前に登録しておいてあげれば無知を晒して恥ずかしい思いをする人も減るだろう。

思い切って

  • 政治家は国会で型チェックを通らない話をしてはならない
  • 国民は政治家に型チェックを通らない要望をあげてはならない

というルールにしておけばいい。

 

逆に誰でも簡単に意見が言えてよくなるという効果もあると思う。

例えば、映画は犯罪率を低下させるのではないかと思っている人がいたとしよう。しかし、実際にそうなのか調査結果がどこにもなかったとする。こう意見を言ったとする。

映画には犯罪率を低下させる効果がある。経済発展のために映画を見るべきだ。

映画を見るべきだというのはその通りだと思うが、話の前後に繋がりがない。これでは相手にしてもらえない。

そこで

映画には犯罪率を低下させる効果がある。犯罪率の低下のために映画を見るべきだ。

とか

映画には犯罪率を低下させる効果がある。犯罪率の低下は経済発展に効果がある。経済発展のために映画を見るべきだ。

などであれば事実関係はともかく理屈は通っている。「じゃあ映画に本当に犯罪率低下の効果があるか、犯罪率低下に経済発展の効果があるか調べるから予算くれ」と話を繋げられる。このように事前に理屈を整頓する機会を与えられることによって、意見を言いやすくする効果があるのではないか。

 

 

 

ブラウザに自動ドネーションAPI機能がほしい

以前からコンテンツ提供者にもっとお金が支払われるべきだと思っていた。

こういう仕組みがほしい。

  • 僕がChromeに「毎月5,000円をコンテンツ提供者に支払う」と設定する
  • Chromeは俺の行動履歴からどのサイトにどのくらい滞在しているかを集計する
  • 月締めで滞在時間に応じて自動的に5,000円を各サイトに分配する

この方法が必要な理由はこうだ。

  1. たとえサイト運営者がPaypalなどでドネーションの申し込み口を設けていたとしても、いちいちそれをクリックするのがめんどくさい
  2. ある程度お金を払っていることを忘れられるUXでないともったいないと感じてしまう(i-Modeの論理)

 

人生の5箇年計画

やりたいことがいっぱいありすぎるので一覧を作る。

5箇年計画と書いたが、実際には何年かかるか全く見当がつかない。

 

  1. 数学検定準一級
  2. TOEIC800点
  3. レッドコーダーになる
  4. Webフロントエンドのベストプラクティスホスティングサービスを作る
  5. Dartのサーバーサイドフレームワークを作る
  6. ビジネスロジックからDBのクエリを自動生成するツールを作る
  7. ALT SQLを作る
  8. 数学検定一級
  9. Google Cloud Certified を取る
  10. 応用情報技術者をとる
  11. データベーススペシャリストをとる
  12. ネットワークスペシャリストをとる
  13. セキュリティスペシャリストをとる
  14. LPIC Level3になる
  15. この辺で年収1000万になる
  16. CPUを作る
  17. OSを作る
  18. 言語を作る
  19. RDBの理論面を勉強する
  20. 型システムの理論面を勉強する
  21. 素晴らしい数式処理システムを作る
  22. 暗号論を勉強する
  23. 新しいWebの仕様を策定しバーナーズリーの後継者になる
  24. 絵を描けるようになる
  25. 何か楽器ができるようになる

30才までに終わるかな。

外部からSSH接続を受け付けるプロダクトのSSH設定

今、デベロッパー向けにこういうサービスあったらいいなーと思っているものがある。

その中でSSH接続を受け付けたいなと思ったから実現方法を考えた。

WordPressホスティングしてvimで記事を編集できるようなものをイメージして欲しい。

 

ルール

・1人1Docker コンテナ

・コンテナを実行するEC2インスタンスを運営側がいつでも変更できるようにする

・サーバーの状態の保持は別問題

・HTTPも別問題

・ユーザー名は全ユーザーで1意、ホストは共通

 

サインアップしたユーザーにDockerコンテナを用意し、そこにSSH接続を受け付けるようなものを作りたい。

 

せっかくなので、1Dockerに複数のユーザーを用意できるようにしたかった。それができると「管理者」「編集者」「記者」等のロールを作れる。しかし、難易度が上がりそうなので今回は諦め。

 

コンテナを実行するEC2インスタンスを運営側がいつでも変更できるようにしたい。

特定のホスト名、ポート番号で待ち受け続けないといけないと運用がきつくなる。

例えばもっと安いEC2インスタンスが発表されたらこっそりコンテナを移動したい。

また、使われてないコンテナを勝手に落としておき、次にアクセスが来たときに空いているインスタンスで立ち上げる仕組みも入れられるようにしたい。

そのため、生EC2アドレスをユーザーに渡すのではなく、SSHプロキシのアドレスを渡してそこからEC2にブリッジする。

 

インスタンスを立ち上げなおしたときにユーザーが変更したファイルの保持をどうするかは、ネットワーク設計とは別問題なので今日は気にしない。それはアプリケーション設計の話だ。

大方 /var/hoge/fuga 以外を変更しても保存は保障しませんよといっておけばいいだろう。

 

HTTPでも受け入れられるようにしたいが、そっちはそっちで話がでかすぎるので別で考える。

 

ユーザー名は全ユーザーで一意にする。

さっきも書いたが本当はホストごとに名前空間を区切って

- admin@user1.some-service.com

- author@user1.some-service.com

- editor@user1.some-service.com

- junior-staff@user1.some-service.com

という風にロールわけをできるようにしたい。

しかし、後述のライブラリがまだHostに対応していないので諦めた。

 

とりあえず

- user1@some-service.com

- user2@some-service.com

というユーザー名体系にする。

 

実現方法

これはまだ試していなくて、こうすれば大丈夫だろうと僕が今想像していることである。

 

用意するもの

・ELB

・セキュリティゾーン

・EC2を3台以上

 ・内2台はSSHプロキシ

 ・もう一台はDockerのせるインスタンス(アプリケーションインスタンスと呼ぶ)

  ユーザーが増えてきたらこのインスタンスを増やしていけば良い

・DockerImage

 

まず、ELBから2台のSSHプロキシサーバーに大してTCPロードバランスを設定

外向きのホスト名はssh.some-service.comで統一する。

 

SSHプロキシサーバー内で下記SSHプロキシツールを起動

https://github.com/tg123/sshpiper

 

アプリケーションインスタンスでDockerを起動

このとき、内向きのSSHポートは22とする。

外向きは可変にする。

docker -p 3333:22 等

 

これで準備は完了。

新しくユーザーがサインアップしたらどこかのアプリケーションインスタンスでDockerを起動する。

鍵ペアを生成し、Docker内に渡すと同時にSSHプロキシにそのインスタンスのホスト名、ポート名、鍵を設置。

ユーザーに秘密鍵を渡す。

 

これでできるんじゃないかな?