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

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

2022/02

運用中のウェブアプリケーションに対して、セキュリティ面を考慮して以下のようなリクエスト制限をかける必要が生じたとします:

・(例えば)10分間で 100 回のリクエストを許可する
・許可数を超えた場合はリクエストを処理しない


クラウドやホスティングサーバーで運用する場合は、クラウド/ホスティング側にそのような機能が提供されていることもあると思います。が、もしそのような機能が提供されていない条件下でこのような要件が生じた場合、アプリケーションの実装としてリクエスト制限を用意する必要が出てくるかもしれません。 今回のブログエントリで紹介するのは、Node.js アプリケーションにリクエスト制限をかける実装方法です。


といっても Node.js (バージョン 14 以上)で Express ライブラリを使っている場合であれば express-rate-limit という Express 向けミドルウェアを使うことで簡単に実装できます:
20220228


以下でサンプルを紹介しますが、サンプルコードはこちらに公開しています:
https://github.com/dotnsf/express-rate-limit-sample


例えば現行のコード(app_old.js)が以下のようになっていたとします:
//. app_old.js
var express = require( 'express' ),
    app = express();

app.get( '/', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, null, 2 ) );
  res.end();
});

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

"GET /" リクエストに対して "{ status: true }" を返すだけの内容ですが、処理内容自体はもっと複雑でも構いません。

このアプリケーションに「10分間で 100 回」というリクエスト制限をかける場合は以下のようなコード(app_new.js)に変更します:
//. app_new.js
var express = require( 'express' ),
    app = express();

//. rate limit : 100 times per 10 minutes
var rate = require( 'express-rate-limit' );
var limit = rate({
  windowMs: 10*60*1000,
  max: 100,
  standardHeaders: true,
  legacyHeaders: false
});
app.use( limit );

app.get( '/', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, null, 2 ) );
  res.end();
});

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

express-rate-limit ライブラリをインスタンス化して、属性を指定して app.use() でリクエスト処理前に実行されるようにしています。なお各属性値は以下のような意味です:
・windowMs: 制限をかける時間(ミリ秒)
・max: windowMs で定義した時間でのリクエスト処理上限数
・standardHeaders: "RateLimit-*" でリクエスト制限情報を HTTP ヘッダに含める※
・legacyHeaders: "X-RateLimit-*" でリクエスト制限情報を HTTP ヘッダに含める※

※上述のサンプルでは standardHeaders: true, legacyHeaders: false に指定している

これだけでアプリケーションレベルでリクエスト制限を実装できます。 ただし、この制限は「1インスタンスごとの制限」である点に注意が必要です。クラウド的な言い方だと「1コンテナ」あたりでこの制限が有効になりますが、複数インスタンスで運用した場合、例えば可用性を高める目的で3つのコンテナを起動して運用した場合は、事実上設定値の3倍のリクエスト処理を受け付けることになる、という点に注意が必要です。



初めて Heroku Connect を使ったアプリケーションを作ってみました。普段使わない環境や手順もあったことに加え、日本語資料をあまり多く見つけられなかったので、自分の備忘録も兼ねて一連の手順をまとめてみました:
2022022700



【Heroku Connect とは】
Heroku Connect は Heroku のアプリケーションリソースの1つで、SalesForce のデータと Heroku Postgres( PostgreSQL データベース)との双方向同期機能です。これによって SalesForce 上のデータを Postgres データベースのデータとして読み書きできるようになる(Heroku アプリケーションからは PostgreSQL DB に接続することで SalesForce のデータを読み書きできるようになる)というものです。ある意味で閉じられた SalesForce データを、オープンな Heroku アプリケーション環境の一部として取り扱うことができるようになります。

なお Heroku Connect にも Heroku Postgres にも無料枠があり、一定条件内であれば無料で動作確認程度はできるものです。

Heroku Connect について、詳しくはこちらも参照ください:
https://devcenter.heroku.com/articles/heroku-connect


【Heroku Connect の設定】
実際に Heroku アプリケーション上で Heroku Connect を有効に設定し、SQL でデータを取り出す、という手順を行うまでの設定手順を紹介します。

前提条件として、Heroku のアカウントはもちろんですが、SalesForce.com でオブジェクト開発のできるアカウントが必要です。無料の Developer Edition も用意されているので、アカウントを持っていない場合はまずはアカウントを作成しておいてください。Developer Edition はこちらから:
https://developer.salesforce.com/ja


順序としてはもう少し後でもいいと思うのですが、SalesForce.com 側の話になっているこのタイミングで Heroku Connect で読み書きするデータオブジェクトを決めておきます。私自身が SalesForce.com にあまり詳しくないので標準オブジェクトから1つ選択しますが、ここでの対象はカスタムオブジェクトでも構わないはずです。

SalesForce.com (Developer Edition)にログインし、「オブジェクトマネージャ」と書かれた箇所をクリックします:
2022022701


標準で用意されている(と、カスタムオブジェクトを追加した場合はカスタムオブジェクトも含めた)オブジェクトの一覧が表示されます。この中から Heroku Connect で取り出す対象を1つ決めます。私はよく分かっていないこともあって、標準オブジェクトでサンプルデータもはじめからいくつか格納済みの「取引先」オブジェクトを対象にする前提で以下を紹介します。別のオブジェクトを使うこともできると思いますが、適宜読み替えてください:
2022022702


