まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

「複数人でプログラミングを体験する」ための環境を作ることになった場合、その環境をどう実現すればいいかを考えました。
computer_mob_programming


より具体的にはこんな状況・前提だと思ってください(余談ですが DX の影響なのか、業務でこういう需要を耳にする機会が増えているように感じてます):
- 複数人でアプリ開発(プログラミング)体験を行いたい
  - 参加者は1チーム5~6人程度
  - 参加者は普段 Windows を使っている。Mac, Linux の経験はほぼゼロ
  - 参加者はコマンドプロンプトなどの CLI は普段使っていない
  - 参加者はプログラミングに関してはほぼ初心者
- チームで1つのアプリを作って動かす、という体験をしたい
- 体験期間は半日、長くて1日
- 参加者の PC に何かをインストールしたり、アカウントを作ったり、設定したりする場合、作業できるのは当日
  - 参加者 PC 以外の Linux サーバーなどに主催者が事前準備するのは自由
- 全員オンライン参加、zoom などのオンラインツールの利用に問題はない


最後の前提は必須ではないですが、昨今だとどうしてもこの条件になるかなあ、ということで加えています。


この状況下でどういう仕組を用意すれば、目的に会ったプログラミング体験ができるかを考えます。


候補案1
「とりあえず正攻法でやってもらう」方法です。ソースコードは git などでリポジトリ管理して(必要であれば事前にサンプルを用意して)、各自はリポジトリから clone したソースコードを画面共有を併用しながら手元で編集して動かしてもらう。環境が許せば Visual Studio Code の LiveShare なども使って共同編集する。。

・・・これが本来のスジであることは重々承知しているし、将来的に役立てることを意識するならこの形を学ぶべきだとは思っています。ただプログラミング初心者相手に半日~1日でここまでやるには少しハードルが高すぎる気がしています。環境準備段階でのハードルの高さもありますし、git も教えないといけないし、LiveShare 使うならアカウントから準備しないといけないし、その上でほぼ未経験のプログラミングをオンラインで・・・ 何を作るかにもよりますが、Hello World 程度すら厳しいかも。。

#おまけとして、業務においてはこの手の「管理者権限が必要な作業」自体が実施上のリスクとなる可能性があることを経験談として触れておきます。


候補案2
参加者がそこそこ Linux に詳しい人だったら、Linux サーバーにサンプルを含むソースコードを集める形にして、全員が個別に SSH や VNC でログインして vi でプログラミングすればよい、です(同時編集によるコンフリクトはいったん目をつぶりますw)。同じサーバーでアプリを動かせば、それぞれがウェブブラウザで動作確認もできます。

でも今回、この形で行うにはハードルが高すぎます。普段から SSH やコマンドプロンプトを使うような人ではなく、ましてやキーバインドにクセのある vi を使わせるのは厳しそうです。

手段の1つとして「SSH でログインして nano エディタを使う」ことや「Linux に X Window まで導入した上で、VNC でログインして簡易テキストエディタを使う」も考えられます。これらはありっちゃあり、懸念があるとすれば慣れない CLI や Linux での操作でしょうか。nano エディタも vi や Emacs ほどクセはありませんが、Linux コマンドを無視することもできませんし、CTRL キーや ALT キーと組み合わせてのメニュー操作は慣れるまでは苦労するかも、という印象です。

その辺りの苦労も体験の一部とみなしてやってもらう、という案は相手次第ではあり、かな。。


ここまで書いておいてアレですが、個人的にはここまでの案1&2は現実的ではないかな、と考えています。オンラインでなければまだしも、オンラインでこの慣れてないはずの作業のサポートをするのはかなり難しそうという印象を持っています。理想どおりに実施するのことがかなり厳しそう・・・


候補案3
いわゆるローコード・ノーコード環境を事前に Linux サーバーに用意して、この環境を使う方法です。

これは目的に対する解答になっていると思います。Linux に例えば Node-REDScratch などをインストール&起動しておいて、参加者は画面共有しながらウェブブラウザで(CLI ではない、ここがでかい!)アクセスしてコーディング作業を行う、というものです。なんといっても参加者の PC に何かを事前にインストールする必要がない(=準備段階のリスクが低い)点がポイントです。ローコードなのでプログラミング開始時の敷居が低く、1日でもそこそこ学べるものだと思っています。

懸念が2つあるとすれば、この環境が1つは共同作業に適したツールかどうかの判断や対応が必要になる点と、もう1つはやはりどうしてもできることの制限があることです。ローコードであるが故に「ループ」や「条件分岐」といったコーディングの基礎のようなことを行う選択肢が少なかったりするわけです(ループができないとは言わないけど「整数配列をその数だけループさせながら加算する」とかは Node-RED では難しい)。動くものは作れるかもしれないけど、プログラミング体験という当初の目的を達成できるものになるかどうか、が鍵だと感じました。


