[ 言語選択: English ]

1. OpenAPI+Sinatraによる検索ページの作成

Dockerを利用して https://opm00h.u-aizu.ac.jp/solr/api/v1/search に検索処理を任せて、検索結果を表示するWebページを作成・表示するアプリケーションを作成します。

1.1. OpenAPI と openapi-generator について

OpenAPIはAPIを記述する手法についての標準化を目指す組織(OpenAPI Initiative)が作成している仕様書の名称です。

openapi-generatorは様々な経緯があって、現在、有志によって作成されているOpenAPIに適合する設計書を元に対応するプログラミングコードのひな形を生成するツールです。

1.2. 準備作業

次のリンク先を参照してpodmanが利用できるように~/.config/containers/storage.confファイルを配置してください。

あらかじめ作業を始めるためのパッケージをgithub.comに登録しています。次のコマンドで任意の場所に、作業ディレクトリ(docker-sccp-sinatra-sample)を作成してください。

$ git clone https://github.com/YasuhiroABE/docker-sccp-sinatra-sample.git
$ cd docker-sccp-sinatra-sample

演習室や自前のPCを利用する場合は、公式ガイドのインストール方法から適切と思われるものを選択し、openapi-generator-cli コマンドを準備してください。

もしコマンド名が "openapi-generator" のように別のものである場合には、Makefileの先頭にあるコマンド名を変更してください。

macOSを利用している場合、付属のrubyコマンドはバージョンが古いようです。 brewを利用して、より新しいバージョンを利用してください。

$ brew install ruby
$ export PATH=/opt/homebrew/opt/ruby/bin:${PATH}

1.3. 最新コードへの更新作業

演習の途中でGitHubの内容が更新された場合など、git cloneで作成したディレクトリの内容を最新にする場合には次のコマンドを実行します。

## docker-sccp-sinatra-sample (.gitディレクトリのある) ディレクトリで次のコマンドを実行する
$ git pull

1.4. まずは実行してみる

意味のある動きはしませんが、とりあえずどんな動きをするのか確認してみることができます。まず、次の通りにコマンドを実行してください。docker-sccp-sinatra-sample/ディレクトリにいることを確認してから進めてください。

$ make gen-code
$ cd code
$ make PORT=8080 run
...
bundle exec rackup --host 0.0.0.0 --port 8080
Puma starting in single mode...
* Puma version: 6.4.2 (ruby 3.2.3-p157) ("The Eagle of Durango")
*  Min threads: 0
*  Max threads: 5
*  Environment: development
*          PID: 5187
* Listening on http://0.0.0.0:8080
Use Ctrl-C to stop

make runのようにPORT-8080の指定をしない場合には、ポート番号は5000〜9999までのランダムな数字になります。複数人でaplisv2.u-aizu.ac.jpなどを共有する場合にはポート番号の衝突を防ぐために利用してください。

次にWebブラウザーを起動し、URL、http://localhost:8080/search 、を表示させて、結果1行が表示されることを確認します。

ブラウザには次のような1文が表示されるはずです。

{"message":"yes, it worked"}

ここで、一度 Ctrl-c を実行し、コマンドの実行を停止します。

1.4.1. ここまでのコマンドの実行例

1.4.2. make gen-code実行時のエラーについて

openapi-generator-cliコマンドのバージョンがインストールされているものと一致しないなど、make gen-codeの実行時にエラーが発生する場合があります。

Download 5.3.0 ...
/usr/local/lib/node_modules/@openapitools/openapi-generator-cli/node_modules/fs-extra/lib/move/move-sync.js:40
    if (err.code !== 'EXDEV') throw err

Error: EACCES: permission denied, rename '/tmp/generator-cli-Le849S/5.3.0' -> '/usr/local/lib/node_modules/@op
enapitools/openapi-generator-cli/versions/5.3.0.jar'
...

この場合には、カレントディレクトリ(CWD)のopenapitools.jsonファイルを削除してください。

## エラー発生時には下記のコマンドを実行し、"make gen-code"を再度実行
$ rm openapitools.json

1.5. 【解説】makeコマンドが実行している命令について

Makefileには、make gen-codemake run に対応した実際の処理内容(コマンドの羅列)が記述されています。

Makefileに書かれている、gen-codeに対応するコードは次のような内容になっています。

OAGEN_CLI = openapi-generator-cli

gen-code:
        $(OAGEN_CLI) generate -g ruby-sinatra -o code -i openapi.yaml
        cp _docker/Makefile code/
        cp _docker/Dockerfile code/
        cp _docker/run.sh code/
        cp _docker/Gemfile code/
        cp _docker/config.ru code/
        mkdir -p code/lib/views
        cp _docker/header.erb code/lib/views/
        cp _docker/main.erb code/lib/views/
        cp _docker/footer.erb code/lib/views/