オブジェクトの表示を絞り込む場合は「クイック検索」フィールドに名前を一部入力すると、オブジェクトのラベルでフィルタリングされます。下の例では「取引先」でフィルタリングした結果です。「取引先」オブジェクトは API 参照名が "Account" となっていることがわかります。この名称は後で使うことになるのでメモしておきましょう:
2022022703


SalesForce.com 側の準備はこれだけです。Heroku Connect で同期するオブジェクトデータが決まっていれば Heroku 側の準備にとりかかります。

改めて Heroku にログインし、アプリケーションを1つ作成します。以下の例では "forceobject" というアプリケーションに対して Heroku Connect を設定する想定で紹介しているので、アプリケーション名は自分のアプリケーション名に読み替えてください。またこの時点では Heroku Connect を含めたアドオンは1つも設定していないものとします:
2022022701


このアプリケーションに Heroku Connect をアドオンします。上記画面の "Configure Add-ons" と書かれた箇所をクリックします:
2022022702


アプリケーションのリソース画面に移動します。この "Add-ons" と書かれたフィールドに "Heroku Connect" と入力すると "Heroku Connect" が見つかります。見つかった名前の部分をクリックします:
2022022703


アプリケーションに Heroku Connect を追加する確認ダイアログが表示されます。使用条件と、プランが "Demo Edition - Free"(無料)となっていることを確認して "Submit Order Form" ボタンをクリックします※1:
2022022704


※1 Heroku Connect Demo Edition には以下の制約があるようです:
 ・最大 10,000 行のデータまで同期
 ・同期間隔の最小値は 10 分
 ・ログは7日間保持


アプリケーションに Heroku Connect がアドオンされました:
2022022701


続けて同期を行うデータベースである Heroku Postgres をアドオンします。先程と同様に "Add-ons" フィールドに "Heroku Postgres" と入力して、候補として見つかる "Heroku Postgres" をクリックします:
2022022701


アプリケーションに Heroku Postgres を追加する確認ダイアログが表示されます。使用条件と、プランが "Hobby Dev - Free"(無料)となっていることを確認して "Submit Order Form" ボタンをクリックします:
2022022702


これで Heroku Postgres もアドオンとして追加できました。次にこの2つ(Heroku Connect と Heroku Postgres)を接続するための設定が必要ですが、これにはスキーマと呼ばれる DB の情報が必要になります。スキーマを確認するため、画面上部の "Settings" と書かれたタブをクリックします:
2022022703


"Config Vars" 節の "Reveal Config Vars" ボタンをクリックして環境変数を確認します:
2022022702


すると環境変数 DATABASE_URL が設定されていることがわかります。その値を確認するため、 "DATABASE_URL" と書かれた行の右にある鉛筆ボタンをクリックします:
2022022703


以下のようなダイアログが表示され、"Value" と書かれたフィールドに環境変数 DATABASE_URL に設定された値を確認できます:
2022022704


この値は以下のような形式になっているはずです。この最後の "/" 文字から右にある部分がスキーマです。このスキーマ値を確認した上で Heroku Connect の設定に移ります:
postgres://*****:*******@ec2-xxx-xxx-xxx-xxx.compute-1.amazonaws.com:5432/(スキーマ)


改めて Heroku Connect の設定を続けるため、"Heroku Connect" と書かれた箇所をクリックします:
2022022701



Heroku Connect の設定画面が表示されます。"Setup Connection" ボタンをクリックします:
2022022702


以下のような画面に切り替わります。"Enter schema name" と書かれたフィールド(デフォルトでは "salesforce" と入力されたフィールド)に先程確認したスキーマ名を入力します。正しく入力できたら画面右上の "Next" ボタンをクリック:
2022022701


ここで SalesForce.com のアカウント認証が必要になります。画面右上の "Authorize" ボタンをクリック:
2022022702


SalesForce.com のログイン画面が表示されるので、ID とパスワードを入力してログインします:
2022022703


正しく認証が行われると Heroku Connection の接続が行われ、以下のような画面になります:
2022022704


Heroku Connect 側の準備としての最後にマッピングを行います。画面上部の "Mapping" タブを選択し、右下の "Create Mapping" ボタンをクリックします:
2022022701


SalesForce 側のオブジェクトの一覧が表示されます。今回は「取引先」を対象に同期したいので、"Account" オブジェクトを選択します:
2022022702


Account オブジェクトのマッピング条件を指定する画面が表示されます。デフォルトでは10分おきに SalesForce からデータベースへの同期のみが有効になっていますが、その条件を変えたい場合はここで指定します。また画面下部には Account オブジェクトの中のどの属性値を同期の対象とするかを指定する表があります:
2022022703


デフォルトでもいくつか指定されていて、そのままでもいいと思います。とりあえず取引先名称となる Name がチェックされていることを確認しておきましょう:
2022022704


画面上部に戻り、最後に "Save" ボタンをクリックして、この条件を保存します:
2022022703


指定された条件が保存され、最初の同期が行われます。少し待つと Status が "OK" となります。これで SalesForce の取引先情報が Heroku Postgres のデータベースに格納されました。Heroku Connect の設定が完了です:
2022022701