候補案4
実はいま個人的にはこれが一番いいかも、と考えているのが、この候補案4です。概要はこんな感じ:
(1) サンプルを含むソースコードを事前に Linux サーバーに用意する
(2) 同サーバーに Eclipse Orion をインストールする
(3) 参加者はウェブブラウザで Eclipse Orion にアクセスして、サーバーのソースコードを直接編集
(4) 誰か一人(主催者でもよい)がサーバーでアプリを起動して動作確認

(1) のサンプルはあらかじめ最低限動くものを用意しておきます。目的にもよりますが、Hello World 表示だけのウェブアプリでもかまわないと思ってます。

(2), (3) の Eclipse Orion は「オンラインテキストエディタ」です。サーバー上で起動し、サーバー内の特定フォルダ以下のテキストファイル(ソースコード)をオンラインで編集できるようになります。テキストエディタとしての基本機能があるので、便利にコーディング作業をすすめることができます。オンラインエディタは必ずしも Orion エディタでなくてもいいと思ってますが、オープンソースであることや docker 環境下で使える便利さもあっておすすめです。

(4) そして編集したソースコードを使って実際に動かし、可能であれば全員がウェブブラウザで動作も確認する、というものです。

この方法も候補案3同様に、参加者 PC 側での事前準備が不要です。CLI 操作もなく、全て GUI 作業です。Java なり JavaScript なりの実行環境もサーバーだけに事前に用意しておけばいいので参加の負担はありません。実際にコーディングも行うので、内容も(分岐やループから、外部API へアクセスしての AI 体験みたいなことまで)自由度高く設計可能です。

プログラミング環境も、Eclipse Orion インスタンスを1つだけ起動して、全員で同じインスタンスに接続して共同プログラミングしてもいいし、Eclipse Orion インスタンスを複数起動して別々に接続させることで同じテーマで個別にプログラミングすることもできます。純粋なプログラミング環境を比較的容易に準備する方法と考えます。


こちらの懸念はあらかじめ用意しておくサンプルをどうするかと、候補案3と異なり「実際にプログラミングを行う(しかも半日~1日で)」ことになる点です。データベースを使うかどうかなど、サンプルに合わせたカリキュラムの検討が必要だと思っています。その代わり「プログラミング体験」という目的に合っていて、「まず一度体験してもらう」ための案としては自由度も高くて悪くない、と思っています。

 

今更感がありますが、「ハンバーガーメニュー」と呼ばれるメニューがあります。三本線の見た目がハンバーガーのように見えることから名付けられたメニューで、ここをクリック(タップ)すると隠れていたメニューアイテムが表示されて選択できるようになる、というものです。スマートフォンなどの、メニューを常時表示するには小さい画面でメニューを実現するためのメジャーな方法です。

このハンバーガーメニューを(ウェブアプリの)CSS だけで実現する方法がいくつか紹介されています。まあコピペすれば動くし、自分も以前はそうしていたのですが、ちゃんと理屈を理解してはいませんでした。そこで今回改めて自分で理解しながら作って公開してみました。まずは一度動かしてみて、ハンバーガーメニューの挙動を確認してみてください:
https://dotnsf.github.io/css_hamburgermenu/

(ハンバーガーメニュー)
2021022001

(タップすると左からメニュー本体が表示され、自分は×印に。×をタップで元に戻る)
2021022002


ハンバーガーメニューに限りませんが、このような動きのある処理を実現するには、一般的には JavaScript を使います。またメニューの見た目である三本線や、メニューが開いた時の×印の表現は一般的には画像を使って表現することが考えられます。上図のページではどちらも使わず、純粋な HTML と CSS だけで実現しています。

同じようなコード(HTML & CSS)のサンプルは数多く存在していて、コピー&ペーストすれば動くようにはなっています。ただコードの中身の解説がされていない場合が多く、「なんでこのコードでハンバーガーメニューになるのか」の理解が難しいのでした。

というわけで、どうやって画像も JavaScript を使わずにハンバーガーメニューを実現しているかを、上図の例を使って解説します。


まず、このページの HTML と CSS は以下のようになっています(よかったらコピペするかダウンロードして、手元の HTML ファイルに保存して動かしてみてください):
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8"/>

<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="apple-mobile-web-app-title" content="CSS Hamburger Menu"/>

<style>
/* ハンバーガーメニューのサイズ(32x32) */
.hamburger-menu{
  vertical-align: center;
  width: 32px;
  height: 32px;
}
/* ハンバーガーメニューの位置と色 */
.menu-btn{
  top: 20px;
  left: 10px;
  display: flex;
  height: 32px;
  width: 32px;
  justify-content: center;
  align-items: center;
  z-index: 90;
  background-color: #005B00;
}
/* メニュー線(本体と before と after で3本表示する) */
.menu-btn span,
.menu-btn span:before,
.menu-btn span:after{
  /* 20x3 の白線 */
  content: '';
  display: block;
  height: 3px;
  width: 20px;
  border-radius: 3px;
  background-color: #ffffff;
  position: absolute;
}
/* before を少し上にずらして描画 */
.menu-btn span:before{
  bottom: 8px;
}
/* after を少し下にずらして描画 */
.menu-btn span:after{
  top: 8px;
}