code/Makefile(_docker/Makefile)に書かれている、runに対応するコードはもう少しシンプルが、やや複雑です。

run: bundle-install
        bundle exec rackup --host $(HOST) --port $(PORT)

bundle-install:
        bundle config set path lib
        bundle install

make runを実行すると、その本体を処理する前に、make bundle-install を実行した場合と同じ処理が実行されます。

これによって、必要なライブラリファイルなどの準備と、プログラムの実行の処理を分けながら、一連の流れを持つ処理として記述することができます。

このサンプルを元にMakefileの内容を変更することで、Ruby言語+Sinatraフレームワーク以外の処理系に対応するコードを出力させることも可能です。

1.6. 出力結果を変更する

まずは /searchにアクセスした時に、 {"message":"yes, it worked"} を表示している場所を探します。

いまは docker-sccp-sinatra-sample/code/ディレクトリにいるはずです。emacs等で api/default_api.rb を開き "yes, it worked" のメッセージを表示している場所を探します。

この"yes, it worked"と書かれている場所は2箇所ありますが、/search に対応している方を選んでください。

編集する行を決めたら、{"message":"yes, it worked"}の1行を削除し、次のようなコードを挿入します。 # the guts live here の行も参考にしてください。

  • api/default_api.rb ファイルの "/search" に対応するセクションを次のように編集する。

  # the guts live here
  ## {"message" => "yes, it worked"}.to_json

  require 'uri'
  require 'httpclient'
  client = HTTPClient.new
  url = URI::HTTPS.build({:host => "opm00h.u-aizu.ac.jp", :path => '/solr/api/v1/search', :query => "q=sccp&wt=json&site="})
  ret = client.get(url)
  @result = JSON.parse(ret.body)
  @result.to_json
end

end が重複しないように注意してください。

変更したファイルを保存したら、再び make PORT=8080 run を実行します。

無事に変更が成功した場合は、先ほどと同じように、次のようなメッセージが表示されます。 間違いがある場合には、Listening on .. のメッセージは表示されずに、エラーメッセージが表示されるので、見比べて修正してください。

$ make run
...
bundle exec rackup --host 0.0.0.0 --port 8080
Puma starting in single mode...
* Puma version: 5.2.2 (ruby 2.7.0-p0) ("Fettisdagsbulle")
*  Min threads: 0
*  Max threads: 5
*  Environment: development
*          PID: 103170
* Listening on http://0.0.0.0:8080

また Webブラウザで http://localhost:8080/search を開き、結果を確認します。

端末からも次のようなコマンドでAPIにアクセスして結果を確認することができます。 こちらの方はjqコマンドで整形するので見やすくなるはずです。

## 別の端末を開いて次のコマンドを実行します。
$ curl http://localhost:8080/search | jq .

curlコマンドの出力は、次のような入れ子構造になっています。

{
  "responseHeader": {
    "zkConnected": true,
    ...
  },
  "response": {
    "numFound": 3344,
    "start": 0,
    "maxScore": 489.15277,
    "docs": [
      {
        "id": "https://web-int.u-aizu.ac.jp/~yasu-abe/ja/sccp/lda4bcl/",
        "title": [
        ...
        ]
      }
    ]
  },
 "highlighting": {
    "https://web-int.u-aizu.ac.jp/~yasu-abe/ja/sccp/lda4bcl/": {
      "content": [
      ...
      ]
    }
  }
}

ここまで実行したら、一旦 make run コマンドの実行を、Ctrl-c を入力するなどして、停止します。

1.6.1. 出力結果を変更する作業例

1.7. 出力結果をさらに変更する

いまは固定的にsccpという単語を検索した結果が入っています。 後で任意のキーワードを検索できるようにしますが、まずは出力を変更します。

先ほど追加した場所の最後にさらに処理を追加します。 先頭の@result.to_jsonと最後のendは api/default_api.rb に書かれ ているので、その間に output = erb :mainの1行を追加します。

 @result.to_json

 output = erb :main
end

今回、追加したのは output から始まる1行だけです。

変更が終ったら、ファイルを保存し、再度 make run コマンドを実行します。

$ make run

再びWebブラウザで http://localhost:8080/search にアクセスします。 次のように検索結果がリスト表示されるはずです。

tutorial websample sinatra.search results overview

ここで画面への表示は、lib/views/main.erb ファイルでコントロールされています。