【アプリケーションから同期されたデータを参照する】
ここまでの設定ができていれば、アプリケーションから(PostgreSQL を参照して)SalesForce のデータを取り出すことができます。といっても、ここまでの設定が済んでいれば該当の PostgreSQL サーバーに接続して、以下の SQL を実行するだけ(この SQL を実行するプログラムを書くだけ)です:
select * from (スキーマ名).Account

例えば Node.js の Express フレームワークを使ったウェブアプリとして実装するとこんな感じになります:
//. app.js
var express = require( 'express' ),
    app = express();

var PG = require( 'pg' );
PG.defaults.ssl = true;
var database_url = 'DATABASE_URL' in process.env ? process.env.DATABASE_URL : ''; 
var schema = '';
if( database_url.indexOf( '/' ) > -1 ){
  var tmp = database_url.split( '/' );
  schema = tmp[tmp.length-1];
}

var pg = null;
if( database_url ){
  console.log( 'database_url = ' + database_url );
  pg = new PG.Pool({
    connectionString: database_url,
    ssl: { require: true, rejectUnauthorized: false },
    idleTimeoutMillis: ( 3 * 86400 * 1000 )
  });
}


app.get( '/', async function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );

  try{
    var conn = await pg.connect();
    var sql = 'select * from ' + schema + '.Account';
    var query = { text: sql, values: [] };
    conn.query( query, function( err, result ){
      if( err ){
        console.log( err );
        res.status( 400 );
        res.write( JSON.stringify( err, null, 2 ) );
        res.end();
      }else{
        res.write( JSON.stringify( result, null, 2 ) );
        res.end();
      }
    });
  }catch( e ){
    console.log( e );
    res.status( 400 );
    res.write( JSON.stringify( e, null, 2 ) );
    res.end();
  }finally{
    if( conn ){
      conn.release();
    }
  }

});

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

(サンプルはこちら)
https://github.com/dotnsf/forceobject


上述の環境変数 DATABASE_URL を指定して、以下のように実行します:
$ DATABASE_URL=postgres://*****:*******@ec2-xxx-xxx-xxx-xxx.compute-1.amazonaws.com:5432/ssssss node app

するとピンク色の部分で DATABASE_URL からスキーマを取り出し、青字の部分で SQL 文を生成して実行する、というものです。実行後にウェブブラウザで http://localhost:8080/ にアクセスすると以下のような SQL 実行結果を得ることができます:
2022022700


実行結果の JSON オブジェクト内の rows キーの配列値として SQL の実行結果が格納されています。各要素に含まれる属性はマッピング時に指定したものになっていて、特に name 属性には取り出したオブジェクト名称(つまり取引先の名称)が格納されていることが確認できます。


これを有効活用するには、まず自分自身がもう少し SalesForce 自体に詳しくなる必要がありそうですが、今は SalesForce 傘下にある Heroku らしいビジネス色の濃い機能が、Heroku の機能にうまく統合されて提供されているようでした。設定段階が比較的面倒な気もしていますが、一度設定できてしまえば後は普通に PostgreSQL を操作する感覚で使えるし、双方向同期を有効にすればウェブアプリ側から SalesForce のデータを活用して作成・更新・削除といった変更処理もできるようになると思います。


 

自作のごくシンプルなウェブアプリを memcached 対応(要は「データベースが memcached でも動くようにする」対応)させようとしてハマってしまった時の様子と、なんとか対応できた結果をブログにまとめました:
2022022300


そのシンプルなウェブアプリではデータの CRUD 処理を行います。その CRUD 処理の中に「全データを取得」という API を作っていました。データベースが MySQL なら MySQL の特定テーブルの全データを、MongoDB なら MongoDB 内の全データを取り出す、という API でした。これの memcached 版を作ろうとした時の話です。ちなみにアプリケーションは Node.js で作っていたので、Node.js で実装できる必要があります。memcached を扱うためのライブラリとしてはおそらく最も一般的なライブラリである npm memcached を使っていました(必ずしもこのライブラリでなくてもいいです)。

memcached はいわゆる Key-Value 型のデータストアです。key の値を頼りにして value を取り出す、というものです。key さえ分かれば value が取り出せるので、「全データを取り出す」のは「全キーが分かれば全データがわかる」ということになります、、、のですが、npm memcached の解説のどこを見てもそんな API は提供されていないようでした。当初は「えー!? ただ全キーを取り出したいだけなのに、その機能がないの!?」と思いました。ちなみに memcached とメモリDBの双璧をなすもうひとつの Redis では keys() というキーの検索メソッドが用意されていて、keys( '*' ) のようにキーにワイルドカードを指定して実行することで全キーを取り出すことができていました。memcached にも同様の機能があるものと期待していたのですが、、、StackOverflow など英語圏の情報も含めていろいろ調べてみたのですが、どうも一筋縄ではいかないらしいことが分かってきました。。さてどうしよう。。


ここで手助けになったのはネットの情報というよりも、実際に telnet コマンドを叩いて調べてみる方法でした。「試行錯誤」というやつです。そうしているうちに「CLI コマンドであれば、コマンドを何回か繰り返して実行する必要はあるが、キー一覧を取り出すことはできそう」だと分かってきました:

2022022301

2022022302