/* メニューをオープンしたら三本線を×にする */
#menu-btn-check:checked ~ .menu-btn span{
  /* メニューオープン時は三本線の真ん中の線を透明にする */
  background-color: rgba( 255, 255, 255, 0 ); 
}
#menu-btn-check:checked ~ .menu-btn span::before{
  /* メニューオープン時は三本線の上の線を 45 度傾ける */
  bottom: 0;
  transform: rotate( 45deg ); 
}
#menu-btn-check:checked ~ .menu-btn span::after{
  /* メニューオープン時は三本線の下の線を -45 度傾ける */
  top: 0;
  transform: rotate( -45deg ); 
}
/* チェックを非表示にする */
#menu-btn-check{
  display: none;
}

/* メニュー装飾 */
.menu-content{
  width: 60%;
  height: 100%;
  position: fixed;
  top: 80;
  left: -100%; /* メニューを画面外へ */
  z-index: 80; /* 下のコンテンツの上にかぶせて表示する */
  background-color: #005B00;
  transition: all 0.5s; /* 0.5秒かけてアニメーションで出し入れする */
}
.menu-content ul{
  padding: 70px 10px 0;
}
.menu-content ul li{
  border-bottom: solid 1px #ffffff; /* メニューアイテムの区切り線 */
  list-style: none;
}
.menu-content ul li a{
  display: block;
  width: 100%;
  font-size: 15px;
  box-sizing: border-box;
  text-decoration: none;
  color: #ffffff;
  padding: 9px 15px 10px 0;
  position: relative;
}

/* メニューの出し入れ */
#menu-btn-check:checked ~ .menu-content{
  left: 0;  /* チェックされたら画面内へ */
}
</style>

<body>

<span class="header">
  <div class="hamburger-menu">
    <!-- ここからメニューボタン -->
    <input type="checkbox" id="menu-btn-check"/>
    <label for="menu-btn-check" class="menu-btn"><span></span></label>
    <!-- ここまでメニューボタン -->

    <!-- ここからメニュー本体 -->
    <div class="menu-content">
      <ul>
        <li><a href="#">メニューアイテム1</a></li>
        <li><a href="#">メニューアイテム2</a></li>
        <li><a href="#">メニューアイテム3</a></li>
      </ul>
    </div>
    <!-- ここまでメニュー本体 -->
  </div>
</span>

</body>
</html>

画像も JavaScript も使っていません。CSS もすべてこの中に含まれていて外部ファイルを参照していません。本当に「これだけ」です。この内容を index.html として保存し、ローカルのブラウザで開けば同じ挙動を確認できるはずです。

HTML の <body> 部分だけを抜き出すとこのようになっています:
<body>

<span class="header">
  <div class="hamburger-menu">
    <!-- ここからメニューボタン -->
    <input type="checkbox" id="menu-btn-check"/>
    <label for="menu-btn-check" class="menu-btn"><span></span></label>
    <!-- ここまでメニューボタン -->

    <!-- ここからメニュー本体 -->
    <div class="menu-content">
      <ul>
        <li><a href="#">メニューアイテム1</a></li>
        <li><a href="#">メニューアイテム2</a></li>
        <li><a href="#">メニューアイテム3</a></li>
      </ul>
    </div>
    <!-- ここまでメニュー本体 -->
  </div>
</span>

</body>

<body> はハンバーガーメニューのボタン部分と、ボタンをタップした時に表示されるメニュー本体部分に分かれています。メニュー本体はそれほど難しくないので、このブログエントリではボタン部分の CSS を中心に、以下の4つの観点から詳しく紹介します:

(1) JavaScript なしでどうやってタップされる前と後を識別しているのか?
(2) 「三本線」の見た目を画像を使わずにどうやって実現しているのか?
(3) (タップ後の)×印の見た目を画像を使わずにどうやって実現しているのか?
(4) JavaScript なしでどうやってメニュー本体の出し入れを実現しているのか?



(1) JavaScript なしでどうやってタップされる前と後を識別しているのか?

<body> のハンバーガーメニューボタン部分に以下のコードが含まれています:
    <input type="checkbox" id="menu-btn-check"/>

この部分はチェックボックスですが CSS で非表示に設定されているので、画面上には現れていません:
/* チェックを非表示にする */
#menu-btn-check{
  display: none;
}

メニューをタップするとチェックが入り、もう一度タップするとチェックが外れます(ただし画面には表示されません)。このチェックの有無でタップされる前と後を識別しているのでした。

また HTML ではこの部分の直後に <label for="menu-btn-check" class="menu-btn"><span></span></label> があります。後述しますがこの <span></span> 部分で三本線や×印を描画します。つまり三本線部分をタップすることで、このチェックボックスのチェックが入ったり消えたりするようになっていて、このチェックの有無で三本線か×印か、どちらを描画するかを切り替えられるような仕組みを実現しています。

以下の説明をわかりやすくするために、ここで一時的にチェックボックスの非表示スタイルを解除してみましょう。以下は該当 CSS 部分にこのようなコメントが入っている前提で説明を続けます:
/* チェックを非表示にする */
#menu-btn-check{
/*  display: none; */
}