1.8. 任意の文字列を検索可能にする

現在は、sccpというキーワードだけを検索し、変更できないようになっています。 変更するべき箇所は、api/default_api.rb ファイルの次の箇所です。

url = URI::HTTPS.build({:host => "opm00h.u-aizu.ac.jp", :path =>'/solr/api/v1/search', :query => "q=sccp&wt=json&site="})

q=に続く部分に、検索したいキーワードを指定すれば、検索結果を変化さ せることができます。

上のurl = で始まる1行を削除して、次の2行で置き換えます。

@param_q = params.has_key?(:q) ? params[:q].to_str  : "sccp"
url = URI::HTTPS.build({:host => "opm00h.u-aizu.ac.jp", :path => '/solr/api/v1/search', :query => "q=#{@param_q}&wt=json&site="})
変更箇所の前後を含む結果
  # the guts live here

  require 'uri'
  require 'httpclient'
  client = HTTPClient.new
  @param_q = params.has_key?(:q) ? params[:q].to_str  : "sccp"
  url = URI::HTTPS.build({:host => "opm00h.u-aizu.ac.jp", :path => '/solr/api/v1/search', :query => "q=#{@param_q}&wt=json&site="})
  ret = client.get(url)
  @result = JSON.parse(ret.body)
  @result.to_json

  output = erb :main
end

再び、make runコマンドでWebサーバーを起動し、Webブラウザから http://localhost:8080/search にアクセスし、画面下のダイアログに適当な文字列を入れて、"Search"ボタンを押下すると、指定した文字列をキーワードに検索結果が変化します。

1.9. 検索文字列を画面に表示する

現在のままでは、何について検索しているのか分かりにくくなっています。 改善のため画面下のテキストフィールドに、あらかじめ現在検索している文字列を表示するようにします。

lib/views/main.erb ファイルをemacsなどのエディタで開き、一番下にある<form …​>〜</form>で囲まれたinputタグの中身を次のように変更します。

<form method="get" action="" >
<input type="text" name="q" value="<%= Rack::Utils.escape_html(@param_q) %>" /><button type="submit">Search</button>
</form>

編集した内容を保存し、Webブラウザに表示されている画面をリロードして結果を確認します。

【Note】
*.erbファイルを編集した場合には、アプリケーションを再起動(C-c & make run)する必要はありません。
画面をリロード(再読み込み)することで、反映させることができます。

tutorial websample sinatra.form value sccp

1.10. 【解説】ERB(Erubi)の働きについて

ERBはRuby特有の機能で、Erubiという高機能な互換ライブラリを利用することもできます。

ERB(Erubi)を利用することで、様々なテキストファイルにRuby言語のプログラムコードを埋め込む事ができます。 今回はHTMLファイルの中に直接コードを記述しています。

次のようなコードを記述すると、自動的に `lib/views/ ディレクトリの中から対応するファイル(main.erb)を検索します。

output = erb :main

lib/views/main.erb ファイルの内容をoutput変数に代入しますが、その際に埋め込まれたプログラムが実行されて出力に反映されます。

1.10.1. 【解説】lib/views/main.erb について

main.erbファイルの全体は次のようになっています。

<h2>Search Results</h2>
<ul>
<% for item in @result["response"]["docs"] %>
  <li><a href="<%= item['id'] %>"><%= item['id'] %></a><br /><% unless @result['highlighting'][item['id']]["content"].to_s == "" %><%= @result['highlighting'][item['id']]["content"][0].to_json %><% end %></li>
<% end %>
</ul>
<form method="get" action="" >
<input type="text" name="q" value="<%= Rack::Utils.escape_html(@param_q) %>" /><button type="submit">Search</button>
</form>

ERBファイルはRuby言語独自の形式で、<%から始まって %>で終るまでの間に、Ruby言語のコードが記述されています。

その他の部分は、そのまま出力されるため、ここではHTMLファイルの中に、 Ruby言語のコードを埋め込むために利用しています。

ここでは、for文を使って@result変数の中から、検索結果の10件を表示するために利用しています。 また、input要素では、value属性に@param_q変数の内容を埋め込む処理を行なっています。

1.11. Webページの見た目を改善する

一般的にはWebページの見た目を改善するために、CSSやJavaScriptが利用されています。 ここまでのコード中ではCSS/JavaScriptは利用していません。

理由としてCSSやJavaScript用の外部ファイルを提供する機能は、Sinatraにはありません。 技術的に不可能ではありませんが、外部ファイルを提供するためのWebサーバーを別に準備するのが一般的な方法です。

そのためCSS/JavaScriptを利用したい場合には、HTMLファイル中にCSS/JavaScriptのコードを埋め込む事が最も現実的です。

アプリケーションの中でWebページの見た目を作るために利用されているファイルは、lib/views/ ディレクトリの中にあります。 どのファイルも自由に書き換えて問題ありません。

1.11.1. 【準備作業】lib/views/にある他のファイルを追加する

api/default_api.rbファイルを編集し、main.erb以外のファイルを追加します。 output = erb :main となっている箇所を次のように変更します。

output = erb :header
output += erb :main
output += erb :footer

再度、`make run コマンドを実行し、http://localhost:8080/search にアクセスします。

