tag:crieit.net,2005:https://crieit.net/users/BrainVader/feed ブレインの投稿 - Crieit Crieitでユーザーブレインによる最近の投稿 2020-10-24T16:23:32+09:00 https://crieit.net/users/BrainVader/feed tag:crieit.net,2005:PublicArticle/16169 2020-10-24T16:23:32+09:00 2020-10-24T16:23:32+09:00 https://crieit.net/posts/Next-js-Dgraph-5-Dgraph Next.jsとDgraphで作るクイズ・アプリ (5) Dgraphの基本 <h1 id="Dgraph操作の基本"><a href="#Dgraph%E6%93%8D%E4%BD%9C%E3%81%AE%E5%9F%BA%E6%9C%AC">Dgraph操作の基本</a></h1> <h2 id="前回"><a href="#%E5%89%8D%E5%9B%9E">前回</a></h2> <p><a href="https://crieit.net/posts/Next-js-Dgraph-4">前回</a>を参照</p> <h2 id="用語"><a href="#%E7%94%A8%E8%AA%9E">用語</a></h2> <p>グラフデータベースなので, ノードで構成されます.</p> <div class="table-responsive"><table> <thead> <tr> <th>用語</th> <th>意味</th> </tr> </thead> <tbody> <tr> <td>ノード</td> <td>データを構成する基本要素. レコードのようなもの</td> </tr> <tr> <td>プレディケート</td> <td>ノードの属性やエッジ(ノード間の関係性)を表す</td> </tr> </tbody> </table></div> <h2 id="スキーマ"><a href="#%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E">スキーマ</a></h2> <p>とりあえず各ノードは以下のようなスキーマ持つとします.</p> <div class="table-responsive"><table> <thead> <tr> <th>Predicate</th> <th>Data Type</th> </tr> </thead> <tbody> <tr> <td>title</td> <td>string</td> </tr> <tr> <td>user</td> <td>string</td> </tr> <tr> <td>version</td> <td>string</td> </tr> <tr> <td>date</td> <td>datetime</td> </tr> <tr> <td>question</td> <td>[uid]</td> </tr> <tr> <td>answer</td> <td>[uid]</td> </tr> <tr> <td>tags</td> <td>[uid]</td> </tr> </tbody> </table></div> <p>[uid]というのは別のノードを参照しているという意味で1対多の関係になります.</p> <blockquote> <p>UID arrays represent a collection of UIDs. This is used to represent one to many relationships.</p> </blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://dgraph.io/docs/tutorial-3/#data-types-for-predicates">Data types for predicates</a></p> <h3 id="question and answer nodes"><a href="#question+and+answer+nodes">question and answer nodes</a></h3> <p>クイズの本体とも言える問とその答えです. 同じデータ構造で表現します.</p> <div class="table-responsive"><table> <thead> <tr> <th>Predicate</th> <th>Data Type</th> </tr> </thead> <tbody> <tr> <td>text</td> <td>string</td> </tr> <tr> <td>content</td> <td>string</td> </tr> </tbody> </table></div> <h3 id="tags"><a href="#tags">tags</a></h3> <p>クイズを分類するタグです. ノードで表現することで一意性をもたせることができます.</p> <div class="table-responsive"><table> <thead> <tr> <th>Predicate</th> <th>Data Type</th> </tr> </thead> <tbody> <tr> <td>tag_name</td> <td>string</td> </tr> </tbody> </table></div> <h2 id="ミューテーション"><a href="#%E3%83%9F%E3%83%A5%E3%83%BC%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3">ミューテーション</a></h2> <p>データに変更を加えるにはsetとdeleteという2つのキーワードを使います.</p> <h2 id="CRUD"><a href="#CRUD">CRUD</a></h2> <h3 id="create"><a href="#create">create</a></h3> <p>setはノードの作成とプレディケートの更新を行います.</p> <pre><code class="json">{ "set": [ { "title": "MonQとは何でしょうか?", "user": "brainvader", "version": "0.0.1", "date": "2020-10-08T18:01:00", "question": [ { "type": "text", "content": "MonQとは何でしょうか?" }, { "type": "text", "content": "またどのようなものを目指しているでしょうか?" } ], "answer": [ { "type": "text", "content": "クイズベースの学習システムです." }, { "type": "text", "content": "クイズ同士の寒冷性や依存性を定義することでクイズをモジュール化することを目指します." } ], "tags": [ { "uid": "_:monq", "tag_name": "monq" } ] } ] } </code></pre> <p>作成されたノードには自動でuidが与えられます.</p> <h3 id="read"><a href="#read">read</a></h3> <p>クエリを用いて取得します. <a target="_blank" rel="nofollow noopener" href="https://dgraph.io/docs/query-language/functions/#has">has関数</a>を使うと指定したプレディケートを持っているノードを取得できます. func引数にhas関数を渡してやります.</p> <pre><code>{ quizzes(func: has(title)) { uid title user date version question { type content } answer { type content } tags { tag_name } } } </code></pre> <p>ここでquizzesは関数名というよりはrootノードです. レスポンスは以下のようになります. quizzesという配列に取得したノードが収められています. 指定したルートノードから辿れるようにな構造になるわけです.</p> <pre><code class="json">{ "data": { "quizzes": [ ... ] } ... } </code></pre> <p>以下のようなグラフが取得できます. ピンクがquとあるのでquestionで緑がanswer, そして紫がtagsです.中心の青はtitleを含むノードです. つまりクイズそのものを指します.</p> <p><a href="https://crieit.now.sh/upload_images/c999c6bf95bbd99d341a53a9c5795a8a5f8e70a39720a.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c999c6bf95bbd99d341a53a9c5795a8a5f8e70a39720a.png?mw=700" alt="image" /></a></p> <h4 id="ブランク・ノード"><a href="#%E3%83%96%E3%83%A9%E3%83%B3%E3%82%AF%E3%83%BB%E3%83%8E%E3%83%BC%E3%83%89">ブランク・ノード</a></h4> <p>ノードを指定する時にuidは自動で作られます. 作成後のノードには全てuidが付番されているので参照することは簡単です. 問題はノードを定義する時に共通のuidを指定したい場合です.</p> <p>例えば共通のtagを持つクイズを同時に作りたい場合です. tagはノードとして管理されるのでuidで識別できます. 同じ文字列を指定しても同じノードを参照しているという関係は表せません. この場合_:identifierという表記を使うと同じuidであることを表せます.</p> <p>以下は英単語の意味を問うよくあるクイズです. これらはどちらもenglishというタグ・ノードを参照するのが妥当でしょう.</p> <pre><code class="json">{ "set": [ { "title": "put on holdとは?", "user": "brainvader", "version": "0.0.1", "date": "2020-10-08T18:01:01", "answer": [ { "type": "text", "content": "put on holdとは?" } ], "question": [ { "type": "text", "content": "保留する" } ], "tags": [ { "uid": "_:english", "tag_name": "english" } ] }, { "title": "hotshotとは?", "user": "brainvader", "version": "0.0.1", "date": "2020-10-08T18:01:01", "answer": [ { "type": "text", "content": "hotshotとは?" } ], "question": [ { "type": "text", "content": "有能な人、やり手, 凄腕" } ], "tags": [ { "uid": "_:english", "tag_name": "english" } ] } ] } </code></pre> <p>先程のクエリを実行すると以下のようなグラフが得られます.</p> <p><a href="https://crieit.now.sh/upload_images/a28995f869f3975eb5f4a6989755647e5f8e7341911b6.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/a28995f869f3975eb5f4a6989755647e5f8e7341911b6.png?mw=700" alt="image" /></a></p> <p>englishというtagsノードを通じてつながっていることが分かります.</p> <h3 id="delete"><a href="#delete">delete</a></h3> <h4 id="ノードの削除"><a href="#%E3%83%8E%E3%83%BC%E3%83%89%E3%81%AE%E5%89%8A%E9%99%A4">ノードの削除</a></h4> <p>よく分からん. とにかく消えないです.</p> <h4 id="プレディケーとの削除"><a href="#%E3%83%97%E3%83%AC%E3%83%87%E3%82%A3%E3%82%B1%E3%83%BC%E3%81%A8%E3%81%AE%E5%89%8A%E9%99%A4">プレディケーとの削除</a></h4> <p>更新処理の前に削除をしてみましょう. 各ノードにはプレディケートというフィールドに該当するものが存在する. ノードをuidで指定してプレディケートをnullに指定するとそのプレディケートは削除されたことになります. 全てのプレディケートがなくなるとそのノードは削除されたとみなされます(とされていますが, コンソールで試すと消えません).</p> <p>ノードの場合は単純にuidを指定すれば良いようです.</p> <p>さて先程の英単語のクイズを削除してみましょう. 以下のようなミューテーションを実行します.</p> <pre><code class="json">{ "delete": [ { "uid": "0x3a982", "title": null, "answer": null, "question": null, "tags": null } ] } </code></pre> <p>この場合tagsは他方のクイズからも消えてしまうでしょうか? このノードからは辿れなくなりますが, ノード自体は残ります.</p> <h4 id="プレディケートの全削除"><a href="#%E3%83%97%E3%83%AC%E3%83%87%E3%82%A3%E3%82%B1%E3%83%BC%E3%83%88%E3%81%AE%E5%85%A8%E5%89%8A%E9%99%A4">プレディケートの全削除</a></h4> <p>以下のような記法を使います.</p> <pre><code>{ delete { <0x3a985> * * . } } </code></pre> <p>プレディケートのフィールド名を使うこともできます.</p> <pre><code>{ delete { <0x3a985> <title> * . } } </code></pre> <p>こえでtitleプレディケートは消せます.</p> <h3 id="update"><a href="#update">update</a></h3> <h4 id="プロパティ・プレディケート"><a href="#%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%83%BB%E3%83%97%E3%83%AC%E3%83%87%E3%82%A3%E3%82%B1%E3%83%BC%E3%83%88">プロパティ・プレディケート</a></h4> <p>単純に値を上書きすれば良いようです. userの名前を変更してみましょう.</p> <pre><code class="json">{ "set": [ { "uid": "0x3a985", "user": "no-brainer" } ] } </code></pre> <p>ではuser名を取得してみましょう.</p> <pre><code>{ quizzes(func: uid(0x3a985)) { user } } </code></pre> <p>user名がbrainvaderからno-brainerに変わっているのが分かります.</p> <p><a href="https://crieit.now.sh/upload_images/ce43595fe29e31547cdeb53c66b173d75f8e815697351.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/ce43595fe29e31547cdeb53c66b173d75f8e815697351.png?mw=700" alt="image" /></a></p> <h4 id="エッジ・プレディケート"><a href="#%E3%82%A8%E3%83%83%E3%82%B8%E3%83%BB%E3%83%97%E3%83%AC%E3%83%87%E3%82%A3%E3%82%B1%E3%83%BC%E3%83%88">エッジ・プレディケート</a></h4> <p>おなじみのクイズを作ってみましょう.</p> <pre><code class="json">{ "set": [ { "title": "MonQとは何でしょうか?", "user": "brainvader", "version": "0.0.1", "date": "2020-10-08T18:01:00", "question": [ { "type": "text", "content": "MonQとは何でしょうか?" }, { "type": "text", "content": "またどのようなものを目指しているでしょうか?" } ], "answer": [ { "type": "text", "content": "クイズベースの学習システムです." }, { "type": "text", "content": "クイズ同士の寒冷性や依存性を定義することでクイズをモジュール化することを目指します." } ], "tags": [ { "tag_name": "monq" } ] } ] } </code></pre> <p>tagを更新して見ます.</p> <pre><code class="json">{ "set": [ { "uid": "0x3a992", "tags" : { "tag_name": "tutorial" } } ] } </code></pre> <p>新しくtutorialというtagが追加されたことが分かります.</p> <pre><code>{ quizzes(func: has(tag_name)) { tag_name } } </code></pre> <h2 id="Q&amp;A"><a href="#Q%26amp%3BA">Q&A</a></h2> <h3 id="プレディケートの前に付く~(ティルダ)の意味とは?"><a href="#%E3%83%97%E3%83%AC%E3%83%87%E3%82%A3%E3%82%B1%E3%83%BC%E3%83%88%E3%81%AE%E5%89%8D%E3%81%AB%E4%BB%98%E3%81%8F%7E%28%E3%83%86%E3%82%A3%E3%83%AB%E3%83%80%29%E3%81%AE%E6%84%8F%E5%91%B3%E3%81%A8%E3%81%AF%3F">プレディケートの前に付く~(ティルダ)の意味とは?</a></h3> <p>エッジの方向を反対に探索することを意味する.</p> <h3 id="anyofterms(predicate, &quot;query word&quot;)"><a href="#anyofterms%28predicate%2C+%26quot%3Bquery+word%26quot%3B%29">anyofterms(predicate, "query word")</a></h3> <p>プレディケーとがqueryかwordにマッチする.</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <ul> <li>uid(value)</li> <li>has(predicate)</li> <li>gt(predicate, value)</li> <li>@recurse(depth: value)</li> <li>@filter(lt(dislikes, 10))</li> <li>eq(tag_name,"devrel")</li> </ul> <h2 id="クエリ集"><a href="#%E3%82%AF%E3%82%A8%E3%83%AA%E9%9B%86">クエリ集</a></h2> <h3 id="タグの取得"><a href="#%E3%82%BF%E3%82%B0%E3%81%AE%E5%8F%96%E5%BE%97">タグの取得</a></h3> <pre><code>{ all_tags(func: has(tag_name)) { tag_name } } </code></pre> <h3 id="特定のタグの削除"><a href="#%E7%89%B9%E5%AE%9A%E3%81%AE%E3%82%BF%E3%82%B0%E3%81%AE%E5%89%8A%E9%99%A4">特定のタグの削除</a></h3> <p>プロパティ・プレディケートがなくなると削除されるので唯一のプレディケートであるtag_nameをnullに設定すれば良い.</p> <p>```json<br /> {<br /> "delete": [<br /> {<br /> "uid": "0x3a992",<br /> "tags" : [{<br /> "uid" : "0x3a994",<br /> "tag_name": null<br /> }<br /> ]<br /> }<br /> ]<br /> }</p> ブレイン tag:crieit.net,2005:PublicArticle/16125 2020-10-11T19:07:14+09:00 2020-10-20T10:56:23+09:00 https://crieit.net/posts/Next-js-Dgraph-4 Next.jsとDgraphで作るクイズ・アプリ (4) データ形式とデータ通信 <h1 id="JSONを用いたデータのやり取り"><a href="#JSON%E3%82%92%E7%94%A8%E3%81%84%E3%81%9F%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E3%82%84%E3%82%8A%E5%8F%96%E3%82%8A">JSONを用いたデータのやり取り</a></h1> <p>今回はクライアントとAPIエンドポイントの間でのデータのやり取りの仕方をまとめておきます.</p> <h2 id="前回"><a href="#%E5%89%8D%E5%9B%9E">前回</a></h2> <p><a href="https://crieit.net/posts/Next-js-Dgraph-3-API-next-connect">前回</a>を参照</p> <h2 id="APIエンドポイント一覧"><a href="#API%E3%82%A8%E3%83%B3%E3%83%89%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88%E4%B8%80%E8%A6%A7">APIエンドポイント一覧</a></h2> <p>以下のようなエンドポイントが必要になります.</p> <div class="table-responsive"><table> <thead> <tr> <th>API</th> <th>リソースの内容</th> </tr> </thead> <tbody> <tr> <td>GET base_url/api/quizzes</td> <td>クイズを取得する</td> </tr> <tr> <td>POST base_url/api/quizzes</td> <td>新規のクイズを作成する</td> </tr> <tr> <td>GET base_url/api/quizzes/[uid]</td> <td>指定したクイズの取得</td> </tr> <tr> <td>DELETE base_url/api/quizzes/[uid]</td> <td>指定したクイズの削除</td> </tr> <tr> <td>PUT base_url/api/quizzes/[uid]</td> <td>指定したクイズの更新</td> </tr> </tbody> </table></div> <h2 id="どんなデータをやり取りするか?"><a href="#%E3%81%A9%E3%82%93%E3%81%AA%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E3%82%84%E3%82%8A%E5%8F%96%E3%82%8A%E3%81%99%E3%82%8B%E3%81%8B%3F">どんなデータをやり取りするか?</a></h2> <h3 id="スキーマ"><a href="#%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E">スキーマ</a></h3> <p>Dgraphではスキーマと呼ばれます.</p> <div class="table-responsive"><table> <thead> <tr> <th>Predicate</th> <th>Type</th> </tr> </thead> <tbody> <tr> <td>title</td> <td>string</td> </tr> <tr> <td>user</td> <td>string</td> </tr> <tr> <td>date</td> <td>datetime</td> </tr> <tr> <td>question</td> <td>[uid]</td> </tr> <tr> <td>answer</td> <td>[uid]</td> </tr> <tr> <td>tags</td> <td>[string]</td> </tr> </tbody> </table></div> <p>questionとanswerはtypeとcontentというstring型のプレディケートを持ちます.</p> <p>---追記---</p> <h3 id="例"><a href="#%E4%BE%8B">例</a></h3> <p>JSONの方が分かりやすと思います. 以下のようなJSONを登録すると上のようなスキーマが設定される感じです. なおanswerとquestionはDgraphでは別のノードとして管理されます. そのため[uid]のような型になるようです.</p> <pre><code class="json">[ { "title": "MonQとは何でしょうか?", "user": "brainvader", "version": "0.0.1", "date": "2020-10-08T18:01:00", "answer": [ { "type": "text", "content": "MonQとは何でしょうか?" } ], "question": [ { "type": "text", "content": "クイズベースの学習システムです." } ], "tags": [ "monq" ] }, { "title": "put on holdとは?", "user": "brainvader", "version": "0.0.1", "date": "2020-10-08T18:01:01", "answer": [ { "type": "text", "content": "put on holdとは?" } ], "question": [ { "type": "text", "content": "保留する" } ], "tags": [ "english" ] } ] </code></pre> <p>こんな感じのデータがAPIを介してエディタとDBの間でやり取りされるわけです.</p> <h2 id="Next.jsにおけるデータ通信"><a href="#Next.js%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E3%83%87%E3%83%BC%E3%82%BF%E9%80%9A%E4%BF%A1">Next.jsにおけるデータ通信</a></h2> <p>サーバー側に送るデータはbodyに渡されます.</p> <pre><code class="javascript">const createQuizHandler = async () => { const body = { data: 'test' } const res = await fetch('/api/quizzes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) const data = await res.json() console.log(`create quiz with uid: ${data.uid}`) } </code></pre> <p>サーバー側ではreq.bodyとして参照できます. 値を返すときはjsonメソッドにJSオブジェクトを渡します. これがクライアント側ではres.jsonとして取得できます.</p> <pre><code class="javascript">const addQuiz = async (req, res) => { const data = req.body.data console.log(`I've gottern ${data}`) res.statusCode = 200 res.setHeader('Content-Type', 'application/json') res.json({ uid: '101' }) } </code></pre> <p>データのやり取りは非常に簡単ですね.</p> <h3 id="パス・パラメータ"><a href="#%E3%83%91%E3%82%B9%E3%83%BB%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF">パス・パラメータ</a></h3> <p>クイズを作成しuidだけもらってエディタ上で開く場合pages/quizzes/[uid].jsというようにパス・パラメータを渡す必要があります. uidはクイズの生成時に動的に決まるのでルートも動的に指定する必要があるからです.</p> <p>useRouterを使うと命令的にルーティングを実行することができ, この時にuidを指定することができるようになります. まずは読み込みます.</p> <pre><code class="javascript">import { useRouter } from 'next/router' const router = useRouter </code></pre> <p>routerのpushメソッドに以下のようにパス・パラメータを指定します.</p> <pre><code class="javascript">router.push({ pathname: '/quizzes/[uid]', query: { uid: uid }, }) </code></pre> <p>これでuidの部分にuidとして渡した値が使われることになります.</p> <p><a target="_blank" rel="nofollow noopener" href="https://nextjs.org/docs/api-reference/next/router#with-url-object">With URL object</a></p> <p>apiの場合はfetchなどの際に文字列テンプレートを使って挿入すればいいです.</p> <pre><code class="javascript">const url = `api/quizzes${uid}` </code></pre> <p>サーバー側では以下のようにqueryオブジェクトから取得できます.</p> <pre><code class="javascript">export default function handler(req, res) { const { query: { pid }, } = req res.end(`Post: ${pid}`) } </code></pre> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>あまり考えないでもNext.js側でやり方を強制してくるので, 使うのは難しくないと感じました.</p> <h2 id="次回"><a href="#%E6%AC%A1%E5%9B%9E">次回</a></h2> <p>簡単にDgraphのクライアントの使い方をまとめます.</p> <p>次回</p> <h2 id="Reference"><a href="#Reference">Reference</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://nextjs.org/docs/api-reference/next/router#routerpush">router.push</a><br /> <a target="_blank" rel="nofollow noopener" href="https://nextjs.org/docs/routing/dynamic-routes">Dynamic Routes</a><br /> <a target="_blank" rel="nofollow noopener" href="https://nextjs.org/docs/api-routes/dynamic-api-routes">Dynamic API Routes</a></p> ブレイン tag:crieit.net,2005:PublicArticle/16124 2020-10-11T18:11:02+09:00 2020-10-11T19:07:40+09:00 https://crieit.net/posts/Next-js-Dgraph-3-API-next-connect Next.jsとDgraphで作るクイズ・アプリ (3) APIルートの設定とnext-connect <h1 id="APIの設定"><a href="#API%E3%81%AE%E8%A8%AD%E5%AE%9A">APIの設定</a></h1> <p>今回はAPIルートを使って簡単なバックエンドを作ります.</p> <h2 id="前回"><a href="#%E5%89%8D%E5%9B%9E">前回</a></h2> <p><a href="https://crieit.net/posts/Next-js-Dgraph-2">前回</a>を参照</p> <h2 id="APIルートの設定"><a href="#API%E3%83%AB%E3%83%BC%E3%83%88%E3%81%AE%E8%A8%AD%E5%AE%9A">APIルートの設定</a></h2> <h3 id="APIルートとは?"><a href="#API%E3%83%AB%E3%83%BC%E3%83%88%E3%81%A8%E3%81%AF%3F">APIルートとは?</a></h3> <p>Next.jsが提供する機能でAPIエンドポイントが作れます. まずpagesフォルダ以下にapiというフォルダを作ります. この中にハンドラを定義します. フォルダのパスがそのままAPIのパスとして使われます.</p> <p>例えばapi/quizzes.jsなら</p> <p><a target="_blank" rel="nofollow noopener" href="http://localhost:3000/api/quizzes">http://localhost:3000/api/quizzes</a></p> <p>としてアクセスできます.</p> <h3 id="ハンドラのシグニチャ"><a href="#%E3%83%8F%E3%83%B3%E3%83%89%E3%83%A9%E3%81%AE%E3%82%B7%E3%82%B0%E3%83%8B%E3%83%81%E3%83%A3">ハンドラのシグニチャ</a></h3> <p>ハンドラはリクエストを受け取ってレスポンスを返すような非同期関数です.</p> <pre><code class="javascript">const handler = async (req, res) => { // do something with local file system, database, or external apis } </code></pre> <p>これをファイルの末尾でエクスポートしておくだけです.</p> <pre><code>export default handler </code></pre> <h3 id="HTTPメソッド"><a href="#HTTP%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89">HTTPメソッド</a></h3> <p>api/quizzesエンドポイントではクイズの取得, クイズの作成ができます. これらはHTTPメソッドの違いで区別されるわけです. メソッドはreq.methodで参照できます.</p> <pre><code class="javascript">export default function handler(req, res) { if (req.method === 'POST') { // Process a POST request } else { // Handle any other HTTP method } } </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://nextjs.org/docs/api-routes/introduction">API Routes</a></p> <p>これは少しめんどくさいです. next-connectを使うともう少し整理した書き方が出来るようになります.</p> <h2 id="dgraph-js-http"><a href="#dgraph-js-http">dgraph-js-http</a></h2> <p>DgraphへのアクセスはJavaScriptクライアントの<a target="_blank" rel="nofollow noopener" href="https://github.com/dgraph-io/dgraph-js-http">dgraph-js-http</a>を使います.</p> <h3 id="インストール"><a href="#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">インストール</a></h3> <pre><code>npm i dgraph-js-http </code></pre> <h3 id="読み込み"><a href="#%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF">読み込み</a></h3> <pre><code>import * as dgraph from 'dgraph-js-http' </code></pre> <h3 id="クライアントの作成"><a href="#%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E3%81%AE%E4%BD%9C%E6%88%90">クライアントの作成</a></h3> <p>その前にDgraph ZeroのURLを.env.localに保存しておきましょう.</p> <pre><code>DGRAPH_URL='http://localhost:8080/' </code></pre> <p>これを使いクライアントを作成します. DgraphClientStubは第一引数にアドレスを取ります.</p> <pre><code>function DgraphClientStub(addr, stubConfig, options) { ... } </code></pre> <p>このインスタンスをDgraphClientに渡します.</p> <pre><code>const clientStub = new dgraph.DgraphClientStub(process.env.DGRAPH_URL) const dgraphClient = new dgraph.DgraphClient(clientStub) </code></pre> <h2 id="next-connect"><a href="#next-connect">next-connect</a></h2> <p>実際にデータをやり取りするにはHTTPメソッドの指定が必要です. またその度にクライアントを作成するのも面倒です. next-connectを使うとHTTPメソッドごとのハンドラのヒモ付やDBクライアント取得処理をミドルウェア化できます. 内部的にはtrouterという機能を使っているようです.</p> <h3 id="導入"><a href="#%E5%B0%8E%E5%85%A5">導入</a></h3> <pre><code>npm i next-connect </code></pre> <h3 id="ミドルウェア"><a href="#%E3%83%9F%E3%83%89%E3%83%AB%E3%82%A6%E3%82%A7%E3%82%A2">ミドルウェア</a></h3> <p>middlware/database.js</p> <pre><code class="javascript">import * as dgraph from 'dgraph-js-http' import nextConnect from 'next-connect'; const clientStub = new dgraph.DgraphClientStub(process.env.DGRAPH_URL) const dgraphClient = new dgraph.DgraphClient(clientStub) async function database(req, res, next) { req.dbClient = dgraphClient; return next(); } const middleware = nextConnect(); middleware.use(database); export default middleware; </code></pre> <p>dbClientとして参照出来るようになります. ハンドラ側ではミドルウェアを利用するようにします.</p> <pre><code class="javascript">import nextConnect from 'next-connect'; import middleware from '../../../middleware/database'; const handler = nextConnect(); handler.use(middleware); export default handler; </code></pre> <h3 id="ハンドラの登録"><a href="#%E3%83%8F%E3%83%B3%E3%83%89%E3%83%A9%E3%81%AE%E7%99%BB%E9%8C%B2">ハンドラの登録</a></h3> <p>各メソッド毎にハンドラを登録できるようになりました.</p> <pre><code class="javascript">const getHandler = async (req, res) => { /*...*/ } handler.get(getHandler) </code></pre> <p>これで</p> <pre><code>GET http://localhost:8080/api/quizzes POST http://localhost:8080/api/quizzes </code></pre> <p>を使い分けることができるようになりました.</p> <h3 id="Dgraphクライアントへのアクセス"><a href="#Dgraph%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E3%81%B8%E3%81%AE%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9">Dgraphクライアントへのアクセス</a></h3> <p>reqから参照できます.</p> <pre><code class="javascript">const getQuizzes = async (req, res) => { const client = req.dbClient } </code></pre> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>next-connectを使うことでHTTPメソッドとハンドラの関係性が記述しやすくなったかなと思います.</p> <h2 id="次回"><a href="#%E6%AC%A1%E5%9B%9E">次回</a></h2> <p><a href="https://crieit.net/posts/Next-js-Dgraph-4">次回</a>を参照</p> <h2 id="Reference"><a href="#Reference">Reference</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://vercel.com/guides/deploying-next-and-mysql-with-vercel">Create a Next.js App with a MySQL Database That Builds and Deploys with Vercel</a><br /> <a target="_blank" rel="nofollow noopener" href="https://developer.mongodb.com/how-to/nextjs-building-modern-applications">Building Modern Applications with Next.js and MongoDB</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/insight3110/items/5514f41404ce2036bd87">Next.jsのAPI Routerを使ってMongoDBからデータを引っ張ってくるAPIを作る(next-connectを利用したミドルウェアも)</a></p> ブレイン tag:crieit.net,2005:PublicArticle/16123 2020-10-11T17:22:58+09:00 2020-10-11T18:11:37+09:00 https://crieit.net/posts/Next-js-Dgraph-2 Next.jsとDgraphで作るクイズ・アプリ (2) プロジェクトの環境構築 <h1 id="プロジェクトの構築"><a href="#%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E6%A7%8B%E7%AF%89">プロジェクトの構築</a></h1> <p>今回からはプロジェクトを構築していく.</p> <h2 id="前回"><a href="#%E5%89%8D%E5%9B%9E">前回</a></h2> <p><a href="https://crieit.net/posts/Next-js-Dgraph-1">前回</a>を参照</p> <h2 id="前提条件"><a href="#%E5%89%8D%E6%8F%90%E6%9D%A1%E4%BB%B6">前提条件</a></h2> <ul> <li>Node.js入ってる?</li> <li>Gitが入ってる?</li> <li>Docker入ってる?</li> </ul> <h2 id="Next.js"><a href="#Next.js">Next.js</a></h2> <p>Next.jsの環境構築は簡単です. create-react-appのようにcreate-next-appを使うとプロジェクト・テンプレートが生成されます.</p> <pre><code>npm i -g create-next-app </code></pre> <p>必要ならログイン・シェルを再起動します.</p> <pre><code>exec $SHELL -l </code></pre> <p>後は適当にプロジェクト名を決めて実行するだけです.</p> <pre><code>create-next-app minq-editor --use-npm </code></pre> <p>ターミナルなどで以下を実行してアプリケーションを起動しましょう.</p> <pre><code class="shell">npm run dev </code></pre> <h2 id="Dgraphクラスタの起動"><a href="#Dgraph%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%BF%E3%81%AE%E8%B5%B7%E5%8B%95">Dgraphクラスタの起動</a></h2> <p><a href="https://crieit.net/posts/Dgraph">無心でDgraph入門</a>を参照</p> <pre><code>docker run --name myq -v minq:/dgraph --rm -it -p 8080:8080 -p 9080:9080 -p 8000:8000 dgraph/standalone:v20.03.0 </code></pre> <p>コンテナの名前, ボリュームの名前, ポート番号などは適当に変えましょう.</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>環境の構築は非常に簡単ですね.</p> <p><a href="https://crieit.net/posts/Next-js-Dgraph-3-API-next-connect">次回</a></p> <h2 id="Reference"><a href="#Reference">Reference</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://shinkufencer.hateblo.jp/entry/2018/11/22/233000">exec $SHELL -l でシェルが再読込されるしくみ</a></p> ブレイン tag:crieit.net,2005:PublicArticle/16122 2020-10-11T16:01:46+09:00 2020-10-11T16:01:46+09:00 https://crieit.net/posts/Next-js-Dgraph-1 Next.jsとDgraphで作るクイズ・アプリ (1) アプリの構想 <h1 id="アプリの構想"><a href="#%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E6%A7%8B%E6%83%B3">アプリの構想</a></h1> <h2 id="どんなアプリか?"><a href="#%E3%81%A9%E3%82%93%E3%81%AA%E3%82%A2%E3%83%97%E3%83%AA%E3%81%8B%3F">どんなアプリか?</a></h2> <p>クイズ・アプリなのでクイズができるアプリになる予定ですが, どちらかというとクイズベースの学習システムと考えています. クイズという名前も通りが良いから使ってるだけで実際は問です. ここで問とは答えがあるものを指します.</p> <p>基本的な機能としては,</p> <ul> <li>クイズ・データベース</li> <li>テスト</li> <li>成績管理</li> <li>スキル・マップ</li> <li>レコメンド</li> <li>サーティフィケート</li> </ul> <p>ができたら良いなぁと思っています. レコメンドやらサーティフィケートとかは現時点では構想というよりは誇大妄想的な感じもします(金力もないですしね). 能力の可視化ができたらとりあえずは完成でしょうか.</p> <h2 id="何故クイズ?"><a href="#%E4%BD%95%E6%95%85%E3%82%AF%E3%82%A4%E3%82%BA%3F">何故クイズ?</a></h2> <p>なんかの本でクイズ(練習問題)が効率的な学習法だと読んだからです. 本は忘れましたが多分以下のどれかの本です.</p> <p><a target="_blank" rel="nofollow noopener" href="http://www.eijipress.co.jp/book/book.php?epcode=2258">『Learn Better』</a><br /> <a target="_blank" rel="nofollow noopener" href="https://www.diamond.co.jp/book/9784478021835.html">『脳が認める勉強法』</a><br /> <a target="_blank" rel="nofollow noopener" href="https://d21.co.jp/book/detail/978-4-7993-1685-6">『「学力」の経済学 』</a></p> <h2 id="技術スタック"><a href="#%E6%8A%80%E8%A1%93%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF">技術スタック</a></h2> <p>上記のことをクリアするには, いろんな技術が必要になります. 当面はクイズ風のブログという感じで公開して行く予定ですが以下のような技術(というかライブラリやらフレームワークやらSaaS)を使う予定です.</p> <ul> <li>Next.js</li> <li>Netlify</li> <li>Semantic UI React</li> <li>Dgraph</li> </ul> <p>他にも以下を検討中です.</p> <ul> <li>モバイル (Android)</li> <li>SaaS</li> </ul> <h3 id="選定理由・用途"><a href="#%E9%81%B8%E5%AE%9A%E7%90%86%E7%94%B1%E3%83%BB%E7%94%A8%E9%80%94">選定理由・用途</a></h3> <h4 id="Next.js &amp; Jamstack"><a href="#Next.js+%26amp%3B+Jamstack">Next.js & Jamstack</a></h4> <p>Next.jsはクイズ用のエディタの作成やクイズの公開に使いたいと思います.</p> <p>Next.jsはSPA(Single Page Application)からSSR(Server Side Rendering)そしてJamstackまでカバーしたWebアプリケーション・フレームワークです. Jamstackというのはwebサイトやwebアプリケーション開発のアーキテクチャの一つです.</p> <blockquote> <p>Jamstack is an architecture designed to make the web faster, more secure, and easier to scale.</p> </blockquote> <p>もともとはJavaScript, APIs, Markupから作られた造語です. フロントは静的サイト・ジェネレータ(Markup)で静的なファイル(HTML)として作ってCDNで配布, 動的な部分はJavaScriptとAPIsを使って更新するというような考え方です. ある意味Ajaxへの回帰と言えるでしょうか.</p> <p><img src="https://d33wubrfki0l68.cloudfront.net/b7d16f7f3654fb8572360301e60d76df254a323e/385ec/img/svg/architecture.svg" alt="image" /></p> <p>Next.jsのポイントはページをReactアプリケーションとして構築できるところです. React自体はUIライブラリという位置づけだったのでWebアプリケーション開発となると面倒なことも多かったです. Reactやって, クライアントサイド・ルーティング勉強して, 別個にAPIサーバーを立ててなどと考える必要はなく, 一つのフレームワークで完結します.</p> <p>Next.jsならファイル・パスとルートが対応したり, API(バックエンド)も作れたりと考えることが少なくて良いです. 過去この点で何度も失敗しているのでNext.jsを触ったときは感動しました.</p> <p>またNext.jsを作っているVercelのホスティング・サービスを使えばサイトのデプロイや配信なども簡単にできるので技術ブログなんかにはちょうど良いのかもしれません.</p> <p>個人的には簡易なサーバーからフロントエンドまで完結する点を評価してエディタの開発に採用しました(これだけ流行っているフレームワークなので小賢しい後付に近いですが).</p> <h4 id="Netlify"><a href="#Netlify">Netlify</a></h4> <p>Netlifyの何かが良かったのですが忘れてしまいました.</p> <h4 id="Semantic UI React"><a href="#Semantic+UI+React">Semantic UI React</a></h4> <p>手頃のUIコンポーネントが欲しかったので適当に選びました.</p> <h4 id="Dgraph"><a href="#Dgraph">Dgraph</a></h4> <p>クイズ間の関係性/依存性が記述できるようなデータベースが良いと思って探していてちょうど見つけたのがDgraphでした. とりあえず静的ファイルのデータソースとして使う文には問題なさそうです. クラウド・サービスもあるようなのでちょっと気になるところです.</p> <h4 id="モバイル (Android)"><a href="#%E3%83%A2%E3%83%90%E3%82%A4%E3%83%AB+%28Android%29">モバイル (Android)</a></h4> <p>当然スマフォで空き時間に勉強できたら便利です. モバイルならとりあえずは成績の管理とかはスマフォ側のストレージでできそうです. Google Driveとかに保存しても良いかもしれません.</p> <p>モバイル開発ではトレンド的にはFlutterなどが注目を集めています. ReactやってるならReact-nativeとかでもいいし, 用途的にはUnityとかでも良いのかもしれません. Android版に限定する予定なので, ネイティブとしても良いのかもしれません. 作ることだけが決めていますがよく分かりません.</p> <h4 id="GS2"><a href="#GS2">GS2</a></h4> <p>ゲーム用のSaaSです. Playfabとかもあるようです. GS2は日本製で条件付きで売上1000万まで無料で使えるので安心して使えるかなとGS2に傾きつつある昨今です.</p> <h2 id="Reference"><a href="#Reference">Reference</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://nextjs.org/">Next.js</a><br /> <a target="_blank" rel="nofollow noopener" href="https://dgraph.io/">Dgraph</a><br /> <a target="_blank" rel="nofollow noopener" href="https://jamstack.org/">Jamstack</a><br /> <a target="_blank" rel="nofollow noopener" href="https://reactnative.dev/">React Native</a><br /> <a target="_blank" rel="nofollow noopener" href="https://flutter.dev/?gclid=Cj0KCQjwt4X8BRCPARIsABmcnOry5Uy1LKLGR43-ZnRuX60vTy5JMG6NFhkF7Z0wPhXeS-5tpmO48wUaAjssEALw_wcB&gclsrc=aw.ds">Flutter</a><br /> <a target="_blank" rel="nofollow noopener" href="https://unity.com/">Unity</a></p> ブレイン tag:crieit.net,2005:PublicArticle/16117 2020-10-06T20:49:24+09:00 2020-10-09T22:17:25+09:00 https://crieit.net/posts/Dgraph 無心でDgraph入門 <h1 id="Dgraphの起動とスキーマ定義"><a href="#Dgraph%E3%81%AE%E8%B5%B7%E5%8B%95%E3%81%A8%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E5%AE%9A%E7%BE%A9">Dgraphの起動とスキーマ定義</a></h1> <h2 id="目標"><a href="#%E7%9B%AE%E6%A8%99">目標</a></h2> <p>とりあえず動かすして遊べるようにする.</p> <h2 id="前提"><a href="#%E5%89%8D%E6%8F%90">前提</a></h2> <ul> <li>Dockerのインストール</li> </ul> <h2 id="Dgraphクラスタの構造"><a href="#Dgraph%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%BF%E3%81%AE%E6%A7%8B%E9%80%A0">Dgraphクラスタの構造</a></h2> <div class="table-responsive"><table> <thead> <tr> <th>ノード名</th> <th>説明</th> </tr> </thead> <tbody> <tr> <td>Zero</td> <td>Dgraph Zero controls the Dgraph cluster, assigns servers to a group, and re-balances data between server groups.</td> </tr> <tr> <td>Alpha</td> <td>Dgraph Alpha hosts predicates and indexes.</td> </tr> <tr> <td>Ratel</td> <td>Ratel serves the UI to run queries, mutations & altering schema.</td> </tr> </tbody> </table></div> <p>ついでにpredicateとindexの説明.</p> <div class="table-responsive"><table> <thead> <tr> <th>用語</th> <th>説明</th> </tr> </thead> <tbody> <tr> <td>Predicate</td> <td>Predicates are either the properties associated with a node or the relationship between two nodes.</td> </tr> <tr> <td>Index</td> <td>Indexes are the tokenizers that can be associated with the predicates to enable filtering using appropriate functions.</td> </tr> </tbody> </table></div> <p><a target="_blank" rel="nofollow noopener" href="https://dgraph.io/docs/get-started/">Get Started - Quickstart Guide</a>から引用</p> <p>少し補足するとPredicateとはノードのプロパティやノードを繋ぐエッジのことです. Indexは他のデータベースと大体同じ意味で索引付けのことで検索するときに指定します.</p> <h2 id="Dgraphコンテナ"><a href="#Dgraph%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A">Dgraphコンテナ</a></h2> <p>とりあえずお試しで動かすならdgraph/standaloneというイメージを使います.</p> <pre><code>docker pull dgraph/standalone:v20.03.0 </code></pre> <p>起動は以下のようにします.</p> <pre><code>docker run --name first-dgraph --rm -it -p 8080:8080 -p 9080:9080 -p 8000:8000 -v my-vol:/dgraph dgraph/standalone:v20.03.0 </code></pre> <p>とりあえ</p> <div class="table-responsive"><table> <thead> <tr> <th>オプション</th> <th>説明</th> </tr> </thead> <tbody> <tr> <td>name</td> <td>実行されるコンテナのインスタンスに付けられる名前です</td> </tr> <tr> <td>rm</td> <td>停止するときにコンテナが削除されます</td> </tr> <tr> <td>it</td> <td>interactive ttyでttyを通じてコンテナとやり取りできます. 今回はひたすらログが流れてきます.</td> </tr> <tr> <td>0</td> <td>ポート・フォーワーディングといってホストとクライアントの間でポートの対応を付けを行います</td> </tr> <tr> <td>v</td> <td>ボリュームをマウントします. my-volはdockerが管理するボリュームで/dgraphがコンテナ側のマウントポイントです</td> </tr> </tbody> </table></div> <p><strong>注意点はボリュームのマウント・ポイントは/dgraphに指定する必要があります</strong>. しないとrmオプションの効果でコンテナを停止するとデータごと消えてしまいます(お試しだから良いのかもしれませんが).</p> <p>実行後以下のコマンドでmy-volボリュームが作られていることを確認しましょう.</p> <pre><code>docker volume ls </code></pre> <h2 id="Ratel UIを使ったSchema編集"><a href="#Ratel+UI%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9FSchema%E7%B7%A8%E9%9B%86">Ratel UIを使ったSchema編集</a></h2> <h3 id="Ratel UIへのアクセス"><a href="#Ratel+UI%E3%81%B8%E3%81%AE%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9">Ratel UIへのアクセス</a></h3> <p><a target="_blank" rel="nofollow noopener" href="http://localhost:8000/">http://localhost:8000/</a>にアクセスします.</p> <p><a href="https://crieit.now.sh/upload_images/ebfc02a9cdb0f0fbd51e9d5690617e895f7c3fdde691d.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/ebfc02a9cdb0f0fbd51e9d5690617e895f7c3fdde691d.png?mw=700" alt="image" /></a></p> <p>真ん中のLatestを選びます.</p> <p><a href="https://crieit.now.sh/upload_images/c3c3e6a02d081d6906e6811d15b4908c5f7c407a37d6d.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c3c3e6a02d081d6906e6811d15b4908c5f7c407a37d6d.png?mw=700" alt="image" /></a></p> <p>スキーマの設定やデータの投入, クエリなどを実行できます.</p> <h3 id="スキーマを設定する"><a href="#%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B">スキーマを設定する</a></h3> <p>スキーマの設定方法は2つあります.</p> <ol> <li>データを投入する</li> <li>手動でスキーマを定義する</li> </ol> <p>基本的にノードを投入すると自動的にスキーマが定義されます. そのため一度スキーマを定義しても新しいプレディケートがあると勝手にスキーマが更新されます.</p> <p>例えばConsoleのMutationタブに以下をコピペします.</p> <pre><code class="json">{ "set":[ { "name": "Michael", "age": 40 } ] } </code></pre> <p>Runをクリックするとスキーマが生成されます. 左のSchemaタブを選ぶとプレディケートとタイプが表示されます.</p> <p><a href="https://crieit.now.sh/upload_images/cd364e29ba8e3259a32cc95f47e3e1055f7c4a70039b5.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/cd364e29ba8e3259a32cc95f47e3e1055f7c4a70039b5.png?mw=700" alt="image" /></a></p> <p>次にアドレスというプレディケートを加えて新しいデータを投入します.</p> <pre><code class="json">{ "set":[ { "name": "Michael", "age": 40, "e-mail" : "example.com" } ] } </code></pre> <p>するとe-mailというプレディケートが追加されました.</p> <p><a href="https://crieit.now.sh/upload_images/1ed70e814b5fa7fa8aaf66607ceb8e465f7c4b2a2950e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/1ed70e814b5fa7fa8aaf66607ceb8e465f7c4b2a2950e.png?mw=700" alt="image" /></a></p> <p>このため導入するデータのバリデーションはアプリケーション側で行うのが良さそうです.</p> <h3 id="スキーマを設定する利点"><a href="#%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B%E5%88%A9%E7%82%B9">スキーマを設定する利点</a></h3> <p>ではスキーマが無駄なのかというと, そうでもないようです. Ratel UIでQueryを入力する際に補完されるようになるようです. ただ階層とかは考慮してくれないようです.</p> <p><a href="https://crieit.now.sh/upload_images/e58e165c855af120703f0f035bd86f8c5f8062ae7e0ca.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/e58e165c855af120703f0f035bd86f8c5f8062ae7e0ca.png?mw=700" alt="image" /></a></p> <h2 id="補足"><a href="#%E8%A3%9C%E8%B6%B3">補足</a></h2> <h3 id="Dockerのボリューム"><a href="#Docker%E3%81%AE%E3%83%9C%E3%83%AA%E3%83%A5%E3%83%BC%E3%83%A0">Dockerのボリューム</a></h3> <p>ボリュームにはもう一つマウント・ボリュームというのがあります. こちらもホスト側のフォルダをコンテナ側にマウントする点は同じですが, フォルダを自分で指定する必要があります. DockerはWindows, macOS, そして各Linuxディストリビューションで提供されマルティ・プラットフォームでコンテナを動かすことができます. しかしこのマウント・ポイントを指定してしまうと互換性がなくなってしまう可能性があります. そこで後からDocker側がボリュームを管理するような仕組みが導入されたようです. 領域はDockerの管理下にあるので, プラットフォームごとの際はDockerが吸収してくれます.</p> <pre><code>docker volume create my-vol </code></pre> <p>などで作ることができます. 例えばデータの投入は以下のようにします.</p> <h2 id="Reference"><a href="#Reference">Reference</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://discuss.dgraph.io/t/drop-all-data-from-dgraph/5866/2">Drop all data from Dgraph</a></p> ブレイン tag:crieit.net,2005:PublicArticle/16040 2020-08-30T22:58:17+09:00 2020-08-30T22:58:17+09:00 https://crieit.net/posts/GitHub-OAuth-App GitHub OAuth Appのトークンを取得失敗した <h1 id="access tokenはクライアント側から取得できない"><a href="#access+token%E3%81%AF%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E5%81%B4%E3%81%8B%E3%82%89%E5%8F%96%E5%BE%97%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84">access tokenはクライアント側から取得できない</a></h1> <h2 id="やりたかったこと"><a href="#%E3%82%84%E3%82%8A%E3%81%9F%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8">やりたかったこと</a></h2> <p>GitHubをJSONデータベース代わりに使いたかった. GitHub APIを使うとGitHubをREST APIから操作できるので, これにフロント付けたら簡易なデータベースを作れるのではと思ったわけです. これは結局Cross-Origin Request Blockedでできないことが分かりました.</p> <p>忘れちゃうのもあれなので概要をメモしておきます.</p> <h2 id="環境"><a href="#%E7%92%B0%E5%A2%83">環境</a></h2> <p>以下のパケージを利用しました.</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://nextjs.org/">Next.js</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://marmelab.com/react-admin/">react-admin</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/octokit/auth-oauth-app.js">octokit/auth-oauth-app.js</a></li> </ul> <p>またreact-adminで認可するロジックは以下を参考にしました.</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/marmelab/ra-example-oauth">ra-example-oauth</a></li> </ul> <h2 id="OAuth Applicationの作成"><a href="#OAuth+Application%E3%81%AE%E4%BD%9C%E6%88%90">OAuth Applicationの作成</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://docs.github.com/en/developers/apps/creating-an-oauth-app">Creating an OAuth App</a>を参考にしましょう.</p> <p>作成されたGitHub OAuth Appには固有の値であるclient idとclient secretが付与されるます.</p> <h2 id="Auth Provider"><a href="#Auth+Provider">Auth Provider</a></h2> <p>react-adminで認証や認可を行うにはauth providerにロジックを書きます.</p> <pre><code class="javascript">const authProvider = { login: params => Promise.resolve(), logout: params => Promise.resolve(), checkAuth: params => Promise.resolve(), checkError: error => Promise.resolve(), getPermissions: params => Promise.resolve(), }; </code></pre> <p>取得したトークンはローカルストレージに保存します. checkAuthではこのトークンがすでに設定されているかを確認し, 無ければログイン画面が表示されます.</p> <pre><code class="javascript">checkAuth: () => { const user_name = localStorage.getItem('username'); console.log(`checkAuth: ${user_name}`) return localStorage.getItem('username') ? Promise.resolve() : Promise.reject(); }, </code></pre> <p>ログイン画面では2つの処理を行います. まず認可ページで認可を行います. 認可後はAuthorization callback URLに指定したページに戻ってくるのですが, この際codeというURLパラメータが付与されます. これとclient id, client secretを使ってトークンを取得します. stateは任意ですが一応付けてます.</p> <pre><code class="javascript">import { createOAuthAppAuth } from '@octokit/auth-oauth-app'; const authorizeURL = new URL('oauth/authorize', process.env.login_url); authorizeURL.searchParams.append('client_id', process.env.client_id); authorizeURL.searchParams.append('scope', 'public_repo'); authorizeURL.searchParams.append('state', 'QAbgG6f9Rit2C5'); const auth = createOAuthAppAuth({ clientId: process.env.client_id, clientSecret: process.env.client_secret, }); login: async (params) => { const { searchParams } = new URL(window.location.href); if (!searchParams.has('code') || !searchParams.has('state')) { redirectToURL(authorizeURL) return Promise.resolve(); } const code = searchParams.get('code'); const state = searchParams.get('state'); const tokenAuthentication = await auth({ type: 'token', code: code, state: state, }); const { token } = tokenAuthentication; localStorage.setItem('username', token); return Promise.resolve(); }, </code></pre> <p>ところがauthでtokenを取得しようとすると失敗します. リバース・プロキシを設置してやるといけるようですが, 試してません.</p> <p><a target="_blank" rel="nofollow noopener" href="https://andreybleme.com/2018-02-24/oauth-github-web-flow-cors-problem/">OAuth Github web flow doesn't support CORS</a></p> <h2 id="補足"><a href="#%E8%A3%9C%E8%B6%B3">補足</a></h2> <p>Next.jsとreact-adminを使うとDocument is not definedというエラーが出ます. hisotryというパッケージが内部でDOM APIを利用しているのですがNext.jsは最初ローカル側(サーバー側)で動くのでこの時点ではDOMが存在しません. クライアント側かどうか調べる必要があります. getStaticPropsはロカールで実行され, ファイルなどを取得してpropsとしてページ・コンポーネントに渡すことができます. この処理が終了するとブラウザ側の処理に移ることを利用するとコンポーネントを切り替えることができるようです.</p> <p><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/58987174/how-to-prevent-parent-component-from-re-rendering-with-react-next-js-ssr-two-p">How to prevent parent component from re-rendering with React (next.js) SSR two-pass rendering?</a></p> ブレイン tag:crieit.net,2005:PublicArticle/15950 2020-06-12T22:31:59+09:00 2020-10-24T14:15:09+09:00 https://crieit.net/posts/macOS-ubuntu-20-04 macOSからubuntu 20.04への移行ガイド <p><del># ubuntu</p> <h2 id="背景"><a href="#%E8%83%8C%E6%99%AF">背景</a></h2> <p>5年ぶりにパソコンを新調して自作PCを買いました. Windows10をいれたもののどうも馴染めず今年リリースされたばかりのUbuntu 20.04 LTSを使うことにしたわけです. Linuxのコマンドラインに慣れるいい機会かなと思います. ちなみに最後のLinuxディストリビューションは修士の頃に使っていたOpenSuseです. Windows VistaにVirtualBoxで入れてた記憶があります.</p> <p>とはいえほぼ10年以上MacOS/macOSを使っていたのでそちらに寄せつつ快適な環境を模索したと思います.</p> <h2 id="環境"><a href="#%E7%92%B0%E5%A2%83">環境</a></h2> <p>想定する環境はこんな感じです.</p> <p>キーボード: Happy Hacking Keyboard Professional HYBRID Type-S<br /> キーボード配列: US配列<br /> キーボード設定: Karabiner-Elements<br /> 元OS: macOS Catalina (Mac Mini Late 2014)<br /> 新OS: ubuntu 20.04 LT (自作PC)</p> <h2 id="ポイント"><a href="#%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88">ポイント</a></h2> <ul> <li>左右のコマンド・キーでの入力ソースの変換を実現する.</li> </ul> <h2 id="UBS"><a href="#UBS">UBS</a></h2> <p>以下を参考にして入れた.</p> <p><a target="_blank" rel="nofollow noopener" href="https://ubuntu.com/tutorials/tutorial-create-a-usb-stick-on-windows#1-overview">Create a bootable USB stick on Windows</a></p> <p>macOSの人は<a target="_blank" rel="nofollow noopener" href="https://ubuntu.com/tutorials/tutorial-create-a-usb-stick-on-macos#1-overview">こちら</a>.</p> <p>後はUSBを指して再起動してブート・メニューをからUSBを選べば良い.</p> <h2 id="表示言語"><a href="#%E8%A1%A8%E7%A4%BA%E8%A8%80%E8%AA%9E">表示言語</a></h2> <p>基本的に表示は英語にしています. 特に意味はないのですが, 検索を英語ですることが多いので表示が英語の方が探すのが. 楽だなぁと思った次第です. これはインストール中に言語で英語を選択しておけば良いです. ただブラウザとかを表示するには日本語が必須です.</p> <p>トップ・メニューの左端にActivitiesを選択して検索窓でlangと検索します. Language Setupが表示されるので選択肢します.</p> <p><a href="https://crieit.now.sh/upload_images/cd1eb1b4e5cb1e31c53baff67b1f954e5ee37024bba04.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/cd1eb1b4e5cb1e31c53baff67b1f954e5ee37024bba04.png?mw=700" alt="image" /></a></p> <p>勝手に読み込みが始まったような気がします.</p> <h2 id="キーボード・レイアウト"><a href="#%E3%82%AD%E3%83%BC%E3%83%9C%E3%83%BC%E3%83%89%E3%83%BB%E3%83%AC%E3%82%A4%E3%82%A2%E3%82%A6%E3%83%88">キーボード・レイアウト</a></h2> <pre><code class="bash">sudo dpkg-reconfigure keyboard-configuration </code></pre> <p>で設定をする. Happy Hacking Keyboardを選んで残りはデフォルトの設定を使う. 再起動する.</p> <p><a target="_blank" rel="nofollow noopener" href="https://jun-networks.hatenablog.com/entry/2019/11/20/160003">HHKB をUbuntuで使う</a></p> <h2 id="日本語入力/Input Method"><a href="#%E6%97%A5%E6%9C%AC%E8%AA%9E%E5%85%A5%E5%8A%9B%2FInput+Method">日本語入力/Input Method</a></h2> <p>検索ぐらいは英語でできても文字は日本語が必要です. mozcとfcitxというのを使います.</p> <pre><code class="bash">sudo apt install fcitx fcitx-mozc -y </code></pre> <p>次にibusという標準の日本語入力システムからfcitexというのに切り替えます. 一番下のkeyboard input method systemをfcitxに変更します.</p> <p><a href="https://crieit.now.sh/upload_images/c3d1906e78aed3617a74e14fddf43ef25ee3726d8ce9f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c3d1906e78aed3617a74e14fddf43ef25ee3726d8ce9f.png?mw=700" alt="image" /></a></p> <p>ibusは消しておいても良いようです.</p> <pre><code class="bash">sudo apt purge ibus -y </code></pre> <p>次にactivitiesからfcitex confg toolを選んで以下のように設定します.</p> <p><a href="https://crieit.now.sh/upload_images/11df00fd66b3268c3096785555ed3c4b5f0309475809a.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/11df00fd66b3268c3096785555ed3c4b5f0309475809a.png?mw=700" alt="image" /></a></p> <p>MozcをダブルクリックしてKeyboad LayoutをEnglish(US)に変更します.</p> <p><a href="https://crieit.now.sh/upload_images/43d04c971294cbd4efb29ca65a602aa35ee373cc4251a.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/43d04c971294cbd4efb29ca65a602aa35ee373cc4251a.png?mw=700" alt="image" /></a></p> <p>再起動します. 入力メソッドの切り替えは次で紹介します.</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.kwonline.org/memo2/2019/11/13/fcitx-mozc-on-ubuntu-19-10/">Ubuntu 19.10の日本語入力をfcitx-mozcにする</a></p> <h2 id="superキーの無効化"><a href="#super%E3%82%AD%E3%83%BC%E3%81%AE%E7%84%A1%E5%8A%B9%E5%8C%96">superキーの無効化</a></h2> <p>ここからが重要という気がします. HHKB Hybridを使っているのですがデフォルトではコマンド・キーを押すとactivitiesが表示されます. これは普通のやり方では変更できません. Settings Shortcutsを確認してもAlt + F1しか確認できません.</p> <p><a href="https://crieit.now.sh/upload_images/a5502ee9440ada8cfacc866f6f1e52795ee375f425a9e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/a5502ee9440ada8cfacc866f6f1e52795ee375f425a9e.png?mw=700" alt="image" /></a></p> <p><a target="_blank" rel="nofollow noopener" href="https://help.ubuntu.com/stable/ubuntu-help/shell-keyboard-shortcuts.html.en">Useful keyboard shortcuts</a>ではSuperが指定されています. gsettingsというcliを使うとこれを無効化できます.</p> <pre><code class="bash">gsettings set org.gnome.mutter overlay-key `` gsettings get org.gnome.mutter overlay-key </code></pre> <p>これでコマンド・キーは無害になるのでお気に入りのショートカットを割り当てることができます.</p> <h2 id="コマンド・キーでの入力メソッドの変換"><a href="#%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%83%BB%E3%82%AD%E3%83%BC%E3%81%A7%E3%81%AE%E5%85%A5%E5%8A%9B%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E5%A4%89%E6%8F%9B">コマンド・キーでの入力メソッドの変換</a></h2> <p>いわゆる空打ちと言うやつですね. fcitx config toolを再び開きます. Global Configタブを選んで一番下のShow Advanced Optionsにチェックを入れます.</p> <p><a href="https://crieit.now.sh/upload_images/676aeb35122871f92cbea78fe85e049a5ee3770e3264b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/676aeb35122871f92cbea78fe85e049a5ee3770e3264b.png?mw=700" alt="image" /></a></p> <p>するとActivate/Inactivate Input Methodという項目が表示されるので左右のSuperキーを割り当てます.</p> <p><a href="https://crieit.now.sh/upload_images/c3b290dc9594ddb43821cdb882dc74865ee3776bc686e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c3b290dc9594ddb43821cdb882dc74865ee3776bc686e.png?mw=700" alt="image" /></a></p> <p>これで変換/無変換をボタン一つで切り替えられるようになりました.</p> <h2 id="日本語フォントの変更"><a href="#%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%83%95%E3%82%A9%E3%83%B3%E3%83%88%E3%81%AE%E5%A4%89%E6%9B%B4">日本語フォントの変更</a></h2> <p>デフォルトはDejavuとかいうフォント設定になっていた気がします.</p> <p>gsettings set org.gnome.desktop.interface font-name 'Noto Sans CJK JP 11'</p> <p>で変更できます.</p> <p><a target="_blank" rel="nofollow noopener" href="https://sicklylife.jp/ubuntu/2004/settings.html#hamideru">文字がはみ出るバグに対応する</a><br /> <a target="_blank" rel="nofollow noopener" href="https://www.yscjp.com/linux/font_iroiro.html">Linuxだって、綺麗にフォントが表示できるんだからねッ!</a></p> <p>あるいはTweaksというアプリから変更できます. こちらの方が便利かもしれません. ActivitiesからTweaksを検索します. TweaksはUbuntu Softwareから導入できます.</p> <p><a href="https://crieit.now.sh/upload_images/c4bfc86fa5afefb17c104f866b35c6b35eeb4b708732b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c4bfc86fa5afefb17c104f866b35c6b35eeb4b708732b.png?mw=700" alt="image" /></a></p> <p>例えばInterface Textなら右側のフォントの部分をクリックすると,検索・指定画面が表示されますので適当に選択しておきます.</p> <p><a href="https://crieit.now.sh/upload_images/c99eb9fb2fc057b280857b022b63e6935eeb4bb79ac12.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c99eb9fb2fc057b280857b022b63e6935eeb4bb79ac12.png?mw=700" alt="image" /></a></p> <p>適当のNotoというフォントを選択しました. CJK (Chinese Japan Korea)とJPで検索すると出てきます. フォントのことは分からないのでRegularにしておきます.</p> <h2 id="Open at Login"><a href="#Open+at+Login">Open at Login</a></h2> <p>MacのDockにはOpen at Loginというオプションがあります. Tweaksというソフトを使うと似たような設定ができます. Ubuntu SoftwareからTweaksをインストールします.</p> <p><a href="https://crieit.now.sh/upload_images/0ddd67f2539da21403ca03e39dfae08a5ee378a2d2d3b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/0ddd67f2539da21403ca03e39dfae08a5ee378a2d2d3b.png?mw=700" alt="image" /></a></p> <p>Tweaksを開いてStart Applicationsに指定します.</p> <p><a href="https://crieit.now.sh/upload_images/f5cac6aaa02603f606320d4250d1ff015ee378c2b92d1.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f5cac6aaa02603f606320d4250d1ff015ee378c2b92d1.png?mw=700" alt="image" /></a></p> <p>これでいちいち開き直す必要がなくなる. ただしターミナルのシェルの情報を消えてしまう.</p> <h2 id="スクリーンショット"><a href="#%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88">スクリーンショット</a></h2> <p>こんな記事を書くのにもスクリーンショットは必要である. Macではスクリーンショットは以下のようなショートカットで行える.</p> <div class="table-responsive"><table> <thead> <tr> <th>コマンド</th> <th>ショートカット</th> </tr> </thead> <tbody> <tr> <td>ウィンドウ全体のスクショ</td> <td>Shift + Super + F3</td> </tr> <tr> <td>エリアを指定してのスクショ</td> <td>Ctrl + Shift + Super + F3</td> </tr> </tbody> </table></div> <p>SettingsのKeyboard Shrotcutsから変更できる.</p> <p><a href="https://crieit.now.sh/upload_images/1a06fe54c3ec9a54fd28bcd65b868a955ee37a4937407.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/1a06fe54c3ec9a54fd28bcd65b868a955ee37a4937407.png?mw=700" alt="image" /></a></p> <h2 id="Firefoxのショットカット"><a href="#Firefox%E3%81%AE%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88%E3%82%AB%E3%83%83%E3%83%88">Firefoxのショットカット</a></h2> <p>パスワードやタブを同期できるのとPocketが便利なのでFirefoxを使っている. コピーがCommand + CとCtrl + Cで違うわけですが変更できないらしいのでCtrlとCommandの押し待ちがいが多発している. もう一つ面倒なのが,</p> <p>Comand + Shift + [ or ]</p> <p>でタブの左右移動ができない. Ctrl + PageUp/PageDownやCtrl + tab, Ctrl + Shift + TabでもできるがHHKB HybridではFnキーを押さないとPageUp/PageDownが押せないのでトリッキーな感じになる. 更にCtrl + TabにShiftを加えると何故かリストが表示されるという非対称性もあって面倒くさい. これは設定変更できる. メニューからPreferencesを開く.</p> <p><a href="https://crieit.now.sh/upload_images/834468e7d227ac48f26716b9b553d9295ee37d211011d.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/834468e7d227ac48f26716b9b553d9295ee37d211011d.png?mw=700" alt="image" /></a></p> <p>検索窓にtabを入力して, 表示されたオプションのCtrl+Tab cycles through tabs in recently used orderを無効化する.</p> <p><a href="https://crieit.now.sh/upload_images/21559e766c9639c34e48c4fd7e72cccd5ee37d9155b2d.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/21559e766c9639c34e48c4fd7e72cccd5ee37d9155b2d.png?mw=700" alt="image" /></a></p> <p>これでCtrl + Tabで右, Shiftを加えると左に移動できる. これで少しましになった. 同様の設定をMac側でもしておくと混乱を減らせる.</p> <h2 id="トップ・バーをセカンダリ・モニタにも表示する"><a href="#%E3%83%88%E3%83%83%E3%83%97%E3%83%BB%E3%83%90%E3%83%BC%E3%82%92%E3%82%BB%E3%82%AB%E3%83%B3%E3%83%80%E3%83%AA%E3%83%BB%E3%83%A2%E3%83%8B%E3%82%BF%E3%81%AB%E3%82%82%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B">トップ・バーをセカンダリ・モニタにも表示する</a></h2> <p>プライマリー・モニタにはトップ・バーが表示されますが, セカンダリ・モニタには何も表示されません. gnom-shell extensionsとしてMulti Monitors Add-Onをインストールします. GitHubの<a target="_blank" rel="nofollow noopener" href="https://github.com/spin83/multi-monitors-add-on">リポジトリ</a>の通りにやればよいのですが, gnome-shellのバージョンだけは確認しておいたほうが良さそうです.</p> <pre><code class="bash">gnome-shell --version </code></pre> <p>指示通りの導入したら拡張を有効にします. Extensionsというアプリケーションがインストールされているのでactivitiesから開きましょう.</p> <p><a href="https://crieit.now.sh/upload_images/8a50b47d04d8e9e4e2a83e0904ccf4985ee5aaa437b03.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8a50b47d04d8e9e4e2a83e0904ccf4985ee5aaa437b03.png?mw=700" alt="image" /></a></p> <p>正常に動作していたらExtensionsの一番上にMulti Monitors Add-Onが表示されているはずですので有効にしておきます.</p> <p><a href="https://crieit.now.sh/upload_images/89d6c8d1a7d236112be5428c8723d1ad5ee5aaeda9ed2.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/89d6c8d1a7d236112be5428c8723d1ad5ee5aaeda9ed2.png?mw=700" alt="image" /></a></p> <p><a target="_blank" rel="nofollow noopener" href="https://askubuntu.com/questions/1030460/ubuntu-18-top-bar-for-all-monitors">Ubuntu 18: Top bar for all monitors</a></p> <h2 id="Dockをセカンダリ・モニタにも表示する"><a href="#Dock%E3%82%92%E3%82%BB%E3%82%AB%E3%83%B3%E3%83%80%E3%83%AA%E3%83%BB%E3%83%A2%E3%83%8B%E3%82%BF%E3%81%AB%E3%82%82%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B">Dockをセカンダリ・モニタにも表示する</a></h2> <p>こちらはコマンドラインから変更できます.</p> <pre><code class="bash">gsettings set org.gnome.shell.extensions.dash-to-dock multi-monitor true </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://askubuntu.com/questions/1080228/how-do-i-get-the-ubuntu-dock-off-the-secondary-monitor">How do I get the Ubuntu Dock off the secondary monitor?</a></p> <h2 id="マウスによる起動を無効にする"><a href="#%E3%83%9E%E3%82%A6%E3%82%B9%E3%81%AB%E3%82%88%E3%82%8B%E8%B5%B7%E5%8B%95%E3%82%92%E7%84%A1%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B">マウスによる起動を無効にする</a></h2> <p>家に返っていると勝手にパソコンが起動していたということがある. これを無効にする.</p> <p><a target="_blank" rel="nofollow noopener" href="https://atsuoishimoto.hatenablog.com/entry/20120711/1341992902">Ubuntuをスリープさせても、マウスが動いただけで復帰してしまう件</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://unix.stackexchange.com/questions/236127/acpi-wakeup-4-letters-code-meaning">USB3はXHC0</a>なので以下のようなスクリプトをrc.localに追加すれば良い.</p> <pre><code class="zsh">#!/bin/zsh # XHC0 r=`cat /proc/acpi/wakeup | grep "XHC0\s*\w\+\s*\*enabled"` if [ -n "$r" ] ; then # echo XHC0 > /proc/acpi/wakeup echo $r fi </code></pre> <p>あるいは以下の方法もある.</p> <p><a target="_blank" rel="nofollow noopener" href="https://codetrips.com/2020/03/18/ubuntu-disable-mouse-wake-from-suspend/">Ubuntu: Disable mouse wake from suspend</a></p> <p>こっちのほうが良い気もするが, 文字列の加工とかが分からないので今回はこれまで.</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/tanyorg/items/401cbadd88fa3fe0b9d3">ubuntu 18.04 で rc.local を使う</a><br /> <a target="_blank" rel="nofollow noopener" href="https://linuxmedium.com/how-to-enable-etc-rc-local-with-systemd-on-ubuntu-20-04/">How to enable /etc/rc.local with SystemD on Ubuntu 20.04</a><br /> <a target="_blank" rel="nofollow noopener" href="https://askubuntu.com/questions/9853/how-can-i-make-etc-rc-local-run-on-startup">How can I make /etc/rc.local run on startup?</a><br /> <a target="_blank" rel="nofollow noopener" href="https://askubuntu.com/questions/252743/how-do-i-prevent-mouse-movement-from-waking-up-a-suspended-computer">How do I prevent mouse movement from waking up a suspended computer?</a></p> <h2 id="ペン型入力デバイスを一つのスクリーンに制限する"><a href="#%E3%83%9A%E3%83%B3%E5%9E%8B%E5%85%A5%E5%8A%9B%E3%83%87%E3%83%90%E3%82%A4%E3%82%B9%E3%82%92%E4%B8%80%E3%81%A4%E3%81%AE%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%81%AB%E5%88%B6%E9%99%90%E3%81%99%E3%82%8B">ペン型入力デバイスを一つのスクリーンに制限する</a></h2> <p>xrandrでモニタの情報を取得する. DPI-0とかHDMI-0など接続されている液タブを確認する. xinputでペン型入力デバイス(ペン)のidを取得する.</p> <p>これを以下のようにマッピングする.</p> <pre><code class="bash">xinput map-to-output 14 HDMI-0 </code></pre> <p>これでペンはHDMI-0端子に接続されたモニターだけで利用できる.</p> <p><a target="_blank" rel="nofollow noopener" href="https://askubuntu.com/questions/1201967/lock-the-mouse-cursor-to-one-screen-in-linux-ubuntu">Lock the mouse cursor to one screen in Linux / Ubuntu</a><br /> <a target="_blank" rel="nofollow noopener" href="https://askubuntu.com/questions/839161/limit-a-graphics-tablet-to-one-monitor">Limit a graphics tablet to one monitor</a></p> <p>xinputの設定ファイルがどこなのか調査中.</p> <h2 id="セカンダリ・モニタをポートレイト表示する"><a href="#%E3%82%BB%E3%82%AB%E3%83%B3%E3%83%80%E3%83%AA%E3%83%BB%E3%83%A2%E3%83%8B%E3%82%BF%E3%82%92%E3%83%9D%E3%83%BC%E3%83%88%E3%83%AC%E3%82%A4%E3%83%88%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B">セカンダリ・モニタをポートレイト表示する</a></h2> <p>現在調査中</p> <p><a target="_blank" rel="nofollow noopener" href="https://askubuntu.com/questions/1230570/cant-rotate-monitor-on-20-04">Can't rotate monitor on 20.04</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://help.ubuntu.com/stable/ubuntu-help/look-resolution.html.en">Change the resolution or orientation of the screen</a><br /> <a target="_blank" rel="nofollow noopener" href="https://askubuntu.com/questions/159086/rotate-only-one-screen">Rotate only one screen</a><br /> <a target="_blank" rel="nofollow noopener" href="https://askubuntu.com/questions/300670/is-there-any-ability-to-set-my-primary-monitor">Is there any ability to set my primary monitor?</a><br /> <a target="_blank" rel="nofollow noopener" href="https://askubuntu.com/questions/408302/rotated-monitor-login-screen-needs-rotation">rotated monitor. login screen needs rotation</a></p> <h2 id="補足"><a href="#%E8%A3%9C%E8%B6%B3">補足</a></h2> <h3 id="ターミナルの起動"><a href="#%E3%82%BF%E3%83%BC%E3%83%9F%E3%83%8A%E3%83%AB%E3%81%AE%E8%B5%B7%E5%8B%95">ターミナルの起動</a></h3> <p>Ctrl + Alt + Tでターミナルを開けます. 結構便利です.</p> <h3 id="偶発的な発見"><a href="#%E5%81%B6%E7%99%BA%E7%9A%84%E3%81%AA%E7%99%BA%E8%A6%8B">偶発的な発見</a></h3> <p>仮想コンソールへの切り替えが突然起きて慌てた. ctrl + alt + f3で仮想コンソールへ切り替えられるらしい. 戻るにはctrl + alt + f2で戻れた.</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.quora.com/How-do-I-switch-between-Console-mode-and-GUI-in-Ubuntu-18-04?share=1">How do I switch between Console mode and GUI in Ubuntu 18.04?</a></del></p> ブレイン tag:crieit.net,2005:PublicArticle/15890 2020-05-08T23:54:31+09:00 2020-05-08T23:54:31+09:00 https://crieit.net/posts/Serde-1-Derive Serde入門 (1) Derive属性の裏側をちょっと覗く <h1 id="Serde入門"><a href="#Serde%E5%85%A5%E9%96%80">Serde入門</a></h1> <h2 id="趣旨"><a href="#%E8%B6%A3%E6%97%A8">趣旨</a></h2> <p>Bayardという全文検索エンジンがあり保存されているドキュメントはJSON形式で取得できる. serde_jsonを使えば良いのだが, serde_jsonではhierarchical_facetというデータが上手く変換できない.</p> <pre><code class="rust">use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] enum Episode { Jo, Ha, Q, } #[derive(Debug, Serialize, Deserialize)] struct Human { id: String, name: String, appears_in: Vec<Episode>, } fn main() { let ikari_shinji = Human { id: "1".to_owned(), name: "Ikari Shinji".to_owned(), appears_in: vec![Episode::Jo, Episode::Ha, Episode::Q], }; let ikari_shinji_str = serde_json::to_string(&ikari_shinji).expect("cant't convert into string"); println!("{}", ikari_shinji_str); // {"id":"1","name":"Ikari Shinji","appears_in":["Jo","Ha","Q"]} let ayanami_rei_str = " { \"id\": \"2\", \"name\": \"Ayanami Rei\", \"appears_in\": [\"/episode/Jo\", \"/episode/Ha\", \"/episode/Q\"] }"; let anayami_rei: Human = serde_json::from_str(&ayanami_rei_str).unwrap(); println!("{:?}", anayami_rei); } </code></pre> <p>のayanami_rei_strをdeserializeしたい. 実行するとパニックが起きる.</p> <pre><code class="bash">thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error("unknown variant `/episode/Jo`, expected one of `Jo`, `Ha`, `Q`", line: 5, column: 36)', src/main.rs:34:30 </code></pre> <p>実はすごく簡単にこのエラーを消せるのですが, 今回はとりあえずserdeを概観し, Derive属性の裏側を覗き見てみましょう.</p> <h2 id="Serdeとは?"><a href="#Serde%E3%81%A8%E3%81%AF%3F">Serdeとは?</a></h2> <blockquote> <p>Serde is a framework for <strong>ser</strong>ializing and <strong>de</strong>serializing Rust data structures efficiently and generically.</p> </blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://docs.serde.rs/serde/">Serde - Docs.rs</a></p> <h3 id="シリアライズとは?"><a href="#%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%BA%E3%81%A8%E3%81%AF%3F">シリアライズとは?</a></h3> <p>直列化とか訳されたりします. 一般にデータの直列化とはバイト列や文字列への変換を指します. 通常Rustの構造体などはメモリ上に散らばって存在し, これらを参照することで一つの実体のように見せています. 対してバイト列や文字列はメモリ上に直線的な列を作って保存されています. また文字列にしておけばファイルに保存することができます. デシリアライズはその逆に直列化したデータを復元する作業です. こうすることで特定のデータをRustで読み込んで操作できるようになります.</p> <p>ここではRustのデータ構造を別のより一般的なデータ形式に変換する操作をシリアライズと定義することにします. デシリアライズはその逆です.</p> <h3 id="登場人物"><a href="#%E7%99%BB%E5%A0%B4%E4%BA%BA%E7%89%A9">登場人物</a></h3> <ul> <li>Rustデータ構造 (以下データ構造)</li> <li>Serdeデータ・モデル (以下データ・モデル)</li> <li>データ形式</li> </ul> <p>データ構造をデータ形式へと変換するのがSerdeの目的でした. 両者を仲介するAPIをデータ・モデルと呼んでいます.</p> <blockquote> <p>The Serde data model is the API by which data structures and data formats interact.</p> </blockquote> <p>つまりSerdeは二段階の処理を踏むわけです.</p> <p>データ構造 ↔️ データ・モデル ↔️ データ形式</p> <p>こんな感じで説明されるわけですがよく分かりませんよね.</p> <h3 id="データ形式"><a href="#%E3%83%87%E3%83%BC%E3%82%BF%E5%BD%A2%E5%BC%8F">データ形式</a></h3> <p>現在対応しているデータ形式は<a target="_blank" rel="nofollow noopener" href="https://docs.serde.rs/serde/#data-formats">Data formats -Docs.rs</a>を参照してください.</p> <h2 id="Serdeの基本"><a href="#Serde%E3%81%AE%E5%9F%BA%E6%9C%AC">Serdeの基本</a></h2> <h3 id="Derive属性"><a href="#Derive%E5%B1%9E%E6%80%A7">Derive属性</a></h3> <p>基本的な使い方は簡単です. Derive属性の引数にトレイト指定することでSerialize/Deserializeトレイトをコンパイル時に実装してくれます.</p> <pre><code class="rust">use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize, Debug)] struct Point { x: i32, y: i32, } fn main() { let point = Point { x: 1, y: 2 }; let serialized = serde_json::to_string(&point).unwrap(); println!("serialized = {}", serialized); let deserialized: Point = serde_json::from_str(&serialized).unwrap(); println!("deserialized = {:?}", deserialized); } </code></pre> <p>この際Cargo.tomlでは,</p> <pre><code class="toml">[dependencies] serde = { version = "1.0", features = ["derive"] } </code></pre> <p>のようにserdeの追加とfeaturesでderiveを指定しておきます.</p> <h3 id="Serialize/Deserializeトレイト"><a href="#Serialize%2FDeserialize%E3%83%88%E3%83%AC%E3%82%A4%E3%83%88">Serialize/Deserializeトレイト</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://serde.rs/derive.html">Using Derive - Serde</a></p> <p>Serialize/Deserializeトレイトで実装すべき振る舞いはserializeとdeserializeだけです.</p> <pre><code class="rust">pub trait Serialize { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer; } pub trait Deserialize<'de>: Sized { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>; } </code></pre> <p>SerializeとDeserializeでは微妙にシグニチャが違いますが, 引数にSerializer/Deserializerという如何にもな名前の型を取ります.</p> <p>どうやら具体的な変換方法はこれらに委譲されるようです. また具体的な変換処理はデータ形式で違ってきますので, Serializer/Deserializerトレイトを実装した型が必要になるわけです.</p> <p>プリミティブ型には全てデフォルトのSerialize/Deserialize実装が存在します.</p> <blockquote> <p>Serde provides such impls for all of Rust's primitive types so you are not responsible for implementing them yourself, ...</p> </blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://serde.rs/impl-serialize.html#serializing-a-primitive">Serializing a primitive - Serde</a><br /> <a target="_blank" rel="nofollow noopener" href="https://docs.serde.rs/serde/ser/trait.Serialize.html?search=#foreign-impls">Implementations on Foreign Types - Serialize</a><br /> <a target="_blank" rel="nofollow noopener" href="https://docs.serde.rs/serde/de/trait.Deserialize.html#foreign-impls">Implementations on Foreign Types - Deserialize</a></p> <p>例えばstr型に対しては以下のようなトレイト実装が与えられます.</p> <pre><code class="rust">impl Serialize for str { #[inline] fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, { serializer.serialize_str(self) } } </code></pre> <p>要するに以下のようなコードがあった時に,</p> <pre><code class="rust">#[derive(Serialize)] struct Human { name: String, age: u8, appears_in: Vec<String>, } </code></pre> <p>次のように展開されることになります.</p> <pre><code class="rust">use serde::ser::{Serialize, SerializeStruct, Serializer}; struct Human { name: String, age: u8, appears_in: Vec<String>, } // This is what #[derive(Serialize)] would generate. impl Serialize for Human { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, { let mut s = serializer.serialize_struct("Person", 3)?; s.serialize_field("name", &self.name)?; s.serialize_field("age", &self.age)?; s.serialize_field("phones", &self.appears_in)?; s.end() } } </code></pre> <p>これを実際にシリアライズすること考えてみましょう. Human構造体はSerializeトレイトを実装しているのでserializeメソッドが自動導出されています.</p> <pre><code class="rust">fn main() { let JO = String::from("Jo"); let HA = String::from("Ha"); let Q = String::from("Q"); let ikari_shinji = Human { name: "Ikari Shinji".to_owned(), age: 14, appears_in: vec![JO, HA, Q], }; // ikari_shinji.serialize(??); } </code></pre> <p>これでシリアライズはできるようになりましたが, 具体的な方法はserializerに委ねられます. この時点ではどのようなフォーマットで出力するのか定まっていないので当然と言えば当然ですが, 適切なSerializer型を定義することでikari_shinjiインスタンスをシリアライズできます.</p> <p><a target="_blank" rel="nofollow noopener" href="https://docs.serde.rs/serde/ser/trait.Serialize.html?search=#required-methods">Required methods - serde::ser::Serialize</a></p> <h3 id="Serializerトレイト"><a href="#Serializer%E3%83%88%E3%83%AC%E3%82%A4%E3%83%88">Serializerトレイト</a></h3> <p>serializerはシリアライズ用のデータ・モデルでデータのシリアライズ用のAPIを提供します. 型に応じて<a target="_blank" rel="nofollow noopener" href="https://docs.serde.rs/serde/trait.Serializer.html#required-methods">メソッド</a>が宣言されています. 例えば構造体なら<a target="_blank" rel="nofollow noopener" href="https://docs.serde.rs/serde/ser/trait.Serialize.html#tymethod.serialize">serialize_struct</a>というメソッドをシリアライズの時に呼び出しています.</p> <pre><code class="rust">use serde::ser::{Serialize, SerializeStruct, Serializer}; struct Person { name: String, age: u8, phones: Vec<String>, } // This is what #[derive(Serialize)] would generate. impl Serialize for Person { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, { let mut s: SerializeStruct = serializer.serialize_struct("Person", 3)?; s.serialize_field("name", &self.name)?; s.serialize_field("age", &self.age)?; s.serialize_field("phones", &self.phones)?; s.end() } } </code></pre> <p>名前はSerde型の前に慣例的にserialize_というプレフィックスがつけられます.</p> <h3 id="Deserializerトレイト/Visitorトレイト"><a href="#Deserializer%E3%83%88%E3%83%AC%E3%82%A4%E3%83%88%2FVisitor%E3%83%88%E3%83%AC%E3%82%A4%E3%83%88">Deserializerトレイト/Visitorトレイト</a></h3> <p>DeserializeプロセスはSerializeとは少し違います. 単純にいろんな入力が想定される点が違います. そのためDeserializerは更に処理をVisitorに委譲します.</p> <p><a target="_blank" rel="nofollow noopener" href="https://docs.serde.rs/serde/de/trait.Visitor.html">serde::de::Visitor</a></p> <p>説明が難しいので具体例としてTomlの場合は見てみましょう. <a target="_blank" rel="nofollow noopener" href="https://docs.rs/toml/0.5.6/toml/de/fn.from_str.html">toml::de::from_str</a>には以下のような例が載っています.</p> <pre><code class="rust">use serde_derive::Deserialize; #[derive(Deserialize)] struct Config { title: String, owner: Owner, } #[derive(Deserialize)] struct Owner { name: String, } fn main() { let config: Config = toml::from_str(r#" title = 'TOML Example' [owner] name = 'Lisa' "#).unwrap(); assert_eq!(config.title, "TOML Example"); assert_eq!(config.owner.name, "Lisa"); } </code></pre> <p>from_strはこの入力文字列をDeserializerに渡して, deserialize関数内部で処理を実行します.</p> <pre><code class="rust">pub fn from_str<'de, T>(s: &'de str) -> Result<T, Error> where T: de::Deserialize<'de>, { let mut d = Deserializer::new(s); let ret = T::deserialize(&mut d)?; d.end()?; Ok(ret) } </code></pre> <p>さて元の例をcargo-expandで展開してみましょう. いろいろコードが増えますが, 実際の呼び出し場所は以下のようになります.</p> <pre><code class="rust">_serde::Deserializer::deserialize_struct( __deserializer, "Config", FIELDS, __Visitor { marker: _serde::export::PhantomData::<Config>, lifetime: _serde::export::PhantomData, }, ) </code></pre> <p>Config構造体に対応して<a target="_blank" rel="nofollow noopener" href="https://github.com/alexcrichton/toml-rs/blob/master/src/de.rs#L290">deserialize_struct</a>が呼び出されています. __Visitorというマクロ展開によって生成されたインスタンスも渡されています. このインスタンスが実際の処理担うわけです. ここからが実際には大変なところで, 興味がある人は処理を追ってみてください.</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>だいぶ内容がSerializerに偏ってしまいました. Serializeは比較的分かりやすいです. 例えばRustのコードをJSON化する場合どのような対応関係なのかはある程度予想がつきます. 少なくともRust側データ構造は決まっているからです. しかしDeserializeは比較的自由なJSONを型のきっちり決まったRustのデータ構造に仕立て直す必要があり, 自然と複雑化します. 例外はserde_jsonでシリアライズした者を復元する場合だけです.</p> <p>いずれにしても特殊なデータ形式に対応したいという開発者以外が独自にSerializeとかDeserializeは不要です. 多くの場合は属性を付与することでSerialize/Deserializeの挙動をカスタマイズできます. 次はそれを</p> <h2 id="課題"><a href="#%E8%AA%B2%E9%A1%8C">課題</a></h2> <p>BayardのJSONのデータをDeserializeする</p> <p>[Need help with #<a target="_blank" rel="nofollow noopener" href="https://users.rust-lang.org/t/need-help-with-serde-deserialize-with/18374">serde(deserialize_with)</a></p> <h2 id="補足"><a href="#%E8%A3%9C%E8%B6%B3">補足</a></h2> <p>補足では本論とは関係ないけど気になったことをメモしておきます. 良かったら読んでみてください.</p> <h3 id="Serdeの設計とSOLID原則"><a href="#Serde%E3%81%AE%E8%A8%AD%E8%A8%88%E3%81%A8SOLID%E5%8E%9F%E5%89%87">Serdeの設計とSOLID原則</a></h3> <p>SOLID原則とは,</p> <ul> <li>Single Responsibility Principle (SRP)</li> <li>Open Closed Principle (OCP)</li> <li>Liskov Substitution Principle (LSP)</li> <li>Interface Segregation Principle (ISP)</li> <li>Dependency Inversion Principle (DIP)</li> </ul> <p>の5つの原則の頭文字をとったものです.</p> <p>LSPにつてはRustには継承を言語レベルで採用していません. ただこの原則が型の互換性(多態性)に関する原則だと見るとトレイト境界で強制できます. serializeメソッドはトレイト境界でSerializerトレイトを実装したSerializer型を引数に取るという条件を課すことでLSPを満たしているとも言えます. そもそも継承だけでインターフェースとかがない頃にメソッドのシグニチャを揃えようというような話だったのかなと想像します. 多態性を実現する手段が継承しかない場合はこのような原則で縛りを設ける必要はありそうです. Rustの場合</p> <blockquote> <p>(あらゆる型の)スーパークラスであるTと境界を設定したサブクラスは代替可能である</p> </blockquote> <p>と表現すればそのまま当てはまりそうです.</p> <p>さてserializeメソッドはシリアライズの実行だけを担い, データの変換はSerializer型が提供するAPIが担いました. 一見すると分けすぎな気もしますが, 様々なデータ形式に対応する時にserialzierを別にしておくことで拡張性が保たれることが分かります. 社会でも指示を出す人と実行する人の役割は違うようにserializeの指示を出すのと実際のデータの変換という役割を適切に分離していると考えることもできます.</p> <p>ここでSerializerは依存性として外側から引数として渡されるような設計になっています. いわゆる依存性注入(DI)です. すでに述べましたがこの依存性はSerializerトレイトを実装している型であれば互換性があります. そもそもSerialize自体もトレイトであり抽象に依存する良い設計と言えそうです(DIP). Serde可能なデータ形式を追加したい開発者はこの関係に従って(縛られて?)ひたすら実装を行うだけです.</p> <p>逆にISPは満たしていないように見えます. <a target="_blank" rel="nofollow noopener" href="https://serde.rs/impl-serializer.html#implementing-a-serializer">Implementing a Serializer</a>にも</p> <blockquote> <p>The Serializer trait has a lot of methods ...</p> </blockquote> <p>とあります. トレイトの粒度としては荒いと言えます. これは不要なメソッドの実装も取り敢えずしなくてはいけないことになります. <del>ser.rsの実装を見ると萎えます</del> ではカスタマイズが必要な場合独自のSerializer型を実装する必要があるのかというと<a target="_blank" rel="nofollow noopener" href="https://serde.rs/attributes.html">serde属性</a>を使うことで特定の要素だけをカスタマイズできるようになっています. 例えばデシリアライズでデフォルト値を設定したい場合は</p> <pre><code class="rust">#[serde(default)] </code></pre> <p>を付与することで可能です.</p> <p>最後はOCPです. この原則は少し分かりにくいですが以下のように表現されます.</p> <blockquote> <p>the open/closed principle states "software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification"</p> </blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://en.wikipedia.org/wiki/Open–closed_principle">Open–closed principle - Wikipedia</a></p> <p>(機能の追加自体がmodificationじゃないのかという感じはしますが)要するに他のコードへの変更なしに新しい機能を追加できるかということです. <a target="_blank" rel="nofollow noopener" href="https://serde.rs/impl-serializer.html">Implementing a Serializer</a>のser.rsを見てみましょう. データ構造, Serialize/Serializerトレイトがエンティティで機能の呼び出し側はto_stringという関数です.</p> <pre><code class="rust">pub fn to_string<T>(value: &T) -> Result<String> where T: Serialize, { let mut serializer = Serializer { output: String::new(), }; value.serialize(&mut serializer)?; Ok(serializer.output) } </code></pre> <p>OCPに適うということは, ある機能を拡張した場合に, このロジックに変更がなければ良いわけです. 今オレオレデータ形式がserde可能だとして考えましょう(oreクレートとでも呼びましょうか). ここでserializerに新しい型が対応したとしましょう. 例えばRustがxxx型に対応したとします(例えばf128型とか?あるいはクラスが追加されたとか?). 慣例に倣ってserialize_xxxが必要になりますが, to_stringのロジックに変更は必要ありません. OCPも満たしているようです.</p> <p>Sedeの面白いところは, Derive属性やserde属性を使うことでエンド・ユーザーはこうした実装を何も考慮しなくてもデータの変換が行えるようにしていることです. 反面黒魔術を多用しているためコードを読むのは難しいですが.</p> <h3 id="Required methodsとProvided methods"><a href="#Required+methods%E3%81%A8Provided+methods">Required methodsとProvided methods</a></h3> <p>Docs.rsにはこのような分類があるのですがRustでメソッドや関数に実装をオプションにするような構文があるという話は知りません. 私の勉強不足かとも思いましたが, 単純にデフォルトの実装の有無で見分けているようです.</p> <p><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/44642662/how-does-rust-know-which-trait-methods-are-required-or-provided">How does Rust know which trait methods are required or provided?</a></p> <p>実際に<a target="_blank" rel="nofollow noopener" href="https://github.com/serde-rs/serde/blob/33438850a6a8b0a3550619a60885cfc6f224e53f/serde/src/de/mod.rs#L1263">serde::de::Visitor</a>を見てみると<a target="_blank" rel="nofollow noopener" href="https://github.com/serde-rs/serde/blob/33438850a6a8b0a3550619a60885cfc6f224e53f/serde/src/de/mod.rs#L1289">expectingメソッド</a>はシグニチャの宣言だけです.</p> <pre><code class="rust">fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result; </code></pre> <p>一方<a target="_blank" rel="nofollow noopener" href="https://github.com/serde-rs/serde/blob/33438850a6a8b0a3550619a60885cfc6f224e53f/serde/src/de/mod.rs#L1294">visit_boolメソッド</a>はエラーを返すような仕様になっています.</p> <pre><code class="rust">fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E> whereE: Error, { Err(Error::invalid_type(Unexpected::Bool(v), &self)) } </code></pre> <h3 id="matchガード"><a href="#match%E3%82%AC%E3%83%BC%E3%83%89">matchガード</a></h3> <p>Derive属性で自動導出した場合どのようなSerializeトレイトが実装されるのでしょうか.</p> <pre><code class="rust">use serde::{Deserialize, Serialize}; #[derive(Serialize)] struct Human { name: String, age: u8, appears_in: Vec<String>, } fn main() { println!("cargo expand") } </code></pre> <p>これを<a target="_blank" rel="nofollow noopener" href="https://github.com/dtolnay/cargo-expand">cargo-expand</a>で展開すると以下のようなSerializeトレイトが実装されます.</p> <pre><code class="rust">let mut __serde_state = match _serde::Serializer::serialize_struct( __serializer, "Human", false as usize + 1 + 1 + 1, ) { _serde::export::Ok(__val) => __val, _serde::export::Err(__err) => { return _serde::export::Err(__err); } }; </code></pre> <p>適切なSerializerメソッドが呼び出されているのが分かります. <a target="_blank" rel="nofollow noopener" href="https://doc.rust-lang.org/reference/expressions/match-expr.html#match-expressions">Match式</a>は式を後置できるのでmathから波括弧までの間が式で, その戻り値でOkかErrかで判断されます. こういうのは<a target="_blank" rel="nofollow noopener" href="https://doc.rust-lang.org/reference/expressions/match-expr.html">Match guards</a>というらしいです. 何らかの形でMatch式が使われていることは予想していたのですが実際のコードを見てみると面白いですね.</p> <h3 id="triマクロ"><a href="#tri%E3%83%9E%E3%82%AF%E3%83%AD">triマクロ</a></h3> <p>例でserde_jsonではなくtomlを使ったのはsede_jsonはserialize/deserializeを呼び出さないことと, triマクロを使っているので説明に適さないと思ったからです.</p> <pre><code class="rust">macro_rules! tri { ($e:expr) => { match $e { crate::lib::Result::Ok(val) => val, crate::lib::Result::Err(err) => return crate::lib::Result::Err(err), } }; } </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/serde-rs/json/blob/9354bec7ddf0733bae7666e64f0078d9d5f029d9/src/lib.rs#L419">lib.rs - serde-rs /json</a></p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://serde.rs/">The Serde Book</a><br /> <a target="_blank" rel="nofollow noopener" href="https://docs.serde.rs/serde/index.html">serde - Docs.rs</a><br /> <a target="_blank" rel="nofollow noopener" href="https://docs.rs/toml/0.5.6/toml/index.html">toml - Docs.rs</a></p> ブレイン tag:crieit.net,2005:PublicArticle/15870 2020-04-26T21:58:35+09:00 2020-06-28T13:07:35+09:00 https://crieit.net/posts/cargo-make cargo-makeによるプロジェクト・ビルド入門 <h1 id="cargo-makeによるプロジェクト・ビルド"><a href="#cargo-make%E3%81%AB%E3%82%88%E3%82%8B%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%83%BB%E3%83%93%E3%83%AB%E3%83%89">cargo-makeによるプロジェクト・ビルド</a></h1> <h2 id="モチベーション"><a href="#%E3%83%A2%E3%83%81%E3%83%99%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3">モチベーション</a></h2> <p>cargoはrustのパッケージ管理ツール兼ビルドツールである.これい自体非常に便利なのだが, Web標準は無視できない. 特にSeedのようなWebフロントエンド・フレームワークによる開発ではWeb標準に合わせる必要も出てくる. 単純なケースでは<a target="_blank" rel="nofollow noopener" href="https://seed-rs.org/guide/quickstart">Quickstart</a>に従ってindex.htmlに直接wasmモジュールを導入すれば良いのですが, 複雑なアプリなどはwebpackなどが使えた方が便利だと思います. 今回のケースではTailwindの導入などがそれに当たります.</p> <h2 id="目標"><a href="#%E7%9B%AE%E6%A8%99">目標</a></h2> <p>CSSフレームワークであるtailwindcssをSeedプロジェクトで利用する.</p> <h2 id="前提条件"><a href="#%E5%89%8D%E6%8F%90%E6%9D%A1%E4%BB%B6">前提条件</a></h2> <ul> <li>rustupの導入</li> </ul> <p>とりあえずこれを導入しておけば諸々の開発環境の導入・管理が行えるようになります.</p> <h2 id="NPM vs Cargo"><a href="#NPM+vs+Cargo">NPM vs Cargo</a></h2> <div class="table-responsive"><table> <thead> <tr> <th>機能</th> <th>NPM</th> <th>Cargo</th> <th>備考</th> </tr> </thead> <tbody> <tr> <td>パッケージ管理</td> <td>⭕️</td> <td>⭕️</td> <td>パッケージのインスタール・公開などができる</td> </tr> <tr> <td>依存性管理</td> <td>⭕️</td> <td>⭕️</td> <td>lockファイルがある点など共通点が多い</td> </tr> <tr> <td>タスク・ランナー</td> <td>⭕️</td> <td>❌</td> <td>Cargoではカスタム・コマンドが開発できる(はず)</td> </tr> <tr> <td>ビルド</td> <td>❌</td> <td>⭕️</td> <td>NPMでは代わりにタスク・ランナーを使う</td> </tr> <tr> <td>コマンド拡張</td> <td>❌</td> <td>⭕️</td> <td>タスク・ランナーから呼び出せば良い</td> </tr> </tbody> </table></div> <p>Cargoには<a target="_blank" rel="nofollow noopener" href="https://docs.npmjs.com/misc/scripts">npm-scripts</a>のようなタスク・ランナーがありませんが, <a target="_blank" rel="nofollow noopener" href="https://doc.rust-lang.org/cargo/reference/external-tools.html#custom-subcommands">カスタム・コマンド</a>で機能を拡張することができます. その一つが<a target="_blank" rel="nofollow noopener" href="https://github.com/sagiegurari/cargo-make">cargo-make</a>です.</p> <h2 id="cargo-makeによるプロジェクト管理"><a href="#cargo-make%E3%81%AB%E3%82%88%E3%82%8B%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E7%AE%A1%E7%90%86">cargo-makeによるプロジェクト管理</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/seed-rs/seed-quickstart">seed-quickstart</a>のMakefile.toml内容を解説する感じです. wachモードなどは<a target="_blank" rel="nofollow noopener" href="https://github.com/brainvader/EvaQL/blob/tags/v0.1/ui/Makefile.toml#L73">EvaQL/ui/Makefile.toml</a>を参照してください.</p> <h3 id="cargo-makeの導入"><a href="#cargo-make%E3%81%AE%E5%B0%8E%E5%85%A5">cargo-makeの導入</a></h3> <p>cargo-makeの実行にはcargo-makeバイナリが必要になるのでインストールしておきます.</p> <pre><code class="bash">cargo install --force cargo-make </code></pre> <p>次にプロジェクトを作成します. これもコマンド一つでできます. 今回はwasmモジュールとして読み込まれるので--libオプションをつけます.</p> <pre><code class="bash">cargo new --lib project-name </code></pre> <p>できたらプロジェクト・ルートに移動し実行してみましょう.</p> <pre><code class="bash">cago make </code></pre> <p>この時点では<a target="_blank" rel="nofollow noopener" href="https://github.com/sagiegurari/cargo-make#default-tasks-and-extending">デフォルトのtomファイル</a>が参照されます. 次にMakefile.tomlファイルを作成します.</p> <pre><code class="bash">cd project-name touch Makefile.toml </code></pre> <p>同様に実行すると今度はMakefile.tomlをもとに実行が行われます. 任意のmakefileを指定するには--makefileオプションを使います.</p> <pre><code class="bash">cargo make --makefile ./my_build.toml test </code></pre> <h3 id="Makefile.tomlの書き方"><a href="#Makefile.toml%E3%81%AE%E6%9B%B8%E3%81%8D%E6%96%B9">Makefile.tomlの書き方</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/seed-rs/seed-quickstart/blob/master/Makefile.toml">seed-quickstart/Makefile.toml</a>の解説です. 必要ない方は飛ばしましょう.</p> <h4 id="タスク"><a href="#%E3%82%BF%E3%82%B9%E3%82%AF">タスク</a></h4> <p>実行するコマンドはタスクという単位で管理します. 何もしないタスクは以下のようになります.</p> <pre><code class="toml">[tasks.do_nothing] # do nothing </code></pre> <h4 id="基本"><a href="#%E5%9F%BA%E6%9C%AC">基本</a></h4> <p>cargoのbuildサブコマンドを呼び出してみましょう.</p> <pre><code class="toml"># cargo make compile [tasks.compile] description = "Build" workspace = false command = "cargo" args = ["build"] </code></pre> <p>それぞれの意味は以下のようになります.</p> <div class="table-responsive"><table> <thead> <tr> <th>セクション</th> <th>意味</th> </tr> </thead> <tbody> <tr> <td>description</td> <td>このタスクの内容</td> </tr> <tr> <td>workspace</td> <td>workspaceでタスクを実行するかどうか</td> </tr> <tr> <td>command</td> <td>実行するメイン・コマンド</td> </tr> <tr> <td>args</td> <td>引数の指定</td> </tr> </tbody> </table></div> <h4 id="依存タスク"><a href="#%E4%BE%9D%E5%AD%98%E3%82%BF%E3%82%B9%E3%82%AF">依存タスク</a></h4> <p>dependencies属性を指定するとコマンドの依存性を指定できます. 要するに呼び出し順序です.</p> <pre><code class="toml"># cargo make start [tasks.start] description = "Combine the build and serve tasks" workspace = false dependencies = ["build"] </code></pre> <p>これでcargo startを実行するとbuildタスクが実行されます.</p> <h4 id="開発サーバーと環境変数"><a href="#%E9%96%8B%E7%99%BA%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%A8%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0">開発サーバーと環境変数</a></h4> <p>開発サーバーとしてmicroserverというクレートを使います. 簡単なWeb UIの開発には便利そうなのとクレート導入の例として紹介しておきます. 開発の趣旨を開発者の人がブログに書いています.</p> <p><a target="_blank" rel="nofollow noopener" href="https://robertohuertas.com/2018/11/01/microserver/">Microserver: local http server with SPA support</a></p> <p>今回のようにあるタスクの前提となるバイナリ・クレートのインストールも記述できます.</p> <pre><code class="toml">[tasks.serve] description = "Start server" install_crate = { crate_name = "microserver", binary = "microserver", test_arg = "-h" } workspace = false command = "microserver" args = ["--port", "${PORT}"] </code></pre> <p>サーバーということでポートの指定もしています. 環境変数もenvセクションで指定できます.</p> <pre><code class="toml">[env] PORT = "8000" </code></pre> <p>別ファイルに指定して読み込むこともできます.</p> <pre><code class="toml">[env] env_files = [ "./my_env.env", ] </code></pre> <h4 id="conditionによる条件設"><a href="#condition%E3%81%AB%E3%82%88%E3%82%8B%E6%9D%A1%E4%BB%B6%E8%A8%AD">conditionによる条件設</a></h4> <p>ある条件を満たすときにタスクを実行することもできます. 環境変数がきちんと指定されている場合だけ実行するという条件ならconditionセクションをタスクに追加します.</p> <pre><code class="toml">[tasks.start] condition = { env_set = [ "PORT" ] } </code></pre> <p>あるいは特定の環境変数を条件にして新しい変数を定義することができる.</p> <pre><code class="toml">[env] PORT_EXISTING = { value = "true", condtion = { env_set = ["PORT"] } } PORT = { value = "8000", condition = { env_not_set = ["PORT"] } } </code></pre> <p>条件によって読みやすい環境変数に変換したり, 環境変数が定義されていない場合に設定したりということができそうです.</p> <h4 id="profileによるモードの切り替え"><a href="#profile%E3%81%AB%E3%82%88%E3%82%8B%E3%83%A2%E3%83%BC%E3%83%89%E3%81%AE%E5%88%87%E3%82%8A%E6%9B%BF%E3%81%88">profileによるモードの切り替え</a></h4> <p>webpackのモードの指定ののようなこともできます.</p> <pre><code class="toml">[env] env_files = [ { path = "./development.env", profile = development }, { path = "./production.env", profile = "production } ] </code></pre> <pre><code class="bash">cargo make --profile production some_task </code></pre> <p>developmentはデフォルト値なので指定する必要はないです.</p> <h4 id="タスクの拡張とリリース・ビルド"><a href="#%E3%82%BF%E3%82%B9%E3%82%AF%E3%81%AE%E6%8B%A1%E5%BC%B5%E3%81%A8%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E3%83%BB%E3%83%93%E3%83%AB%E3%83%89">タスクの拡張とリリース・ビルド</a></h4> <p>タスク名をextend属性で指定するとタスクを拡張できます. 例えばcompileタスクをリリース・モードでビルドするように拡張すると以下のようになります.</p> <pre><code class="toml">[tasks.compile_release] description = "Release Build " extend = "compile" args = ["build", "--release"] </code></pre> <p>プラットフォームごとの拡張も簡単にできます.</p> <pre><code class="toml">[tasks.hello-world] script = [ "echo \"Hello World From Unknown\"" ] [tasks.hello-world.linux] script = [ "echo \"Hello World From Linux\"" ] [tasks.hello-world.mac] script = [ "echo \"Hello World From macOS\"" ] </code></pre> <h4 id="スクリプティング"><a href="#%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0">スクリプティング</a></h4> <p>シェルスクリプトを指定して実行することもできます.</p> <pre><code class="toml">[tasks.echo] script = [ "echo hello world" ] </code></pre> <p>script_runner属性を指定することでpythonなどスクリプトのランナーを指定できます.</p> <pre><code class="toml">[tasks.python] script_runner = "python" script_extension = "py" script = [ ''' print("Hello, World!") ''' ] </code></pre> <p>またファイルを指定して実行することもできます.</p> <pre><code class="toml">[tasks.run_from_script] script = { file = "hello.py" } </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/sagiegurari/cargo-make#rust-code">@rust</a>の指定でRustを実行することもできます.</p> <h4 id="run_tasks"><a href="#run_tasks">run_tasks</a></h4> <p>実行するタスクを指定します. dependenciesで指定したタスクは事前に実行されますが, run_task属性で指定したタスクは事後に実行されます.</p> <pre><code class="toml">[tasks.pre_task] script = [ "echo pre task"] [tasks.post_task] script = [ "echo post task"] [tasks.do_something] dependencies = ["pre_task"] run_task = "post_task" </code></pre> <p>この例の場合pre_task -> do_something -> post_taskの順で実行されます. 並列実行やフォークなど細かいタスクのフローを設定することできます. 詳しくは<a target="_blank" rel="nofollow noopener" href="https://github.com/sagiegurari/cargo-make#sub-task">Sub Task</a>を参照してください.</p> <h4 id="エイリアス"><a href="#%E3%82%A8%E3%82%A4%E3%83%AA%E3%82%A2%E3%82%B9">エイリアス</a></h4> <p>タスクを別名で参照できます.</p> <pre><code class="toml">[tasks.build] alias = "default_build" </code></pre> <p>Seedの<a target="_blank" rel="nofollow noopener" href="https://github.com/seed-rs/seed/tree/master/examples">examples</a>フォルダは複数のクレートが含まれており, そこにはMakefile.tomlが存在します. それぞれのクレートではプロジェクト・ルートのMakefile.tomlを拡張する形でルートのタスクを参照しています. つまり実行する処理は同じといことです.</p> <h4 id="条件付き実行"><a href="#%E6%9D%A1%E4%BB%B6%E4%BB%98%E3%81%8D%E5%AE%9F%E8%A1%8C">条件付き実行</a></h4> <p>条件を満たした場合にタスクが実行されます.</p> <pre><code class="toml">[tasks.test-condition] condition = { platforms = ["windows", "linux"], channels = ["beta", "nightly"], profiles = ["development", "production"], rust_version = { min = "1.39.0", max = "1.42.0" } } script = [ "echo \"condition was met\"" ] </code></pre> <p>このタスクはmacでstableチャネルを利用している人は実行されません. またscriptの代わりにrun_taskで他のタスクを条件を満たした時だけ実行するということもできます.</p> <h4 id="watchモード"><a href="#watch%E3%83%A2%E3%83%BC%E3%83%89">watchモード</a></h4> <p>watch属性をつけるとwatchモードで実行できます.</p> <pre><code class="toml">[tasks.run_from_script] script = { file = "hello.py" } watch = true </code></pre> <p>監視対象からの除外のような設定もできます.</p> <pre><code class="toml">[tasks.watch] description = "Start building project in watch mode" workspace = false dependencies = ["build", "build_wasm"] watch = { ignore_pattern="pkg/*" } </code></pre> <p>watchモードでサーバーを起動することはできません. この場合run_taskのparallelを使うとファイルの変更を監視しながら配信もで可能です.</p> <pre><code class="toml">[tasks.dev] description = "Build in watch mode while serving file" run_task = [ { name = ["watch", "serve"], parallel = true } ] </code></pre> <h3 id="tailwindcssの導入"><a href="#tailwindcss%E3%81%AE%E5%B0%8E%E5%85%A5">tailwindcssの導入</a></h3> <p>npmを使います.</p> <pre><code class="bash">npm init # if needed npm install tailwindcss </code></pre> <p>これでtailwindというコマンドがパッケージ上で使えるようになります. cssフォルダを作成して以下の内容をstyle.cssファイルを新規作成します.</p> <pre><code class="css">@tailwind base; @tailwind components; @tailwind utilities; </code></pre> <p>これをビルドして利用します. publicフォルダを同じ階層に作っておいて, css用のフォルダを作ります. carg-makeのタスクを追加しましょう.</p> <pre><code class="toml"># cargo make tailwind [tasks.tailwind] script = [ "npx tailwind build ./css/style.css -o ./public/css/style.css", ] </code></pre> <p>style.cssからstyle.cssが出力されますが中身を見ると見れ慣れたCSSファイルです. 出力されたファイルをpublic/index.htmlに読み込めばtailwindが提供するユーティリティ・クラスを利用できます.</p> <p>Seedでtailwindを使ってみましょう. Seedの説明はしませんがRustのマクロを使って要素を記述できます. 注目するのはclassマクロです. ここに指定された文字列がtailwindcssのユーティリティ・クラス名です.</p> <pre><code class="rust">fn view(model: &Model) -> impl View<Msg> { let button_class = class!["bg-gray-400", "px-8", "py-4"]; div![ class![ "flex", "flex-col", "justify-center", "items-center", "h-screen", "text-gray-600" ], button![ button_class, simple_ev(Ev::Click, Msg::Increment), format!("Click Me!") ], div![ class!["w-56", "text-center", "mt-2"], format!("Click {} times", model.counter) ] ] } </code></pre> <p>こんな感じの表示になりちゃんと表示されました(クリック時にカウント数を表示するラベルが動くバグがありますが・・・)</p> <p><a href="https://crieit.now.sh/upload_images/06db047230e8021afca815dae9b4f1595ea58291385eb.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/06db047230e8021afca815dae9b4f1595ea58291385eb.png?mw=700" alt="Seed with tailwindcss" /></a></p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>もうちょっとまとまりがあれば良いと思ったのですが, 意外と機能が多く詳細は公式の<a target="_blank" rel="nofollow noopener" href="https://github.com/sagiegurari/cargo-make">README.md</a>を読むのがいいと思います. <a target="_blank" rel="nofollow noopener" href="https://github.com/sagiegurari/cargo-make/tree/master/examples">examplesフォルダ</a>に例が豊富なので参考になると思います.</p> <p>tailwindcssはかなり使いやすいですしSeedもいい感じです(ただビルドが遅いですが・・・).</p> <h2 id="補足"><a href="#%E8%A3%9C%E8%B6%B3">補足</a></h2> <h3 id="WorkspaceとWorkspace Flow"><a href="#Workspace%E3%81%A8Workspace+Flow">WorkspaceとWorkspace Flow</a></h3> <p>タスクにworkspace属性を指定できました. Workspaceとは何でしょうか?</p> <blockquote> <p>A workspace is a set of packages that share the same Cargo.lock and output directory.</p> </blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html#cargo-workspaces">Cargo Workspaces - The Rust Programming Language</a></p> <p>要するに複数のパッケージを一つにまとめたものです. ただし単一のプロジェクトとして管理される前提なので最終生成物やCargo.lockなどで全体のクレートのバージョンなどは共通化されています. 実態は以下のような内容のCargo.tomlとmembers属性で指定されたメンバーとなるパッケージが存在するフォルダです.</p> <pre><code class="toml">[workspace] members = [ "client", "server", ] </code></pre> <p>こうした構成はSeedの<a target="_blank" rel="nofollow noopener" href="https://github.com/seed-rs/seed/tree/master/examples/server_integration">server_integration</a>という例が参考になると思います. 例えば適当なウォークスペースにmakefileを作りworkspace属性を指定します.</p> <pre><code class="toml">[tasks.do_something] workspace = false </code></pre> <p>なぜこのような設定が必要なのでしょうか. 通常cargo-makeのタスクはworkspace直下では実行されません. タスクの要求はメンバー・クレートで実行されます(workspace flow). このおかげでウォークスペースで実行したビルド処理が各クレートで実行されることになり, 共通の処理をウォークスペースにまとめられるので構成ファイルを小さくできます.</p> <p>しかしウォークスペースで実行したい場合もあるでしょう. その場合にこの機能をオフにするのがworkspace属性の意味です. この値はデフォルトでtrueになっています.</p> <pre><code class="toml">[config] default_to_workspace = false </code></pre> <p>のようにも指定するとデフォルト値をfalseに上書きできます.</p> <p>あるいはコマンド実行時にオプションとして渡すこともできます.</p> <pre><code class="bash">cargo make --no-workspace mytask </code></pre> <h3 id="CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILEフラグ"><a href="#CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE%E3%83%95%E3%83%A9%E3%82%B0">CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILEフラグ</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/sagiegurari/cargo-make#automatically-extend-workspace-makefile">CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILEフラグ</a>はウォークスペース直下のmakefileに指定します. そうすると自動的に個々のメンバー・クーレトが持つmakefileはルートのmakefileをextendで読み込み参照できるようになるようです.</p> <h3 id="WASM in A Nutshell"><a href="#WASM+in+A+Nutshell">WASM in A Nutshell</a></h3> <p>WebAssemblyという技術の略称がWASMでコードの拡張子にもなっている. 通常ブラウザはJavaScriptのランタイムを備えており(V8やスパイダーモンキー)JavaScriptのみを実行できる. JIT(Just In Time)コンパイラによる最適化など高速化されたが, 原理的にはランタイムはJavaScriptを逐次解釈してマシンコードに翻訳しそれを実行するために遅い. このプロセスを飛ばせれば, ネットーワークにるRTT(Round-Trip Time)を無視すればネイティブ並みに高速化できるわけです. これはPythonやRubyなどのインタプリタ言語がC/C++やRustなどの言語より遅い事と基本的には同じ関係と言えそうです.</p> <p>そこでWeb版のアセンブラを導入しようという話になるわけです. 通常アセンブリ言語はマシン語と1対1に対応するニーモニックを用いて表現されますが, WASMがターゲットとするのは複数のマシンを抽象化したマシンになります.</p> <blockquote> <p>So WebAssembly is a little bit different than other kinds of assembly. It’s a machine language for a conceptual machine, not an actual, physical machine.</p> </blockquote> <p><img src="https://2r4s9p1yi1fa2jd7j43zph8r-wpengine.netdna-ssl.com/files/2017/02/04-03-toolchain07.png" alt="image" /></p> <p><a target="_blank" rel="nofollow noopener" href="https://hacks.mozilla.org/2017/02/creating-and-working-with-webassembly-modules/">Creating and working with WebAssembly modules</a></p> <p>この説明を聞くとJavaに近い感じを受ける. 実際に(この比較はおかしいけど)WASIとJavaの類似性を指摘した記事なんかもある.</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.infoq.com/jp/news/2019/05/wasi-wasm-system-interface/">MozillaがWASIイニシアティブを発表、WebAssemblyをすべてのデバイス、コンピュータ、オペレーティングシステムで動作可能に</a></p> <p>また公式ではWASMを以下のように定義している.</p> <blockquote> <p>WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine.</p> </blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://webassembly.org/">WebAssembly</a></p> <p>スタック・マシンは良くわからないけどWikipediaによると<a target="_blank" rel="nofollow noopener" href="https://ja.wikipedia.org/wiki/Java仮想マシン">Java仮想マシン</a>も似たような定義で紹介されている.</p> <blockquote> <p>Java仮想マシン (Java virtual machine、Java VM、JVM) は、Javaバイトコードとして定義された命令セットを実行するスタック型の仮想マシン。</p> </blockquote> <h2 id="Reference"><a href="#Reference">Reference</a></h2> <ol> <li><a target="_blank" rel="nofollow noopener" href="https://doc.rust-lang.org/cargo/">The Cargo Book</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/sagiegurari/cargo-make#usage-watch">cargo-make</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/seed-rs/seed-quickstart/blob/master/Makefile.toml">Makefile.toml - seed-quickstart</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://seed-rs.org/guide/view">View - Seed</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://tailwindcss.com/docs/installation">Installation - tailwindcss</a></li> </ol> <h2 id="例題"><a href="#%E4%BE%8B%E9%A1%8C">例題</a></h2> <p>タスクの依存関係, watchモードやcrateの導入などcargo-makeの基本的な使い方を学べる.</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/seed-rs/seed-quickstart">seed-quickstart</a></p> <p>examplesフォルダからルート・フォルダにあるMakefile.tomlの参照法などが参考になりました.</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/seed-rs/seed/tree/master/examples">examples -seed</a></p> <h2 id="課題"><a href="#%E8%AA%B2%E9%A1%8C">課題</a></h2> <p>SeedのようなWebフロントエンドの開発では, プロジェクトをcargoパッケージとしてマインに構成するのかnpmパッケージとしてメインに構成するのかが問題になる. cargo-makeがない場合はnpmパッケージ以下にcargoパッケージを作らないとビルド・プロセスが自動にできない. seed-quickstart-webpackもwebpackを使ってrustライブラリのビルドからwasmモジュールの読み込みなどを行なっている. これをcargo-makeベースに置き換えたい. npm-scriptでコマンド化しておけば, cargo-makeから呼び出せる.</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/seed-rs/seed-quickstart-webpack">seed-quickstart-webpack</a></p> <h2 id="Further Reading"><a href="#Further+Reading">Further Reading</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://hacks.mozilla.org/2017/02/a-crash-course-in-assembly/">A crash course in assembly</a><br /> <a target="_blank" rel="nofollow noopener" href="https://hacks.mozilla.org/2017/02/a-cartoon-intro-to-webassembly/">A cartoon intro to WebAssembly</a><br /> <a target="_blank" rel="nofollow noopener" href="https://hacks.mozilla.org/2017/02/what-makes-webassembly-fast/">What makes WebAssembly fast?</a></p> ブレイン tag:crieit.net,2005:PublicArticle/15774 2020-03-20T12:45:54+09:00 2020-03-20T12:45:54+09:00 https://crieit.net/posts/pass passの使い方 <h1 id="passの使い方"><a href="#pass%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9">passの使い方</a></h1> <p>パスワードストアのpasssの使い方をメモしておきます. <a target="_blank" rel="nofollow noopener" href="https://www.passwordstore.org/">公式</a>の内容を薄めた程度です.</p> <h2 id="用語"><a href="#%E7%94%A8%E8%AA%9E">用語</a></h2> <p>以下passはコマンドのことを指し, passで操作できる対象をパスワードストアと呼ぶ(gitに対するgitリポジトリのような関係です).</p> <h2 id="passの特徴"><a href="#pass%E3%81%AE%E7%89%B9%E5%BE%B4">passの特徴</a></h2> <ul> <li>passはコマンド・ライン・インターフェース(CLI)なのでターミナルから使います.</li> <li>passというコマンドと複数のサブコマンドから操作します.</li> <li>パスワードはGPG(GNU Privacy Guard)で暗号化してパスワードストアに保存される.</li> </ul> <h2 id="passコマンド"><a href="#pass%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89">passコマンド</a></h2> <p>passコマンドを単体で実行するとパスワードストア一覧が表示される. またパスワードストアのパスを指定することで復号化されたパスワードを表示できる.</p> <pre><code>pass test/password </code></pre> <p>復号化されたパスワードは--clipか-cオプションでクリップボードにコピーできます.</p> <pre><code>pass -c test/password </code></pre> <p>既存のパスワードをコピペしたい場合に使えそうです.</p> <h2 id="サブコマンド一覧"><a href="#%E3%82%B5%E3%83%96%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E4%B8%80%E8%A6%A7">サブコマンド一覧</a></h2> <div class="table-responsive"><table> <thead> <tr> <th>サブコマンド名</th> <th>用途</th> </tr> </thead> <tbody> <tr> <td>init</td> <td>パスワードストアの新規作成</td> </tr> <tr> <td>generate</td> <td>パスワードの作成</td> </tr> <tr> <td>rm</td> <td>パスワードの削除</td> </tr> <tr> <td>insert</td> <td>既存のパスワードをパスワードストアに登録する</td> </tr> </tbody> </table></div> <h3 id="init"><a href="#init">init</a></h3> <p>GPGキーを指定して, パスワードストアを新規作成する. デフォルトではパスワードストアは~/.password-storeフォルダに作成される.</p> <pre><code>pass init gpg_key </code></pre> <h3 id="generate"><a href="#generate">generate</a></h3> <p>パスワードのパスと長さを指定してパスワードを生成できます.</p> <pre><code>pass generate test/password 15 </code></pre> <p>-nか--no-symbolsオプションを指定すると数字だけのパスワードが生成できます.</p> <pre><code>pass generate -n test/password 15 </code></pre> <h3 id="rm"><a href="#rm">rm</a></h3> <p>パスワードの削除を行います.</p> <pre><code>pass rm test/password </code></pre> <h3 id="insert"><a href="#insert">insert</a></h3> <p>既にあるパスワードでパスワードストアに登録されていないものを登録する場合に使います.</p> <pre><code>pass insert </code></pre> <p>-mか--multilineオプションをつけると複数行の情報を暗号化して管理できます. 例えばユーザー名, パスワード, e-mailなどの情報をまとめておきたい時などです. スキーマはないので</p> <pre><code>pass insert -m test/user </code></pre> <p>プロンプトが表示されるので入力してCtrl + Dで保存します. 複数のデータをまとめて保存したい場合に使えるかもしれません.</p> <h2 id="Reference"><a href="#Reference">Reference</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://www.passwordstore.org/">pass - the standard unix password manager</a></p> ブレイン tag:crieit.net,2005:PublicArticle/15657 2019-12-29T19:09:51+09:00 2019-12-29T19:09:51+09:00 https://crieit.net/posts/MinQ-8-Go-Ruby-on-Rails MinQ開発日記 (8) GoからRuby on Railsへ <h1 id="GoからRuby on Railsへ"><a href="#Go%E3%81%8B%E3%82%89Ruby+on+Rails%E3%81%B8">GoからRuby on Railsへ</a></h1> <h2 id="変更理由"><a href="#%E5%A4%89%E6%9B%B4%E7%90%86%E7%94%B1">変更理由</a></h2> <p>そもそも自分のサービスをGoで始めたのは</p> <p>GOなら小さくできるので貧乏VPSでも動くだろう</p> <p>というのが主要な理由と</p> <p>デプロイが簡単そう</p> <p>という理由でした. 使いにくいというわけではないのですが, 個人開発のプロジェクトとして面倒が増えてきました.</p> <ul> <li>DBとの連携</li> <li>フロントエンド(主にSPA)との連携</li> </ul> <p>またRuby on Rails(RoR)はAPIサーバー開発が面倒と勝手に思い込んでいたのもありました.</p> <div class="table-responsive"><table> <thead> <tr> <th>理由</th> <th>RoRでの対処</th> </tr> </thead> <tbody> <tr> <td>DB</td> <td>ActiveRecordがある</td> </tr> <tr> <td>SPA</td> <td>webpackオプションが指定できる</td> </tr> <tr> <td>APIサーバー</td> <td>apiオプションが指定できる</td> </tr> <tr> <td>構成</td> <td>検索できる</td> </tr> </tbody> </table></div> <p>Goのフレームワーク選定がイマイチだったのかもしれませんが, 検索できるってのは安心材料ですし, セキュリティについても懸念が払拭できませんでした. RoRなら素人が頭を悩ますような問題はフレームワーク側でしっかりやってくれているでしょう. 対象が勉強のツールというのもRoRへ変更しようと思った理由です. 開発速度を上げるには多少深い理解を犠牲にして, 作ってしまおうとk我慢できなくなったわけです. これと関連してビルド待ちが面倒というのもあります.</p> <h2 id="Goをやって良かった所"><a href="#Go%E3%82%92%E3%82%84%E3%81%A3%E3%81%A6%E8%89%AF%E3%81%8B%E3%81%A3%E3%81%9F%E6%89%80">Goをやって良かった所</a></h2> <p>「俺にはGoは早すぎた」という結論なのですが, Goをやって良かったのはGoは基礎を学ぶのに良い言語だということです. C with Memory Managementという感じのシンプルな文法や比較的低レベルの話がGoでは読めたりしますので, また勉強していきたいです. 後は少しWeb関係の語彙が増えたぐらいでしょうか.</p> <h2 id="RoRの解法"><a href="#RoR%E3%81%AE%E8%A7%A3%E6%B3%95">RoRの解法</a></h2> <p>上述の点をもう少し明確にしておこうと思います.</p> <h3 id="プロジェクト作成"><a href="#%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E4%BD%9C%E6%88%90">プロジェクト作成</a></h3> <p>この辺のGo + Echoでメンドくさかった所です.</p> <pre><code class="bash">rails new project_name </code></pre> <h3 id="DB連携"><a href="#DB%E9%80%A3%E6%90%BA">DB連携</a></h3> <p>DBの指定も簡単です.</p> <pre><code class="bash">rails new project_name --database=mysql rails new project_name -d mysql </code></pre> <h3 id="APIサーバー"><a href="#API%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC">APIサーバー</a></h3> <p>RoRのことはよく知りませんがMVCパターンでWebアプリを開発するということは知っていました. そのためAPIサーバーの開発に使えるとは思っていませんでした.</p> <pre><code class="bash">rails new project_name -d mysql --api --skip-turbolinks </code></pre> <h3 id="SPA"><a href="#SPA">SPA</a></h3> <p>Reactも指摘できます.</p> <pre><code class="bash">rails new project_name -d mysql --api --skip-turbolinks --webpack=react </code></pre> <p>こんなに簡単でいいんでしょうかって感じですが便利です.</p> <h2 id="Reference"><a href="#Reference">Reference</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://guides.rubyonrails.org/api_app.html">Using Rails for API-only Applications</a><br /> <a target="_blank" rel="nofollow noopener" href="https://dev.to/able/building-and-consuming-a-json-api-with-rails-and-react-42p6">Building and Consuming a JSON API with Rails and React</a></p> ブレイン tag:crieit.net,2005:PublicArticle/15630 2019-12-21T15:38:21+09:00 2019-12-21T15:38:21+09:00 https://crieit.net/posts/MinQ-7-Git MinQ開発日記 (7) 認証情報を含んだプロジェクトをGitで管理する <h1 id="認証情報を含んだプロジェクトをGitで管理する"><a href="#%E8%AA%8D%E8%A8%BC%E6%83%85%E5%A0%B1%E3%82%92%E5%90%AB%E3%82%93%E3%81%A0%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92Git%E3%81%A7%E7%AE%A1%E7%90%86%E3%81%99%E3%82%8B">認証情報を含んだプロジェクトをGitで管理する</a></h1> <h2 id="モチベーション"><a href="#%E3%83%A2%E3%83%81%E3%83%99%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3">モチベーション</a></h2> <p>GORMでデータベースへアクセスする場合ユーザー名やパスワードが必要でした. こうした認証情報をgitで管理してGitHubのようなリモート・リポジトリに上げてしまうのは例え非公開でも大変危険です. かといってこれをプロジェクト外部で管理すると, デプロイ時にめんどいうになりそうです. 後間違えコミットしてしまうということも有り得ます.</p> <h2 id="用語"><a href="#%E7%94%A8%E8%AA%9E">用語</a></h2> <h3 id="PGP (Pretty Good Privacy)"><a href="#PGP+%28Pretty+Good+Privacy%29">PGP (Pretty Good Privacy)</a></h3> <p>暗号化プログラムで, 公開暗号方式を使うようです.</p> <h3 id="GPG (GnuPG)"><a href="#GPG+%28GnuPG%29">GPG (GnuPG)</a></h3> <p>GNUによるオープンソースなPGPです.</p> <h3><a target="_blank" rel="nofollow noopener" href="https://www.gnupg.org/software/pinentry/index.html">pinentry</a></h3> <p>GnuPGがパスフレーズやPIN番号を読み取れるようにするプログラムのようです. なくても良さそうでは有ります.</p> <h3><a target="_blank" rel="nofollow noopener" href="https://linux.die.net/man/1/gpg-agent">gpg-agent</a></h3> <p>秘密鍵を管理してくれるデーモンのようです. ssh-agentと同じようなもののようです.</p> <blockquote> <p>gpg-agent is a daemon to manage secret (private) keys independently from any protocol. It is used as a backend for gpg and gpgsm as well as for a couple of other utilities.</p> </blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://linux.die.net/man/1/gpg-agent">gpg-agent(1) - Linux man page</a></p> <blockquote> <p>If the agent process has the key, it provides it to gpg. If it doesn't, it attempts to load the encrypted key from your keyring, and prompts you for the key's passphrase. Once the agent has obtained the decrypted key, it passes it to the gpg process. In addition to GPG keys, Gpg-agent can similarly store SSH keys and provide them to SSH processes, like the ssh-agent program that comes with SSH.</p> </blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://unix.stackexchange.com/a/188813/382631">How does GPG agent work?</a></p> <blockquote> <p>gpg-agent is mostly used as daemon to request and cache the password for the keychain.</p> </blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://wiki.archlinux.org/index.php/GnuPG#gpg-agent">gpg-agent</a></p> <blockquote> <p>gpg-agent can be configured via the pinentry-program stanza to use a particular pinentry user interface when prompting the user for a passphrase.</p> </blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://wiki.archlinux.org/index.php/GnuPG#pinentry">pinentry</a></p> <h3 id="用語の整理"><a href="#%E7%94%A8%E8%AA%9E%E3%81%AE%E6%95%B4%E7%90%86">用語の整理</a></h3> <div class="table-responsive"><table> <thead> <tr> <th>名前</th> <th>用途</th> </tr> </thead> <tbody> <tr> <td>GPG(GnuPG)</td> <td>暗号化や署名の生成を行うプログラム</td> </tr> <tr> <td>Pinetnry</td> <td>GPGにパスフレーズやPIN番号を安全に渡すプログラム</td> </tr> <tr> <td>gpg-agent</td> <td>GPGの代理として鍵を管理するプログラム?</td> </tr> </tbody> </table></div> <p>GPG本体とgpg-agentの違いというか役割分担がよく分かっていませんね😅</p> <h2 id="GnuPG"><a href="#GnuPG">GnuPG</a></h2> <h3 id="導入"><a href="#%E5%B0%8E%E5%85%A5">導入</a></h3> <p>brewを使うと簡単にできます</p> <pre><code class="bash">brew install gnupg gnupg2 pinentry-mac gpg-agent </code></pre> <p>gpg-agentにpinentry-programを指定します.</p> <pre><code class="bash">which pinentry-mac echo "pinentry-program /usr/local/bin/pinentry-mac" >>~/.gnupg/gpg-agent.conf cat ~/.gnupg/gpg-agent.conf </code></pre> <p>ところがgpg-agentは存在しません. GPG 2.1以降はgpg-agent付属して配布されているようです.</p> <p><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/52435650/gpg-agent-not-found-for-homebrew">gpg-agent not found for homebrew</a></p> <blockquote> <p>It expects that the reader is familiar with GnuPG version 2.0 and aware that GnuPG consists of gpg, gpgsm, and gpg-agent as its main components.</p> </blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://www.gnupg.org/faq/whats-new-in-2.1.html">GnuPG</a></p> <p>これでGPGで鍵を生成するときにパスフレーズが求められるとpinentry-programであるpinentry-macが起動するはずです. が, どうも2.0以降はpinentry-programの設定が変わっているようで動きませんでした. Stackoverflowに質問しておきました.</p> <p><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/59433705/where-should-i-specify-a-pinentry-program-for-gnupg-2-0-and-later">Where should I specify a pinentry program for GnuPG 2.0 and later?</a></p> <p>とりあえずなしでいきます.</p> <h3 id="鍵の生成"><a href="#%E9%8D%B5%E3%81%AE%E7%94%9F%E6%88%90">鍵の生成</a></h3> <p>必要となる情報は以下です.</p> <ul> <li>名前</li> <li>e-mail</li> <li>パスフレーズ</li> </ul> <pre><code class="bash">gpg --generate-key </code></pre> <p>で用意した情報を入力しましょう.</p> <h2 id="git-crypt"><a href="#git-crypt">git-crypt</a></h2> <h3 id="導入"><a href="#%E5%B0%8E%E5%85%A5">導入</a></h3> <p>こちらもbrewでインストールします.</p> <pre><code class="bash">brew install git-crypt </code></pre> <h3 id="利用"><a href="#%E5%88%A9%E7%94%A8">利用</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://dev.classmethod.jp/tool/git/git-crypt/">git-crypt を使って秘密情報を版管理する</a>を参考にしてやってみましょう.</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>これで公開鍵を知らない人は中身を見れなくなりました. よってGitHubにアップロードして問題ないと思われます. VPSに公開鍵をアップロードしておけばgit crypt unlockで復号化されます.</p> <h2 id="課題"><a href="#%E8%AA%B2%E9%A1%8C">課題</a></h2> <p>デプロイするときに面倒が増します. Gitのフックはpush時しか出来ないからです. のでGitHubはリモート・リポジトリと割り切ってデプロはAnsibleとか使った方が良いと思いました.</p> <p><a target="_blank" rel="nofollow noopener" href="https://satoshun.github.io/2015/02/ansible-go_deploy/">AnsibleでGoアプリをデプロイ</a></p> <h2 id="Reference"><a href="#Reference">Reference</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://www.digitalocean.com/community/tutorials/an-introduction-to-managing-secrets-safely-with-version-control-systems">An Introduction to Managing Secrets Safely with Version Control Systems</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/daisukeoda/items/c6b6c36009fa3409dc39">本番用の.envを外部に一切知られずに安全にgithubで保存する方法</a><br /> <a target="_blank" rel="nofollow noopener" href="https://dev.classmethod.jp/tool/git/git-crypt/">git-crypt を使って秘密情報を版管理する</a></p> ブレイン tag:crieit.net,2005:PublicArticle/15627 2019-12-20T16:47:14+09:00 2019-12-20T17:17:45+09:00 https://crieit.net/posts/MinQ-6-Go MinQ開発日記 (6) ローカルのGoの開発環境の構築とスキーマ・マイグレーション <h1 id="Echoによるエコー・サーバー"><a href="#Echo%E3%81%AB%E3%82%88%E3%82%8B%E3%82%A8%E3%82%B3%E3%83%BC%E3%83%BB%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC">Echoによるエコー・サーバー</a></h1> <p>とりあえずEchoサーバーを立てる. またローカルに開発環境を構築していなかったことを思い出す. やり方は<a href="https://crieit.net/posts/MinQ-3-Docker-Go">MinQ開発日記 (3) Docker上にGo開発環境を構築</a>と同じと思ったのですがGo Modulesというのがあるようです.</p> <h2 id="構成"><a href="#%E6%A7%8B%E6%88%90">構成</a></h2> <ul> <li>Echo (Webサーバー兼アプリケーション)</li> <li>MariaDB Docker Container</li> <li>React (認証画面とCRUDインターフェース)</li> </ul> <h2 id="goenvとGo Modules"><a href="#goenv%E3%81%A8Go+Modules">goenvとGo Modules</a></h2> <p>Goのバージョン1.13以降は何もしなくてもGo Modulesが使えるようです.</p> <p>その前にGoのバージョン管理ができるようにしましょう. pyenvやnodeenvのようにgoenvというのがあります. 公式の<a target="_blank" rel="nofollow noopener" href="https://github.com/syndbg/goenv/blob/master/INSTALL.md">Installation</a>ガイドを読むと大体わかります. GOENV_ROOTがgoenvバイナリのロケーションです.</p> <pre><code class="bash">export GOENV_ROOT="$HOME/.goenv" export PATH="$GOENV_ROOT/bin:$PATH" eval "$(goenv init -)" export GOPATH="$HOME/go" export PATH="$PATH:$GOPATH/bin" </code></pre> <p>goenvでインストール可能なバージョンを列挙するには以下のようにします.</p> <pre><code class="bash">goenv install - </code></pre> <p>バージョンがずらっと並ぶので, この中から一つ選んでインストールします. 最新版(2019/12/14時点)である1.13.4をインストールします.</p> <pre><code class="bash">goenv install 1.13.4 goenv global 1.13.4 </code></pre> <p>これでgoのバージョンは1.13.4です. Go Modulesは好きなところにフォルダを作れます.</p> <pre><code class="bash">cd ~/your_folder mkdir minq && cd minq go mod init </code></pre> <p>これでgo.modというファイルができました. 適当にserver.goというファイルを作ります(多分main.goの方が良い気もする).</p> <pre><code class="go">package main import ( "net/http" "github.com/labstack/echo" ) func main() { e := echo.New() e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) e.Logger.Fatal(e.Start(":1323")) } </code></pre> <p>minqフォルダ内部で以下を実行すると必要なパッケージ(例えばecho)なんかを勝手にダウンロードしてきて, ビルドしてくれます.</p> <pre><code class="bash">go build </code></pre> <p>minqフォルダの生成物はフォルダ名と同じになるのでこの場合はminqです.</p> <pre><code class="bash">./minq </code></pre> <p>サーバーが起動したらオッケーです. 特別依存性を指定しなくてもimportから解決してくれるっぽいのは良いですが, 反面githubのレポジトリを直接指定する必要があるのは少し面倒でもあります.</p> <h2 id="MariaDB"><a href="#MariaDB">MariaDB</a></h2> <p>brewで入れます.</p> <pre><code class="bash">brew install mariadb </code></pre> <p>起動もbrewを使います.</p> <pre><code class="bash">brew services start mariadb </code></pre> <p>起動したデータベースへアクセスします.</p> <pre><code class="bash">sudo mariadb -uroot </code></pre> <h3 id="GORMで必要になる情報"><a href="#GORM%E3%81%A7%E5%BF%85%E8%A6%81%E3%81%AB%E3%81%AA%E3%82%8B%E6%83%85%E5%A0%B1">GORMで必要になる情報</a></h3> <p>GORMからMariaDBにアクセスするには以下の情報が必要になります.</p> <ul> <li>username</li> <li>password</li> <li>IPアドレス/ホスト名</li> <li>ポート番号</li> <li>データベース名</li> </ul> <h4 id="username &amp; password"><a href="#username+%26amp%3B+password">username & password</a></h4> <p>上の例でmysqlというデータベースがあると思います. このデータベースにuserというテーブルがあります.</p> <pre><code class="SQL">SELECT user FROM mysql.user; </code></pre> <p>名前が被らないように, ここに新しいユーザーを登録します. <a target="_blank" rel="nofollow noopener" href="https://mariadb.com/kb/en/library/create-user/">CREATE USER</a>を使います.</p> <pre><code class="SQL">CREATE USER 'new_name'@'localhost' IDENTIFIED BY 'your_password'; </code></pre> <p>new_nameというユーザーにyour_passwordでアクセスできます. ホストはlocalhostです.</p> <h4 id="ポート番号"><a href="#%E3%83%9D%E3%83%BC%E3%83%88%E7%95%AA%E5%8F%B7">ポート番号</a></h4> <p>システム変数を指定すると以下のように表示できるようです.</p> <pre><code class="bash">show variables like 'port'; </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://mariadb.com/kb/en/library/show-variables/">SHOW VARIABLES</a><br /> <a target="_blank" rel="nofollow noopener" href="https://mariadb.com/kb/en/library/server-system-variables/#port">port</a></p> <p>ちなみに私の環境では3306でした(多分誰でも同じ).</p> <h4 id="データベース名"><a href="#%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E5%90%8D">データベース名</a></h4> <p>MariaDBがアクセスしたユーザー用に管理してるデータベースを表示するのは簡単です.</p> <pre><code class="bash">SHOW DATABASES; </code></pre> <p>testというテスト用のデータベースが存在するはずです. とりあえずこれを使います.</p> <pre><code class="SQL">USE test; </code></pre> <p>これでデータベースがtestに切り替わります. 後でGOMRでちゃんとスキーマが設定できたかを確認しましょう.</p> <h2 id="GORM"><a href="#GORM">GORM</a></h2> <p>GORMでtestデータベースにアクセスしてみましょう. ハンドラを作っておきます. ステータス・コードなんかは現状適当です(DBサーバーでエラーが出たらどうしたら良いのかわからないので).</p> <pre><code class="go">func accessDB(context echo.Context) error { db, err := gorm.Open("mysql", "root:@(127.0.0.1:3306)/test?charset=utf8&parseTime=true") defer db.Close() if err != nil { return context.String(http.StatusOK, err.Error()) } return context.String(http.StatusOK, "Connect to DB") } </code></pre> <p>この時点では以下のようなエラーが出力されます. これはrootユーザーでアクセスする場合sudoer出ないと実行できないからのようです.</p> <pre><code class="bash">Error 1698: Access denied for user 'root'@'localhost' </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/37239970/connect-to-mysql-server-without-sudo">Connect to mysql server without sudo</a></p> <p>ここで上で作ったnew_nameユーザーを使いましょう(名前は適宜読み替えてください). testというデータベースがデフォルトで存在するはずなのでそれを指定しましょう.</p> <pre><code class="go">db, err := gorm.Open("mysql", "new_name:@(localhost:3306)/test?charset=utf8&parseTime=true") </code></pre> <p>該当箇所を変更してビルド&リスタートするとConnect to DBと表示されるはずです(末尾のクエリ文字列的なのは一旦無視します).</p> <pre><code class="go">e.GET("/v1/example/mariadb", accessDB) </code></pre> <h3 id="スキーマ・マイグレーション"><a href="#%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E3%83%BB%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3">スキーマ・マイグレーション</a></h3> <p>データベースのスキーマの更新を行う作業をスキーマ・マイグレーション, あるいは単にマイグレーションと言うようです. ORM(Object Relational Mapping/Mapper)ではオブジェクト(Goの構造体)のフィールドからスキーマを決めるようです. つまり個々のテーブル上のレコードと構造体のインスタンスが対応関係にあるわけです.</p> <pre><code class="go">type User struct { ID int Name string } </code></pre> <p>テーブル名は自動的に雛形となる構造体名の複数形になります. この場合だとusersというテーブルがtestデータベースに追加されます.</p> <pre><code class="go"><br />if db.HasTable("users") { return context.JSON(http.StatusCreated, "User table is already existed") } db.AutoMigrate(&User{}) return context.JSON(http.StatusCreated, "Create users table") </code></pre> <p>適当なハンドラ関数を作って, エンドポイントに紐付けます. MariaDBに戻って以下を実行します.</p> <pre><code class="SQL">SHOW COLUMNS FROM users; </code></pre> <p>これでusersの構造が表示されるはずです.</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>EchoからGORMを通じてMariaDBにアクセスすることができました. GORMのチュートリアルでは利用するデータベースの操作には慣れている前提なのか少しつまづきました.</p> <h2 id="今後"><a href="#%E4%BB%8A%E5%BE%8C">今後</a></h2> <h3 id="React"><a href="#React">React</a></h3> <p>データベースを管理するCRUD用のインターフェースを作ります. <a target="_blank" rel="nofollow noopener" href="https://jaredpalmer.com/formik/docs/overview">formik</a>や<a target="_blank" rel="nofollow noopener" href="https://draftjs.org/">Draft.js</a>を使います. これはできたらPWA(Chrome Desktop)にしてネイティブ・アプリのように実行できるようにしたいです. 詳細に踏み込みたくないのでcreate-react-appを使います. またテストにはCypress.jsを使ってみたいです.</p> <h3 id="認証"><a href="#%E8%AA%8D%E8%A8%BC">認証</a></h3> <p>現状他の人は使わない前提なので, 実験的にWebAuthenticationを使おうと思っています.</p> <h3 id="Gitの導入"><a href="#Git%E3%81%AE%E5%B0%8E%E5%85%A5">Gitの導入</a></h3> <p>プロジェクトはバージョン管理をするのですが, このままだとgom.Openに指定したデータベースのパスワードが丸見えです. 色々方法があるようです. 最初はコマンドライン引数で渡せばいいのかと思ったのですが, Stackoverflowで質問してみるとダメだそうです. コマンドラインにセンシティブなデータを渡すことがそもそもタブーのようです.</p> <p><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/59390753/what-is-a-standard-way-to-specify-password-for-mariadb-in-an-api-server">What is a standard way to specify password for MariaDB in an API server?</a></p> <h4 id="じゃあどうするのか?"><a href="#%E3%81%98%E3%82%83%E3%81%82%E3%81%A9%E3%81%86%E3%81%99%E3%82%8B%E3%81%AE%E3%81%8B%3F">じゃあどうするのか?</a></h4> <p>まずそもそも構成ファイルをgitの管理下に置かないという手があります(.gitignore). あるいは構成ファイルに.exampleのような拡張子を付け適当なパスワードを入れて本番用とは切り分けるやり方です.</p> <p><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/a/2397905/12036118">What is the best practice for dealing with passwords in git repositories?</a></p> <p>ただこの方法はどちらかというとプロジェクト・テンプレートを公開するような用途に使った方がいい気もします. どちらの方法も認証情報は別の方法で管理する必要があります.</p> <p>もう一つは<a target="_blank" rel="nofollow noopener" href="https://www.vaultproject.io/">Vault</a>というソフトウェアを使う方法ですが, こちらはかなり大げさなようです.</p> <p><a target="_blank" rel="nofollow noopener" href="https://opensource.com/article/19/2/secrets-management-tools-git">4 secrets management tools for Git encryption</a></p> <p>今回は<a target="_blank" rel="nofollow noopener" href="https://www.agwa.name/projects/git-crypt/">git-crypt</a>や<a target="_blank" rel="nofollow noopener" href="https://git-secret.io/">git-secret</a>というのがあるようでこの辺を調べてみようと思います.</p> <p><a target="_blank" rel="nofollow noopener" href="https://techblog.bozho.net/storing-encrypted-credentials-in-git/">STORING ENCRYPTED CREDENTIALS IN GIT</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/daisukeoda/items/c6b6c36009fa3409dc39">本番用の.envを外部に一切知られずに安全にgithubで保存する方法</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/jqtype/items/9b0524baa4b7fe6dbde0">Gitリポジトリ暗号化のススメ - git-secret -<br /> Git</a></p> <h2 id="Reference"><a href="#Reference">Reference</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/seicode/items/9ffce10086f0646379a1">【Go】goenvを使ってGo1.13.4の環境構築</a><br /> <a target="_blank" rel="nofollow noopener" href="https://tech.opst.co.jp/2019/07/09/go-modulesも触れてみるgo入門/">Go Modulesも触れてみるGo入門</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/gorilla0513/items/27cd34433a48fc8b65db">Go言語のGORMを使ってみた①</a><br /> <a target="_blank" rel="nofollow noopener" href="http://psychedelicnekopunch.com/archives/639">Golang + GORM + MySQL でデータをやりとりする</a></p> ブレイン tag:crieit.net,2005:PublicArticle/15600 2019-12-12T21:21:54+09:00 2019-12-12T21:21:54+09:00 https://crieit.net/posts/MinQ-5-MinQ MinQ開発日記 (5) MinQが目指すもの & テーブル設計 <h1 id="MinQとは何なのかを考える"><a href="#MinQ%E3%81%A8%E3%81%AF%E4%BD%95%E3%81%AA%E3%81%AE%E3%81%8B%E3%82%92%E8%80%83%E3%81%88%E3%82%8B">MinQとは何なのかを考える</a></h1> <h2 id="動機"><a href="#%E5%8B%95%E6%A9%9F">動機</a></h2> <p>勉強がてら適当に作っていたAPIサーバー. このデータベースのテーブル設計を考え始めたらどんなデータを収めるべきなのかを考える必要が出てきた. めんどくさいわけだが, これはMinQを具体的に掘り下げるのにいい機会にもなると思った.</p> <h2 id="MinQを作ろうと思った理由"><a href="#MinQ%E3%82%92%E4%BD%9C%E3%82%8D%E3%81%86%E3%81%A8%E6%80%9D%E3%81%A3%E3%81%9F%E7%90%86%E7%94%B1">MinQを作ろうと思った理由</a></h2> <p>基本的には勉強履歴を付けようと思ったからです. ただそのためのルールとかサービスで自分に合うものがなかった(見つけられなかった)のでポートフォリオがわりになるかと思って作り始めました.</p> <h2 id="より良い勉強法とは?"><a href="#%E3%82%88%E3%82%8A%E8%89%AF%E3%81%84%E5%8B%89%E5%BC%B7%E6%B3%95%E3%81%A8%E3%81%AF%3F">より良い勉強法とは?</a></h2> <p>勉強に王道はないとよく言われる. 特に楽な方法はないとされます(<a target="_blank" rel="nofollow noopener" href="https://wired.jp/2014/08/15/learn-languages-while-sleeping/">睡眠学習にも一定の効果があると言う話もある</a>という話もありますが・・・). 反復学習が必要でモノの本には脳に汗をかかせることが大事と言う趣旨のことが書いてあった記憶があります. とはいえ闇雲にやっても暗中模索で, 迷子になるのがオチだし, 忘却曲線のように効果がある勉強法もいくつか存在するのは確かです. アウトプットが良いなんて話もあって, 勉強したことをブログに書くと言うこともあるだろう. 備忘録にもなるので設定や環境構築なんかではやる人も多いわけですが, 実際はググった方が速かったりします.</p> <h2 id="勉強法"><a href="#%E5%8B%89%E5%BC%B7%E6%B3%95">勉強法</a></h2> <p>いち初学者として自分が勉強する上でどうやれば良いのかを考えた. また教育系の一般書に目を通して比較的効率的な学習法も調べた. その結果現状あまり良い勉強法と言うか環境が提供されていない気がした.</p> <div class="table-responsive"><table> <thead> <tr> <th align="center">勉強法</th> <th align="center">利点</th> <th align="center">弱点</th> </tr> </thead> <tbody> <tr> <td align="center">アウトプット</td> <td align="center">情報を共有できる</td> <td align="center">グーグル検索問題</td> </tr> <tr> <td align="center">動画</td> <td align="center">分かりやすい</td> <td align="center">効率がいまいち</td> </tr> <tr> <td align="center">文章チュートリアル</td> <td align="center">分かりやすい. 動画よりは効率的</td> <td align="center">探すのが辛い</td> </tr> <tr> <td align="center">スクール</td> <td align="center">ピア・プレッシャでモチベーションを維持できる</td> <td align="center">高額, 初心者向けしかない</td> </tr> <tr> <td align="center">検定(情報処理技術者試験, LPIC)</td> <td align="center">基準が明確</td> <td align="center">試験のインターバルが長い</td> </tr> <tr> <td align="center">本</td> <td align="center">情報の精度は高い(はず)</td> <td align="center">古い, カリキュラムが組みにくい</td> </tr> </tbody> </table></div> <p>誰でも一つまたは複数の勉強法を試したことがあるはずです. さてこのようにコンテンツは沢山あり勉強するには良い時代・・・となりそうですが(別に否定はしませんが), 個人的な不満があったりします.</p> <h3 id="本"><a href="#%E6%9C%AC">本</a></h3> <p>情報が古いとかは承知で買うしそれはそれで勉強になったりするのですが, 復習が困難です. 本によっては課題や復習問題が付いていますが, それを自分で管理するのは面倒です. 特に大量にインプットしたらもう一度同じ本を読むのは億劫です. また全体のカリキュラムも描き難いです. 内容ごとに言語にまたがることも多いので, 効率性も微妙な気はします.</p> <h3 id="検定"><a href="#%E6%A4%9C%E5%AE%9A">検定</a></h3> <p>テストは学習において効果が高いとされていますが, 難易度の調整が微妙な気がします. もっと柔軟な習熟度に合わせた学習法が実現できそうです. 他にもテストのインターバルが長すぎる, 学習履歴が残らないと点が不満です.</p> <h3 id="動画"><a href="#%E5%8B%95%E7%94%BB">動画</a></h3> <p>動画は見ていると楽しいものもありますが, プログラムの場合追いにくい場合があります. 結局GitHubなどに置かれている完成品を読んだり写経するという羽目になったりします. 復習もやりずらいです.</p> <h3 id="スクール"><a href="#%E3%82%B9%E3%82%AF%E3%83%BC%E3%83%AB">スクール</a></h3> <p>金がない(以上)</p> <h3 id="文書"><a href="#%E6%96%87%E6%9B%B8">文書</a></h3> <p>ブログのことと思えばいいです. 検索してヒットするとありがたいのですが勉強法としては, 疑問です. 結局ドキュメントのメモだったりするものもあり, ドキュメントを読むということも多々あります. ただエラーへの対処とかは便利だと思います.</p> <h3 id="アウトプット"><a href="#%E3%82%A2%E3%82%A6%E3%83%88%E3%83%97%E3%83%83%E3%83%88">アウトプット</a></h3> <p>勉強記録とかGitHubにひたすらコードをあげるという感じです. この開発日記もその一つでしょう. 人に教えるというのは高い学習効果があるとされていますのである程度の効果は見込めそうです. しかし見返す前に同じ内容をググってしまったり, 単なるメモや記事として価値がないようなもの(この記事みたいな?)があったりします. さらに時間がかかります. とはいえ自分の記事に助けられるという機会があった人もいるでしょう.</p> <h2 id="MinQはどうする?"><a href="#MinQ%E3%81%AF%E3%81%A9%E3%81%86%E3%81%99%E3%82%8B%3F">MinQはどうする?</a></h2> <p>これらを踏まえてMinQはどうするかです. MinQはクイズベースです. 形式としてはLPICが一番近い気がしますが, 別にプログラミングに限る必要はなく英語や数学なんかもやってみたいです. また学習履歴も細かく管理したいですが, 学習管理サービスは別に作るつもりなのでここでは考えません.</p> <h3 id="なぜクイズ?"><a href="#%E3%81%AA%E3%81%9C%E3%82%AF%E3%82%A4%E3%82%BA%3F">なぜクイズ?</a></h3> <p>テスト形式が比較的学習効果が高いそうです(確かそうだったはずです). 作りやすいし, 還元的な手法は物事を理解する基本だと思います. 解析とかもしたいのでデータを細かく取りたいというのもあります. 学習者(現状対象者は私だけですが)は理解していることよりもしていないことを知りたいわけです. また隙間時間で学習できるようにしたいというのもありこの形式を採用しました.</p> <ul> <li>学習のリズムを作る</li> <li>適切な問を適切な時に</li> </ul> <h3 id="従来のクイズ・アプリ/サービスとの違いは?"><a href="#%E5%BE%93%E6%9D%A5%E3%81%AE%E3%82%AF%E3%82%A4%E3%82%BA%E3%83%BB%E3%82%A2%E3%83%97%E3%83%AA%2F%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%A8%E3%81%AE%E9%81%95%E3%81%84%E3%81%AF%3F">従来のクイズ・アプリ/サービスとの違いは?</a></h3> <p>あくまで個人の実験用アプリなのであんまり詳しく考えてませんが, 以下のような感じでしょうか.</p> <ul> <li>隙間時間の有効活用</li> <li>復習のしやすさ</li> <li>忘却曲線</li> <li>知識/能力の可視化</li> </ul> <p>究極の目標は知識/能力の可視化です. クイズだと単位が小さすぎるのでどうなるか分かりませんが, 知識や技術をネット上で短く切り売りできたら面白いかなと思っています. PRaasS (PRogrammer As a Service)とでも呼んでおきましょうか.</p> <p>そういう意味ではクイズが目的というよりは能力を可視化するというのが大きな目標になりそうです.</p> <h2 id="テーブル設計"><a href="#%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E8%A8%AD%E8%A8%88">テーブル設計</a></h2> <p>でようやく本題ですが, 関係データベースはテーブルから構成されるのですが, テーブルはスキーマと呼ばれる構造を持っています. まずはデータを編集するための管理画面(adminページ)用のテーブルが必要にあります.</p> <div class="table-responsive"><table> <thead> <tr> <th>カラム名</th> <th>用途</th> </tr> </thead> <tbody> <tr> <td>id</td> <td>主キー</td> </tr> <tr> <td>user_name</td> <td>ユーザーの名前</td> </tr> <tr> <td>email</td> <td>メール・アドレス</td> </tr> <tr> <td>password</td> <td>暗号化したパスワード</td> </tr> </tbody> </table></div> <p>こんなところでしょうか. 次はクイズ・テーブルが必要です.</p> <div class="table-responsive"><table> <thead> <tr> <th align="center">カラム名</th> <th align="center">用途</th> </tr> </thead> <tbody> <tr> <td align="center">id</td> <td align="center">主キー</td> </tr> <tr> <td align="center">question</td> <td align="center">問題文</td> </tr> <tr> <td align="center">answer</td> <td align="center">回答/解説</td> </tr> <tr> <td align="center">tag</td> <td align="center">グループ/カテゴリ名</td> </tr> </tbody> </table></div> <p>こんなカラムがあれば良いと思うのですが, 問題はanswerカラムとtagカラムです.</p> <h3 id="問答のフォーマット"><a href="#%E5%95%8F%E7%AD%94%E3%81%AE%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88">問答のフォーマット</a></h3> <p>いろんな形式がありますが, MinQでは<a target="_blank" rel="nofollow noopener" href="https://nbformat.readthedocs.io/en/latest/">The Jupyter Notebook Format</a>を参考しようと思います. この形式の利点は以下です.</p> <ul> <li>複数の形式を簡単に扱える</li> <li>ビューをシンプルに保てる</li> </ul> <p>リスト構造なのでビュー側での表示のロジックが簡単になります. またセルごとに形式を指定するのでパースして内容を解析するという工程が不要です. 例えばMarkdownに数式やコードを埋め込むと, 該当箇所を判別して違うレンダラを利用する必要があります. これをセル単位で管理すると文章か数式かコード片か, あるいは画像かなどは即座に判断できます. 表示もセルのリストなので比較的簡単です.</p> <h3 id="tagと関連実体"><a href="#tag%E3%81%A8%E9%96%A2%E9%80%A3%E5%AE%9F%E4%BD%93">tagと関連実体</a></h3> <p>クイズの分類のためにタグをつける場合, クイズが複数の分類に跨ることは普通にありえることです.そのためクイズには複数のタグを持たせられるようにするべきです. 一方でタグからクイズを絞り込むということもできた方が便利でしょう.</p> <p><a target="_blank" rel="nofollow noopener" href="https://charlesleifer.com/blog/a-tour-of-tagging-schemas-many-to-many-bitmaps-and-more/">A Tour of Tagging Schemas: Many-to-many, Bitmaps and More</a>にはいくつかの方法が解説されていて面白いです.</p> <p>もっとも素朴な解決策はタグをCSV(Character Seperated Value)形式で保存する方法です. 表示だけのことを考えるとこれで十分ですが, タグをつける理由は表示のためではないでしょう. またビットマップという手法もあります. ビット列をそれぞれのタグの有無と対応づける方法です. n番目のビットが1ならAタグが付いていることと見なすわけです. この方法は検索は効率的ですが, ビット列以上のタグを持てません.</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.shoeisha.co.jp/book/detail/9784798124704">達人に学ぶDB設計 徹底指南書</a>では学生エンティティと講義エンティティの例が示されています. 両者を繋ぐのが関連実体/関連エンティティの受講です. この受講エンティティと呼ばれるエンティティは学生エンティティと講義エンティティの主キーを組み合わせたキーを主キーします. こうしたテーブル間の関係を多対多の関連 (Many-to-Many relationship)というらしいですが, GORMでは<a target="_blank" rel="nofollow noopener" href="http://gorm.io/docs/many_to_many.html">Joinテーブル</a>という中間テーブルで管理するようです. この機能を使うようにします.</p> <p>## MariaDB</p> <p>RDBMS(Relational DataBase Management System)としてはMariaDBを使おうと思います.</p> <p>### 導入</p> <p>CentOS 7にはMariaDBが入っていますが, 5.5とバージョンが古いです.</p> <p>```bash<br /> $ yum list installed | grep mariadb</p> <p>mariadb.x86_64 1:5.5.64-1.el7 @base<br /> mariadb-libs.x86_64 1:5.5.64-1.el7 @base<br /> mariadb-server.x86_64 1:5.5.64-1.el7 @base<br /> ```</p> <p>これをまずアンインストールします. <a target="_blank" rel="nofollow noopener" href="https://mariadb.com/resources/blog/installing-mariadb-10-on-centos-7-rhel-7/">How to install MariaDB 10 on CentOS 7 / RHEL 7</a>に従って, インストールします. 現状ではバージョン10.4が安定バージョンのようです.</p> <p><a href="https://crieit.now.sh/upload_images/f9bf88a0a074e399496de2154d0154d25df208e9769e9.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f9bf88a0a074e399496de2154d0154d25df208e9769e9.png?mw=700" alt="Screenshot 2019-12-12 at 18.30.01.png" /></a></p> <p><code>bash sudo systemctl enable mariadb sudo systemctl start mariadb</code></p> <p>これでMariaDBが起動しました.</p> <h3 id="セキュリティ"><a href="#%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3">セキュリティ</a></h3> <p>次はセキュリティの設定です.</p> <pre><code class="bash">sudo mysql_secure_installation </code></pre> <p>基本的には全てYesでいいらしいです. 途中rootパスワードの設定が必要なのでパスワードを用意しておきましょう.</p> <pre><code class="bash">mysql -u root -p </code></pre> <p>上で設定したパスワードを入力してMariaDBと表示されればオッケーのようです.</p> <h3 id="文字コードの変更と寿司ビール問題"><a href="#%E6%96%87%E5%AD%97%E3%82%B3%E3%83%BC%E3%83%89%E3%81%AE%E5%A4%89%E6%9B%B4%E3%81%A8%E5%AF%BF%E5%8F%B8%E3%83%93%E3%83%BC%E3%83%AB%E5%95%8F%E9%A1%8C">文字コードの変更と寿司ビール問題</a></h3> <p>デフォルトの文字コードはutf8とlatin1が混在しているのですが, これをutf8mb4という絵文字も扱える方式に統一するという話です. またutf8mb4にしても照合順序の関係で寿司とビールの絵文字が同値として扱われるという問題があるそうです. ただいずれも絵文字に対応する処理なので現状関係なさそうなのでほっときます.</p> <p><a target="_blank" rel="nofollow noopener" href="https://onoredekaiketsu.com/mariadb-secure-installation-and-chenge-to-utf8mb4/">MariaDB(MySQL)初期設定時のセキュリティとutf8mb4化の手順</a>を参照するなりググルなりしてください.</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>次は管理画面とログイン機能を作っていきたいと思います.</p> <h2 id="Reference"><a href="#Reference">Reference</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://linuxize.com/post/install-mariadb-on-centos-7/">Install MariaDB on CentOS 7</a><br /> <a target="_blank" rel="nofollow noopener" href="https://www.digitalocean.com/community/tutorials/how-to-install-mariadb-on-centos-7">How To Install MariaDB on CentOS 7</a></p> ブレイン tag:crieit.net,2005:PublicArticle/15592 2019-12-08T16:47:41+09:00 2019-12-08T16:47:41+09:00 https://crieit.net/posts/MinQ-4 MinQ開発日記 (4) 本番環境の構築 <h1 id="本番環境構築"><a href="#%E6%9C%AC%E7%95%AA%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89">本番環境構築</a></h1> <h2 id="CentOS 7"><a href="#CentOS+7">CentOS 7</a></h2> <p>CentOS7が標準で選べます.</p> <p><a href="https://crieit.now.sh/upload_images/ceb0d333b6cc5873df69c90c42453fdd5de8fa3f0b9ca.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/ceb0d333b6cc5873df69c90c42453fdd5de8fa3f0b9ca.png?mw=700" alt="Screenshot 2019-12-05 at 18.47.08.png" /></a></p> <h2 id="SSHの設定"><a href="#SSH%E3%81%AE%E8%A8%AD%E5%AE%9A">SSHの設定</a></h2> <p>パケット・フィルタリングという機能がVPSに備わっておりこれを無効にする必要があったりします.</p> <p><a target="_blank" rel="nofollow noopener" href="http://www.wisesips.com/service/staff_column/207.html">【初心者向け】さくらVPSのSSH初期設定で22番以外のポートに繋がらない</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/Qsugi/items/2bfaf1f72831c58331f8">SSHポート番号を変更したら繋がらなくなった(さくらVPS CentOS7)</a><br /> <a target="_blank" rel="nofollow noopener" href="https://femoghalvfems.info/archives/20672">さくらの VPS に CentOS7 を入れて SSH と Firewalld の設定につまづいた話 – TURNIP 2</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/JohnDoe8231/items/fd95374ef85f9ebdc420">さくらのVPSにおけるポート開放の落とし穴</a></p> <h3 id="手順"><a href="#%E6%89%8B%E9%A0%86">手順</a></h3> <ol> <li>パッケージのアップデート</li> <li>一般ユーザーの設定 & sudoer</li> <li>ウェルノウン・ポートからの変更</li> <li>ルート・ログインの不許可</li> <li>sshd & firewalldの起動</li> <li>パケット・フィルタリングの無効化</li> <li>その他</li> </ol> <h3 id="1. パッケージのアップデート"><a href="#1.+%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%87%E3%83%BC%E3%83%88">1. パッケージのアップデート</a></h3> <p>sshでサーバーに接続します.</p> <pre><code class="bash">ssh -p 22 [email protected] </code></pre> <p>IPアドレス(@マーク以下のxxx.xxx.xxx.xxxのところ)はさくらVPSのコンソールからコピーできます. 以下の画像のIPv4テーブルのアドレス項目に記載されているCSV(Character Seperated Value)です.</p> <p><a href="https://crieit.now.sh/upload_images/eda4aeaa6533e12e92e44e0ed1d5009b5deb288dc9f05.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/eda4aeaa6533e12e92e44e0ed1d5009b5deb288dc9f05.png?mw=700" alt="名称未設定 - さくらのVPSコントロールパネル 2019-12-07 13-17-02.png" /></a></p> <p>root@と指定したので, ルート・ログインです. ログインしたらとりあえずyumでパッケージを更新しておきます.</p> <pre><code class="bash">yum -y update </code></pre> <h3 id="2. 一般ユーザーの設定"><a href="#2.+%E4%B8%80%E8%88%AC%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%AE%E8%A8%AD%E5%AE%9A">2. 一般ユーザーの設定</a></h3> <p>ルート・ユーザーで管理をやるのは問題があるので, 必要な時だけルート権限で実行できるようにします. ユーザー名を決めて以下を入力します. 今回はcentkunとして進めます.</p> <pre><code class="bash">adduser centkun && passwd centkun </code></pre> <p>とするとプロンプトが表示されるのでパスワードを確認用も含めて2回入力します.</p> <p>次はsudoerを設定します. suでルート権限が必要な時にルート・ユーザーになるのも良いのですが, sudoの方が個人的に慣れているのでそうします.</p> <pre><code class="bash">usermod -aG wheel centkun </code></pre> <p>一般ユーザーに切り替えるにはsuコマンドを利用します.</p> <pre><code class="bash">su centkun </code></pre> <p>SSHの設定のためにsshd_configへのアクセスが必要ですがcentkunではできないことを確認しましょう.</p> <pre><code class="bash">cat /etc/ssh/sshd_config </code></pre> <p>Permission deniedと出ればオッケーです. このように一般ユーザーにはルートにしか編集や閲覧ができないなどのアクセス制限が課せられています. suやsudoを使うと一時的にその権限を取得できます. 一先ずサーバーから抜けてcentkunで接続し直します.</p> <pre><code class="bash">exit </code></pre> <pre><code class="bash">ssh -p 22 [email protected] </code></pre> <p>ここでcentkunをsudoerとして登録します. これはCentOSではwheelというグループに登録することです.</p> <p><a target="_blank" rel="nofollow noopener" href="https://vps-news.sakura.ad.jp/tutorials/centos7-initial-settings/">wheel グループに対する sudo 設定の有効化</a></p> <p>一旦suコマンドでルート・ユーザーにになってwheelグループにcentkunを追加します.</p> <pre><code class="bash">su usermod -aG wheel $USER exit </code></pre> <p>これで適当なコマンドでsudoerとしてアクセス権を取得できているか見てみましょう.</p> <pre><code class="bash">sudo ls </code></pre> <p>centkunのパスワードを実行してカレント・ディレクトリの内容が表示されたらオッケーです. 以後はsudoで権限の切り替えを行います.</p> <h3 id="3. ウェルノウン・ポートからの変更"><a href="#3.+%E3%82%A6%E3%82%A7%E3%83%AB%E3%83%8E%E3%82%A6%E3%83%B3%E3%83%BB%E3%83%9D%E3%83%BC%E3%83%88%E3%81%8B%E3%82%89%E3%81%AE%E5%A4%89%E6%9B%B4">3. ウェルノウン・ポートからの変更</a></h3> <p>ポートとは同一ホスト上(コンピュータのこと)で動いている複数のサービス(常駐のプログラムのこと)を区別するための番号です. httpdやsshdなどによって指定するポートが違っています. ポートには三種類あります.</p> <div class="table-responsive"><table> <thead> <tr> <th>ポート番号名</th> <th>範囲</th> <th>説明</th> </tr> </thead> <tbody> <tr> <td>Well-know Ports</td> <td>1~1023</td> <td>IANAが管理している. 特定のサービス以外には使えない</td> </tr> <tr> <td>Registered Ports</td> <td>1024~49151</td> <td>IANAが管理している. 自由に使える</td> </tr> <tr> <td>Dynamic Ports</td> <td>49152~65535</td> <td>IANAが管理していない. 自由に使える</td> </tr> </tbody> </table></div> <p>通常はSSHのウェルノウン・ポートである22が指定されています. これまであえてpオプションで明示的に指定しました. またこの設定はさくらVPSのダッシュボードからパケット・フィルタリングで有効/無効に設定できます.</p> <p>SSHのサービスを提供するsshdの設定なのでsshd_configからポート番号を変更します.</p> <pre><code class="bash">sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.orig sudo vim /etc/ssh/sshd_config </code></pre> <p>Port項目をアンコメントして番号を22から1024以降の番号に変更しましょう. 次はfirewalldの設定を行います.</p> <pre><code class="bash">sudo vim /user/lib/firewalld/services/ssh.xml </code></pre> <p>のportの部分を利用するポート番号へ変更しておきましょう. これで指定ポートがファイアウォールを通り抜けられます.</p> <h3 id="4. ルート・ログインの不許可"><a href="#4.+%E3%83%AB%E3%83%BC%E3%83%88%E3%83%BB%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AE%E4%B8%8D%E8%A8%B1%E5%8F%AF">4. ルート・ログインの不許可</a></h3> <p>次はルートでSSHを通じてログイン出来ないようにします.</p> <pre><code class="bash">sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.orig sudo vim /etc/ssh/sshd_config </code></pre> <p>PermitRootLoginをアンコメントしnoに変更します.</p> <h3 id="5. sshdの再起動 &amp; firewalldの起動"><a href="#5.+sshd%E3%81%AE%E5%86%8D%E8%B5%B7%E5%8B%95+%26amp%3B+firewalld%E3%81%AE%E8%B5%B7%E5%8B%95">5. sshdの再起動 & firewalldの起動</a></h3> <p>sshdの設定(sshd_config)を読み込ませるためにsshdを再起動します.</p> <pre><code class="bash">sudo systemctl restart sshd </code></pre> <p>そしてfirewalldは起動していないので, 起動します.</p> <pre><code class="bash">systemctl start firewalld </code></pre> <p>後は<a target="_blank" rel="nofollow noopener" href="https://www.sakura-vps.net/centos7-setting-list/ssh-firewall-security-settings-for-sakura-vps-centos7/">接続時のセキュリティの設定(CentOS 7)</a>を参考に設定してみましょう.</p> <pre><code class="bash">firewall-cmd --info-service ssh </code></pre> <p>を確認してportsで指定した番号が表示されていればOKです.</p> <pre><code class="bash">sudo firewall-cmd --zone=public --list-all </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://vps-news.sakura.ad.jp/tutorials/centos7-initial-settings/">応用:SSH 接続用ポート番号の変更</a><br /> <a target="_blank" rel="nofollow noopener" href="https://knowledge.sakura.ad.jp/10583/">ネコでもわかる!さくらのVPS講座 ~第七回「ファイアウォール”firewalld”について理解しよう」</a></p> <h3 id="6. パケット・フィルタリングの無効化"><a href="#6.+%E3%83%91%E3%82%B1%E3%83%83%E3%83%88%E3%83%BB%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0%E3%81%AE%E7%84%A1%E5%8A%B9%E5%8C%96">6. パケット・フィルタリングの無効化</a></h3> <p>さくらVPSには<a target="_blank" rel="nofollow noopener" href="https://vps-news.sakura.ad.jp/vps-pf/?_gl=1*1a8iyj1*_gcl_aw*R0NMLjE1NzUwMzkxNTEuQ2owS0NRaUFvSVB2QlJEZ0FSSXNBSHNDdzBfekVzbC1mWFlaWkc5NW1xaHFHb0I3UmYyYnBIbUZ0ZGpKMmpRQ2dqaVcwZHNCR29hT3pmQWFBa3RxRUFMd193Y0I.&_ga=2.33919909.565247476.1575450354-1929858640.1570088790&_gac=1.216837860.1574593957.CjwKCAiA8ejuBRAaEiwAn-iJ3lTkKK9FNP2R5dkKDyHTAu6tEXn6rc7UTRZop6oAtuHSEKhDsLJF3hoCVmMQAvD_BwE">パケット・フィルタリング</a>という機能があってこれはすでにピットフォール(はまりポイント)として知られているようです.</p> <p>基本的な設定としてWebやSSHなどのサービスへのWellknow-portは変更しておく必要があるらしい. しかしパケット・フィルタリングが有効になっているとファイアウォールやSELinuxの設定をするとリモート接続できなくなる.</p> <p>無効にするのが重要らしいがこのパケット・フィルタリングの何が嬉しいのかよく分からないです. とりあえず無効にします.</p> <p><a href="https://crieit.now.sh/upload_images/6512a2cfed52c183cf6a3e08770197865dea44bbef2c7.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6512a2cfed52c183cf6a3e08770197865dea44bbef2c7.png?mw=700" alt="Screenshot 2019-12-06 at 21.07.56.png" /></a></p> <p>注意書通りにファイアウォールとセキュリティ設定を行いましょう.</p> <p>後は<a target="_blank" rel="nofollow noopener" href="https://www.sakura-vps.net/centos7-setting-list/ssh-firewall-security-settings-for-sakura-vps-centos7/">接続時のセキュリティの設定(CentOS 7)</a>を参考に設定行います.</p> <h3 id="7. その他"><a href="#7.+%E3%81%9D%E3%81%AE%E4%BB%96">7. その他</a></h3> <h4 id="SELinuxの設定 &amp;&amp; パスワード認証の禁止など"><a href="#SELinux%E3%81%AE%E8%A8%AD%E5%AE%9A+%26amp%3B%26amp%3B+%E3%83%91%E3%82%B9%E3%83%AF%E3%83%BC%E3%83%89%E8%AA%8D%E8%A8%BC%E3%81%AE%E7%A6%81%E6%AD%A2%E3%81%AA%E3%81%A9">SELinuxの設定 && パスワード認証の禁止など</a></h4> <p><a target="_blank" rel="nofollow noopener" href="https://www.shoeisha.co.jp/book/detail/9784798146379">ゼロからはじめるLinuxサーバー構築・運用ガイド 動かしながら学ぶWebサーバーの作り方</a>では必要なように書いていました.</p> <p><a target="_blank" rel="nofollow noopener" href="https://weblabo.oscasierra.net/openssh-sshd-centos7-change-port/">CentOS 7 で sshd のポート番号を変更する方法</a>によると</p> <ul> <li>SELinuxは指定してないポートからプロセスの起動を禁止する</li> <li>Firewallは許可されていないポートへそもそもアクセス出来ない</li> </ul> <p>という違いがあるようです. 気になる人は設定してみると良いのかもしれません. 現状以下のようにリモートログインしている.</p> <pre><code class="bash">ssh -p 22 [email protected] </code></pre> <p>これはパスワード認証方式と呼ぶが, パスワード漏洩・推測などのリスクがある. 暗号を使った公開鍵方式を使うと安全性が高まるらしい.</p> <p><a target="_blank" rel="nofollow noopener" href="https://vps-news.sakura.ad.jp/tutorials/centos7-initial-settings/">公開鍵認証を使ったログイン設定と確認</a></p> <h4 id="ファイウォールの状態"><a href="#%E3%83%95%E3%82%A1%E3%82%A4%E3%82%A6%E3%82%A9%E3%83%BC%E3%83%AB%E3%81%AE%E7%8A%B6%E6%85%8B">ファイウォールの状態</a></h4> <p>サービスとしてファイアウォールが有効かどうかを確認する.</p> <pre><code class="bash">$ firewall-cmd --state </code></pre> <p>runningと出れば起動しています. あるいは</p> <pre><code class="bash">$ systemctl status firewalld.service </code></pre> <p>こちらは情報量が多いですがactive (running)となっていれば起動していることがわかります.</p> <h4 id="ファイアウォールの現在の設定を確認する."><a href="#%E3%83%95%E3%82%A1%E3%82%A4%E3%82%A2%E3%82%A6%E3%82%A9%E3%83%BC%E3%83%AB%E3%81%AE%E7%8F%BE%E5%9C%A8%E3%81%AE%E8%A8%AD%E5%AE%9A%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B.">ファイアウォールの現在の設定を確認する.</a></h4> <pre><code class="bash">firewall-cmd --list-all </code></pre> <h4 id="ファイアウォールの管理"><a href="#%E3%83%95%E3%82%A1%E3%82%A4%E3%82%A2%E3%82%A6%E3%82%A9%E3%83%BC%E3%83%AB%E3%81%AE%E7%AE%A1%E7%90%86">ファイアウォールの管理</a></h4> <p>サービスとしてのファイアウォールを管理するにはsystemctlコマンドを使います.</p> <pre><code class="bash">systemctl start firewalld systemctl stop firewalld systemctl restart firewalld </code></pre> <p>さくらVPSはデフォルトではfirewalldが起動していなかったのでstartしておく必要があります.</p> <p><a target="_blank" rel="nofollow noopener" href="https://knowledge.sakura.ad.jp/10583/">ネコでもわかる!さくらのVPS講座 ~第七回「ファイアウォール”firewalld”について理解しよう」</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/kenjjiijjii/items/1057af2dddc34022b09e">CentOS 7 firewalld よく使うコマンド</a><br /> <a target="_blank" rel="nofollow noopener" href="http://mzgkworks.com/post/cetos7-ssh-port-change/">CentOS 7でSSHのポート番号を変更する(SELinux・firewalld)</a></p> <p>以上これで良い, といことはないんでしょうがLinuxの勉強不足でよく分かりません.</p> <h2 id="アプリケーションの環境"><a href="#%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E7%92%B0%E5%A2%83">アプリケーションの環境</a></h2> <h3 id="goのインストール"><a href="#go%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">goのインストール</a></h3> <p>以下の画像ようにリンクを辿ります.</p> <p><a href="https://crieit.now.sh/upload_images/252ff481829f372373c99af1f47f36d05deb87fe63141.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/252ff481829f372373c99af1f47f36d05deb87fe63141.png?mw=700" alt="Documentation - The Go Programming Language 2019-12-07 20-04-57.png" /></a></p> <p><a href="https://crieit.now.sh/upload_images/e2cbcd8f6bd01ab5ae330f1915d886ca5deb889fee178.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/e2cbcd8f6bd01ab5ae330f1915d886ca5deb889fee178.png?mw=700" alt="Getting Started - The Go Programming Language 2019-12-07 20-08-44.png" /></a></p> <p><a href="https://crieit.now.sh/upload_images/4b80fc5d30abdcfdd81ea7efb39098605deb88ab05c98.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/4b80fc5d30abdcfdd81ea7efb39098605deb88ab05c98.png?mw=700" alt="Downloads - The Go Programming Language 2019-12-07 20-09-28.png" /></a></p> <p>最後の画像のリンク先をコピーします. ダウンロードしたバイナリを/usr/local以下に配置します.</p> <pre><code class="bash">sudo wget https://dl.google.com/go/go1.13.5.linux-amd64.tar.gz sudo tar -xvf go1.13.5.linux-amd64.tar.gz mv go /usr/local </code></pre> <p>.bashrc以下にgoのパスを設定します. goのパスはやや独善的(opinionated)に決定されていますが, プロジェクトの保存場所に迷ったり生成した実行可能バイナリに一々パスを通さなくていいので便利です. ただし実際に配置されるのはwebアプリのバイナリだけなので, こんな複雑なフォルダ構成は必要ない気もしますが・・・.</p> <pre><code class="bash">export PATH=$PATH:/usr/local/go/bin # 1 export GOPATH=$HOME/go/package:$HOME/go/workspace # 2 export PATH=$HOME/go/package/bin:$HOME/go/workspace/bin:$PATH # 3 </code></pre> <ol> <li>goコマンドへのパスです. これでコマンドラインからgoが使えます.</li> <li>GOPATHはgoのプロジェクトを管理するフォルダを指定します. プロジェクトには二つあって, 外部パッケージと自作パッケージです. go getで取得した外部パッケージはGOPATHの最初に指定したフォルダにインストールされます. 自作パッケージはworkspace以下に作成します.</li> <li>コンパイルしたプロジェクトはbin以下に出力されますので, このパスを追加しておくとコマンドラインから直ぐ呼び出せます.</li> </ol> <h3 id="MariaDBのインストール"><a href="#MariaDB%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">MariaDBのインストール</a></h3> <p>yumでインストールできます.</p> <pre><code class="bash">sudo yum -y install mariadb-server.x86_64 </code></pre> <p>MariaDBはサービスとして実行されるのでsystemctlを使って起動します.</p> <p><a target="_blank" rel="nofollow noopener" href="https://linuxize.com/post/install-mariadb-on-centos-7/">Install MariaDB on CentOS 7</a></p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>サーバーはめんどくさいということでしたが, 基本的なことしかしてないためかそれほど迷うことはなかったです.</p> <p>とりあえずGoとMariaDBはやったので, 次回はデプロイをやりたいです. 公開までもう少しです.</p> <h2 id="Reference"><a href="#Reference">Reference</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://www.sakura-vps.net/centos7-setting-list/ssh-firewall-security-settings-for-sakura-vps-centos7/">接続時のセキュリティの設定(CentOS 7)</a><br /> <a target="_blank" rel="nofollow noopener" href="https://www.digitalocean.com/community/tutorials/an-introduction-to-selinux-on-centos-7-part-1-basic-concepts">An Introduction to SELinux on CentOS 7 – Part 1: Basic Concepts</a><br /> <a target="_blank" rel="nofollow noopener" href="https://linuxize.com/post/how-to-setup-a-firewall-with-firewalld-on-centos-7/">How to Set Up a Firewall with FirewallD on CentOS 7</a><br /> <a target="_blank" rel="nofollow noopener" href="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/system_administrators_guide/sec-security">1.6. ENHANCING SYSTEM SECURITY WITH A FIREWALL, SELINUX AND SSH LOGINGS</a></p> ブレイン tag:crieit.net,2005:PublicArticle/15580 2019-12-04T19:16:21+09:00 2019-12-04T19:16:21+09:00 https://crieit.net/posts/MinQ-3-Docker-Go MinQ開発日記 (3) Docker上にGo開発環境を構築 <h1 id="Goの導入"><a href="#Go%E3%81%AE%E5%B0%8E%E5%85%A5">Goの導入</a></h1> <p>前回の続きです. まずGoを入れてファイアウォールの設定をして静的サイトを配信できるかチェックしてみようと思います.</p> <p><a target="_blank" rel="nofollow noopener" href="https://knowledge.sakura.ad.jp/serialization/understood-cats-vps/">ネコでもわかる!さくらのVPS講座</a>の<a target="_blank" rel="nofollow noopener" href="https://knowledge.sakura.ad.jp/8541/">第三回「Apacheをインストールしよう」</a>と<a target="_blank" rel="nofollow noopener" href="https://knowledge.sakura.ad.jp/9006/">第四回「phpとMariaDBをインストールしよう」</a>の内容がベースになります.</p> <h2 id="Go言語を選んだ理由"><a href="#Go%E8%A8%80%E8%AA%9E%E3%82%92%E9%81%B8%E3%82%93%E3%81%A0%E7%90%86%E7%94%B1">Go言語を選んだ理由</a></h2> <p>本当はRustのRocketとかでやりたかった. のだがnightlyとか非同期とか色々と不安定な時期を完全に脱したとは言えない. 加えてRust自体が結構難しく(基本はそうでもないのだが)ベストプラクティスが不明である. それに比べるとGoは学習の困難が少ないように見える(最適化とかは知らん).</p> <ol> <li>メモリ管理不要</li> <li>静的な型</li> <li>ビルトインWebサーバーとその容易な拡張性</li> <li>クラスと継承がない</li> <li>文法が親しみやすい</li> <li>プログラミング全体を学ぶ教材が揃っている気がする</li> <li>クリーン・アーキテクチャとの相性が良さそう</li> </ol> <p>1は大抵の動的な言語でも満たすので意味はないですが, 静的な型があるのは良い場合があります. クリーン・アーキテクチャは各層を依存性逆転で依存の方向が内側に向かうようにしています. これがPythonでは不可能ではないですが不自然さは否めません. Goはインターフェースがあるので特に悩まず実装できそうです.</p> <p>ローカルなどの開発環境で動かせてもプロダクション環境では色々設定が必要になるのは面倒です. シンプルな三層アーキテクチャとしてもWebサーバー, APサーバー, DBサーバーが必要で, それ以外にアプリケーション・ロジックをWebフレームワークで作る必要があります. このうちWebサーバー, APサーバー, アプリケーション・ロジックがGoなら一纏めでできると思ったからです. 要はさっさとデプロイしたいということです.</p> <blockquote> <p>Composition over Inheritance</p> </blockquote> <p>らしく継承がないです. そもそもクラスないです. 構造体を組み合わせることになります. その他の文法もシンプルです. メソッド名の前にレシーバーを指定する記法は少し面食らいますが, selfを暗に陽に第一引数として取るよりは特別な引数感が出せます. なお同期処理なんかも簡単らしいです.</p> <p>学習用言語としてはPythonが動く擬似コードなんて呼ばれることがありますが, Pythonの利用用途は数値計算とかその応用である機械学習などに集中しているように思います. 一方Goはランタイムがあるものの『Goならわかるシステムプログラミング』とか『Real World HTTP』, 『Goプログラミング実践入門』などいろんなことが基礎から学べます. 他にも『Writing A Compiler In Go』や『Writing A Interpreter In Go (Go言語でつくるインタプリタ) 』なんかもあり学習用言語としても優秀かなと思いました.</p> <p>最後はMVCではなくクリーン・アーキテクチャを導入したいと思っているという話です. すでにこの話はしましたが, 型がある方が有利と思います.</p> <h3 id="PHPでない理由"><a href="#PHP%E3%81%A7%E3%81%AA%E3%81%84%E7%90%86%E7%94%B1">PHPでない理由</a></h3> <p>PHPの良いところはHTMLとの親和性だと思う. テンプレートが必要な場合(動的レンダリングなど)では強みになると思う. Wordpressなんかが良い例だと思う. 一方Web APIのようにテンプレートが原則必要ない場合はこの優位点は消えると思う. MVCのようにサーバー側で色々やるからPHPの特色が生かせるのではないか. Vが切り離された状況なら他の言語でも良い気がしてならない. Laravelとか便利そうだがAPIサーバーを作るにはデカい. Lumenというのもあるが, そこまでしてPHPを使うべきなのか良くわからなくなってしまった.</p> <h3 id="Pythonでない理由"><a href="#Python%E3%81%A7%E3%81%AA%E3%81%84%E7%90%86%E7%94%B1">Pythonでない理由</a></h3> <p>最終的にはクイズの成績をデータ分析とかしてみたいという野望がある. この点から言えばPythonを選択することは将来性があるとは思う. 文法的にもPythonの方が慣れているとは思う. Flaskのデコレータを使ったルートの定義は視認性が高いし, FastAPIというイベント駆動なWebフレームワークもある. ただデータ分析となるとバッチ処理になる気はする. 流行りのAIでクイズをレコメンドしたり最適な教材を生成したりとかなると意義はあるのかもしれないが, その元となるAPIがPythonである必要性はない気はする. よってクイズの成績とかを管理するWebアプリはPythonでやってみたい.</p> <h3 id="Rubyでない理由"><a href="#Ruby%E3%81%A7%E3%81%AA%E3%81%84%E7%90%86%E7%94%B1">Rubyでない理由</a></h3> <p>Rubyの他のWebフレームワークは知らないのだが, RubyやるならやっぱりRoRがやりたいとも思うが, やはりデカすぎる気がする. 後昔gemが動かなかったトラウマからRubyに苦手意識がある.</p> <h3 id="Node.jsでない理由"><a href="#Node.js%E3%81%A7%E3%81%AA%E3%81%84%E7%90%86%E7%94%B1">Node.jsでない理由</a></h3> <p>JavaScriptはフロントだけで十分です(しかもこいつだけ言語じゃねぇ). APIサーバーを立てるわけですから, APIができたらJAMStackな構成でサイトを作ってみたいです.</p> <h3 id="Elixirでない理由"><a href="#Elixir%E3%81%A7%E3%81%AA%E3%81%84%E7%90%86%E7%94%B1">Elixirでない理由</a></h3> <p>楽しそうではあるんですが, 同時にだるさもあります. サーバー(当然関数)自体を再帰的に呼び出すことで無限ループを作るらしいです.</p> <h2 id="Goの導入"><a href="#Go%E3%81%AE%E5%B0%8E%E5%85%A5">Goの導入</a></h2> <h3 id="Goバイナリのインストール"><a href="#Go%E3%83%90%E3%82%A4%E3%83%8A%E3%83%AA%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">Goバイナリのインストール</a></h3> <p>Goには<a target="_blank" rel="nofollow noopener" href="https://hub.docker.com/_/golang/">公式のイメージ</a>がありますが, <a href="https://crieit.net/posts/MinQ-2-Docker-CentOS">前回</a>は構築したCentOSベースのイメージに追加します. /usr/local/srcは自分でビルドするプログラムのソースをダウンロードするフォルダです. といってもビルドはしないので<a target="_blank" rel="nofollow noopener" href="https://golang.org/doc/install">公式のダウンロード・ページ</a>からバイナリを取って来て/usr/local以下に展開するだけです.</p> <pre><code class="dockerfile">ARG GO_VERSION=1.13.4 WORKDIR /usr/local/src ADD https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz ./ RUN tar -C /usr/local/ -xzf go${GO_VERSION}.linux-amd64.tar.gz && \ rm /usr/local/src/*.tar.gz </code></pre> <p>Goのバージョンはbuild-argオプションで変更できるようにARG命令で定義しました. 2019/11/30の最新バージョンである1.13.4をデフォルト・バージョンとしています. また<a target="_blank" rel="nofollow noopener" href="https://tech.librastudio.co.jp/entry/index.php/2018/02/20/post-1792/">GolangのGOPATHやGOROOTについて</a>に従って自作パッケージと外部パッケージのインストール・フォルダを分けます. なおADDでgoバイナリを取得していますが, wgetでもcurlでも同じです.</p> <pre><code class="dockerfile">ENV PATH=$PATH:/usr/local/go/bin ENV GOPATH=$HOME/go/package:$HOME/go/workspace ENV PATH=$HOME/go/package/bin:$HOME/go/workspace/bin:$PATH </code></pre> <p>まずgo自体にパスを通す必要があります. GOPATHには二つのパスを指定しています. go getは最初に指定したパスにパッケージをインストールしてくれます. 上の例では以下のように分けました.</p> <div class="table-responsive"><table> <thead> <tr> <th align="center">フォルダ名</th> <th align="center">用途</th> </tr> </thead> <tbody> <tr> <td align="center">workspace</td> <td align="center">自作のGoプロジェクト</td> </tr> <tr> <td align="center">package</td> <td align="center">サード・パーティ製のパッケージのインストール</td> </tr> </tbody> </table></div> <p>それぞれのフォルダにはsrc, bin, そしてpkgというフォルダが必要です.</p> <div class="table-responsive"><table> <thead> <tr> <th align="center">フォルダ名</th> <th align="center">用途</th> </tr> </thead> <tbody> <tr> <td align="center">src</td> <td align="center">ソースコードの保存</td> </tr> <tr> <td align="center">pkg</td> <td align="center">パッケージをコンパイルした生成物</td> </tr> <tr> <td align="center">bin</td> <td align="center">ビルド後の生成物</td> </tr> </tbody> </table></div> <p>Goはパッケージという単位でソースをモジュール化します. それぞれがコンパイルされ, その後プロジェクトのmainパッケージを中心にリンクされ最終生成物として実行可能バイナリが出力されるようです. そのためbinフォルダにパスを設定しておきます. こうしておくとビルドされたコマンドを即呼び出せます.</p> <p>Goのサンプルを以下のようにインストールして, 実行してみましょう. 折角なのでバージョンをコマンドラインから指定してみましょう.</p> <pre><code class="bash">docker build -t minq:0.1 --build-arg=1.13.4 context/ docker run -it --rm minq:0.1 </code></pre> <p>後は対話モードで以下を実行します.</p> <pre><code class="bash">$ go get github.com/golang/example/hello $ hello Hello, Go examples! </code></pre> <p>リモート・パッケージも簡単にインストールでき, 実行できることがわかります. Dockerfileでは以下のようになります. workspaceとpackageフォルダそれぞれにsrc, bin, pkgを作成し, echoをインストールします.</p> <pre><code class="bash">RUN mkdir -p ${HOME}/go/workspace ${HOME}/go/workspace/src ${HOME}/go/workspace/pkg ${HOME}/go/workspace/bin && \ mkdir -p ${HOME}/go/package ${HOME}/go/package/src ${HOME}/go/package/pkg ${HOME}/go/package/bin && \ mkdir -p ${HOME}/go/workspace/src/minq && \ go get -u github.com/labstack/echo/... </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/Esfahan/items/cf0fd87d0e3bc3194bd8">Golang + Chrome on CentOS7(Docker)でスクレイピング</a><br /> <a target="_blank" rel="nofollow noopener" href="https://linuxize.com/post/how-to-install-go-on-centos-7/">How to Install Go on CentOS 7</a><br /> <a target="_blank" rel="nofollow noopener" href="https://golang.org/doc/install">Download the Go distribution</a></p> <h3 id="Goの環境変数のまとめ"><a href="#Go%E3%81%AE%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0%E3%81%AE%E3%81%BE%E3%81%A8%E3%82%81">Goの環境変数のまとめ</a></h3> <div class="table-responsive"><table> <thead> <tr> <th align="center">環境変数</th> <th align="center">用途</th> </tr> </thead> <tbody> <tr> <td align="center">GOROOT</td> <td align="center">Goバイナリのパス</td> </tr> <tr> <td align="center">GOPATH</td> <td align="center">作業フォルダのパス</td> </tr> </tbody> </table></div> <h4 id="GOROOT"><a href="#GOROOT">GOROOT</a></h4> <p>GOROOTは指定する必要は特にないそうです. 例えば複数バージョンのGoをインストールした場合にGOROOTを使うとバイナリのパスを取得できます.</p> <pre><code class="bash">$ go env GOROOT /usr/local/go </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://golang.org/doc/install#extra_versions">Installing extra Go versions</a></p> <h4 id="GOPATH"><a href="#GOPATH">GOPATH</a></h4> <p>GOPATHはデフォルト値として$HOME/goが指定されています.</p> <pre><code class="bash">$ go env | grep GOPATH GOPATH="/home/vpsuser/go" </code></pre> <p>getサブコマンドで外部からパッケージを取得する場合のインストール先もGOPATH直下になります. つまり/home/vpsuser/go以下に展開されます.</p> <blockquote> <p>If the specified package is not present in a workspace, go get will place it inside the first workspace specified by GOPATH.</p> </blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://golang.org/doc/code.html#remote">Remote packages</a><br /> <a target="_blank" rel="nofollow noopener" href="https://golang.org/doc/code.html">How to Write Go Code</a></p> <h3 id="Echo"><a href="#Echo">Echo</a></h3> <p>Webサーバーとして<a target="_blank" rel="nofollow noopener" href="https://echo.labstack.com/">Echo</a>というのを使います. <a target="_blank" rel="nofollow noopener" href="https://echo.labstack.com/guide">Quick Start</a>を参考にserver.goというファイルをcontextフォルダに作成します. プロジェクト名はminqとします.</p> <pre><code class="dockerfile">COPY server.go ${HOME}/go/workspace/src/minq </code></pre> <p>ビルドした後コンテナを起動してポートを転送しておきます.</p> <pre><code class="bash">docker run -it --rm -p 1323:1323 minq:0.1 </code></pre> <p>コンテナに入ってコマンドプロンプトに以下のコマンドを打ち込みましょう.</p> <pre><code class="bash">$ go install minq </code></pre> <p>これで${HOME}/go/workspace/bin以下にminqという実行可能バイナリが出力されます. すでにbinフォルダのパスは通っているので, コマンドラインから以下を実行するとサーバーが起動します.</p> <pre><code class="bash">$ minq </code></pre> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>これまでの作業をまとめるとこんな感じでしょうか.</p> <pre><code class="dockerfile">FROM centos:7 AS builder RUN yum -y update && yum -y install \ useradd \ usermod \ chpasswd \ sudo \ wget \ which \ git \ yum clean all ARG GO_VERSION=1.13.4 WORKDIR /usr/local/src ADD https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz ./ RUN tar -C /usr/local/ -xzf go${GO_VERSION}.linux-amd64.tar.gz && \ rm /usr/local/src/*.tar.gz ENV USER_NAME vpsuser ENV HOME=/home/${USER_NAME} RUN useradd --create-home ${USER_NAME} && \ usermod -aG wheel ${USER_NAME} && \ echo "${USER_NAME}:tekitou" | chpasswd # ENV GOROOT=/usr/local/go ENV PATH=$PATH:/usr/local/go/bin # ENV GOPATH=$HOME/go # ENV PATH=$PATH:$GOPATH/bin ENV GOPATH=${HOME}/go/package:${HOME}/go/workspace ENV PATH=${HOME}/go/go/bin:${HOME}/go/workspace/bin:${PATH} USER ${USER_NAME} WORKDIR ${HOME} RUN mkdir -p ${HOME}/go/workspace ${HOME}/go/workspace/src ${HOME}/go/workspace/pkg ${HOME}/go/workspace/bin && \ mkdir -p ${HOME}/go/package ${HOME}/go/package/src ${HOME}/go/package/pkg ${HOME}/go/package/bin && \ mkdir -p ${HOME}/go/workspace/src/minq && \ go get -u github.com/labstack/echo/... COPY server.go ${HOME}/go/workspace/src/minq </code></pre> <h2 id="Reference"><a href="#Reference">Reference</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://knowledge.sakura.ad.jp/serialization/understood-cats-vps/">ネコでもわかる!さくらのVPS講座</a><br /> <a target="_blank" rel="nofollow noopener" href="https://knowledge.sakura.ad.jp/serialization/linux-security/">Linuxセキュリティ入門</a><br /> <a target="_blank" rel="nofollow noopener" href="https://knowledge.sakura.ad.jp/serialization/getting-start-docker/">Docker入門</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/i35_267/items/10789865dd240e075eba">Docker : firewalldでssh, http, httpsのみ許可する。(CentOS 7)</a></p> <p><a target="_blank" rel="nofollow noopener" href="http://gihyo.jp/dev/serial/01/game_mysql">ゲームを題材に学ぶ 内部構造から理解するMySQL</a><br /> <a target="_blank" rel="nofollow noopener" href="http://gihyo.jp/dev/serial/01/mysql-road-construction-news">MySQL道普請便り</a></p> ブレイン tag:crieit.net,2005:PublicArticle/15567 2019-11-30T15:32:41+09:00 2019-11-30T15:32:41+09:00 https://crieit.net/posts/MinQ-2-Docker-CentOS MinQ開発日記 (2) DockerでCentOS環境構築 <h1 id="Dockerで環境構築"><a href="#Docker%E3%81%A7%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89">Dockerで環境構築</a></h1> <p>Docker上に<a target="_blank" rel="nofollow noopener" href="https://knowledge.sakura.ad.jp/serialization/understood-cats-vps/">ネコでもわかる!さくらのVPS講座</a>のCentOSの環境を構築してみます.</p> <p>この際非対話的な環境構築(つまりDockerfile)に関しては以下を参考にしました.</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/Riliumph/items/3b09e0804d7a04dff85b">Dockerで開発環境を仮想化する</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/libra_lt/items/dd98297cb00aaf1c4c67">Docker(CentOS)イメージから新規ユーザを追加し運用できるようにするまで</a></li> </ul> <p>手元の環境を出来るだけVPS側と共通の環境を構築するのが目的です.</p> <h2 id="流れ"><a href="#%E6%B5%81%E3%82%8C">流れ</a></h2> <ul> <li>CentOS7のイメージの取得</li> <li>パッケージの更新</li> <li>作業用ユーザーの設定</li> <li>SSHによる接続</li> <li>rootユーザーによるSSHによるログイン規制</li> </ul> <h2 id="環境構築"><a href="#%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89">環境構築</a></h2> <h3 id="CentOS7のイメージの取得"><a href="#CentOS7%E3%81%AE%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8%E3%81%AE%E5%8F%96%E5%BE%97">CentOS7のイメージの取得</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://hub.docker.com/_/centos/?tab=tags">CentOSイメージ</a>を取得します. バージョンは7で良いと思います. image_name:tagという形式で指定します.</p> <pre><code class="Dockerfile">FROM centos:7 </code></pre> <h3 id="パッケージの更新"><a href="#%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%81%AE%E6%9B%B4%E6%96%B0">パッケージの更新</a></h3> <p>イメージを作成した時点でパッケージの状態は止まっています. 更新しておきましょう.</p> <pre><code class="Dockerfile">RUN yum -y update </code></pre> <h3 id="作業用ユーザーの設定"><a href="#%E4%BD%9C%E6%A5%AD%E7%94%A8%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%AE%E8%A8%AD%E5%AE%9A">作業用ユーザーの設定</a></h3> <p>普通この作業は以下のコマンドを使い対話的に行います.</p> <ul> <li>adduser</li> <li>passwd</li> </ul> <p>しかしDockerfileではこれらは使えません. 代わりに以下を使うようです.</p> <ul> <li>useradd</li> <li>chpasswd</li> </ul> <p>一般にパスワードもハードコードはご法度です🔑 このコンテナをデプロイするわけではないので正直ハードコードしても同じなのですが環境変数の設定もしておきましょう. docker inspectのCmdセクションを見ると一目瞭然だからです. あくまでローカルで動かす利便性のための処置です.</p> <p>また現状のCentOSには何も入っていないので必要なパッケージも追加しておきましょう.</p> <pre><code class="dockerfile">ENV USER_NAME vpsuser ENV HOME /home/${USER_NAME} RUN yum -y update && yum -y install \ useradd \ chpasswd RUN useradd --create-home ${USER_NAME} && \ echo "${USER_NAME}:tekitou" | chpasswd </code></pre> <p>このDockerfileからイメージを作成します.</p> <pre><code class="bash">docker build -t your_image:0.1 . </code></pre> <p>名前は自由に決めましょう. これでhome以下にvpsuserが追加されました.</p> <h3 id="一般ユーザーでのログイン"><a href="#%E4%B8%80%E8%88%AC%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%A7%E3%81%AE%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3">一般ユーザーでのログイン</a></h3> <p>これは万が一ルート権限を奪われても被害を最小限にするための重要な設定らしいです. Dockerでは必要ない気もしますが, なんとなくルートでシステムを触るのも憚られるので設定をしておきます. Dockerの場合ログインというより一般ユーザーとしてコンテナを実行するということになると思います.</p> <p>その前にこの時点でどのような状態なのかを調べておきます.</p> <pre><code class="bash">docker run -it --rm your_image:0.1 whoami </code></pre> <p>でrootと出ればルート・アカウントでログインしていることになります.</p> <p>USER命令を使うとユーザーが指定できます.</p> <blockquote> <p>The USER instruction sets the user name (or UID) and optionally the user group (or GID) to use when running the image and for any RUN, CMD and ENTRYPOINT instructions that follow it in the Dockerfile.</p> </blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://docs.docker.com/engine/reference/builder/#USER">Dockerfile reference</a></p> <p>また作業ディレクトリも変更しておきます.</p> <blockquote> <p>the WORKDIR instruction sets the working directory for any RUN, CMD, ENTRYPOINT, COPY and ADD instructions that follow it in the Dockerfile.</p> </blockquote> <pre><code class="dockerfile">USER ${USER_NAME} WORKDIR ${HOME} </code></pre> <p>この状態で再びコンテナを対話モードで起動して</p> <pre><code class="bash">yum -y update </code></pre> <p>を実行するとPermission deniedというエラーが出るはずです. sudoコマンドを使ってみましょう.</p> <pre><code class="bash">vpsuser is not in the sudoers file. This incident will be reported. </code></pre> <p>sudoersファイルにvpsuserはいないよと怒られてしまいました. sudoerというのはグループでここにvpsuserが登録されていると指定されたコマンドが実行できるようになります. /etc/sudoersというファイルで設定されています. 変更方法はいくつかあります.</p> <ul> <li>visudo</li> <li>usermod</li> <li>gpasswd</li> </ul> <p>visudoは/etc/sudoersを直接編集するので面倒です. usermodとgpasswdは同じようなことができるようです.</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/tk1024/items/16e98031b84a31df469b">usermodでグループを追加するのは危険</a></p> <p>コマンドよりもどのグループを指定するかの方が問題です. 現状を確認しておきましょう.</p> <pre><code class="bash">groups vpsuser </code></pre> <p>とするとvpsuserはvpsuserというグループに所属しています(プライマリ・グループと呼ぶようです). 確かに/etc/groupの末尾にvpsuserというのが存在します. CentOSではwheelグループに登録するといいようです(これをセカンダリ・グループと呼ぶようです).</p> <pre><code class="bash">usermod -aG wheel ${USER_NAME} </code></pre> <p>こうするとvpsuserはsudoerとしてsudoでルート権限で実行が許されたコマンドも実行できるようになります.</p> <p><a target="_blank" rel="nofollow noopener" href="http://codaholic.org/?p=1757">[Linux] なぜ sudo する権限のあるグループが「 wheel 」という名前なのか</a><br /> <a target="_blank" rel="nofollow noopener" href="https://www.atmarkit.co.jp/ait/articles/1706/02/news014.html">Linuxのユーザーとグループって何だろう?</a></p> <h3 id="SSHによる接続"><a href="#SSH%E3%81%AB%E3%82%88%E3%82%8B%E6%8E%A5%E7%B6%9A">SSHによる接続</a></h3> <p>SSHによる接続はインターネット経由でサーバーにアクセスする場合に通信の内容を傍受されないように暗号化するためです. ので無視します.</p> <h3 id="Webサーバー"><a href="#Web%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC">Webサーバー</a></h3> <p>Apacheはwebサーバーですが, Goの場合いらないようです. 正確には要らないというよりは<a target="_blank" rel="nofollow noopener" href="https://golang.org/pkg/net/http/">net/httpパッケージ</a>としてwebサーバーに必要な機能が提供されています.</p> <blockquote> <p>Package http provides HTTP client and server implementations.</p> </blockquote> <p>がこの辺はPHPより取り扱いが楽な予感がします. PHPの<a target="_blank" rel="nofollow noopener" href="https://www.php.net/manual/en/features.commandline.webserver.php">Built-in web server</a>には以下のような警告文が掲載されています.</p> <blockquote> <p>Warning<br /> This web server was designed to aid application development. It may also be useful for testing purposes or for application demonstrations that are run in controlled environments. It is not intended to be a full-featured web server. It should not be used on a public network.</p> </blockquote> <p>つまり開発用サーバーということでネットに公開するならApacheやnginxなどのwebサーバーが必要になります. ただしGoでもApacheとかnginxとかと使う例とかもあるのであったほうがいいケースとかもあるのかなとは思います.</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/unokun/items/dcf3ed4fbc4f532f664a">nginx + golangでWeb API開発</a><br /> <a target="_blank" rel="nofollow noopener" href="https://echo.labstack.com/cookbook/load-balancing">Load Balancing Recipe</a><br /> <a target="_blank" rel="nofollow noopener" href="https://echo.labstack.com/cookbook/reverse-proxy">Reverse Proxy Recipe</a><br /> <a target="_blank" rel="nofollow noopener" href="https://medium.com/web-developer/golangーzero-downtime-deploys-and-rollbacks-go-http-serverーecho-web-framework-apache-12a9a21bfc25">GoLang: Zero downtime deploys and Rollbacks | Go HTTP server−Echo Web Framework & Apache</a></p> <h3 id="アプリケーション・サーバー(APサーバー)"><a href="#%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%83%BB%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%28AP%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%29">アプリケーション・サーバー(APサーバー)</a></h3> <p>APサーバーも不要なようです. 3層アーキテクチャを前提にすると, APサーバーはWebサーバーから転送されてくるデータを基にDBサーバーと連帯してデータの取得・加工を行うプログラムというのが私の理解です.</p> <p>しかしその存在意義はよく分かりませんでした.</p> <p><a target="_blank" rel="nofollow noopener" href="https://wyukawa.hatenablog.com/entry/20131125/1385367711">Web APサーバーとWebフレームワークをつなぐもの</a>によるとWebサーバーとWebフレームワークの差異を吸収するために存在するようです.</p> <blockquote> <p>Apache以外にもlighttpd, nginxなどさまざまなWebサーバがあるわけだが、そうした多様なWebサーバ環境ごとの差異を吸収するためのコードをWebフレームワーク(例:Catalyst, Django, Rails)が書くのは大変である。ましてWebフレームワークは沢山あるわけだし。そうした状況を解決するために出てきたのがPythonならWSGIであり、RubyならRackであり、PerlならPSGIである。</p> </blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://gihyo.jp/dev/feature/01/wsgi/0001">WSGIとPythonでスマートなWebアプリケーション開発を</a>でも似たようなことが書かれています.</p> <blockquote> <p>Pythonには,Zope,Twisted,Django,TurboGearsなどのさまざまなWebアプリケーションフレームワークが存在します。以前は,このような幅広い選択肢は,新しくPythonを使用しようとするユーザにとって都合が悪いことがありました。使用するフレームワークによって,利用可能なサーバが制限されてしまったり,逆にサーバによって利用可能なWebアプリケーションフレームワークが制限されるといったことがあったためです。そのようなPythonの状況とは対照的に,Javaにおいては,多数のWebアプリケーションフレームワークがあるにもかかわらず,サーバとWebアプリケーションをつなぐ際に,統一されたJava Servelet APIを用いることで,アプリケーションフレームワークを気にせずにServelet APIが利用できる環境で動作させることが可能です。このような,サーバとWebアプリケーションをつなぐ共通のインターフェースをPythonで定義したものが,PEP333(日本語訳版)のWSGI(Web Server Gateway Interface)です。</p> </blockquote> <p>つまりWebフレームワークで作ったWebサービスを起動するサーバー(常駐プロセス?)で, APサーバーのおかげでWebサーバーとWebフレームワークの組み合わせを柔軟に選択できるようです. Goの場合Webサーバーの機能がパッケージとして提供されているのでAPサーバーのようなミドルウェアで切り離す必要がない, と勝手に考えて不要としておきます.</p> <h2 id="まとめと課題"><a href="#%E3%81%BE%E3%81%A8%E3%82%81%E3%81%A8%E8%AA%B2%E9%A1%8C">まとめと課題</a></h2> <p>スケールだ負荷分散だと言い出すと色々と複雑になるのかもしれませんが, シンプルにAPIサーバーを運用するならミドルウェアを省略できたりGoの標準機能でカバーできたりとシンプルに書けて良さそうです. ネコでもわかる!さくらのVPS講座ではこの後Apacheを入れてファイアウォールの設定をして簡単な静的ファイルを配信して確認します. そしてPHPやDBサーバー(MariaDB)を導入しますが, Goの場合はGo言語を入れないとnet/httpが使えないので, まずGoを入れることにします.</p> <h3 id="DB"><a href="#DB">DB</a></h3> <p>後はGoでAPIサーバーということになると必要なのはDBサーバーということになりますがとりあえず置いておきましょう.</p> <h3 id="ファイアーウォールの設定"><a href="#%E3%83%95%E3%82%A1%E3%82%A4%E3%82%A2%E3%83%BC%E3%82%A6%E3%82%A9%E3%83%BC%E3%83%AB%E3%81%AE%E8%A8%AD%E5%AE%9A">ファイアーウォールの設定</a></h3> <p>これはDockerfileからは設定できないようです. つまり起動したコンテナから設定する必要があると思いますが, httpd(Apache)が動いている前提なので, 先にGoでWebアプリケーションを作ってからにしようと思います.</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/i35_267/items/10789865dd240e075eba">Docker : firewalldでssh, http, httpsのみ許可する。(CentOS 7)</a></p> <h2 id="Reference"><a href="#Reference">Reference</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://knowledge.sakura.ad.jp/serialization/understood-cats-vps/">ネコでもわかる!さくらのVPS講座</a><br /> <a target="_blank" rel="nofollow noopener" href="https://vps-news.sakura.ad.jp/tutorials/centos7-initial-settings/?_gl=1*1w3olts*_gcl_aw*R0NMLjE1NzQ1OTM5NTcuQ2p3S0NBaUE4ZWp1QlJBYUVpd0FuLWlKM2xUa0tLOUZOUDJSNWRrS0R5SFRBdTZ0RVhuNnJjN1VUUlpvcDZvQXR1SFNFS2hEc0xKRjNob0NWbU1RQXZEX0J3RQ..&_ga=2.81152852.218098909.1574770419-1929858640.1570088790&_gac=1.12806469.1574593957.CjwKCAiA8ejuBRAaEiwAn-iJ3lTkKK9FNP2R5dkKDyHTAu6tEXn6rc7UTRZop6oAtuHSEKhDsLJF3hoCVmMQAvD_BwE">チュートリアル:CentOS 7(さくらのVPS)サーバ作成直後に設定しておくべき初期セキュリティ設定</a><br /> <a target="_blank" rel="nofollow noopener" href="https://support.conoha.jp/vps/guide/manga/?btn_id=v-study-01-breadcrumbs_guide-manga">マンガで学ぶConoHa</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/Riliumph/items/3b09e0804d7a04dff85b">Dockerで開発環境を仮想化する</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://www.mtioutput.com/entry/2018/09/20/135157">【UNIX】passwdとchpasswdの違い</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/kimullaa/items/f556431b8103e686f356">DockerのscratchイメージでHello Worldする</a></p> <h3 id="その他"><a href="#%E3%81%9D%E3%81%AE%E4%BB%96">その他</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/kkeisuke/items/7cd4d5834386666faab3">Docker + Go + Gin の開発環境を準備する</a><br /> <a target="_blank" rel="nofollow noopener" href="https://docs.docker.com/develop/dev-best-practices/">Docker development best practices</a><br /> <a target="_blank" rel="nofollow noopener" href="https://japan.zdnet.com/article/35125490/">APIセキュリティ入門</a><br /> <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Learn/JavaScript/Client-side_web_APIs/Introduction">Web API の紹介</a><br /> <a target="_blank" rel="nofollow noopener" href="https://www.atmarkit.co.jp/ait/series/2628/">CentOS 7で始める最新Linux管理入門</a><br /> <a target="_blank" rel="nofollow noopener" href="https://www.atmarkit.co.jp/ait/series/4683/">基礎から理解するLinuxサーバー[Cent OS 7.0編]</a><br /> <a target="_blank" rel="nofollow noopener" href="https://linuxize.com/">linuxize</a><br /> <a target="_blank" rel="nofollow noopener" href="https://www.atmarkit.co.jp/ait/series/3016/">“応用力”をつけるためのLinux再入門</a></p> <h2 id="Appendix"><a href="#Appendix">Appendix</a></h2> <h3 id="イメージのサイズの軽量化"><a href="#%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8%E3%81%AE%E3%82%B5%E3%82%A4%E3%82%BA%E3%81%AE%E8%BB%BD%E9%87%8F%E5%8C%96">イメージのサイズの軽量化</a></h3> <p>実際に(貧乏)VPS上で走らせるにはalpineとかMulti-stage buildsといった節約が必要になります.</p> <p><a target="_blank" rel="nofollow noopener" href="https://medium.com/@chemidy/create-the-smallest-and-secured-golang-docker-image-based-on-scratch-4752223b7324">Create the smallest and secured golang docker image based on scratch</a><br /> <a target="_blank" rel="nofollow noopener" href="https://docs.docker.com/develop/develop-images/multistage-build/">Use multi-stage builds</a></p> <h3 id="RUN vs CMD"><a href="#RUN+vs+CMD">RUN vs CMD</a></h3> <p>RUNで指定したコマンドは実行された結果がレイヤーとしてイメージに取り込まれるが, CMDはコンテナ内で実行されるコマンドを指定する.</p> <h3 id="Dockerにおける機密情報の扱い"><a href="#Docker%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E6%A9%9F%E5%AF%86%E6%83%85%E5%A0%B1%E3%81%AE%E6%89%B1%E3%81%84">Dockerにおける機密情報の扱い</a></h3> <p>今回はローカルでの開発環境を構築したいだけなので特にセキュリティ面を考慮する必要性はないのですが, ある程度やり方を押さえておくのはいいかと思います.</p> <p>buildサブコマンドの--build-argオプションにはビルド時の引数を渡せる. DockerfileではARGに指定する. <a target="_blank" rel="nofollow noopener" href="https://docs.docker.com/engine/reference/builder/">Dockerfile reference</a>によると機密情報なんかはコマンドで見えてしまうので, Dockerfileに含めるのは良くないようです.</p> <blockquote> <p>Warning: It is not recommended to use build-time variables for passing secrets like github keys, user credentials etc. Build-time variable values are visible to any user of the image with the docker history command.</p> </blockquote> <p>要するにイメージに埋め込まれちゃうのでやめとけと言うことです. hisotryサブコマンドで見えてしまうのと, コンテナの再利用が難しくなってしまいます.</p> <p>公式に用意されている機能としては, Docker secretesと言う機能があるようです(Swarmモードでしか使えないらしいですが).</p> <p><a target="_blank" rel="nofollow noopener" href="https://docs.docker.com/engine/swarm/secrets/">Manage sensitive data with Docker secrets</a></p> <p>もう一つの環境変数を使う方法です. こちらもDockerfileに書いてしまうとinspectサブコマンドでみれてしまうようです.</p> <h4 id="squash option"><a href="#squash+option">squash option</a></h4> <p><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/a/42125241/12036118">Using SSH keys inside docker container</a></p> ブレイン tag:crieit.net,2005:PublicArticle/15558 2019-11-22T21:27:02+09:00 2019-12-12T23:54:35+09:00 https://crieit.net/posts/minq-1-Docker minq開発日記 (1) Dockerの基本語彙 <h1 id="MinQ(ミンク)"><a href="#MinQ%28%E3%83%9F%E3%83%B3%E3%82%AF%29">MinQ(ミンク)</a></h1> <p>個人用的に勉強したことを一問一答という感じで返してくれるAPIを作ろうと思います. VPS上にDockerコンテナを・・・と思ったのですが現状ではコンテナの軽量化やセキュリティなど難しそうなのでとりあえず地道に勉強していこうと思います. 当面はローカル上にデプロイするのと同じ環境を構築しようと思います.</p> <h2 id="Dockerの語彙"><a href="#Docker%E3%81%AE%E8%AA%9E%E5%BD%99">Dockerの語彙</a></h2> <p>具体的な話の前に, Dockerの基本用語を確認しておこうと思います.</p> <h3 id="Dockerイメージ"><a href="#Docker%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8">Dockerイメージ</a></h3> <p>レイヤーで構成された何か. コンテナのテンプレートという表現もドキュメントには見られる.</p> <blockquote> <p>An image is a read-only template with instructions for creating a Docker container.</p> </blockquote> <p>どう表現するべきかいまいち難しいがソースコードがDockerfileとすると実行可能ファイルがイメージという感じだろうか. イメージを実行(run)することでコンテナを起動することができる. 突き詰めるとファイルと言うことになるのだが, それは大抵のプログラムがそうなのでなんとも表現しがたい.</p> <h3 id="Dockerコンテナ"><a href="#Docker%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A">Dockerコンテナ</a></h3> <p>イメージをDockerエンジン上でインスタンス化したものがコンテナとなる. もう少し詳しく言うとイメージの上に書き込み可能のレイヤー(コンテナ・レイヤー)を加えたものをコンテナと呼ぶ. 仮に複数のコンテナが同じイメージから生成されても, コンテナ・レイヤーにアプリの固有の状態を記録することで違うアプリケーションとして運用できる. 逆にコンテナ・レイヤー以下のイメージ・レイヤは読み込み専用なので変更はできない. なおこの層のファイルなどの変更もコンテナ・レイヤーに記録されるらしい.</p> <h3 id="イメージ・レイヤー"><a href="#%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8%E3%83%BB%E3%83%AC%E3%82%A4%E3%83%A4%E3%83%BC">イメージ・レイヤー</a></h3> <blockquote> <p>A Docker image is built up from a series of layers. Each layer represents an instruction in the image’s Dockerfile. Each layer except the very last one is read-only.</p> </blockquote> <p>Dockerイメージを構成する要素で, Dockerfileの各インストラクションに対応するらしい. つまりインストラクションごとに生成されたレイヤーが積層されてDockerイメージはできている. これらは全て読み込み専用で, 実態はファイルらしいです. historyサブコマンドを使うとイメージのレイヤーが構築された歴史を見ることができる.</p> <h3 id="コンテナ・レイヤー"><a href="#%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%83%BB%E3%83%AC%E3%82%A4%E3%83%A4%E3%83%BC">コンテナ・レイヤー</a></h3> <blockquote> <p>All changes made to the running container, such as writing new files, modifying existing files, and deleting files, are written to this thin writable container layer.</p> </blockquote> <p>ファイルの新規作成, 修正, そして削除はコンテナ・レイヤーに書き込まれる.</p> <blockquote> <p>Docker allocates a read-write filesystem to the container, as its final layer.</p> </blockquote> <p>また読み書き可能なファイルシステムとも表現されている.</p> <p>イメージ・レイヤーはread-onlyなレイヤーでコンテナを実行中は不変に保たれる(変更は再ビルドが必要). 実行中に行われるファイルへの変更はイメージ・レイヤーの上に構築されるコンテナ・レイヤーに書き込まれる.</p> <h3 id="ベース・イメージ"><a href="#%E3%83%99%E3%83%BC%E3%82%B9%E3%83%BB%E3%82%A4%E3%83%A1%E3%83%BC%E3%82%B8">ベース・イメージ</a></h3> <p>Scratchという名前のイメージのことをDockerではベース・イメージと呼ぶそうです. 一方あるイメージの元になったイメージは親イメージ(parent image)と呼び分けているようです.</p> <h3 id="Dockerデーモン"><a href="#Docker%E3%83%87%E3%83%BC%E3%83%A2%E3%83%B3">Dockerデーモン</a></h3> <p>Dockerエンジンとか単にDockerといった場合これを指すことが多い. イメージやコンテナ(と言ったDockerオブジェクト)の管理をしてくれる.</p> <blockquote> <p>The docker daemon process running on the host which manages images and containers (also called Docker Engine)</p> <p>The daemon creates and manages Docker objects, such as images, containers, networks, and volumes.</p> </blockquote> <p>Dockerコマンドと言ったコマンドライン・インターフェースが提供されておりユーザーはこれを使ってDockerデーモンと対話する. このクライアント・サーバー・アプリケーション自体をDockerエンジンと呼ぶこともあるらしい.</p> <blockquote> <p>Docker Engine is a client-server application with these major components:</p> </blockquote> <p>何れにしてもDockerと呼ばれるシステムの中核にこのDockerデーモンがあることは間違いないようです.</p> <h3 id="Docker Container Format"><a href="#Docker+Container+Format">Docker Container Format</a></h3> <p>デフォルトではlibcontainerというらしいですが, 以下の三つの機能から実現されているようです.</p> <ul> <li>Namespaces</li> <li>Control Group (cgroup)</li> <li>Union File System (UnionFS)</li> </ul> <p>ドキュメントにも説明があるのですが正直よく分かりません.</p> <h4 id="Namespace"><a href="#Namespace">Namespace</a></h4> <blockquote> <p>貨物輸送のコンテナのように,隔離された空間にプロセスが入っているので,この空間を『コンテナ』と呼ぶわけです。</p> </blockquote> <p><img src="https://gihyo.jp/assets/images/admin/serial/01/linux_containers/0002/thumb/TH400_002.jpg" alt="image" /></p> <blockquote> <p>OSを通して使用できるコンピュータのリソースを各コンテナごとに隔離して,ホストOS上で直接動作するプロセスや他のコンテナから独立した空間を作り出し,リソースを分割,分配,制限するわけです。</p> </blockquote> <p>この説明はDockerでも通用しそうです.</p> <blockquote> <p>Docker uses a technology called namespaces to provide the isolated workspace called the container.</p> </blockquote> <p>隔離された作業空間をコンテナと呼んでいます. この隔離された空間を作る機能を提供しているのが名前空間でかなり核心的な機能だと言えそうです. 大雑把に名前空間 = コンテナと考えていいでしょうか.</p> <p>では名前空間は具体的にどのような要求に答えているのでしょうか. これはプログラミングでも同じで名前空間を使って名前の衝突を回避したりします. 同じように名前空間が別の空間では同じ名前が使えます. PIDは通常OSで一意となるはずですが, 名前空間を使って隔離されていると共通のIDが使えるようになるようです. またこの空間は隔離されているので外界は見えません. 例えばIPCは同一空間内に限定されます.</p> <p><a target="_blank" rel="nofollow noopener" href="https://gihyo.jp/admin/serial/01/linux_containers/0002">LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術 第2回 コンテナの仕組みとLinuxカーネルのコンテナ機能[1]名前空間とは?</a></p> <h4 id="cgroup"><a href="#cgroup">cgroup</a></h4> <blockquote> <p>cgroupは"Control Group"の略です。プロセスをグループ化して,そのグループ内に存在するプロセスに対して共通の管理を行うために使います。たとえば,ホストOSが持つCPUやメモリなどのリソースに対して,グループごとに制限をかけることができます。</p> </blockquote> <p>コンテナごとにグループ化してリソース管理できるという機能のようです. コンテナ内のプロセスもホストOS(Linux限定?)が管理しますので, cgroupのような機能でコンテナごとに別グループとしておくことことは重要でしょう. ドキュメントでも同じような解説がされています.</p> <blockquote> <p>Control groups allow Docker Engine to share available hardware resources to containers and optionally enforce limits and constraints. For example, you can limit the memory available to a specific container.</p> </blockquote> <p>なんとなく他の二つに比べると具体的な感じがつかめません. リソース管理はOSがしてくれると基本的な書籍に良く書いてあるのですが, ホストOSと切り離されたコンテナと言う単位でリソースを管理する場合はこう言う仕組みが必要になるのかもしれません.</p> <h4 id="Union FileSystem(UnionFS)"><a href="#Union+FileSystem%28UnionFS%29">Union FileSystem(UnionFS)</a></h4> <blockquote> <p>Docker コンテナが軽量だと言われる理由のひとつが、ユニオンファイルシステムにあります。ユニオンファイルシステムは、複数のファイルシステム上のディレクトリやファイルをレイヤとして重ね合わせ、それらを仮想的に一つのファイルシステムとして扱う技術です。</p> </blockquote> <p>イメージやコンテナのレイヤーをを管理するための機構のようです. レイヤーの実態はファイル群なのですが, このUnion FileSystemを使って仮想的なファ入りシステムとして扱えるようになるようです.</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.techscore.com/blog/2018/12/10/docker-images-and-layers/">知らないと損する Docker イメージのレイヤ構造とは</a></p> <p>実際UnifonFSがどのような機能を提供するかは以下が参考になります. Ubuntuのunionfs-fuseというパッケージを例に説明されています.</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.techscore.com/blog/2014/07/30/introduction-to-unionfs/">まとめて束ねるUnionFSの不思議な世界</a></p> <p>なおDockerはいくつかのUnionFSを採用しており, 以下のコマンドを使って調べると私のMacの場合はoverlay2でした.</p> <pre><code class="bash">docker info | grep Storage </code></pre> <h3 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h3> <ul> <li>コンテナはOS上で動くプロセスである(Linuxカーネルの場合).</li> <li>ビルドされたDockerイメージからDockerコンテナを実行する.</li> <li>名前空間を用いて隔離された作業空間をコンテナと呼ぶ.</li> <li>リソースはcgroupでOSによる自動的な割り振りを離れてよりきめ細やかに管理できる.</li> <li>Dockerの内部構造であるレイヤーはUnion FileSystemの機能を使って構築されている.</li> </ul> <p>間違いや勘違いがあれば適宜訂正したいと思います.</p> <p>2019-11-22</p> <h2 id="Referece"><a href="#Referece">Referece</a></h2> <h3 id="公式"><a href="#%E5%85%AC%E5%BC%8F">公式</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://docs.docker.com/engine/docker-overview/">Docker overview</a><br /> <a target="_blank" rel="nofollow noopener" href="https://docs.docker.com/v17.09/engine/userguide/storagedriver/imagesandcontainers/">About images, containers, and storage drivers</a><br /> <a target="_blank" rel="nofollow noopener" href="https://docs.docker.com/glossary/">Glossary</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://docs.docker.com/develop/develop-images/multistage-build/">Use multi-stage builds</a><br /> <a target="_blank" rel="nofollow noopener" href="https://medium.com/@chemidy/create-the-smallest-and-secured-golang-docker-image-based-on-scratch-4752223b7324">Create the smallest and secured golang docker image based on scratch</a><br /> <a target="_blank" rel="nofollow noopener" href="https://medium.com/@jessgreb01/digging-into-docker-layers-c22f948ed612">Digging into Docker layers</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/Saint1991/items/dcd6a92e5074bd10f75a">FROM scratchから始める軽量Docker image for Go</a><br /> <a target="_blank" rel="nofollow noopener" href="https://dev.classmethod.jp/server-side/docker-server-side/hello-world-use-docker-scratch/">DockerのscratchイメージでHello Worldする</a></p> ブレイン tag:crieit.net,2005:PublicArticle/15528 2019-11-05T17:21:37+09:00 2019-11-05T17:21:37+09:00 https://crieit.net/posts/Mockito Mockito入門 ~モックとスタブ~ <h2 id="概要"><a href="#%E6%A6%82%E8%A6%81">概要</a></h2> <p>Mockitoでスタブとモックについてのまとめです. 実際の使い方というよりはMockとStubが必要なのは何故か, そしてどんな場合かといことを簡単に書き留めておきたいと思います. 使い方はMockitoパッケージのページを見ればだいたい分かると思います.</p> <h2 id="Q. 何故モックを使うのか?"><a href="#Q.+%E4%BD%95%E6%95%85%E3%83%A2%E3%83%83%E3%82%AF%E3%82%92%E4%BD%BF%E3%81%86%E3%81%AE%E3%81%8B%3F">Q. 何故モックを使うのか?</a></h2> <p>コードが複雑化して, 他のオブジェクトを依存性として持ち処理を委譲するようになると, 単体テストのように値の妥当性を検証するだけでは品質を保証できなくなる. 依存性がきちんと呼び出されていることを検証し(verify), その結果が得られた値が期待通りのものであることが求められるから(expect). 前者の検証ではメソッドが呼び出されたことを検証できる機能が必要でその機能を提供するのがモックである.</p> <h2 id="テスト・ダブルについて"><a href="#%E3%83%86%E3%82%B9%E3%83%88%E3%83%BB%E3%83%80%E3%83%96%E3%83%AB%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">テスト・ダブルについて</a></h2> <p>テストで使われる偽物オブジェクトの総称をテスト・ダブルと呼びます. Martin Fowler氏の<a target="_blank" rel="nofollow noopener" href="https://martinfowler.com/articles/mocksArentStubs.html">Mocks Aren't Stubs</a>では以下5つが紹介されている.</p> <div class="table-responsive"><table> <thead> <tr> <th align="center">テスト・ダブル</th> <th align="center">説明</th> </tr> </thead> <tbody> <tr> <td align="center">Dummy</td> <td align="center">何も実装しない</td> </tr> <tr> <td align="center">Fake</td> <td align="center">本物の簡易版</td> </tr> <tr> <td align="center">Stubs</td> <td align="center">決まった値を返す機能</td> </tr> <tr> <td align="center">Spies</td> <td align="center">実際のクラスのメソッドの呼び出しを諜報する</td> </tr> <tr> <td align="center">Mocks</td> <td align="center">メソッドの呼び出しを記録する</td> </tr> </tbody> </table></div> <p>これらは元々はMeszarosという人が定義したらしいです. 元々の定義ではDummyはテスト・ダブルではないと主張されているそうです.</p> <p><a target="_blank" rel="nofollow noopener" href="https://gotohayato.com/content/483">自動テストのスタブ・スパイ・モックの違い</a></p> <p>それぞれの役割は二つに分けられるそうです.</p> <ul> <li>State verification</li> <li>Behavior verification</li> </ul> <p>MockだけがBehavior verificationのためのテスト・ダブルだそうですが, スパイとの違いがよく分かりません. FlutterのMockitoの元になったJavaのMockitoの<a target="_blank" rel="nofollow noopener" href="https://javadoc.io/static/org.mockito/mockito-core/3.1.0/org/mockito/Spy.html">Spy</a>の使い方を見ると, 実際のオブジェクトのラッパーとして使うようです. 対象となるクラスとの交信(メソッド呼び出し)を傍受する(spy)と考えると理解しやすいかもしれません.</p> <p><a target="_blank" rel="nofollow noopener" href="https://thoughtbot.com/blog/a-closer-look-at-test-spies">A Closer Look at Test Spies</a><br /> <a target="_blank" rel="nofollow noopener" href="https://blog.pragmatists.com/test-doubles-fakes-mocks-and-stubs-1a7491dfa3da">Test Doubles — Fakes, Mocks and Stubs.</a></p> <h2 id="StubとMock"><a href="#Stub%E3%81%A8Mock">StubとMock</a></h2> <p>Mockitoの<a target="_blank" rel="nofollow noopener" href="https://pub.dev/packages/mockito#-readme-tab-">ドキュメント</a>を見るととりあえずStubとMockの違いがわかっていれば良さそうです(Fakeもありますが割愛します). 両者はテスト対象が状態(値)か振る舞いかで分類できました.</p> <p>スタブでは値とメソッドの呼び出しを対応づけします. あるメソッドを呼び出したら, 決まった値を返すというルールを決めるわけです.</p> <p>一方Mockはメソッドが正しく呼び出されたか検証します. スタブだけだと, 期待する結果が得られる過程を検証できません. 正しい順番でメソッドが呼び出され(あるいは呼び出されなかった)結果として期待する値が得られたことを確かめる必要があります.</p> <h3 id="Mockitoでモックを作る"><a href="#Mockito%E3%81%A7%E3%83%A2%E3%83%83%E3%82%AF%E3%82%92%E4%BD%9C%E3%82%8B">Mockitoでモックを作る</a></h3> <p>Mockはテスト対象(System Under Test)が実行される際に必要な別のオブジェクトの偽物です. いわゆる依存性というやつです. Mockitoが提供するMockクラスを拡張しモックとしてAPIが必要な対象をimplementsします.</p> <pre><code class="dart">class MockClass extends Mock implements WanToBeMock {} final mock = MockClass(); final sud = SystemUnderTest(mock); </code></pre> <p>SystemUnderTestクラスがテスト対象となるクラスです. このクラスの依存性としてモックが渡されているというのがポイントです. Mock化したクラスには以下の特徴があります.</p> <ul> <li>呼び出したメソッドを記録している.</li> <li>メソッドは全てnullを返す.</li> </ul> <p>モック・インスタンスは仕様通りメソッドを呼び出せるものの何もしません. つまり値を返せないわけです. ただ値の検証はテストの大事な要素なのでスタブで実現します.</p> <h3 id="MockitoにおけるStubbing"><a href="#Mockito%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8BStubbing">MockitoにおけるStubbing</a></h3> <p>メソッド呼び出しが特定の値を返せばスタブで達成したいことを実現できます. whenとthenReturn関数を使います.</p> <pre><code class="dart">when(mock.doSomething()).thenReturn(100); </code></pre> <p>これでmockのdoSomethingメソッドが呼び出された時100を返すというという条件を明示できました.</p> <h3 id="コード"><a href="#%E3%82%B3%E3%83%BC%E3%83%89">コード</a></h3> <pre><code class="dart">class MockClass extends Mock implements WanToBeMock {} void main() { SystemUnderTest sud; MockClass mock; setUp(() { mock = MockClass(); sud = SystemUnderTest(mock); }); test('should return 100 when mock calculates 10 to the power of 2 (10^2).', () { // Arrange when(mock.pow(10, 2).thenReturn(100); // Act sud.pow(10, 2); final result = sud.getData(); // Assert verify( mock.pow(10, 2) // (1) ); expect(result, equals(100)); // (2) }); } </code></pre> <p>SystemUnderTestは様々な計算をラップしたクラスのようです. また実際の処理は別のオブジェクトに委譲しているようです. この場合単に100を返すようなべき乗の計算をSystemUnderTestのpowに実装するだけでは不十分です.</p> <p>実際の計算処理はmockインスタンスに委譲する必要があります. しないとテストがパスしません. テストにパスするためには, powの結果が100であるだけでは不十分です.</p> <ul> <li>mockのpowが内部で呼び出されている (1)</li> <li>powの結果が100である (2)</li> </ul> <p>という両方を満たすようにSystemUnderTestを実装する必要があります.</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>テスト対象に依存性がない場合はわざわざMockitoを使う必要はありません. testパッケージを利用した所謂単体テストで十分です.</p> <p><a target="_blank" rel="nofollow noopener" href="https://flutter.dev/docs/cookbook/testing/unit/introduction">An introduction to unit testing</a></p> <p>多くの場合全ての処理を自前で書くことはないので, 何らかのライブラリ(自作を含む)が提供するクラスを使う必要があります. それらをモックとして利用することで, 単に値が妥当である以上のことをテストできるようになります.</p> <h2 id="補足 (テストの手順など)"><a href="#%E8%A3%9C%E8%B6%B3+%28%E3%83%86%E3%82%B9%E3%83%88%E3%81%AE%E6%89%8B%E9%A0%86%E3%81%AA%E3%81%A9%29">補足 (テストの手順など)</a></h2> <p>テストを書けと言われても何を書いていいか分かりにくいです. 特にMockを使うような場合, つまり依存性が存在する場合に指針があると分かりやすいです. またこの一般的な手順が分かっているとテストコードを読む時もまとまりが見えてきます.</p> <h3 id="AAA"><a href="#AAA">AAA</a></h3> <p>AAAはArange, Act, Assertの略でテストを書くときの一般的な手順を表します. Arrangeではwhenによるstubbingなんかを行います. Actではテスト対象を実際に実行します. Asseertではメソッドは正しく呼び出せているかやその結果の戻り値が正しいかを検証します. 頭の中でこのフェーズを意識しておくことでテストコードが書きやすくなると思います.</p> <div class="table-responsive"><table> <thead> <tr> <th align="center">フェーズ</th> <th align="center">説明</th> </tr> </thead> <tbody> <tr> <td align="center">Arrange</td> <td align="center">テスト・ダブルやインスタンスの準備</td> </tr> <tr> <td align="center">Act</td> <td align="center">テスト対象の呼び出し</td> </tr> <tr> <td align="center">Assert</td> <td align="center">結果の検証</td> </tr> </tbody> </table></div> <h3 id="Red, Green, Refactor"><a href="#Red%2C+Green%2C+Refactor">Red, Green, Refactor</a></h3> <p>TDD(Test Driven Development)ではテストは最初失敗するように書きます(Red). これは実装がないところからテストを始めるのでActが失敗するのは当然です. その後テストをパスできるように実際のコードを実装します(Green). その後このプロセスを繰り返しながらコードの品質の向上を目指します(Refactor).</p> <h3 id="Fixture"><a href="#Fixture">Fixture</a></h3> <p>Fixtureはテスト用のデータのことです. 例えばHTTP通信でJSONを受け取るような場合に, 実際の通信相手がまだ存在しない, あるいは利用できない場合が考えられます. Fixtureとして定義しておくことでこうしたバックエンドの実装に左右されることなく, テストを実行できます.</p> ブレイン