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

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

タグ:http

とある要件を実現するツールを作りました。同じことに悩む人がいた場合を想定して、ツールをソースごと公開することにしました。

某ウェブプラットフォームのサービス終了が決まり(わかる人はこれだけで何の話か推測されそうだけど・・)、現在稼働中のウェブアプリケーションを引っ越しすることになりました。引っ越しそのものはさほど難しくないのですが、問題は「サービスの URL が変わってしまう」ことでした。

図に示すとこのような感じです。これまで運用していたサービスの運用環境を A から B に引っ越しした結果、これまでの URL とは異なる URL で引き続き運用することになりました:
2022070601


これまで使っていたユーザーに対しても「サービスの URL が変わった」ことを知らせてあげたいのですが、具体的にどうするべきでしょうか?A で運用中の画面に「URL が変更になった」と注意書きを含めて、改めて B にアクセスしてもらうこともできます。が、もう少し気の利くやり方として「A にアクセスしたら自動的に B に転送させて、B で運用中の画面で URL が変更になった旨を記載しておく(そのままブックマークできるようにする)」という方法もあります。


この後者の方法を実現するためには A にアクセスした利用者に対して "301" という HTTP ステータスコードと、続けて変更先の URL を Location ヘッダに含めて返すことで実現できます。この HTTP ステータスコード 301 は "Moved Permanentaly" を意味していて「URL が(一時的ではなく)恒久的に変更になった」ことを示しています。続けて Location ヘッダに新しい URL を含めておくことでウェブブラウザ側で新しい URL に自動的に遷移してくれます。つまり「A にアクセスしたら自動的に B に移動させる」ことが実現できます(そして B 側で「URL が変わったので現在のページをブックマークして」といったメッセージを記しておく、といった対応になります)。ウェブページの引っ越しを行う場合の一般的な手段でもあります:
2022070602



問題となるのは、この「301 という HTTP ステータスコードを返す」機能です。A 側のサービスでそのような転送機能や転送の設定が提供されていればそれを使えばいいのですが、必ずしも提供されていないことも考えられます。そのようなケースに対応するため、今回「アプリケーションの機能として 301 HTTP ステータスコードと、引っ越し先 URL を返すアプリケーション」を作ったので、公開することにしました。これまで A で動いてたアプリケーションの代わりにこのアプリケーション(下図の app)をデプロイすることで、A へのアクセスがあった場合に、新しい B への URL に無条件で転送させることができるようになります:
2022070603


このような挙動を実現するための Node.js アプリケーションのソースコードを以下で公開しました:
https://github.com/dotnsf/301movedpermanently

動作確認する場合は、Node.js が導入済みの環境にソースコードをダウンロードするか git clone して、環境変数 URL に転送先の URL を指定して実行します。なおアプリケーションはデフォルトで 8080 番ポートで待ち受けますが、このポート番号を変えたい場合は環境変数 PORT に指定して実行してください:
$ git clone https://github.com/dotnsf/301movedpermanently

$ cd 301movedpermanently

$ npm install

$ URL=https://www.yahoo.co.jp/ node app (全てのリクエストを Yahoo! トップページに転送する場合)