3つのファイルに分割せずに、1つのmain.erbファイルに全ての内容をまとめることは可能です。 しかし、一般的には見た目を各ページで統一するために、header.erb, footer.erb には各ページで共通の見た目にするためのコードを記述します。

main.erb ではアプリケーション用の画面を出力するために必要なコードを記述しています。

1.11.2. ここまでの変更作業の例

1.11.3. 【作業1】画面の見た目をBootstrap5を利用して変更する

例えば、CSSフレームワークであるBootstrap5を利用するための準備をするだけでも、画面の見た目はずいぶんと変化します。

エディタで、lib/views/header.erb ファイルを開いて、<html>〜<body>の間に、次のようになるよう、<head>〜</head>全体をコピーしてください。

<html>
<head>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css"
        rel="stylesheet" integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor"
        crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-pprn3073KE6tl6bjs2QrFaJGz5/SUsLqktiwsUTF55Jfv3qYSDhgCecCxMW52nD2"
        crossorigin="anonymous"></script>
    <style type="text/css">
        body {
            background-color: #ffffff;
        }
    </style>
</head>
<body>

この後に、Webブラウザから確認すると、文字の大きさや色が少し変化したことが判るはずです。

1.11.4. 【作業2】{}を省いてコンテンツの内容を表示する