この時点でハンバーガーメニューや×印の上部に(今まで非表示だった)チェックボックスが表示されているはずです:

2021022003

2021022004


(2) 「三本線」の見た目を画像を使わずにどうやって実現しているのか?

今回のブログを書くために色々調べる前は、ここが最大の謎でした。この三本線が画像でないとしたら何? SVG で描画しているのかと思ったけどそういう記述はないし・・・ と興味津々だった謎でした。

これを実現している HTML 及び CSS 部分は以下です:
(HTML)
<label  class="menu-btn" for="menu-btn-check"><span></span></label>

(CSS)
/* メニュー線(本体と before と after で3本表示する) */
.menu-btn span,
.menu-btn span:before,
.menu-btn span:after{
  /* 20x3 の白線 */
  content: '';
  display: block;
  height: 3px;
  width: 20px;
  border-radius: 3px;
  background-color: #ffffff;
  position: absolute;
}
/* before を少し上にずらして描画 */
.menu-btn span:before{
  bottom: 8px;
}
/* after を少し下にずらして描画 */
.menu-btn span:after{
  top: 8px;
}

HTML の <span> 部分に線を描画します。そのため CSS で背景色(#ffffff = 白)、高さ(3px)、幅(20px)の塗りつぶし矩形を描画しています。実際は白い矩形ですが、20x3 と横に細長いので白線に見えるというわけです。親要素である .menu-btn の指定で縦横とのセンタリングされて(つまり 32x32 のメニューボタンの真ん中に)表示されます。

これだけだと線が1本表示されるだけですが、CSS では span, span:before, span:after すべてで同じ指定がされています。つまり before と after も合わせると3本の線が描画されることになります(まさかこんな方法で3本描いていたとは・・。この方法だと4本に増やすことはできないかも。。)。別途 before は bottom:8px; で本体よりも少し上に、after は top:8px; で本体よりも少し下に描画するよう指定しており、これらの結果3本の白線が縦に並んでハンバーガーメニューとして表示されているのでした。これ考えた人天才だな。。

2021022005



(3) (タップ後の)×印の見た目を画像を使わずにどうやって実現しているのか?

今回の調査をする前に一番の謎だったのが (2) とすると、調査し終わって一番感動したのがこの (3) の実現方法でした。前述の方法で三本線が表示できるとして、これをタップした時にどうやって表示内容を×印に(画像も JavaScript も使わずに)切り替えるのか、です。

上述したように、まずタップしたかどうかはチェックボックスのチェックの有無で判断できるようになっています。つまり #menu-btn-check:checked の時の .menu-btn の見た目を三本線から×印に変えればよい、ということになります。ここまではなんとなくわかります。

では今度はどのようにして×印の見た目を作るのか、その答えがこちらです。チェックが付いている時の span, span:before, span:after に適用しているスタイルです:
/* メニューをオープンしたら三本線を×にする */
#menu-btn-check:checked ~ .menu-btn span{
  /* メニューオープン時は三本線の真ん中の線を透明にする */
  background-color: rgba( 255, 255, 255, 0 ); 
}
#menu-btn-check:checked ~ .menu-btn span::before{
  /* メニューオープン時は三本線の上の線を 45 度傾ける */
  bottom: 0;
  transform: rotate( 45deg ); 
}
#menu-btn-check:checked ~ .menu-btn span::after{
  /* メニューオープン時は三本線の下の線を -45 度傾ける */
  top: 0;
  transform: rotate( -45deg ); 
}

まず三本線の真ん中の白線(span)については background-color: rgba( 255, 255, 255, 0 ); のスタイルを適用します。これはいわば「透明にする」ためのスタイルで、要するに真ん中の白線を透明にして見えなくします。

次に三本線の上の白線(span:before)は bottom:0; で真ん中の白線と同じ高さに移動した上で transform: rotate( 45deg ); を適用します。この指定は「45度回転させる」スタイルです。つまり水平線(ー)だったものをバックスラッシュ(\)のような線に変えています。

同様に三本線の下の白線(span:after)も top:0; で真ん中の白線と同じ高さに移動した上で transform: rotate( -45deg ); を適用してます。この指定は「-45度回転させる」スタイルであり、水平線(ー)だったものをスラッシュ(/)のような線に変えています。
2021022005


その結果が×印になっていたのでした。眼からウロコ、そんな方法だったのか・・・


(4) JavaScript なしでどうやってメニュー本体の出し入れを実現しているのか?

最後のこの部分は、ちゃんと勉強し直す前から自分で理解できていた、という意味で「一番簡単な仕組み」でした。大まかな仕組みとしてはメニューそのものを作った後、チェックボックスにチェックが入っていない時は表示されない位置に配置し、チェックが入ったら表示される位置まで transition 指定でアニメーションをかけてあげればよいだけのことです(そしてチェックが外れたら再び表示されない位置までアニメーションで戻す)。