起動後にアプリケーションにアクセスすると、全てのリクエストが環境変数 URL で指定した値(上の例だと https://www.yahoo.co.jp/)に転送されます。なおコード内ではメソッド/パス/パラメータに関係なく、全てのリクエストを GET リクエストに変換して転送しています。GET/HEAD メソッド以外のリクエストについては HTTP ステータス 308 を返して対応するケースもありますが、今回のような「サービスごと引っ越し」のケースでは新 URL のトップページに転送することが多いと思うので、今回は説明を控えます(下の青字部分が実行されます):
app.all( '*', function( req, res ){
  if( post_redirect ){
    var method = req.method;
    if( method == 'GET' || method == 'HEAD' ){
      //. https://developer.mozilla.org/ja/docs/Web/HTTP/Status/301
      res.status( 301 );
    }else{
      //. https://developer.mozilla.org/ja/docs/Web/HTTP/Status/308
      res.status( 308 );
    }
  }else{
    res.status( 301 );
  }

  res.set( 'Location', url );
  res.end();
});

古いサイト A が動いている間だけ有効な強制転送方法ですが、他に方法がない場合はこんな感じで古いサイトを訪ねた人を強制的に B へ転送する方法が有効だと考えています。

なお、docker イメージとしても公開しているので、移行元で docker が使える環境であればこちらを使っていただくのが手っ取り早いと思っています:
dotnsf/301movedpermanently


HTTP プロトコルを使っていると、「HTTP ステータスコード(以下、「ステータスコード」)」を意識することがあります:
http-status-codes


ステータスコードは HTTP リクエストを実行した結果を端的に表す3桁の数字です。最も有名かつ一般的なステータスコードは 200 で "OK" の意、つまり「リクエストが成功した場合のステータスコード」(「HTTP リクエストが成功すると 200 というステータスコードが返ってくる」と理解するとわかりやすいかも)です。

200 以外だと 404("Not Found" URL やファイルが存在しない場合など)や 403("Forbidden" アクセス権限がない場合など)あたりが比較的多く目にする機会があるものでしょうか。まあ本来あってはならない 500("Internal Server Error" サーバー内部エラー)とかもたまに見ることありますけど。。 なお規格として定義されているステータスコードの一覧はこちらを参照してください:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Status


ともあれ、特にアプリケーションを開発したり運用したりしていると、「必ずしも 200 のステータスコードばかりが返ってくるわけではない」ことを意識する必要に迫られることがあります。なんらかの原因でエラーが発生してしまうことはあるし、どういうエラーとなった場合にどのような挙動にするのか、ということまで考慮して開発・運用する必要がある、という意味です。


一方でそこまで意識していたとしても、開発した/運用中のアプリケーションがエラーに遭遇するのは稀なケースのはずです。「こういうエラーの時はこういう挙動にする」ための準備をしていても、思いのままに任意のエラーを出せるわけではないのです。


そんなケースを想定して、「任意の HTTP ステータスコードを返す」アプリを作ってみました。URL パラメータに返してほしいステータスコードを指定して GET リクエストを送ると、レスポンスは指定したステータスコードが返ってくる※、というものです。

※本来のあるべき挙動とは異なります。要はアプリケーションとしては正しく動いている(ステータスコードは 200 になるべき)にも関わらず、本来返すべきステータスコードとは異なる 404 等を返す、という挙動をします。あくまで上述のようなケースのデバッグ用および動作確認用アプリケーションです。


アプリケーションのソースコードはこちらで公開しています:
https://github.com/dotnsf/statuscode_generator

また Docker イメージもこちらから取得可能です:
https://hub.docker.com/r/dotnsf/statuscode_generator


ローカルの Docker エンジンを使って動かす場合は、以下のように指定して実行します(8080 番ポートでアクセス可能にする場合):
$ docker run -d --name statuscode-generator -p 8080:8080 dotnsf/statuscode-generator

起動しているアプリケーションに GET /status リクエストを実行します。URL パラメータ code を付けて実行すると、指定したステータスコードが返されます(デフォルトは 200):
$ curl -v http://localhost:8080/status

*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /status HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Date: Sun, 24 Oct 2021 05:32:20 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
< Transfer-Encoding: chunked
<
{
  "code": 200,
  "reference_url": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status",
  "timestamp": 1635053540644
* Connection #0 to host localhost left intact

$ curl -v http://localhost:8080/status?code=404

*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /status?code=404 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
>
< HTTP/1.1 404 Not Found
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Date: Sun, 24 Oct 2021 05:33:03 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
< Transfer-Encoding: chunked
<
{
  "code": 404,
  "reference_url": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status",
  "timestamp": 1635053583162
* Connection #0 to host localhost left intact

なお、起動時に環境変数 ALLOW_CORS が設定されていると、指定した値のサーバーからであれば CORS リクエストを受け付けるようになります(デフォルトでは CORS 無効):
(例 全てのサーバーからの CORS リクエストを受け付ける場合)
$ docker run -d --name statuscode-generator -e ALLOW_CORS=* -p 8080:8080 dotnsf/statuscode-generator

この ALLOW_CORS=* が指定された状態で起動しているサーバーを heroku 上に用意しておきました。heroku 無料枠で動かしているので初回アクセス時は(停止状態から起動するまで)少し時間がかかると思いますが、よかったらお使いください:
https://statuscode-generator.herokuapp.com/


なお、この任意のステータスコードを返す API は GET /status で直接実行できますが、(上述の heroku アプリのように) GET / でアクセスすると簡易 UI が現れ、簡単な動作確認ができます。返してほしいステータスコードを指定して AJAX ボタンをクリックすると、AJAX で GET /status?code=(指定したステータスコード) が実行され、その結果が画面下部に表示されます(下図は 403 を指定して実行した時の例):
2021102401


特にクラウドやコンテナ環境などで、アプリケーション・サーバーとは別にエッジサーバーがあってリバースプロクシーを使っている場合などリバースプロクシー側の動作設定をする場合に便利かと思い作ってみました(アプリケーションサーバーエラーが起こった場合はエッジサーバー側で特定のエラーページに遷移させる、といった設定をしている場合の動作確認時など)。自分以外にこれが役立つ人がどのくらいいるかわからないのですが、よかったら使ってください。役立つことがあれば嬉しいです。

なお、ステータスコードとして 1xx(百番台)を指定した場合ですが、HTTP ステータスコードとしては指定したものが返されます。が、1xx は実行途中の経過を返すものだったりして、実際の挙動という点ではこの続きがないと不自然な挙動となります。そのあたりまで考慮されているわけではない点をご了承ください。






IBM Cloud から提供されている 30 日間無料 Kubernetes サービスIBM Kubernetes Service 、以下 "IKS")環境を使って利用することのできるコンテナイメージを1日に1個ずつ 30 日間連続で紹介していきます。

環境のセットアップや制約事項については Day0 のこちらの記事を参照してください。

Day 2 は超有名な Apache HTTP イメージをデプロイする例を紹介します。
eyecatch-apache


【イメージの概要】
このイメージは昔から HTTP サーバーとして広く使われてきた Apache HTTP サーバーです。静的なコンテンツの公開だけでなく、CGI などの仕組みと組み合わせてアプリケーション・サーバーとして利用されることが多いという印象です。


【イメージのデプロイ】
Apache HTTP イメージのデプロイ用 YAML ファイルを公開したので、このファイルを指定してデプロイします。以下のコマンドを実行する前に Day 0 の内容を参照して ibmcloud CLI ツールで IBM Cloud にログインし、クラスタに接続するまでを済ませておいてください。

そして以下のコマンドを実行します:
$ kubectl apply -f https://raw.githubusercontent.com/dotnsf/yamls_for_iks/main/apache.yaml

以下のコマンドで Apache HTTP 関連の Deployment, Service, Pod, Replicaset が1つずつ生成されたことと、サービスが 30080 番ポートで公開されていることを確認します:
$ kubectl get all

NAME                          READY   STATUS    RESTARTS   AGE
pod/apache-76695bf577-pj9w5   1/1     Running   0          4m2s

NAME                 TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service/apache       NodePort    172.21.151.226   <none>        80:30080/TCP   4m4s
service/kubernetes   ClusterIP   172.21.0.1       <none>        443/TCP        26d

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/apache   1/1     1            1           4m4s

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/apache-76695bf577   1         1         1       4m4s

この後に実際にサービスを利用するため、以下のコマンドでワーカーノードのパブリック IP アドレスを確認します(以下の例であれば 161.51.204.190):
$ ibmcloud ks worker ls --cluster=mycluster-free
OK
ID                                                       パブリック IP    プライベート IP   フレーバー   状態     状況    ゾーン   バージョン
kube-c3biujbf074rs3rl76t0-myclusterfr-default-000000df   169.51.204.190   10.144.185.144    free         normal   Ready   mil01    1.20.7_1543*

つまりこの時点で(上述の結果であれば)アプリケーションは http://169.51.204.190:30080/ で稼働している、ということになります。早速実行してみます。ウェブブラウザか curl コマンドを使って、アプリケーションの URL(上述の方法で確認した URL)にアクセスしてみます:
20210723_apache01


分かる人にはおなじみの(笑)Apache HTTP サーバーの初期起動メッセージ "It works!" が表示されました。無事にデプロイできて、IKS 環境で動いている状態が作れました。



【YAML ファイルの解説】
YAML ファイルはこちらを使っています:
apiVersion: v1
kind: Service
metadata:
  name: apache
spec:
  selector:
    app: apache
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
    nodePort: 30080
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: apache
spec:
  replicas: 1
  selector:
    matchLabels:
      app: apache
  template:
    metadata:
      labels:
        app: apache
    spec:
      containers:
      - name: apache
        image: httpd
        ports:
        - containerPort: 80

Deployment 1つと、Service 1つのごくごくシンプルな YAML ファイルですが、一応解説を加えておきます。アプリケーションそのものは 80 番ポートで動作するように作られているため、NodePort 30080 番を指定して、外部からは 30080 番ポートでアクセスできるようにしています(NodePort として指定可能な番号の範囲は 30000 ~ 32767 です、指定しない場合は空いている番号がランダムに割り振られます)。また ReplicaSet は1つだけで作りました。


デプロイしたコンテナイメージを削除する場合はデプロイ時に使った YAML ファイルを再度使って、以下のコマンドを実行します。不要であれば削除しておきましょう:
$ kubectl delete -f https://raw.githubusercontent.com/dotnsf/yamls_for_iks/main/apache.yaml


【紹介したイメージ】
https://hub.docker.com/_/httpd


【紹介記録】
Dayカテゴリーデプロイ内容
0準備準備作業
1ウェブサーバーhostname
2Apache HTTP
3Nginx
4Tomcat
5Websphere Liberty
6データベースMySQL
7phpMyAdmin
8PostgreSQL
9pgAdmin4
10MongoDB
11Mongo-Express
12Redis
13RedisCommander
14ElasticSearch
15Kibana
16CouchDB
17CouchBase
18HATOYA
19プログラミングNode-RED
20Scratch
21Eclipse Orion
22Swagger Editor
23R Studio
24Jenkins
25アプリケーションFX
262048
27DOS Box
28VNC Server(Lubuntu)
29Drupal
30WordPress

このブログエントリを書くきっかけとなったのはこの記事でした:
2021年5月のWebサーバ利用シェア、「Nginx」が「Apache」を初めて上回る


ウェブサーバー(HTTP サーバー)のシェア争いで、ついに Nginx が Apache を抜いて首位にたった、というものです。この2つの合計だけで全体の 70% 前後あるという「2トップ」ですが、少しずつ差を縮めた結果、5月になってわずかながら Nginx のシェアが Apache を上回ったようでした。

・・・で、その結果は置いといて(苦笑)、自分自身が気になったのは順位では6位に位置づけられている "Node.js" です:



Nginx や Apache は、例えば WordPress や Drupal のような有名な CMS のウェブサーバーとしても使われていたりするし、他にも Ruby on Rails を使う場合のウェブサーバーとして利用されるので、多くのプログラミング言語/コンテンツ管理システムのウェブサーバーとして採用されていることがシェアが高い背景として想像できます。

その観点で見た場合、6位の "Node.js" は異色といえます。なぜならば Node.js はあくまでプログラミング言語であって、これ自体はウェブサーバーではないからです。統計としては「Node.js で作ったウェブサイトのページ」をカウントしているのだと想像できるし、(Node.js がウェブサーバーではない以上)他に分類のしようがないことも理由だと思いますが、いろんな言語で作られたページのウェブサーバーである Nginx や Apache がランクインしている中では「違うカテゴリーの人が混ざってる」感は否めません。

そして最も気になったのは「そもそも、どうやって Node.js で作ったページだとわかったのか?」でした。Nginx や Apache だとわざと 404 エラーを出してそのエラー画面から調べる、という方法もあるのですが、Node.js では(特に明示しない限り)味気ない 404 エラーがでるだけなので調べようがないはずです。どうやって Node.js ウェブページのシェアを数えたのだろう? という興味が湧いて調べてみたのでした。

結論としてはごくシンプルで、404 エラーを出さなくても HTTP レスポンスヘッダの中に答がありました:
2021052501


Node.js でウェブアプリを作る際にほぼ 100% 使われるであろう Express ライブラリを使ってアプリを作って起動し、そこに curl コマンドを HTTP レスポンスヘッダのみ返すようなオプションをつけて実行してリクエストとレスポンスのヘッダ情報も確認しました。するとレスポンスヘッダの中に "X-Powered-By: Express" という項目がありました。なるほど、「この情報が含まれるページは Node.js 製」と判断したんだろうな。。


で、この情報がわかると、同様にウェブサーバーを併用しない開発言語である Python はどうなっているんだろう? と気になりました。ウェブサーバーランキングには入ってないけど、Python で作ったウェブページかどうかも Node.js と同様にして調べることができるのだろうか・・・と。

で、こちらも Flask ライブラリを使ってアプリを起動し、同様に curl コマンドで HTTP レスポンスヘッダを取得するモードで実行してヘッダを確認すると・・・ こちらはレスポンスヘッダの中に "Server: Werkzeug/0.14.1 Python/3.6.7" というわかりやすい項目がありました。Python の場合は「ウェブページを作る際のライブラリはほぼ 100% Flask」とは言い切れない所がありますが、まあでも同様に調べることはできそうですね。
2021052502


(おまけ)ところで、このブログエントリの中でも使っているのですが、「curl コマンドで HTTP レスポンスヘッダのみ調べる」方法を調べました。複数のオプションを組み合わせる形で実現しているのですが、結論としてはこんな感じでした:
$ curl -D - -s -o /dev/null http://localhost:nnnn/

3つのオプションを組みあわせて実現しています:
  • -D - : dump header オプション、標準出力にヘッダを出力する
  • -s : slient オプション、途中経過を出力しない
  • -o /dev/null : output オプション、(ヘッダ以外の)本体を /dev/null へ出力して廃棄


【参照】
https://qiita.com/yousan/items/fcc15e1046939c465ab7

Node.js + Express の環境で SSL を使う(https でアクセスできるようにする)方法を調べたのでまとめました。


まず SSL を使うための鍵ファイルと証明書ファイルを用意します。公式なドメインを所有していて、本物の鍵/証明書ファイルを持っているのであればそれを使っても構いません。試験的に試すのであれば、いわゆる「オレオレ証明書」を作成します。Linux 環境であれば openssl コマンドを使って、以下のように入力します:
$ openssl genrsa -out server_key.pem 2048
Generating RSA private key, 2048 bit long modulus
...............+++
..........................+++
e is 65537 (0x10001)

$ openssl req -batch -new -key server_key.pem -out server_csr.pem -subj "/C=JP/ST=Chiba/L=Funabashi/O=Jugeme/OU=Dev/CN=juge.me"

$ openssl x509 -in server_csr.pem -out server_crt.pem -req -signkey server_key.pem -days 73000 -sha256
Signature ok
subject=/C=JP/ST=Chiba/L=Funabashi/O=Jugeme/OU=Dev/CN=juge.me
Getting Private key

$

↑この例では juge.me というホスト名で運用する前提での、サーバーの鍵ファイル(server_key.pem)と証明書ファイル(server_crt.pem)を作成しています。

この2つのファイルと同じディレクトリに app.js というファイル名で、Node.js のソースコードを以下の内容で作成しました。アプリケーションそのものはドキュメントルート(/)にアクセスがあった場合に「ハローワールド」と表示するだけの単純なものです:
//. app.js

var express = require( 'express' ),
    http = require( 'http' ),
    https = require( 'https' ),
    cfenv = require( 'cfenv' ),
    fs = require( 'fs' ),
    app = express();
var appEnv = cfenv.getAppEnv();

//. 鍵ファイルと証明書ファイル var options = { key: fs.readFileSync( './server_key.pem' ), cert: fs.readFileSync( './server_crt.pem' ) };
//. 鍵ファイルと証明書ファイルを指定して、https で待受け var server = https.createServer( options, app ).listen( appEnv.port, function(){ console.log( "server stating on " + appEnv.port + " ..." ); });
//. ドキュメントルートにリクエストがあった場合の処理 app.get( '/', function( req, res ){ res.write( 'ハローワールド' ); res.end(); });

このコードを実行すると待受ポート番号が動的に決定して表示されます(↓の例だと 6015 番ポートで https が待ち受けていることが分かります):
$ node app.js
server stating on 6015 ...

ではウェブブラウザで実際にアクセスしてみます。本当に使っている本物のドメイン/ホスト名であればそのままアクセスできると思いますが、試験的に実行している場合はこのホストに "juge.me" という名前でアクセスできる必要があります。必要に応じて hosts ファイルを編集するなどして、目的のホスト(node app.js を実行したホスト)に "juge.me" というホスト名でアクセスできるような準備をしておいてください。

そしてウェブブラウザで "https://juge.me:(ポート番号)" にアクセスします:
2017041301


オレオレ証明書名物「安全な接続ではない」という警告画面になると思います。ここからの手順はウェブブラウザの種類にもよりますが、FireFox の場合であれば「エラー内容」ボタンをクリックしてから「例外を追加」をクリックして、このサイトの警告を無視するための設定を行います。

そして「セキュリティ例外の追加」ダイアログにて、この URL の「セキュリティ例外を承認」します:
2017041302


すると警告が消えて、プログラムで用意したコードが実行され、「ハローワールド」というメッセージが表示されることが確認できます:
2017041303


IBM Bluemix のような PaaS 環境だと、このあたりも含めてアプリケーションサーバー環境が用意されるので楽ですが、素で Node.js 環境を構築する場合はアプリケーション側でも https 対応を実装する必要があり、意外と面倒ね。

このページのトップヘ