結論としてのコマンド順序は、
 1. "stats items" コマンドを実行し、各 slab に何件のデータが登録されているのかを調べ、
 2. 1. で調べた結果に従って、"stats cachedump" コマンドで slab 内をダンプする
 3. key 一覧だけでなく value の一覧が必要な場合は getMulti() 関数で value も取り出す
という感じでできそうでした。


参考: 
https://shim0mura.hatenadiary.jp/entry/20140125/1390647044


後はこの 1, 2, 3 の処理を Node.js と memcached ライブラリで実装すればよい、ということになります。こんな感じで作ってみました:
var Memcached = require( 'memcached' );
var memcached = new Memcached( "localhost:11211" );   // memcached が localhost:11211 で稼働している場合

async function getAll(){
  return new Promise( async ( resolve, reject ) => {
    if( memcached ){
      memcached.items( function( err, results ){
        if( err ){
          resolve( { status: false, error: err } );
        }else{
          var cnt = 0;
          var values = [];
          results.forEach( function( result ){
            var obj_keys = Object.keys( result );
            if( obj_keys.length == 0 ){
              resolve( { status: true, results: [] } );
            }else{
              Object.keys( result ).forEach( function( slabid ){
                if( slabid != 'server' ){
                  memcached.cachedump( result.server, parseInt( slabid ), result[slabid].number, function( err, key_results ){
                    var keys = [];
                    if( 'length' in key_results ){
                      key_results.forEach( function( key_result ){
                        var key = key_result['key'];
                        keys.push( key );
                      });
                    }else{
                      var key = key_results['key'];
                      keys.push( key );
                    }
  
                    memcached.getMulti( keys, function( err, data ){
                      if( !err ){
                        keys.forEach( function( key ){
                          values.push( data[key] );
                        });
                      }

                      cnt ++;
                      if( cnt == results.length ){
                        resolve( { status: true, results: values } );
                      }
                    });
                  });
                }
              });
            }
          });
        }
      });
    }else{
      resolve( { status: false, error: 'no db' } );
    }
  });
};

これで、
var result = await getAll();

といった感じで全データ( result.results )が取得できるようになりました。

自分であちこち見て回った上で、「難しそう」とか「できないわけではない・・・」といった情報は得られたのですが、実際に取得できる Node.js サンプルソースコードを見つけることができなかったので、同じように悩んでいる人のお役にたてば。

Docker Desktop ショックがあって以来、なるべく少ない制約の下で docker を動かせる環境を色々調べています。そんな中で見つけた1つの方法が Docker in Docker(以下、DinD)です。

DinD はその名前の通りで、コンテナクラスタ(親)の中で動くコンテナ(子)として Docker サーバー&クライアントを動かす、というものです。多くの場合、「親コンテナ=Kubernetes」となることが多いので、正確には "Docker in Kubernetes" と表現すべきかもしれませんが、広い意味(?)での "Docker in Docker" ということだと思います。

単なる運用環境の観点だと「Kubernetes が使えるなら Docker は要らないのでは?」と思うかもしれませんが、特に開発段階だとコマンドとしての docker CLI を使いたいことがあったり、プロダクション環境とは別の小さな開発環境を Docker で作っておけると便利なことも多くあります。そういった意味で Kubernetes があってもそれとは別に Docker 環境が欲しくなることがあるのでした。


IBM Cloud からも IKS(IBM Kubernetes Services)ROKS(Redhat Openshift Kubernetes Services) が提供されていて、これらの環境でも DinD を使うことができます。特に今回は IKS の 30 日無料版を使って DinD 環境を作って使う手順を紹介します。なお、IKS 30 日無料版の制約事項や環境準備手順についてはこちらの過去記事を参照ください:
http://dotnsf.blog.jp/archives/1079287640.html


↑この記事最後の "$ kubectl get all" コマンドが成功するまでになれば準備完了です。なお Kubernetes クラスタに対して "$ kubectl get all" コマンドが実行できるようになっていれば IKS 以外の他の Kubernetes クラスタ環境でも以下同様にして DinD 環境を作ることができると思います。


【(IKS に) DinD の Pod を作る】
では早速 DinD 環境を作ります。DinD 環境といっても大それたものではなく、コンテナ的に言えば「DinD の Pod を1つ作る」ことになります。そしてその1つの Pod の中に「Docker デーモンのコンテナ」と「Docker クライアントのコンテナ」を1つずつ作ります(つまり1つの Pod の中で2つのコンテナを動かします)。

実際の作成に関しても、以下の内容のマニフェストファイルを用意するだけです(この内容を dind.yml という名前で保存してください):
apiVersion: v1
kind: Pod
metadata:
  name: dind
spec:
  containers:
    - name: docker
      image: docker:19.03
      command: ["docker", "run", "nginx:latest"]
      env:
        - name: DOCKER_HOST
          value: tcp://localhost:2375
    - name: dind-daemon
      image: docker:19.03-dind
      env:
        - name: DOCKER_TLS_CERTDIR
          value: ""
      resources:
        requests:
          cpu: 20m
          memory: 512Mi
      securityContext:
        privileged: true

そして、kubectl コマンドで以下を実行してマニフェストを適用します:
$ kubectl apply -f dind.yml

※または dind.yml を用意しなくても、このコマンドでも同じ結果になります:
$ kubectl apply -f https://raw.githubusercontent.com/dotnsf/dind_iks/main/dind.yml