上記のコード内では以下のように指定しています:
/* メニュー装飾 */
.menu-content{
  width: 60%;
  height: 100%;
  position: fixed;
  top: 80;
  left: -100%; /* メニューを画面外へ */
  z-index: 80; /* 下のコンテンツの上にかぶせて表示する */
  background-color: #005B00;
  transition: all 0.5s; /* 0.5秒かけてアニメーションで出し入れする */
}
  :
  :
/* メニューの出し入れ */
#menu-btn-check:checked ~ .menu-content{
  left: 0;  /* チェックされたら画面内へ */
}


メニュー本体は .menu-content クラスで定義された部分です。チェックがついていない時点ではここに left: -100%; を指定することで画面の幅1つぶん左の位置に表示しておきます。

そしてチェックが付いた時のスタイルで left: 0; を適用します。これでチェックが付くと画面の左端からメニューが表示されるようになります(チェックが再び外れると、また画面の左端の更に左へ移動して見えなくなります)。この時に .menu-content クラス内で transition: all 0.5s; を適用しておくことで、これらの移動を 0.5 秒かけたアニメーション処理で行うようになります。つまりチェックが付くと左端からすーっと表示され、チェックが外れると左端にすーっと吸い込まれて見えなくなる、という視覚効果をつけることができるのでした。


これら (1), (2), (3), (4) をあわせて適用することで、(画像も JavaScript も使わずに)CSS だけでハンバーガーメニューを実現することができる、というものでした。この「CSS だけで実現する」というのは、「スマホに適した処理として実現できる」とも言えます(簡単に言うと「GPU 併用で処理できるので、比較的貧弱なスマホの CPU に大きな負担をかけずに実現できる」ということです。詳しくはこちら)。この CSS だけの方法だとハンバーガーメニューの見た目を更に変更するのはなかなか難しいなどの制約もありますが、「工夫次第でここまでできる」というクラフトマンシップというか、ある意味天才的な発想を垣間見たような気分で、今後もこの方法を使っていきたいと思う内容でした。

で、動作を確認し終わったら最初にコメントしたチェックボックスの非表示設定はもとに戻しておきましょう。
/* チェックを非表示にする */
#menu-btn-check{
  display: none;
}






以前からこのテーマに興味があって、色々調べたり試したりしてもなかなかうまく行かず、半ばあきらめかけていた所で成功したので、その手順をまとめておきました。後述しますが、このテーマは今後多くの人が興味を持つ可能性が高いと思っているので、そんなみなさんのお役に立てれば。。


【背景】
おそらく最もメジャーなコンテナ技術の1つである docker は、docker hub に公開された多くのイメージが利用できる便利さとの相乗効果もあって、多くのコンテナ技術者に利用されています。自分も勉強用・動作検証用の簡易アプリイメージを docker hub に格納して使っています。

ただ最近になって docker 利用時に少し困ることも起こるようになりました。困っている内容を端的に表現すると「自分が公開しているアプリイメージは特定のアーキテクチャ向けに作られていて、異なるアーキテクチャの docker からは使えない」という問題でした。

もう少し詳しく説明します。自分はメインの開発環境としては Windows 10 を使っています。docker 環境は Windows 10 に Docker Desktop をインストールしてサーバーとして利用し、この環境に WSL2 の Ubuntu から docker コマンドを使って利用する、という使い方をしています。が、このメインマシン以外にも数台の PC があり、その中にはラズベリーパイも含まれています。ラズベリーパイにも docker を含めた開発環境が導入されていて、ラズベリーパイでも docker 環境が利用できます。

この環境が問題でした。Windows 10 や WSL2 の docker は linux/amd64 という「インテルアーキテクチャ CPU の 64 ビット Linux」向けの docker です。一方ラズベリーパイの docker は linux/arm/v7 という「ARM CPU の 32 ビット Linux」向けの docker です。これら2つのアーキテクチャは CPU が異なることもあり、バイナリ互換はありません。したがってどちらかで動くバイナリは、もう一方では動かないことになります。この問題は docker でも(後述の方法を使わない限りは)解決しておらず、一方の docker 環境で作成したイメージは、もう一方では動かない、という現象が発生していたのでした。このため Windows + WSL2 で docker イメージを作って docker hub に登録しても、ラズベリーパイからは使えないし、逆にラズベリーパイで docker イメージを作って docker hub に登録しても、Windows + WSL2 では使えない問題が発生していました。

この問題を解決するのが今回のテーマであるマルチ CPU アーキテクチャ対応 docker イメージです。名前の通りで複数のアーキテクチャ(今回であれば linux/amd64 と linux/arm/v7)に対応した docker イメージを作って docker hub に登録することで、docker pull を実行した環境のアーキテクチャに対応したイメージがダウンロードされ、Windows + WSL2 からもラズベリーパイからも docker 環境内で利用することができるようになるものです。このマルチ CPU アーキテクチャ対応 docker イメージの作り方を以下に紹介します。


【操作環境】
- Windows 10 Pro に Docker Desktop をインストール(docker エンジンは 19.03.13)。
- WSL2 に docker CLI をインストール(バージョン 19.03.13、19.03 以上であれば後述の buildx コマンドが使えます)