検索結果に {"contents":[…​.} のように出力されています。 これを変更するには次のような方法が考えられます。

lib/views/main.erb を次のように修正します。

### 変更前
   <li><a href="<%= item['id'] %>"><%= item['id'] %></a><%= @result['highlighting'][item['id']].to_json %></li>

### 変更後
   <li><a href="<%= item['id'] %>"><%= item['id'] %></a><br /><%= @result['highlighting'][item['id']]["content"][0].to_json %></li>

ついでに改行 <br /> を間に入れることで、見た目を少し変更しています。

1.11.5. 【作業3】この他の見た目の変更のためのヒント

Ruby言語の知識がない状態では変更できる範囲に限界はありますが、カスタミングの手掛かりを得る方法はいくつかあります。

【作業2】では item 連想配列(ハッシュ) 変数と、@result 変数を利用していますが、内部の構造が分からないと使えません。 次のように lib/views/main.erbuis ファイルを編集すると、それぞれの変数の内部が分かります。

<%= item.inspect %>
<hr />
<%= @result["highlighting"].inspect %>
<hr />

1.12. 【答え合せ】ここまでの作業を全て適用する

ここまで編集した内容と同等のapi/default_api.rbファイルは次の場所にあります。

最新版をgit cloneした場合には、docker-sccp-sinatra-sample/ディレクトリの中に含まれています。

次のようなコマンドを実行すると、ここまでの作業に追い付きます。

$ cp ../api.default_api.rb api/default_api.rb
$ cp ../views.header.erb lib/views/header.erb
$ cp ../views.main.erb lib/views/main.erb
$ make run

1.13. 【エラー対応】Address already in use とエラーが表示される

既に $ make run コマンドを実行しているのに、別の端末を開いて、make run を重複して実行するなどして、使われている8080番のポートを開こうとしてメッセージが表示されます。

$ make run
env FORM_BASEURI="http://127.0.0.1:8080/search" \
        bundle exec rackup --host 127.0.0.1 --port 8080
[2019-12-16 17:31:09] INFO  WEBrick 1.4.2
[2019-12-16 17:31:09] INFO  ruby 2.5.1 (2018-03-29) [x86_64-linux-gnu]
Traceback (most recent call last):
        16: from /usr/local/bin/rackup:23:in `<main>'
        15: from /usr/local/bin/rackup:23:in `load'
        14: from /var/lib/gems/2.5.0/gems/rack-2.0.7/bin/rackup:4:in `<top (required)>'
        13: from /var/lib/gems/2.5.0/gems/rack-2.0.7/lib/rack/server.rb:148:in `start'
        12: from /var/lib/gems/2.5.0/gems/rack-2.0.7/lib/rack/server.rb:297:in `start'
        11: from /var/lib/gems/2.5.0/gems/rack-2.0.7/lib/rack/handler/webrick.rb:31:in `run'
        10: from /var/lib/gems/2.5.0/gems/rack-2.0.7/lib/rack/handler/webrick.rb:31:in `new'
         9: from /usr/lib/ruby/2.5.0/webrick/httpserver.rb:47:in `initialize'
         8: from /usr/lib/ruby/2.5.0/webrick/server.rb:108:in `initialize'
         7: from /usr/lib/ruby/2.5.0/webrick/server.rb:127:in `listen'
         6: from /usr/lib/ruby/2.5.0/webrick/utils.rb:65:in `create_listeners'
         5: from /usr/lib/ruby/2.5.0/socket.rb:762:in `tcp_server_sockets'
         4: from /usr/lib/ruby/2.5.0/socket.rb:227:in `foreach'
         3: from /usr/lib/ruby/2.5.0/socket.rb:227:in `each'
         2: from /usr/lib/ruby/2.5.0/socket.rb:764:in `block in tcp_server_sockets'
         1: from /usr/lib/ruby/2.5.0/socket.rb:201:in `listen'
/usr/lib/ruby/2.5.0/socket.rb:201:in `bind': Address already in use - bind(2) for 127.0.0.1:8080 (Errno::EADDRINUSE)
Makefile:15: recipe for target 'run' failed

原因としては、自分や以前に端末を利用していた別ユーザーが8080ポートを利用するプログラムを起動したまま放置していることが考えられます。

この場合には、8080ポートを既に利用しているプロセスを停止することで、自分が8080ポートを利用できるようになります。

## lsofコマンドを利用して、ポート番号を利用しているプロセスをリストアップします。
$ sudo lsof -i -P | grep 8080
ruby2.7   7355    s12xxxxx    5u  IPv4 146566      0t0  TCP *:8080 (LISTEN)

## 左から2番目の数字(プロセスID)を指定して、プロセスを停止します。"7355"の部分は毎回変化します。
$ sudo kill 7355

2. まとめ

この段階で、見た目や不足している機能はありますが、Webアプリケーションを構成する基本的な要素は揃いました。

まだ不足している機能や見た目を改善することができます。

  • ページにタイトルや画面全体に結果が表示されている見た目の改善

  • 10件以上の検索結果がある場合でも、最初の10件までしか表示されない問題

  • params['q']が""(空文字列)の時にresult['response']['docs']が存在しないためエラーになる問題の改善 (対応済み)

改善の方法についてはBootstrap5について調べてみたり、コードの中で条件分岐や表示対象を限定するといった方法を検討してください。

2.1. 【中・上級者向け】ページネーションを実現する

ページネーションは表示したい件数が1ページの範囲を越える場合に利用します。

任意の開始位置からページに表示するべき検索結果を取得するためには少なくとも次のような情報が必要です。

  1. 1ページに表示する最大件数 (例: 10件/ページ)

  2. 表示する先頭の件番号 (例: 10〜19番目を表示する場合 → 10)

この他に現在のページ数を表示させたり、最後のページであればそれ以上は表示させないなどの細かい制御を行う場合には条件分岐が必要となります。

例えば@param_q = ..からret = client.get(url)までの間を次のように変更します。

  @param_q = params.has_key?(:q) ? params[:q].to_str  : "sccp"
  @param_start = params.has_key?(:start) ? params[:start].to_i  : 0
  @param_rows = params.has_key?(:rows) ? params[:rows].to_i  : 10
  url = URI::HTTPS.build({:host => "opm00h.u-aizu.ac.jp", :path => '/solr/api/v1/search', :query => "q=#{@param_q}&wt=json&site=&start=#{@param_start}&rows=#{@param_rows}"})
  ret = client.get(url)

これによって次のようなURLから、11件目から30件目までのデータが表示されます。

ページに戻るや進むといったボタンを追加しようと思います。

次のような内容のファイルをlib/view/pagination.erbとして配置します。

<%# -*- mode: web -*- %>
<% num_pages = (1.0 * @result["response"]["numFound"].to_i / @param_rows).round %>
<% num_max_items = (num_pages < @param_rows) ? num_pages : @param_rows %>
<% start_index = ( @param_start.to_i / 10 ) * 10  %>
<% end_index = ( @result["response"]["numFound"].to_i / 10 ) * 10 %>

<% prev_start = ( @param_start - @param_rows > 0 ) ? @param_start - @param_rows : 0 %>
<% next_start = ( @param_start + @param_rows < @result["response"]["numFound"].to_i ) ? @param_start + @param_rows : @result["response"]["numFound"].to_i %>

<% # show first button %>
<a style="margin-left:1em;" href="?q=<%= Rack::Utils.escape_html(@param_q) %>&start=0&rows=<%= @param_rows %>"><button class="btn btn-outline-primary btn-sm">First</button></a>

<a style="" href="?q=<%= Rack::Utils.escape_html(@param_q) %>&start=<%= prev_start %>&rows=<%= @param_rows %>"><button class="btn btn-outline-primary btn-sm">Previous</button></a>

<button type="button" class="btn btn-default ms-3"
disabled="disabled"><%= @param_start %> / <%=
@result["response"]["numFound"].to_i %></button>

<a style="margin-left:1em;" href="?q=<%= Rack::Utils.escape_html(@param_q) %>&start=<%= next_start %>&rows=<%= @param_rows %>"><button class="btn btn-outline-primary btn-sm">Next</button></a>

<% # show last button %>
<a href="?q=<%= Rack::Utils.escape_html(@param_q) %>&start=<%= end_index %>&rows=<%= @param_rows %>"><button class="btn btn-outline-primary btn-sm">Last</button></a>

このファイルをlib/views/main.erbから次のように呼び出します。

...
<h2>Search Results</h2>
<%= erb :pagination %>
<ul>
...

ページの下部にも追加してください。

3. Dockerによるアプリの実行

3.1. コンテナの作成と実行

Makefileの中にDockerコンテナとして実行するために必要な作業が記述されています。

$ make docker-build
$ make docker-build-prod
$ make docker-tag

## push image to Harbor
$ podman login inovtst9.u-aizu.ac.jp
$ make docker-push
$ podman logout inovtst9.u-aizu.ac.jp

## run container for test
$ make PORT=8080 docker-run

3.1.1. Apple Silicon (macOS) を利用している場合

次の方法でaarm64版とamd64版の両方のイメージをビルドすることができます。

$ make docker-buildx-init    ## 最初の一回のみ。エラーが出ても無視すること
$ make docker-buildx-setup

## build an image and push the image to Harbor
$ docker login inovtst9.u-aizu.ac.jp
$ make docker-buildx-prod
$ docker logout inovtst9.u-aizu.ac.jp

## run container for test
$ make PORT=8080 docker-runx

この2つのコマンドでDockerコンテナの準備、起動ができます。 無事に起動に成功すると、Webブラウザからアプリを動かすことができるようになります。

C-cで停止することができます。

Makefileの内部をみて、それぞれ実行されているコマンドを確認してください。

実行した結果は、まったく変化ないはずです。

しかし、Dockerコンテナとしてパッケージ化することで、他のコンピュータ上でもDockerコマンド以外のアプリケーションやライブラリを必要とせず、開発環境と同様に動作することが期待できます。

4. Kubernetesからの利用

Harborに登録をしたDockerイメージを利用してみます。

Webアプリケーションを利用する例として、公式Dockerチュートリアル Part2で利用したYAMLファイル、"bb.yaml"を流用しています。

ここでは、次のファイルをダウンロードして先に進んでください。 [ Download: tutorial-websample-sinatra.01.deploy-sinatra.yaml ]

$ wget https://web-int.u-aizu.ac.jp/~yasu-abe/ja/sccp/manual/tutorial-websample-sinatra.01.deploy-sinatra.yaml

ダウンロードした01.deploy-sinatra.yamlファイルの内容は以下のようになっているはずです。

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sinatra-webapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sinatra-webapp
  template:
    metadata:
      labels:
        app: sinatra-webapp
    spec:
      containers:
      - name: sinatra-webapp
        image: inovtst9.u-aizu.ac.jp/yasu-abe/sinatra-webapp:0.1.1
        imagePullPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
  name: sinatra-webapp
  labels:
    app: sinatra-webapp
spec:
  type: ClusterIP
  selector:
    app: sinatra-webapp
  ports:
  - port: 80
    targetPort: 8080

このファイルの中にある"yasu-abe"の部分を最初に作成したHarborのProject名(自分のAINS IDと同じ)に変更してから、次のようにkubernetesに適用させます。

$ kubectl -n $(id -un) apply -f tutorial-websample-sinatra.01.deploy-sinatra.yaml

service/sinatra-webapp configuredのようなメッセージが表示されれば成功です。 設定が反映され、PodとServiceが無事に作成されたか確認します。

$ kubectl -n $(id -un) get all -l app=sinatra-webapp

次のような出力が得られるはずです。

NAME                                  READY   STATUS    RESTARTS   AGE
pod/sinatra-webapp-7ffc8dbcdd-hfvzp   1/1     Running   0          6m19s

NAME                     TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/sinatra-webapp   ClusterIP   10.233.30.149   <none>        80/TCP    3m42s

NAME                                        DESIRED   CURRENT   READY   AGE
replicaset.apps/sinatra-webapp-7ffc8dbcdd   1         1         1       6m19s

不足するものがあれば、YAMLファイルの編集を間違っていないか、確認してください。

4.1. Proxyを利用したアプリケーションへの接続

WebブラウザからLoadBalancerを利用する方法は、EXTERNAL-IPの総数に限りがあるので、各自が準備したリバースプロキシーを利用します。

リバースプロキシーの設定については、下記のリンク先を確認してください。

この時に追加するproxy_pass 行は次のようになります。

      proxy_pass http://sinatra-webapp/;

次のように inovtst9.u-aizu.ac.jp 経由で自分のアプリにアクセスできることを確認してください。

4.1.1. Proxyがうまく動作しない場合の対応

Proxnの動作で問題があれば、次の手順でProxyを上書きすることことができます。

$ curl "https://web-int.u-aizu.ac.jp/~yasu-abe/ja/sccp/manual/tutorial-websample-sinatra.configmap-proxy.yaml" | sed -e "s/s12xxxxx/$(id -un)/" | kubectl -n $(id -un) apply -f -

$ kubectl -n $(id -un) apply -f https://web-int.u-aizu.ac.jp/~yasu-abe/ja/sccp/manual/ingress-proxy.deploy-proxy.yaml

$ curl "https://web-int.u-aizu.ac.jp/~yasu-abe/ja/sccp/manual/ingress-proxy.svc-proxy.yaml" | sed -e "s/s12xxxxx/$(id -un)/" | kubectl -n $(id -un) apply -f -

$ kubectl -n $(id -un) delete pod -l app=my-proxy

4.2. Kubernetes上でうまく動かない場合のトラブルシュート

うまくいかない時には、現状の把握 → 分析 → 対応 といったステップを踏む事になります。

そのためには、自分がなにをしようとしているのか、ある程度理解する必要があります。 なかなかうまくいかず、大変かもしれませんが、頑張ってください。

4.2.1. ステータスを確認する

先ほどのコマンドを再度実行し、pod, service, replicaset.apps の3つのステータスが表示されている事を確認します。

## ↑で実行したものと同じコマンド
$ kubectl -n $(id -un) get all -l app=sinatra-webapp

表示されていない行があれば、正しく作成されていないことを意味していますので、再度kubectl applyから足りていない定義をYAMLファイルを利用して作成します。

4.2.2. Podが動いているかステータスを確認する

先ほど実行したコマンドから、Podのみの状態を確認します。

$ kubectl -n $(id -un) get pod -l app=sinatra-webapp

次のような結果が画面に出力されるはずです。

NAME                                  READY   STATUS    RESTARTS   AGE
pod/sinatra-webapp-7ffc8dbcdd-hfvzp   1/1     Running   0          6m19s

この最後の AGE6m19s の部分には、podが稼動してからの時間が書かれています。 作業をやり直している場合など、再起動をしたはずが、先週からずっと動いていた、という事がないか確認します。

また、STATUSRunning の部分を確認し、他のメッセージが表示されていないか確認します。 Running以外のステータスでは、Webブラウザからアクセスすることができません。

これを応用し、Proxyのpodが正常に動いているか次のコマンドの出力から確認しましょう。

$ kubectl -n $(id -un) get pod -l app=my-proxy

もしProxyがずっと動いているようであれば、リバースプロキシー - Proxyを利用して自分のサービスに接続する を確認し、Proxyのpodを削除することで、再起動してください。

4.2.3. 次にProxyの設定を確認する

↑でproxyのpodを再起動したのに改善されていない場合には、cm/nginx-conf の内容を確認します。

$ kubectl -n $(id -un) get cm nginx-conf -o yaml

次のような出力が得られるはずです。

apiVersion: v1
data:
  proxy.conf: |
    server {
      listen 80;
      location /yasu-abe/search/ {
         proxy_pass http://sinatra-webapp/;
      }
      location /yasu-abe/s/ {
         proxy_pass http://sinatra-webapp/;
      }
    }
kind: ConfigMap
metadata:

annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","data":{"proxy.conf":"server {\n  listen 80;\n  location /yasu-abe/ {\n    proxy_pass
    http://my-nginx/;\n  }\n}\n"},"kind":"ConfigMap","metadata":{"annotations":{},"labels":{"app":"my-proxy"},
"name":"nginx-conf","namespace":"yasu-abe"}}
  creationTimestamp: "2022-04-25T11:30:53Z"
  labels:
    app: my-proxy
  name: nginx-conf
  namespace: yasu-abe
  resourceVersion: "97659475"
  uid: b9dd26f0-dd91-43b9-987d-58e818f30d99

この中の、data:行 から kind: ConfigMap行までの内容が、nginxの設定内容となります。

これまでの作業の結果によって、この内容は、さまざま異なるはずです。まったく同じである必要はありません。

トラブルが発生している場合には、この内容を最小限の内容に変更しましょう。

  proxy.conf: |                               ## ← 変更しない "|" を消さないこと!
    server {                                  ## この行は変更しない
      listen 80;                              ## 同じく、この行は変更しない
      location /yasu-abe/search/ {            ## ← "yasu-abe" の部分は自分のIDに変更する
         proxy_pass http://sinatra-webapp/;   ## この行も変更しない
      }                                       ## ここも変更しない。server { に対応する閉じかっこを忘れないこと
4.2.3.1. cm/nginx-confのトラブル例

上の例で、先頭の"|"文字を消してしまうと、次のような画面出力になります。

apiVersion: v1
data:
  proxy.conf: server { listen 80; location /yasu-abe/search/ { proxy_pass http://sinatra-webapp/;
    } location /yasu-abe/s/ { proxy_pass http://sinatra-webapp/; } }
kind: ConfigMap
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"v1","data":{"proxy.conf":"server {\n  listen 80;\n  location /yasu-abe/ {\n    proxy_pass
    http://my-nginx/;\n  }\n}\n"},"kind":"ConfigMap","metadata":{"annotations":{},"labels":{"app":"my-proxy"},
"name":"nginx-conf","namespace":"yasu-abe"}}
  creationTimestamp: "2022-04-25T11:30:53Z"
  labels:
    app: my-proxy
  name: nginx-conf
  namespace: yasu-abe
  resourceVersion: "99884213"
  uid: b9dd26f0-dd91-43b9-987d-58e818f30d99

data:行がつぶれて、とても見にくくなってしまいますが、この状態でproxyは正しく動作します。 しかし、人間が正しく設定を追加することはとても難しいため、proxy.conf: | に修正し、元の状態にすることをお勧めします。

編集する場合には、リバースプロキシー から kubectlの edit コマンドを実行している箇所を探して、コマンドを確認してください。

基本的には、これらの方法で、ほとんどの問題は対処できるはずです。

4.2.4. Proxy設定の初期化とhttp://sinatra-webapp/ への接続

どうしてもProxyがうまく動作しない時の最終手段として手順をまとめておきます。

これは既存の設定を上書きし、URLリストにある自分のURL(https://inovtst9.u-aizu.ac.jp/s13xxxxx/)から直接接続するための最終手段です。

いずれの手順も既存のオブジェクトを上書きし、情報が失われますので注意してください。

4.2.4.1. deploy/my-proxy の初期化

この手順は一般的には必要ありませんが、まだリバースプロキシーのPod(my-proxy)が稼動していない場合には必要となります。

念のためにリバースプロキシー (Reverse Proxy) にある以下のコマンドを実行してください。

複数回実行しても問題になることはありません。

$ kubectl -n $(id -un) apply -f https://web-int.u-aizu.ac.jp/~yasu-abe/ja/sccp/manual/ingress-proxy.deploy-proxy.yaml
4.2.4.2. cm/nginx-conf の初期化

この設定ファイルのURLを指定して、次のようなコマンドを実行してください。

$ curl "https://web-int.u-aizu.ac.jp/~yasu-abe/ja/sccp/manual/tutorial-websample-sinatra.configmap-proxy.yaml" | sed -e "s/s12xxxxx/$(id -un)/" | kubectl -n $(id -un) apply -f -
4.2.4.3. svc/s13xxxxx-svc の初期化

selector: で指定するラベルと一致するPodにアクセスしますが、これが app: my-proxy になってない場合が多いようです。Ingress からの接続が my-proxy に向うように次のように設定してください。

$ curl "https://web-int.u-aizu.ac.jp/~yasu-abe/ja/sccp/manual/ingress-proxy.svc-proxy.yaml" | sed -e "s/s12xxxxx/$(id -un)/" | kubectl -n $(id -un) apply -f -
4.2.4.4. Proxyサーバーを再起動する
$ kubectl -n $(id -un) rollout restart deploy proxy
4.2.4.5. 動作確認

URLリストに戻って自分のURLをクリックしてください。