初回のみイメージのダウンロードで少し時間がかかりますが、しばらく待つと dind という名前の Pod が1つ(コンテナは docker dind-daemon の2つ)起動します:
$ kubectl get pods

NAME READY STATUS RESTARTS AGE dind 2/2 Running 2 101m $ kubectl describe pod dind Name: dind Namespace: default Priority: 0 Node: 10.144.222.147/10.144.222.147 Start Time: Sun, 20 Feb 2022 21:52:46 +0900 Labels: Annotations: cni.projectcalico.org/containerID: 0c110243107d25c9e27b82202871504047d9ac691ad294d8f89bb1a9b114ca5e cni.projectcalico.org/podIP: 172.30.216.78/32 cni.projectcalico.org/podIPs: 172.30.216.78/32 kubernetes.io/psp: ibm-privileged-psp Status: Running IP: 172.30.216.78 IPs: IP: 172.30.216.78 Containers: docker: Container ID: containerd://0dbd05bc4171f96caaa7601a10e1b2f853511b4ed7087ab3958c016024c65f1c Image: docker:19.03 Image ID: docker.io/library/docker@sha256:ea1f0761c92b600417ad14bc9b2b3a30abf8e96e94895fee6cbb5353316f30b0 Port: Host Port: Command: docker run nginx:latest State: Running Started: Sun, 20 Feb 2022 22:53:02 +0900 Last State: Terminated Reason: Completed Exit Code: 0 Started: Sun, 20 Feb 2022 21:53:08 +0900 Finished: Sun, 20 Feb 2022 22:53:01 +0900 Ready: True Restart Count: 2 Environment: DOCKER_HOST: tcp://localhost:2375 Mounts: /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-gz5fb (ro) dind-daemon: Container ID: containerd://5a6a15eefa9ebdf8a0dcc825f1c87fa4a5a37daab005a1d83e5df8f9a33ff7bb Image: docker:19.03-dind Image ID: docker.io/library/docker@sha256:c85365ad08c7f6e02ac962a8759c4a5b8512ea5c294d3bb9ed25fca52e9e22e5 Port: Host Port: State: Running Started: Sun, 20 Feb 2022 21:53:07 +0900 Ready: True Restart Count: 0 Requests: cpu: 20m memory: 512Mi Environment: DOCKER_TLS_CERTDIR: Mounts: /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-gz5fb (ro) Conditions: Type Status Initialized True Ready True ContainersReady True PodScheduled True Volumes: kube-api-access-gz5fb: Type: Projected (a volume that contains injected data from multiple sources) TokenExpirationSeconds: 3607 ConfigMapName: kube-root-ca.crt ConfigMapOptional: DownwardAPI: true QoS Class: Burstable Node-Selectors: Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 600s node.kubernetes.io/unreachable:NoExecute op=Exists for 600s Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Created 43m (x3 over 103m) kubelet Created container docker Normal Started 43m (x3 over 103m) kubelet Started container docker Normal Pulled 43m (x2 over 103m) kubelet Container image "docker:19.03" already present on machine d


DinD 環境はあっけなく完成しました。


【(IKS の) DinD 内の docker を操作する】
次に完成した DinD 環境を実際に CLI で使ってみます。そのためにまずは Docker クライアントが使える環境のシェルにアタッチする必要があります。Docker クライアントは docker という名前のコンテナで動いていることが分かっているので、以下のコマンドを実行します:
$ kubectl exec -it dind -c docker -- /bin/sh

/ #

プロンプトが "/ #" という記号に変わればアタッチ成功です。ここからは docker CLI コマンドが実行できます。試しに "docker version" コマンドを実行するとクライアント&サーバー双方の docker バージョン情報(下の例ではどちらも "19.03.15")を確認できます:
/ # docker version

Client: Docker Engine - Community
 Version:           19.03.15
 API version:       1.40
 Go version:        go1.13.15
 Git commit:        99e3ed8
 Built:             Sat Jan 30 03:11:43 2021
 OS/Arch:           linux/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.15
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       99e3ed8
  Built:            Sat Jan 30 03:18:13 2021
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.3.9
  GitCommit:        ea765aba0d05254012b0b9e595e995c09186427f
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683


実際にサーバーイメージをデプロイして動作確認してみましょう。というわけで、まずは nginx イメージを 8000 番ポートでデプロイしてみます:
/ # docker run -d --name ningx -p 8000:80 nginx

成功したら早速アクセスして動作確認を・・・と思ったのですが、この docker コンテナには HTTP クライアントが curl 含めてインストールされていないようでした。というわけで動作確認用のコマンドもインストールしておきます。とりあえず curl と w3m あたりでいいですかね。。:
/ # apk add --update curl

/ # apk add --update w3m

インストールが成功したら、まずは curl で http://localhost:8000/ にアクセスしてみます:
/ # curl http://localhost:8000/

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

うぉっ、と。少なくとも HTTP サーバーとして動いているらしいことは確認できたのですが、これだとちょっと見にくいですね。というわけで、先程一緒にインストールした w3m で確認してみます:
/ # w3m http://localhost:8000/

Welcome to nginx!

If you see this page, the nginx web server is successfully installed and working. Further configuration is required.

For online documentation and support please refer to nginx.org.
Commercial support is available at nginx.com.