未確認ですが、docker は linux/amd64 の macOS 環境であっても以下の操作は可能です。ただ M1 と呼ばれる新しい環境の macOS から利用できるかどうかはわかりません。またラズベリーパイの docker や、CentOS7 環境に用意した docker 環境では後述の buildx コマンド自体は使えるのですが、他プラットフォーム向けのビルドに対応していないようで、操作環境としては事実上不適格でした。

docker CLI を WSL ではなく Windows から実行するケースも未確認ですが、このような open issue もあって、もしかすると動かない可能性もあるのではないかと思っています:
https://github.com/docker/for-win/issues/4991


【操作前の準備(ウェブアプリと docker hub アカウントの用意)】
最初に docker イメージを作る際のアプリケーションを用意します。もちろん自分でアプリを開発できる方はそれを使っていただいても構いません。そうでない人向けに自分が以下で解説する際に使った hostname アプリのソースコードを公開しているので、こちらをダウンロードするか git clone して使って試していただいても構いません:
https://github.com/dotnsf/hostname

このアプリは Node.js で記述されたシンプルなアプリで、以下のような挙動です(もちろん特定のアーキテクチャに依存するような内容ではありません):
- 8080 番ポートで HTTP リクエストを待ち受け
- "/" にアクセス(例えば同じホストからであれば http://localhost:8080/ にアクセス)すると、/etc/hostname ファイルの内容をそのまま text/plain で返す


普通の Windows(WSL) や macOS 、ラズペリーパイを含む Linux 環境では /etc/hostname にはそのマシンのホスト名称が記載されているのが一般的ですが、特に docker 環境においては /etc/hostname には稼働中のコンテナIDが記載されます。したがって k8s などを併用して複数インスタンスを起動してこのアプリのイメージを実行してアクセスした場合、アクセス結果からどのコンテナに振り分けられて処理されたのかが視覚化されて便利なので、そういった動作確認時などに使っていたアプリでした。
//.  app.js
var express = require( 'express' ),
    fs = require( 'fs' ),
    app = express();

app.get( '/', function( req, res ){
  res.contentType( 'text/plain; charset=utf-8' );
  fs.readFile( '/etc/hostname', "utf-8", function( err, text ){
    if( err ){
      res.write( JSON.stringify( err, 2, null ) );
      res.end();
    }else{
      res.write( text );
      res.end();
    }
  });
});

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );


ちなみに Dockerfile は以下のような内容です:
# base image
FROM node:12-alpine

# working directory
WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 8080
CMD ["node", "app.js"]


ベースイメージに node:12-alpine を指定しています。ここで注意点として、ベースイメージにアーキテクチャ依存の指定を含まないようにする必要があります。例えばラズペリーパイ向けのイメージを明示的に作ろうとすると、ここは
FROM arm32v7/node:12-alpine

のようにラズベリーパイで使われている arm32v7 アーキテクチャ向けの node:12-alpine イメージをベースとする、という指定を明示的にすることも可能です。が、このような指定をしてしまうとインテル CPU アーキテクチャ向けのイメージを作る際のエラーの原因となってしまいます。このような特定アーキテクチャ向けのベースイメージ指定はしないようにしてください。


そして、もしもまだ docker hub のアカウントをお持ちでない場合は、実際の操作の最後に必要になるので、今のうちに docker hub をアカウントを取得しておいてください。取得済みの人はログインできるように ID とパスワードを思い出しておいてください。


【操作内容】
では実際にアプリケーションファイルと Dockerfile からマルチCPUアーキテクチャー対応 docker イメージを作って docker hub に push してみます。なお今回は linux/amd64(インテルCPU向け 64 ビット Linux)と linux/arm/v7(ラズベリーパイ)の2種類のアーキテクチャー向けにイメージを作る例を紹介しますが、同様の操作で linux/ppc64le(POWER アーキテクチャー向け 64 ビット Linux)や linux/s390x(zLinux)を対象としたマルチCPUアーキテクチャー対応イメージを作ることも可能です。


(追記 2021/02/15)
その後イメージをアップデートし、現在は linux/arm/64, linux/ppc64le, linux/s390x を含めた5アーキテクチャ対応イメージを公開しています
(追記終わり)


まず最初に、今回行うマルチCPUアーキテクチャー対応イメージのビルドは、docker の試験的機能の1つであり、これを行うには試験機能を有効にしておく必要があります。そのためターミナルや WSL2 のコンソールを開き、~/.bashrc に以下の行を追加するなどして、環境変数 DOCKER_CLI_EXPERIMENTAL の値を enalbed に設定しておいてください:
export DOCKER_CLI_EXPERIMENTAL=enabled

設定後、ターミナルを一度閉じてから再度開きます(これで上記環境変数の設定が有効な状態でターミナルが開きます)。

確認のため、以下のヘルプコマンド("docker --help")を実行します。結果が下図のように buildx* といった * が付いた試験機能を含めたコマンドリストのヘルプが表示されれば試験機能が有効になっています:
$ docker --help

Usage:  docker [OPTIONS] COMMAND

A self-sufficient runtime for containers

Options:
      --config string      Location of client config files (default "/home/dotnsf/.docker")
  -c, --context string     Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var and
                           default context set with "docker context use")
  -D, --debug              Enable debug mode
  -H, --host list          Daemon socket(s) to connect to
  -l, --log-level string   Set the logging level ("debug"|"info"|"warn"|"error"|"fatal") (default "info")
      --tls                Use TLS; implied by --tlsverify
      --tlscacert string   Trust certs signed only by this CA (default "/home/dotnsf/.docker/ca.pem")
      --tlscert string     Path to TLS certificate file (default "/home/dotnsf/.docker/cert.pem")
      --tlskey string      Path to TLS key file (default "/home/dotnsf/.docker/key.pem")
      --tlsverify          Use TLS and verify the remote
  -v, --version            Print version information and quit

Management Commands:
  app*        Docker Application (Docker Inc., v0.8.0)
  builder     Manage builds
  buildx*     Build with BuildKit (Docker Inc., v0.4.2-tp-docker)
  config      Manage Docker configs
  container   Manage containers
:
:

ではここからは実際にマルチCPUアーキテクチャー対応 docker イメージを作るための操作を行います。まず最初に "docker buildx ls" を実行して現在のビルダーインスタンスの一覧を確認します:
$ docker buildx ls

NAME/NODE    DRIVER/ENDPOINT             STATUS  PLATFORMS
default *    docker
  default    default                     running linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6

default という名前の標準ビルダーインスタンスが見つかります(横の * は現在選択されているビルダーインスタンスを示します)。このビルダーインスタンスは linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6 のビルドに対応している、ということがわかります。

※なお CentOS7 版の docker や、ラズベリーパイ版の docker でこのコマンドを実行すると buildx コマンドそのものは利用できるのですが、ビルドの対応プラットフォームがあまりに少なく、クロスビルド環境としては不十分であることがわかります。なので今回は Windows(WSL) 版の docker コマンドを使って作業します。

ということはこの default ビルダーインスタンスを使うことで linux/amd64 と linux/arm/v7 向けイメージをビルドできそうに見えるのですが、実際にやってみるとこのエラーになってしまいます。理由や原因はよくわからないのですが、新しいビルダーインスタンスを作成して使うことで回避できそうなので、今回はその方法を紹介します。


といった事情もあり、まずはクロスビルド用のビルダーインスタンスを作成します。以下のコマンドを実行してビルダーインスタンスを(下の例では mybuilder という名前で)新たに作成&選択&起動します:
$ docker buildx create --name mybuilder

$ docker buildx use mybuilder $ docker buildx inspect --bootstrap

この時点でのビルダーインスタンス一覧は以下のようになります。mybuilder が作成されて選択(* 印)され、起動(STATUS が running) の状態になっているはずです:
$ docker buildx ls

NAME/NODE    DRIVER/ENDPOINT             STATUS  PLATFORMS
mybuilder *  docker-container
  mybuilder0 unix:///var/run/docker.sock running linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
default      docker
  default    default                     running linux/amd64, linux/arm64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6

ここまでの設定で新たに作成した mybuilder ビルダーインスタンスを使って docker イメージを(クロス)ビルドする準備が整いました。では早速クロスビルドしようと思うのですが、その前に少し注意が必要です。

ただ単に linux/amd64 と linux/arm/v7 向けにクロスビルドを実行しようとすると、アプリケーションソースと Dockerfile の存在するディレクトリで以下のコマンドを実行することになります(エラーになるので実行しなくていいです):
$ docker buildx build --platform linux/amd64,linux/arm/v7 -t dotnsf/hostname --load .

クロスビルドの対象プラットフォームを --platform オプションに続けて指定します(上の例では linux/amd64,linux/arm/v7)。また -t オプションに続けてイメージの名称(上の例では dotnsf/hostname)を指定するのですが、一般的には (docker hub のログイン名)/(アプリケーション名) という形で指定します。僕の場合はそれが dotnsf/hostname となるわけですが、ここは皆さんの環境に合わせて変更してください(僕の hostname アプリを使う場合は dotnsf の部分だけを皆さんの docker hub ログイン名に変えてください)。

コマンドとしてはこれで指定したプラットフォーム用の docker イメージがビルドされる・・・はずなのですが、docker は自分のビルド環境(今回の場合は linux/amd64)と異なるプラットフォーム( linux/arm/v7)のイメージを出力できないという制約があります。そのためこのコマンドをそのまま実行してもエラーとなってしまうのでした。

この制約を回避するため、docker のクロスビルド結果をローカル docker ではなく、直接 docker hub に向けて出力することにします。普段はこういう使い方はあまりしないと思うのですが、マルチ CPU アーキテクチャー対応 docker イメージを作る場合の例外方法だと認識する必要がありそうです。

そのためビルドを実行する前に docker hub へログインします。"docker login" コマンドを実行し、自分の docker hub アカウントのユーザー名とパスワードを指定してログインします:
$ docker login

Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: dotnsf
Password: ********
Login Succeeded