Thank you for using nginx.

2022022101


かろうじて Nginx のトップ画面らしきものが確認できました。というわけで一応動いていると思います。


【まとめ】
というわけで DinD が IBM Cloud の 30 日無料版 Kubernetes クラスタ環境でも動かせることが確認できました。

とはいえ、もともとは Docker Desktop の代替になるような Docker 環境を探していたことに立ち返ると、この(クライアント側のような) CLI だけの Docker 環境はどうしても使い道が限られてしまうように感じます。ある程度、Docker を理解している人向けに、CLI だけで完結する使いみちであればなんとか、といったところでしょうか。


本当はここで作った Pod を外部に EXPOSE できるといいんですが、自分で試行錯誤している限りではまだうまく行ってません。もし方法をご存じの方がいらっしゃったら是非教えてください。


(2022/02/21 追記ここから)
試行錯誤の中で外部公開する方法がわかりました。

上述の "$ kubectl apply -f ..." コマンドを実行する箇所の内容を以下のように変更してください:
$ kubectl apply -f https://raw.githubusercontent.com/dotnsf/dind_iks/main/dind_expose.yml

なお、ここで指定している dind_expose.yml の内容は以下のようなものです。Pod を Deployment に書き換えた上で Service オブジェクトを追加して、8000 番ポートでの待受けを 30800 番ポートから転送するように(この後の作業で 8000 番ポートで待ち受けるアプリケーションをデプロイする想定で)あらかじめ公開しています:
apiVersion: v1
kind: Service
metadata:
  name: dind
spec:
  selector:
    app: dind
  ports:
  - port: 8000
    name: port8000
    protocol: TCP
    targetPort: 8000
    nodePort: 30800
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dind
spec:
  selector:
    matchLabels:
      app: dind
  replicas: 1
  template:
    metadata:
      labels:
        app: dind
    spec:
      containers:
        - name: docker
          image: docker:19.03
          command: ["docker", "run", "nginx:latest"]
          env:
            - name: DOCKER_HOST
              value: tcp://localhost:2375
        - name: dind-daemon
          image: docker:19.03-dind
          env:
            - name: DOCKER_TLS_CERTDIR
              value: ""
          resources:
            requests:
              cpu: 20m
              memory: 512Mi
          securityContext:
            privileged: true

このコマンドの後、"$ kubectl get all" を実行すると以下のような結果になります:
$ kubectl get all
NAME READY STATUS RESTARTS AGE pod/dind-7d8546bc8-fxbhw 2/2 Running 1 19s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/dind NodePort 172.21.8.233 8000:30800/TCP 21s service/kubernetes ClusterIP 172.21.0.1 443/TCP 3d10h NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/dind 1/1 1 1 20s NAME DESIRED CURRENT READY AGE replicaset.apps/dind-7d8546bc8 1 1 1 21s

以前の方法では Pod 名は "dind" 固定だったのですが、ここでは "dind-" に続けてランダムな文字列が付与されています(上例では "dind-7d8546bc8-fxbhw" となっています)。この Pod 名を指定して以下を実行して docker コンテナのシェルに接続します:
$ kubectl exec -it dind-7d8546bc8-fxbhw -c docker -- /bin/sh

/ #

この後は以前の方法と同様にして docker コンテナ内で NGINX を 8000 番ポートで起動します:
/ # docker run -d --name ningx -p 8000:80 nginx

これで以前と同様に k8s のワーカーノード内で(NodePort サービスを使って) NGINX が 8000 番ポートで起動します。ただ今回は Service オブジェクトで 8000 番リクエストを 30800 番ポートで外部公開しているので、 http://(ワーカーノードのパブリックIPアドレス):30800/ にアクセスすればクラスタ外部からでもこの NGINX に接続できるようになっています。


ワーカーノードのパブリック IP アドレスは IBM Cloud の IKS 環境であれば、IBM Cloud ダッシュボード画面から確認することができます(この例では "169.51.206.71" となっています):
2022022102


というわけで、改めてウェブブラウザで http://169.51.206.71:30800/ にアクセスしてみると、、、期待通りの画面が表示されました! IBM Cloud の30日無料版 Kubernetes クラスタ環境で構築した DinD のコンテナを外部に公開することができることが確認できました:
2022022100


というわけで、30 日間無料の IBM Cloud Kubernetes 環境を使って、Docker および Kubernetes クラスタの実行環境を構築することができました。

(2022/02/21 追記ここまで)
 

自作のマークダウン CMS を一週間ほど自分だけで使ってみて、まあまあな感じだったので一般公開します。

"My info" を略して "Mynfo" と名付けています。ソースコードは GitHub に公開してます。自分でも使ってみたい場合はフォークして使ってください(フォーク先は public でも private でも構いませんが、public の場合は GitHub 上のコンテンツが公開される点に注意してください):
https://github.com/dotnsf/mynfo


また、サンプルとして(皆さんには編集権限はありませんが)この公開ソースコードで作ったコンテンツサービスを以下で公開しています:
https://mynfo.herokuapp.com/