ログイン成功後に改めてクロスビルドを行います。今度は上述のコマンドの最後のオプションとして指定した --load の代わりに --push オプションを指定します(これでビルド結果を直接 docker hub に送信します):
$ docker buildx build --platform linux/amd64,linux/arm/v7 -t dotnsf/hostname --push .

このコマンドの実行にはかなりの時間がかかります。通信環境やビルド内容にもよりますが、自分の場合は hostname アプリを2つのプラットフォーム向けにビルドして docker hub へ送信し終わるまで約10分かかりました。

このコマンドが成功すると docker hub にビルドしたイメージが保存されているはずです。自分が上のコマンドを実行した結果はこちらから確認できます:
https://hub.docker.com/r/dotnsf/hostname

20210213

↑Tags タブを参照すると、このイメージが linux/amd64 及び linux/arm/v7 の両プラットフォームに対応していることが確認できます。

これで目的のマルチ CPU アーキテクチャー対応 docker イメージをビルドして docker hub に push することができました。今後、この mybuilder ビルダーインスタンスをそのまま使い続ける場合はそのままでもいいのですが、元の default ビルダーインスタンスに戻して使い続ける場合は以下のコマンドを実行しておいてください:
$ docker buildx stop mybuilder  (mybuilder 停止)

$ docker buildx use default  (default を選択)

$ docker buildx rm mybuilder  (mybuilder を削除する場合はこれも)


【動作確認】
では実際に上で作成したイメージで動作確認してみます。linux/amd64 か linux/arm/v7 の docker 環境(可能であれば両方)がある方も以下のコマンドで動かすことができるので是非お試しください。上述のビルダーインスタンス作成などは不要です。

まずは linux/amd64 の docker 環境で以下のコマンドを実行します:
$ docker run -d --name hostname -p 8080:8080 dotnsf/hostname

初めて実行した場合は dotnsf/hostname イメージのダウンロードから始まるので起動まで少し時間がかかるかもしれませんが、ダウンロード完了後(2度目以降の実行であればすぐに)コンテナが作成されて実行され、8080 番ポートで HTTP リクエストを待ち受ける状態になります。

起動後、以下のコマンドでコンテナ ID を確認しておきます(以下の例では c4335ca15762):
$ docker ps

CONTAINER ID   IMAGE             COMMAND                  CREATED          STATUS          PORTS                    NAMES
c4335ca15762   dotnsf/hostname   "docker-entrypoint.s…"   32 seconds ago   Up 29 seconds   0.0.0.0:8080->8080/tcp   hostname

curl コマンドでこのアプリケーションにアクセスしてみます。このコンテナの /etc/hostname の値が出力されます:
$ curl http://localhost:8080/

c4335ca15762

先程確認したコンテナ ID と同じ値が表示されるはずです。というわけで、この linux/amd64 プラットフォームでは dotnsf/hostname イメージが期待通りに動きました。


続けて、全く同じコマンドを linux/arm/v7(ラズベリーパイ)環境の docker でも実行してみます。まずは dotnsf/hostname イメージを指定してコンテナを作成&起動します:
$ docker run -d --name hostname -p 8080:8080 dotnsf/hostname

起動後、以下のコマンドでコンテナ ID を確認しておきます(以下の例では 6a0b27c4ad76):
$ docker ps

CONTAINER ID   IMAGE             COMMAND                  CREATED          STATUS          PORTS                    NAMES
6a0b27c4ad76   dotnsf/hostname   "docker-entrypoint.s…"   29 seconds ago   Up 27 seconds   0.0.0.0:8080->8080/tcp   hostname

curl コマンドでこのアプリケーションにアクセスしてみます。このコンテナの /etc/hostname の値が出力されます:
$ curl http://localhost:8080/

6a0b27c4ad76

先程確認したコンテナ ID と同じ値が表示されるはずです。というわけで、この linux/arm/v7 プラットフォームでも dotnsf/hostname イメージが期待通りに動きました。

したがって、全く同じ docker イメージ(dotnsf/hostname)を指定して、linux/amd64 でも linux/arm/v7 でも同様に動くコンテナを起動することができました。マルチ CPU アーキテクチャー対応 docker イメージが正しく作れていることが確認できました。

なお作成したコンテナの起動を止める場合は以下のコマンドを実行してください(両環境共通):
$ docker stop (コンテナID)

起動を止めたコンテナを削除する場合は続けて以下のコマンドも実行してください(両環境共通):
$ docker rm (コンテナID)


【感想】
このマルチ CPU アーキテクチャー対応 docker イメージはまだ試験機能ということもあり、準備にも色々面倒な手順が必要です。ただ異なるアーキテクチャでも同じコマンドでアプリケーションを起動できることができるのが docker の強みでもあると思っています。今回は未確認ですが、今後 M1 mac 環境でも docker が正式対応したりすると、マルチ CPU アーキテクチャー対応 docker イメージの需要も増えてゆくものと想像しています。そうなった場合に備えて今のうちに勉強できました。


【参考】
https://docs.docker.jp/docker-for-mac/multi-arch.html


このページのトップヘ