実際にみなさんもこの仕組みを使ってコンテンツサービスを公開する場合、必須ではありませんが heroku のアカウントを所有している場合は自分の GitHub リポジトリにコンテンツをコミットすることで CI/CD で最新コンテンツに更新できる仕組みがあります(しかもこの方法だとコンテンツの更新履歴が Git 側に残る、という大きなメリットがあります)。この機能を使う想定で設定方法を README.md 内で解説しているので、heroku アカウントと併せて使うことを推奨します(試しにローカルホストで動かしてみる、程度であれば不要です)。


【ローカルホストで動かしてみる】
試しにローカルホストで動かすには Node.js 環境が必要です。事前にセットアップしておきましょう。

フォークしたコードを git clone して、npm install してから node app で実行します:
$ git clone https://github.com/dotnsf/mynfo.git ("dotnsf" 部分はあなたの Github アカウント名に読み替えてください)

$ cd mynfo

$ npm install

$ node app

デフォルトでは 8080 番ポートで HTTP リクエストを待ち受けます。ウェブブラウザで localhost:8080 にアクセスします。以下のような画面が表示されれば起動成功です(初期状態では README.md にシンボリックリンクした md/index.md の内容が(マークダウンが HTML に変換されて)表示されています。英語で一通りの使い方が紹介されています):
2022021901

(↑皆さんが使うときには md/index.md と README.md とのシンボリックリンクは切っちゃって構いません)


画面右上のハンバーガーメニューをクリックすると、リポジトリ内の md/ フォルダ以下を対象フォルダとするファイル選択画面が表示されます(デフォルト状態では md/ フォルダ以下に about_markdown.md, index.md, sample.md の3つのマークダウンファイルが存在しており、それらが(.md 拡張子が省略された形で)一覧表示されています:
2022021902


試しに一番上の about_markdown を選択すると md/about_markdown.md ファイルの内容が HTML に変換されて表示されます:
2022021901


また初期画面で表示されている README.md の最初にも記述していますが、実行時の環境変数や settings.js ファイル内で値を指定することで挙動を変えることができます。例えばサービスを不特定多数の人に公開するのではなく、一部の人向けにクローズドな形で公開したい場合は環境変数 BASIC_USERNAME と BASIC_PASSWORD を指定して実行することでベーシック認証が有効になり、正しい ユーザー ID とパスワードを入力しないとコンテンツは見ることができません。

見栄えに関しては環境変数 CONTENTS_TITLE でサイトの題名(デフォルト値は "Mynfo")を、BOOTSTRAP_THEME で Bootstrap のテーマ(デフォルト値は "warning")を指定できます。例えば前者を ".NSF Mynfo"、後者を "info" に指定するとこのような見栄えになります:
2022021904


基本機能としてはこのような感じです。プロジェクトフォルダ内の md/ フォルダ内にあるマークダウン(.md)ファイルを参照することができる、というものです。ただこのサービス自体にはマークダウンの編集機能はありません。このままサーバー上で(普通に)公開する場合は、別途 md/ フォルダ内のマークダウンファイルを編集する仕組み(サーバー上で直接編集するなど)が必要です。

動くか動かないかでは動くのですが、個人的にはこのような使い方はあまり想定していません。あくまで後述のような、GitHub リポジトリと連携する使い方を想定して開発しています。


【heroku のデプロイパイプラインを使う】
サーバー上の .md ファイルを直接編集するような使い方は本来想定したものではありません。この Mynfo の最大の特徴は以下のような使い方ができることです:

・ローカルホストで(好きなマークダウンエディタで).md ファイルを編集して、
・Github にコミット&プッシュすると、
・デプロイパイプラインによって、サービス側のコンテンツも更新される

この流れを heroku を使って実現する方法も README.md に(英語で)記載していますが、このブログでは同じ内容を日本語で紹介します。

まず heroku のアカウントを作成します。heroku には複数種類のアカウントが存在していますが、ただ動かすだけであれば(容量や可用性などを考慮せずに、小規模1インスタンスだけで動かす前提であれば)無料プランで構いません。

※なお heroku の無料プランで作成するアプリは 30 分間アクセスがないと止まってしまい、次にアクセスした時に再起動してからアプリが稼働します(この場合、アプリ画面が表示されるまで再起動のぶん少し時間がかかります)。24 時間ずっと稼働状態をキープしたい場合は有料プランで運用することを推奨します。

そして heroku にログインし、画面右上から New - Create new app を選択します:



次にアプリケーション名(下図では "mynfo")を入力し、サービスを作成する地域(下図では "United States")を指定します。そして次に(Create app ではなく) Add to pipeline を選択します:



Choose a pipeline と書かれた箇所をクリックし、Create new pipeline を選択します:



そして作成するパイプラインの名称(下図では "mynfo-pipeline")を適当に入力し、デプロイ先のステージを指定します。例えば「一度ステージング環境にデプロイし、動作確認ができたらプロダクション環境へデプロイ」するようなケースも想定できますが、今回はそのままプロダクション環境へデプロイするシンプルな例を紹介します。というわけでここではパイプラインのデプロイ先として production を直接指定します。最後に Create app をクリックします:



これでパイプラインそのものは作成されましたが、まだ GitHub リポジトリとの連携ができていません。このまま作業を続けます。

デプロイ方法として GitHub 連携をしたいので Deployment Method に GitHub を選択します:



そして対象となる GitHub リポジトリ(クローンしたリポジトリ)を指定します。Connect to GitHub 欄で自分の GitHub アイコンを選択し、対象リポジトリ名(そのままであれば "mynfo")を入力して Search します:



すると該当の GitHub リポジトリが見つかるので、横にある Connect ボタンをクリックします:



これで GitHub との接続ができました。最後に自動デプロイのための対象ブランチを指定するので、このまま続けます:



Automatic Deploy 欄でパイプラインが参照する GitHub リポジトリのブランチを指定します。特にブランチを作っていない場合は main だけが選択できるので、この main が選択された状態で Enable Automatic Deploy ボタンをクリックします(別途ブランチを作成して、main 以外のブランチを指定することも可能です):



これでフォークした GitHub リポジトリの main ブランチに変更がコミットされると、パイプラインが動いて最新コードが heroku 上のアプリケーションとして https://(アプリ名).herokuapp.com/ という URL で動き出すようになります:



また、この時点で作成したパイプラインの状態を参照すると、GitHub リポジトリの指定したブランチを接続されて、更新時の自動デプロイが有効になっている旨を確認できます:



この状態で試しに1ファイルを追加して動作確認しています。ローカルホスト上で1ファイル追加して git コマンドでコミットしてもいいのですが、今回は GitHub のウェブ画面を使って追加コミットする例を紹介します。

まずはウェブブラウザで該当 GitHub リポジトリのページ(フォークしたリポジトリのページ)を開き、md フォルダを選択します:



ここからファイルを指定して編集することもできますが、今回は新しいファイルを1つ追加することにします。画面右の Add file ボタンをクリックして、表示されるメニューから Create new file を指定します:



GitHub リポジトリに直接ファイルを追加する編集画面が表示されるので、試しにファイル名を test.md 、ファイルの中身には "# テスト" と見出し行だけ入力します:



画面を下にスクロールすると、GitHub に直接コミットする情報を指定する箇所が現れます。ここでコミットコメント(下図では "md/test.md added.")、を指定し、"Commit directly to the main branch" が選択された状態のまま Commit new file ボタンをクリックします:



これで GitHub リポジトリにマークダウンファイルが1つ追加でコミットされました(ローカルホストでも編集する場合はこの状態を git pull して、ローカルホスト側も更新しておいてください)。

そして指定ブランチがコミットされたことで、heroku に作成したパイプラインが自動実行されます。パイプライン実行そのものも数秒で終わってしまいますが、下図はビルド中(デプロイ完了前)のパイプライン画面です。アプリケーションのビルド中である旨が表示されています:



デプロイが完了するとこのような画面になり、ここから最新の(追加コードが反映された状態の)アプリケーションを開くこともできます:



確認のため、アプリケーションを開きます(Open app と書かれたボタンで開きます)。そして画面右上のハンバーガーメニューをクリックします:



すると存在していなかった test.md というファイルが確認できます。試しにこのファイルを選択します:



作成した通りの内容( "# テスト")であることが確認できます:



このように GitHub リポジトリと連携して、ローカルホストや GitHub 画面でマークダウンファイルを追加・更新・削除してサーバーにコミットすると、自動で公開アプリにも反映される仕組みができました。こうしておくことでローカルホストでは VSCode など好きなマークダウンエディタで編集することができるようになって、後は編集後にコミット&プッシュすればアプリにも反映される、という仕組みになりました。

冒頭でも書きましたが、このコンテンツサービスそのものはベーシック認証を有効にすることで一部の人だけに公開することもできます。ただそれとは別に GitHub リポジトリを公開するか/しないかを考慮する点があることに注意してください。基本的にベーシック認証をかけるサービスであれば、フォーク先の GitHub リポジトリも private 扱いにするべきだと思っています。

また環境変数 GITHUB_REPO_URL (リポジトリURL)と GITHUB_BRANCH (ブランチ名)を指定して実行すると、コンテンツの各画面内に GitHub のコンテンツメニューが追加され、各ページやファイル選択画面内から GitHub の対象ページ画面に移動したり、直接編集ページに移動したり、フォルダに新しいファイルを追加することも可能です(編集は編集権限を持っているユーザーのみ使えます):
2022021906

2022021905


この GitHub と連携した使い方はちょっとクセがあるので、普段 Git を使っていない人からすると(編集画面も無いし)むしろ不便に感じるかもしれません。が、Git やマークダウンに慣れたエンジニアであれば、むしろこの使い方で(慣れたマークダウンエディタとも連携して)コンテンツを更新情報まで含めて管理ができることになって便利だと思っていて、このようなサービスを作ってみました。

ちなみに、このアプリでブログ的な使い方をする場合は、コンテンツのファイル名を日付(例えば 20220219.md)にして、環境変数 REVERSE_FILES の値を 1 にして実行することで(新しい日付のファイルがリストの上にくるように並ぶので)使いやすいのでは、と思っています。



Git やマークダウンをバッチリ使っている自分からはなかなか便利に使えているサービスなので、同様の属性を持った人に使ってみていただきたいです。

なお現時点では GitHub と heroku のパイプラインを使う想定で README.md を記述していますが、GitLab など GitHub 以外の Git システムや、GitHub Actions など heroku パイプライン以外の自動デプロイ化の仕組みを使って運用することもできると考えています。ぜひ多くのパターンで挑戦していただきたいです。

このページのトップヘ