nginxコンテナを自己署名証明書でHTTPS化する
前回、Dockerでnginxを入れて基本的なことを試して以下の記事を書きました。
このときHTTPS化も試していたのですが話が長くなるので別の記事にすることにしました。
はじめに
今やサービスの入り口は全てHTTPS化するのが常識となりました。外部公開しないサービスも同様です。開発環境もHTTPS化しています。今後、HTTPSを設定をする機会は増えてくるのでしっかりと理解しておきたいところです。
今回はブラウザとnginxの間をHTTPS化します。
nginxとWebアプリケーションとの間は引き続きhttpにします。暗号化/復号化処理はnginxが実行してWebアプリケーションはアプリケーションの実行に専念する構図です。
最近ではコンテナ間通信もHTTPS化する流れがありますが、Webアプリケーション側まで手を入れると時間がかかるので今回はやめました。
証明書をどうするか?
HTTPS化するには証明書が必要です。
証明書ををどうするか考えてみました。選択肢は以下の3つです。
この記事では自己署名証明書を使ったケースを紹介します。
2の自己署名認証局を使ったケースは別の記事で紹介しています。
3も試す予定でしたが2でお腹いっぱいになったのでやめました。
自己署名証明書
通称、オレオレ証明書です。
本来、サーバ証明書の署名欄にはその証明書を発行したCAの署名が入ります。サーバ証明書とは第3者のCAが「私はこのサーバを調査し、その結果、このサーバが信頼に値することを保証します。」と宣言している保証書なのです。
CAの署名の代わりにサーバ自身の署名が入るのが自己署名証明書です。「オレぁはいいヤツだぜぇ!オレがそう云うんだからまちげぇねぇ!」と自らが喧伝しているわけです。そんなことする輩がまともなわけありません。出会ったら、すぐ逃げて下さい。
危険な自己署名証明書ですが、証明書が欲しいときには最も簡単に作成できます。opensslコマンド3発で完成です。お金はかかりません。CAによる実存性確認もありません。
お手軽な反面、ブラウザがセキュリティ警告をガンガン出すので保護されている気分に全くなれないのが大きな欠点です。また、フロントエンドのAPIが動かなくなることもあります。
結論を言うとこの方法はおすすめしません。簡易的なHTTPS環境を作りたい場合は2の自己署名認証局を使うことを勧めます。
nginxコンテナのTCP 443番を公開
それでは構築していきます。
これまで作業してきたnginxコンテナは下記の通りTCP 80番を公開してました。
docker run -it --name nginx -p 8080:80 nginx /bin/bash
HTTPSはTCP 443番を使うので、このままではHTTPS化できません。
新しいnginxコンテナを起動します。このとき下記の通りTCP 443番を公開します。
docker run -it --name nginx -p 443:443 nginx /bin/bash
秘密鍵と証明書を作成する
過去のPKIに関する記事を参考にnginxコンテナの中でopensslコマンドを3発、叩きます。
色々聞かれますが基本、空欄のままEnter押下で問題ありません。ただし秘密鍵作成のときに要求されるpass phraseの入力は必須です。pass phraseは後で使うので忘れないで下さい。
# openssl genrsa -aes192 -out mykey.key 4096 <略> Enter pass phrase for mykey.key:
また、Certificate Signing Requestを作成するときに下記の通りCommon Nameで「localhost」と入れるのも重要です。もしnginxにちゃんとしたホスト名を割りあてている場合は「localhost」ではなくそのホスト名を入れて下さい。
Common Name (e.g. server FQDN or YOUR name) []:localhost
最初、これをうっかり忘れてフロントエンドのfetch APIが動かなくなりました。fetch APIはSubjectのCommon Name(CN)がURLと一致するかチェックしているようです。
opensslコマンドが作成したファイルです。mycert.crtが証明書、mkey.keyが秘密鍵です。mycsr.csrはCSRですがここから先は出番はありません。
# ls /etc/nginx/conf.d/ssl/ mycert.crt mycsr.csr mykey.key
証明書をチェック
証明書の内容をチェックします。
SubjectのCNが「localhost」になっているのでOKです。
Issuer(発行者)もlocalhostなので自己署名証明書であることがわかります。
# openssl x509 -text -noout -in mycert.crt <略> Issuer: CN=localhost <略> Subject: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd, CN = localhost
nginxの設定変更
/etc/nginx/conf.d/default.conf以下のように変更します。
vimが入っていない場合は前回の記事を参考にvimをインストールして下さい。
前回からの変更点は443をlistenしていることと、証明書と秘密鍵を指定していることの3点のみです。
server { listen 443 ssl; <<< 変更1 server_name localhost; ssl_certificate /etc/nginx/conf.d/ssl/mycert.crt; <<< 変更2 ssl_certificate_key /etc/nginx/conf.d/ssl/mykey.key; <<< 変更3 access_log /var/log/nginx/host.access.log main; location / { root /usr/share/nginx/html; index index.html index.htm; } location /api { proxy_pass http://host.docker.internal:5000; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } }
設定に問題がないかconfigtestしてからnginxを再起動します。 このとき秘密鍵を生成するときに入力したpass phraseを要求されます。
/etc/init.d/nginx configtest /etc/init.d/nginx restart
これでnginxは準備完了です。
フロントエンド修正
前回に続きTCP 5000番で待っている適当なWebアプリケーションをローカルPCで立ち上げています。
HTTP環境で作成したのでフロントエンドのfetch APIのアクセス先がhttpです。
const request_url = "http://localhost:8080/api";
これではフロントエンドがデータを取得できません。httpsに変更します。
const request_url = "https://localhost/api";
これでnginxもフロントエンドもHTTPSに対応しました。最後に動作確認をしましょう。
動作確認
https://localhostへアクセスします。
chromeの場合、以下のようにERR_CERT_AUTHORITY_INVALIDエラーが表示されます。
「詳細設定」→「localhostにアクセスする(安全ではありません)」をクリックします。
無事、情報が表示されました。ChromeもフロントエンドのJavaScriptも問題なく動いてます。
ただしロケーションバーには「保護されていない通信」と表示されています。「https」には取り消し線が入ってます。
HTTPSを導入したはずですが保護されている感じがしません。
「保護されていない通信」をクリックすると証明書が「無効」となっています。
「無効」をクリックするとオレオレ証明書であることがわかります。
結論
短時間でHTTPS化することができました。その感想としては「これでいいの?」です。
外部公開するサービスでやるのは当然論外です。
しかし外部公開しない内輪向けのサービスだとしても信頼できない証明書を許容するようなことをユーザにさせていいのでしょうか?ユーザのセキュリティ感覚を麻痺させて危険にさらしていないでしょうか?
検証用だとしてもオレオレ証明書で動いていたフロントエンドが本番のサーバ証明書で動くでしょうか?安全なAPIに無理やりオレオレ証明書を許容させるようなコードや設定をしたときその検証に意味はあるのでしょうか?
次の記事では「自己署名認証局が署名した証明書」を紹介します。
<環境構築不要>お手軽にKubernetesを試す
Kubernetesの公式Tutorial環境で自作コンテナのデプロイを試した話です。
動機と結果まとめ
勉強のために自作の超簡単サービスをDockerやAWSに簡易的にデプロイしてみました。
k8sでも試したいと思ったのですが環境を用意するほどの気力と余裕がありません。
ひょっとしたらk8sの公式Tutorial環境でできのではないか?と思ったのがきっかけです。
manifestファイルを使わずにrun/expose/scale/rolloutのみで実施しました。
結果として、環境固有の制約はありましたが一応デプロイできました。
なお今回、デプロイしたコンテナは1つだけです。複数コンテナで構成されるサービスもできると思いますがmanifestファイルなしでやるのは厳しそうだし、雰囲気だけわかれば十分なのでやめました。
事前準備
Dockerhubに自作コンテナをpushしておきます。
ローリングアップデートを試したい場合はv1、v2などタグを2つ用意してくださいい。
私はなにかのTutorialで使ったNode.jsのコンテナを利用しました。
自作コンテナがない方はDockerhub上のnginx:<適当なタグ>とかでもよいと思います。
DockerhubへのPushはここで紹介しています。
k8sの公式Tutorial
ここです。
環境を立ち上げる
上記サイトにアクセスして適当なTutorialを選んで下さい。
Tutorialは複数ありますがどれでもOKです。
強いて勧めるなら「Kubernetesの基本を学ぶ」の「アプリケーションのデプロイ」が無難だと思います。minikubeが起動済みで、deploymentやserviceが事前に作成されてないのですぐに自作コンテナを立ち上げる作業に着手できます。
一番最初の「Hello Minikube」だけUIが特殊で使いづらいのでやめた方がよいです。
各Tutorialはテキストと「対話型チュートリアル」で構成されています。テキストは無視して「対話型チュートリアル」を選択してください。ブラウザにk8s環境が表示されます。
確認
以下のコマンドを実行してゴミがないか確認し、あれば削除します。なお「アプリケーションのデプロイ」を選んだ場合はゴミはないはずです。
kubectl get deploy kubectl get pods kubectl get svc
「kubectl get deploy」と「kubectl get pods」は空ならOKです。
「kubectl get svc」はデフォルトのkubernetes serviceのみ表示されればOKです。
自作コンテナのPodを立ち上げる
kubectl run my-deploy --image=myacount/myapp:v1 --port 3000
イメージ名やポート番号は自分の用意したコンテナに合わせて変更して下さい。
警告が出ることもありますが問題ありません。
Dockerhubからmyaccount/myapp:v1を取得してPodを起動します。
PodのTCPポート3000番がコンテナの3000番とマップされます。
PodだけでなくdeploymentとreplicaSetも同時に作成されます。
kubectl get deploy kubectl get pods kubectl describe pods
作成したPodを外部に公開する
この時点ではPodはk8sクラスタ内のみからアクセス可能な状態です。serviceを作成して外部からアクセスできるようにします。
本番環境のようにtype="LoadBalancer"を使いたいところですがminikubeではサポートされていないためNodePortで我慢します。
kubectl expose deployment/my-deploy --type="NodePort" --port 3000
ポート番号は自分の用意したコンテナに合わせて変更して下さい。
新しいserviceが作成されます。
kubectl get svc kubectl desribe svc
「kubectl get svc」の「PORT(S)」欄に「3000:32215/TCP」みたいな表示があります。「32215」が外部に公開されているポート番号です。このポート番号はランダムに変わります。
minikubeのこのポート番号にブラウザでアクセスしたいところですが、残念ながらそれはできません。仕方ないのでブラウザ内のターミナルでcurlコマンドを実行して動作確認をします。
curl $(minikube ip):32215
期待したレスポンスがあれば成功です。
スケールする
Podを4台に増やします。
kubectl scale deployment/my-deploy --replicas=4
確認します。
kubectl get rs kubectl get deploy kubectl get pods
減らすこともできます。
kubectl scale deployment/my-deploy --replicas=2
ローリングアップデート
イメージをv1からv2にアップデートします。
kubectl set image deployments/my-deploy my-deploy=myaccount/myapp:v2
v2を使う新しいPodが順次作成され、古いPodは順次削除されます。
kubectl describe pods kubectl rollout status deployments/my-deploy
うまく行けば「kubectl describe pods」で見えるimage名が変わってるはずです。
ロールバックもできます。
kubectl rollout undo deployments/my-deploy
削除
終了する場合は削除しなくても大丈夫です。
やり直したい場合はdeploymentとserviceを削除して下さい。
kubectl delete deploy my-deploy kubectl delete service my-deploy
これでPodやreplicaSetも削除されます。
トラブルシューティング
よくある間違いがkubectl runでイメージ名を間違えることですが、これが気づきにくいです。
kubectl runコマンド自体は正常に終了します。
kubectl get deployで確認してもPodの立ち上げに時間がかかっているようにしか見えません。
kubectl get podsを確認するとコンテナの作成に失敗したことがわかります。
kubectl describe podsでログを確認するとイメージの取得に失敗したことがわかります。
deployを削除してkubectl runを再度実施しましょう。
docker-composeを使ってみる
dockerしか使ったことない人間がdocker-composeを使います。
動機
Flaskアプリケーション作成のTutorialをやったらコンテナ化したくなりました。
コンテナ化したら今度はnginxコンテナやMySQLコンテナと連携させたくなりました。
連携させたら複数のコンテナをビルド/実行/停止/削除する作業があまりに面倒でdocker-composeに行き着きました。
dockerとの比較
dockerは1つのコンテナを扱います。だからコンテナの数だけdockerコマンドを打つ必要があります。私のケースだとnginxコンテナ、Flaskコンテナ、MySQLコンテナに対して合計3回のdockerコマンドを打ちます。1回のビルド/実行/停止/削除には合計12回コマンド投入が必要です。
加えてコマンドのオプションが多いので1つ1つのコマンドが長いです。何回もビルド/実行/停止/削除を繰り返していると疲弊します。
docker-composeは複数のコンテナを束ねて1つのserviceとして扱います。複数のコンテナをdocker-composeコマンドを1つで操作できます。1回のビルド/実行/停止/削除に必要なコマンド投入回数は4です。
またオプションはYAMLファイルに記載するので1つ1つのコマンドが短いです。それでも何回もビルド/実行/停止/削除を繰り返していると疲弊しますがdockerで戦うより遥かに楽です。
Kubernetesとの比較
これまで「コンテナを組み合わせたらKubernetes」と安直に考えていて、docker-composeの立ち位置がわかりませんでした。ですが今回その有難みを痛感しました。
昨今の開発、デプロイでは小さなアプリケーションでも複数コンテナになります。小さなアプリケーションに対してKubernetesは大きすぎます。
Kubernetesは最大の拡張性と可用性をアプリケーションに提供することを念頭に作られたモンスターです。Kubernetesのパワーを必要としないアプリケーションは沢山あります。そんなときにはdocker-composeの方が便利です。
大規模なシステムをdocker-composeで運用することも可能です。しかし高い拡張性と可用性が求められる場面ではKubernetesの方が構築も運用も楽になるでしょう。
docker-compose.yml
YAMLファイルにサービスを定義します。
nginxコンテナ、Flaskコンテナ、MySQLコンテナで構成されるサービスの記述例です。
version: "3" services: app: image: app build: ./app environment: - APP_ENV=docker working_dir: /app command: ["./wait-for-it.sh", "mydb:3306", "--", "python3", "app.py"] proxy: image: proxy build: ./proxy ports: - "8080:80" depends_on: - app db: image: mysql:latest environment: - MYSQL_ROOT_PASSWORD=mysecret - MYSQL_DATABASE=books volumes: - mydb-volume:/var/lib/mysql volumes: mydb-volume: driver: local
- version:docker-compose.ymlの仕様版数。本日時点で3.8が最新。
- services:この中に複数のコンテナを定義する。
- app、proxy、db:サービス名。各コンテナ定義の箱。
- build:指定されたフォルダのDockerfileでビルドする。
- depends_on:起動順序を指定。
残りはdockerの基本的なコマンドがわかればイメージできると思います。
ビルド
docker-compose build
キャッシュを使わない場合は--no-cacheオプションを付けます。キャッシュを使わないでビルドすると過去のイメージがdangling(tagなし)状態になるのでdocker image pruneで掃除しましょう。
実行
docker-compose up -d
ログ確認
docker-compose logs -f
サービス名を指定すればそのサービスのログのみ表示できます。
コンテナに接続
docker-compose exec -it <コンテナ名> /bin/<シェル>
シェルはベースがalpine系ならsh
かash
、buster等debian系ならbash
です。
終了
docker-compose down
コンテナを終了、削除します。networkも削除します。imageとvolumeは残ります。
-vオプションをつけるとvolumeも削除します。
コンテナ間通信
上記のdocker-compose.ymlのようになにもnetworkを指定しないと自動的にbridge networkが作成されて全てのコンテナが接続されます。
各コンテナのサービス名(app、proxy、db)がnetwork-aliasとして自動登録されるので、コンテナ同士はサービス名で通信することができます。network-aliasはdocker container inspectで確認できます。
実験したところコンテナ名でも名前解決できました。よってコンテナ名で通信することもできます。ただしdocker container inspectで確認してもnetwork-aliasとして登録されていません。不思議です。
docker-compose downするとネットワークも削除されます。
コンテナ名
docker-composeを利用するとコンテナ名は自動で<プロジェクト名>_<サービス名>_<連番>になります。
例:docker-compose-study_app_1
環境変数COMPOSE_PROJECT_NAMEを変更するとプロジェクト名部分を変更できます。
名前全体を明示的に変更したい場合はcontainer_nameを使用します。
volume名
同様にvolume名も自動で<プロジェクト名>_<volume名>_<連番>になります。
nameで変更することができます。
name: my-volume
起動順序の管理
順序性は発生しないのがベストですが現実としてはコンテナを起動する順序は重要です。
nginxコンテナとFlaskコンテナの順序性
nginxは起動時にproxy_passで設定されたコンテナ名(proxy_pass https://myapp:5000の「myapp」)が名前解決できるか確認します。その前にFlaskコンテナが起動していないとコンテナ名で名前解決ができずnginxコンテナが終了してしまいます。
FlaskコンテナとMySQLコンテナの順序性
Flaskコンテナは起動するとDBの初期化(db.create_all())を実行します。このときMySQLサービスが利用不可能だとエラーが発生してFlaskコンテナは終了してしまいます。
今回、期待する起動順序
このため今回のサービスは以下の順序で起動させる必要があります。
MySQLコンテナ起動 → MySQLサービスアップ → Flaskコンテナ起動 → nginxコンテナ起動
depends_on
コンテナの起動順序を制御する仕組みとしてdocker-composeにはdepends_onがあります。depends_onは起動を開始する順序を保証してくれます。
下記のようにnginxコンテナをFlaskコンテナにdepends_onすればnginxコンテナをFlaskコンテナより先に起動してくれます。
proxy: <省略> depends_on: - app
nginxコンテナが名前解決を始めた頃にはFlaskコンテナの名前は名前解決可能な状態になっています。これでnginxコンテナとFlaskコンテナの順序性の課題は解決されます。
残念ながらdepends_onは起動が完了する順序は保証してくれません。MySQLコンテナを先に起動してもMySQLのサービスが立ち上がる前にFlaskコンテナがDB初期化処理を開始してしまう可能性は残ります。
実際に試したところやはりMySQLサービスが起動完了する前にFlaskコンテナあDB初期化処理を開始してしまいFlaskコンテナは終了してしまいました。nginxコンテナとFlaskコンテナの順序性の課題はdepeds_onでは解決できません。
MySQLサービスの起動を待つ方法
docker-composeの仕組みでは難しく他のツールを活用します。dockerizeやwait-for-it.shが有名です。ここでは導入が容易なwait-for-it.shを紹介します。
docker-compose.ymlに以下のように記述すればwait-for-it.shがmydbの状態を監視してmydbが利用可能になった後に「python3 app.py」を実行します。
command: ["./wait-for-it.sh", "mydb:3306", "--", "python3", "app.py"]
wait-for-it.shはGitで公開されています。
https://github.com/vishnubob/wait-for-it
wait-for-it.shはbashが必要です。Flaskコンテナのベースがalpine(ash系)だと苦労します。当初、私はalpineベースのFlaskコンテナを利用していましたが、bashをインストールするとpipやapk addの動きがおかしくなったのでslim-busterベースに替えました。alpineベースのコンテナもMySQL対応で肥大化していたのでslim-busterに変えてもコンテナサイズは変わりませんでした。
DockerでMySQLを試してみる
先日、nginxのDockerを試しました。その勢いでデータベースのDockerも試してみます。
まずは一番有名なMySQLからです。MySQLは大昔に触ったことがありますが、Dockerでセットアップするのは始めてです。Docker固有のポイントがあると思うので勉強していきたいと思います。
コンテナ実行
docker run -d --name mydb --env MYSQL_ROOT_PASSWORD=mysecret mysql
MYSQL_ROOT_PASSWORDは必須の環境変数です。Rootのパスワードを指定します。
タグを指定していないのでmyslq:latestが使われます。myslq:latestのDockerfileを覗いてみるとdebian:buster-slimでMySQLのバージョンは8.0だそうです。
コンテナに接続
docker exec -it mydb /bin/bash
Dockerfileを確認したらENTRYPOINTを使っているので「docker run -it mysql /bin/bash」のようにしてシェルを起動することはできません。大人しくdocker execでコンテナに接続します。
ENTRYPOINT ["docker-entrypoint.sh"] CMD ["mysqld"]
MySQLの起動に使われている/usr/local/bin/docker-entrypoint.shはかなり手の混んだシェルスクリプトで私には読めませんでした。
MySQLにログイン
mysql -u root -p
パスワードを要求されたらMYSQL_ROOT_PASSWORDで指定した「mysecret」を入力します。
データベース作成
create database test; show databases; connect test;
当たり前ですがmysqlの中に入ると普通のMySQLと同じです。
テーブル作成
create table books ( id int auto_increment not null primary key, title varchar(64) not null, in_stock boolean not null default 0); show tables; show create table books;
以下のようなテーブルを作成しました。
- idは自動採番
- titleは必須。型は文字列。
- in_stockは真偽値でデフォルトはFalse。真偽値は内部ではtiny_intという数値。
エントリ作成
insert into books (title) values ("Docker 101"); insert into books (title, in_stock) values ("Introduction to MySQL", true); select * from books;
コンテナが消えるとデータも消える
exit docker stop mydb docker start mydb docker exec -it mydb /bin/bash mysql -u root -p <データはまだある>
コンテナの停止、開始ではデータは消えません。
exit docker stop mydb docker rm mydb docker run -d --name mydb --env MYSQL_ROOT_PASSWORD=mysecret mysql docker exec -it mydb /bin/bash mysql -u root -p <データはない>
コンテナを削除、再作成するとデータはありません。データを維持するにはvolumeを使います。
volume
volumeにはnamed volumeとbind mountがあります。bind mountは開発用、named volumeは本番用という気がするのでnamed volumeを使います。
docker volume create mydb-volume docker volume ls docker run -d -v mydb-volume:/var/lib/mysql --name mydb --env MYSQL_ROOT_PASSWORD=mysecret mysql
こうすればコンテナを削除、再作成してもデータは消えません。
終了
docker stop mydb
掃除
docker rm mydb docker rmi mysql:latest docker volume prune
docker volume pruneで未使用なvolumeを一括削除できます。
おまけ:Flaskから利用する
Flask_SQLAlchemy + sqlite3を使うFlaskアプリのTutorialは溢れていますがMySQLを使うFlaskアプリは少ない印象です。そこでFlask_SQLAlchemy + sqlite3構成をFlask_SQLAlchemy + MySQL構成に置き換えてみます。
Flask_SQLAlchemy + sqlite3のFlaskアプリがある前提です。
まずSQLALCHEMY_DATABASE_URIを書き換えます。
SQLALCHEMY_DATABASE_URI = 'mysql://root:mysecret@myapp-db/test'
MySQLではデータベース名に「.db」を付けません。sqlite3と違って「@myapp-db/test.db」とするとエラーになります。
これだけで済むかと思ったらもう少し手間が必要でした。
次にMySQLdbがないと怒られました。所謂「DBAPI」です。調べるとMySQLdbよりPyMySQLの方がよいそうなのでPyMySQLを入れます。
pipenv install PyMySQL
MySQL 8からデフォルトの認証方式が変更になりcryptographyがないと怒られます。入れます。
pipenv install cryptography
データベースがないと怒られます。sqlite3だとdb.create_all()で自動でデータベースが作成されますがMySQLでは予め作成しておく必要があります。
create database test;
後はいつもの初期化のおまじないです。
export FLASK_APP=myapp flask shell from myapp import db db.create_all() exit()
これでテーブルが作成されました。
ここから先はsqlite3と同じコードで動きました。
コードの変更はSQLALCHEMY_DATABASE_URIだけで済みましたがライブラリやDB側のセットアップ作業に少し追加作業が必要でした。
dockerでnginxを試してみる
nginxをよく見かけます。
1度くらい触ってみようと思って数年。ようやく重い腰を上げたいと思います。
とりあえず試して削除したいときに便利なのがdockerです。
dockerを利用してローカルPCで基本機能を試してみました。
コンテナ実行
docker run -it --name nginx -p 8080:80 nginx /bin/bash
ローカルにnginxのイメージがなければDockerhubから自動で取得して実行してくれます。
本来のnginxコンテナの使い方としては末尾の「/bin/bash」は不要ですが、今回は中に入って色々いじってみたいのでこのようにしました。
実行するとコンテナの中に自動で入ります。
タグを指定していないのでnginx:latestが使われます。nginx:latestのベースはdebian:buster-slimですのでコンテナの中ではdebian系のコマンドを使う必要があります。
nginx実行
docker runでコマンドを指定(/bin/bash)したので、デフォルトの「CMD ["nginx", "-g", "daemon off;"]」が上書きされしまい、nginxが起動していません。
手動で起動します。
/etc/init.d/nginx start /etc/init.d/nginx status
確認
ブラウザでhttp://localhost:8080にアクセスしてWelcomeページが表示されることを確認します。
設定確認
基本的な設定を確認します。
more /etc/nginx/nginx.conf
nginx.confのhttpディレクティブに以下の行があります。具体的なWebサイト関連の設定については/etc/nginx/conf.d/配下のconfファイルを参照する設定です。
include /etc/nginx/conf.d/*.conf;
/etc/nginx/conf.d/を見に行くとdefault.confがあります。
default.confを確認すると以下の行があります。/usr/share/nginx/htmlにあるファイルをクライアントに返す設定です。
location / { root /usr/share/nginx/html; index index.html index.htm; }
Welcomeページの変更
/usr/share/nginx/html/index.htmlを編集してWelcomeページを変更しましょう。
buster-slimは軽量なのでエディタがありません。viをインストールします。
apt-get update apt-get install vim
index.htmlを適当に編集します。
vi /usr/share/nginx/html/index.html
ブラウザを再読み込みしてWelcomeページが変更されたことを確認します。
リバースプロキシ
nginxといえばリバースプロキシとして利用されることが多いです。リバースプロキシを試します。
default.confを変更します。localhost:8080/へのアクセスをGoogleに転送する設定です。非現実的な構成ですが手っ取り早いです。
location / { proxy_pass http://www.google.com; }
設定ファイルのValidationをしてからnginxを再起動します。
/etc/init.d/nginx configtest /etc/init.d/nginx restart
ブラウザを再読み込みしてGoogleが表示されることを確認します。
静的ファイル配信
APIコールはWebアプリケーションに転送して、静的ファイルはnginxが配信するというのもよく見る構成です。
TCP 5000番で待っている適当なWebアプリケーションをローカルPCで立ち上げます。
default.confを以下のように変更してnginxを再起動します。
location / { root /usr/share/nginx/html; index index.html index.htm; } location /api { proxy_pass http://host.docker.internal:5000; }
localhost:8080にアクセスするとnginxが静的ファイルを返します。html、js、cssファイルやfavconなどを返す想定です。
localhost:8080/api配下へのAPIコールはTCP 5000番で待っているアプリケーションに転送されます。
「host.docker.internal」はdockerを実行しているローカルPCを表しています。Docker for Mac固有のようです。これでローカルPCの5000番で待機しているアプリケーションに転送されます。誤って「localhost」と記載するとコンテナ自身を表すのでnginxコンテナの5000番に転送してエラーになります。
「proxy_pass http://host.docker.internal:5000」の後ろに/をつけると厄介なことになります。このリンクに説明があります。
https://www.xmisao.com/2014/05/09/nginx-proxy-pass.html
アプリケーションもコンテナの場合
本番環境ではアプリケーションもコンテナ化してDockerで起動することでしょう。その場合はコンテナ同士はコンテナ名やnetwork-aliasで通信できます。
よって本番ではproxy_passディレクティブは以下のようにアプリケーションコンテナの名前(myapp等)にします。アプリケーションは5000番を外部にEXPOSEする必要はありません。
location /api { proxy_pass http://myapp:5000; }
コンテナの起動順序
nginxは起動時にupstream(上の例のmyapp)の名前解決を行っているようです。名前解決できないと「host not found in upstream 'myapp'」のようなエラーを出して起動に失敗します。
これはdocker-composeを利用するときに注意が必要です。myappコンテナの起動がmydbコンテナの起動待ちで遅れるとnginxが起動できなくなります。depends_onなどで起動順序を制御しましょう。
try_filesディレクティブ
try_filesディレクティブを使う場合はこうなります。
location / { root /usr/share/nginx/html; try_files $uri $uri/ @webapp; } location @webapp { proxy_pass http://host.docker.internal:5000; }
私が用意したアプリケーションは静的ファイルを持っていません。try_filesだとアプリケーションに不要な静的ファイル要求が多く飛ぶ気がするのでやめました。
プロキシヘッダの付与
アプリケーションのアクセスログを見ると要求を送信したクライアントではなくnginxのIPアドレスからアクセスがあったように見えます。nginxがパケットの送信元アドレスを書き換えてしまうからです。
本当に見たいのは要求を送信したクライアントのアドレスです。default.confを以下のように変更してnginxを再起動します。
location /api { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://host.docker.internal:5000; }
nginxはパケットを書き換えるときにX-Forwarded-ForヘッダにクライアントPCのアドレスを追記します。多くのアプリケーションはX-Forwarded-Forヘッダがある場合はアクセスログにX-Forwarded-Forヘッダの値を出力します。これでアクセスログにクライアントのアドレスが出力されるようになります。
Macではまった
上記の設定でDocker for Mac(Docker version 20.10.0, build 7287ab3)ではうまく動いてくれずdocker bridgeのアドレスが出力されました。悩んだ末、Centosで試したら期待通りに動きました。バージョン20.10.0の不具合かDocker for Mac固有の問題かはわかりません。
bridgeがだめなら、と試したhost networkが動かなかったり、Mac上でのDockerは落とし穴が多いです。
その他のヘッダ情報の転送
アドレスだけでなくホスト名やスキームなど諸々の情報もアプリケーションへ転送したい場合は下記のようにします。
location /api { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://host.docker.internal:5000; }
Basic認証
認証をかけてみましょう。
さくっと試せるBasic認証を使います。
htpasswdコマンドを利用するためにパッケージを追加します。
apt-get install apache2-utils
ログインIDとパスワードを登録します。
htpasswd -c /etc/nginx/.htpasswd hoge New password: hoge Re-type new password:hoge Adding password for user hoge
default.confのlocation /とlocation /apiに下記の2行を追加してngnxを再起動します。
auth_basic "Login Required."; auth_basic_user_file /etc/nginx/.htpasswd;
ブラウザでlocalhost:8080/にアクセスするとログイン画面が表示されるようになります。ログインするとコンテンツが表示されます。
Postmanを使って認証なしでlocalhost:8080/apiをGETすると「401 Authorization Required」が返ってきます。Basic認証付きでGETすると値を取得できました。
HTTPS
長くなるので別の記事として投稿します。
ログ
default.conf内の下記行のコメントアウトを削除してnginxを再起動します。
access_log /var/log/nginx/host.access.log main;
/var/log/nginx/host.access.logにアクセス情報が記録されます。
終了
exitするとnginxコンテナも終了します。
exit
掃除
終了したnginxコンテナを削除します。
docker rm nginx
nginxのDockerイメージを削除します。後でまた試したいなら残しておいてもいいです。
docker rmi nginx:latest
DockerhubへのイメージのPush
Dockerhubにアカウントを作成すると自分でBuidしたDockerイメージをリポジトリにpushすることができます。リポジトリのイメージはdocker pullで取得することができます。
Dockerhubは利用するけどアカウントは持っていない、という人は少なくないと思います。誰かが作った便利なイメージを取得するためだけにDockerhubを利用している人も多いのではないでしょうか。
それもよいのですが、リポジトリを持つことにもメリットがあります。まず自分のコンテナを世界中の様々な環境に瞬時に展開できるようになります。データセンタでもAWSでも同僚のPCでも一瞬です。大変、便利です。またコンテナ全盛の昨今、コンテナを公開するメリットや作成上の注意点を実感する意味でもよい勉強になります。
今回はDockerイメージをDockerhubにPushする方法を紹介します。
1. Dockerイメージを作成する
docker image build -t <イメージ名>:<タグ> . docker image ls
タグ名はlatestとすることが多いです。
2. 無駄がないか確認(任意)
docker image history <イメージ名>
イメージの各レイヤの情報が表示されます。どのレイヤがどの程度、ディスクを消費するかも確認できます。無駄なレイヤ、順序性が悪い箇所があればDockerfileを修正して再度、ビルドします。
3. スキャン(任意)
docker scan <イメージ名>
脆弱性の有無を確認します。
イメージをDockerhubにアップするということはそのイメージは他人に見られたり、利用されたりする可能性があります。作成者として安全性を確認しましょう。
4. イメージにDockerhub用のタグを付与する
docker image tag <image id> <アカウント名>/<リポジトリ名>:<タグ名>
このタグ名はlatestとすることが多いです。
5. Dockerhubのアカウントへログインする
docker login
6. Pushする
docker image push <アカウント名>/<リポジトリ名>:<タグ名>
docker searchに反映されなくても心配はいりません。問題なくdocker pullできます。
TDDで役立つIntelliJ IDEA機能
cleancodersの動画を見ていて気がついた、TDDに便利なIntelliJ IDEAの機能を5つご紹介します。
1. Live Template
TDDでは沢山のテストメソッドを書きます。毎回、「public void test ...」なんてタイプするのは面倒です。
そんなときに便利なのがLive Templateです。
例えば「test」と入力して「tab」を押します。
するとテストメソッドの雛形に変換されます。
「test」はデフォルトでは登録されていないので自分でIDEに登録する必要があります。
Preferences / Editor / Live Templates / otherで「+」ボタンを押します。下の画像を参考に必要な情報を入力してください。個人的にハマったのは一番下の「Applicable in」を選択し忘れたことです。
2. Show Context Actions
TDDでは存在しないクラスやメソッドをテストクラスで利用します。
例えば下のテストメソッドの例ではMyClassクラスがないのでIDEが赤字で警告を発しています。
MyClassを作成してMyClass#executeを実装する必要があります。このとき便利なのがShow Context Actionsです。
警告が出ているMyClassにカーソルを合わせてoption + Enterと押すとエラーを解消するための選択肢が表示されます。「Create class 'MyClass'」を選べば簡単にMyClassクラスが作成できます。
同様に、executeメソッドにカーソルを合わせてoption + Enterから「Create method 'execute' in 'MyClass'」を選択すればMyClassクラスにexecuteメソッドのひな形を作ってくれます。
勿論、普通にMyClass.javaファイルを作成してMyClassクラスとexecuteメソッドを書くこともできます。でも書いたテストメソッドから直接、option + Enterで次の作業に移動する方がTDDとしてリズムがよいと感じます。
3. テストの再実行
TDDでは頻繁にテストを実行します。その度にマウス操作していてはリズムが悪くなります。
Ctrl + rで前回実行したテストを再実行できます。
4. Introduce Field
複数のテストを記述していく中でテスト間で重複処理が発生します。こうした部分はsetUpなどに移して重複を排除します。
TDDでは先にRefactorしてはいけないので重複が発生するとわかっていても最初からsetUpを書くことはできません。このため発生した重複をsetUpへ移す作業は頻繁に発生します。
変数をフィールドにしてsetUpに移すときに便利なのが「Introduce Field」。option + Command + Fです。
右クリック、Refactorからもできますが頻繁に発生するのでショートカットの方が断然、効率がよいです。
5. Run with Coverage
ある程度、実装が進むと気になるのがCoverageです。テストクラスを右クリックして「Run <テストクラス> with Coverage」を選択することで簡単に確認することができます。
テストが完了するとCoverageが以下のようにクラス名の隣に表示されます。StackクラスはInterfaceなのでCoverageが表示されません。BoundStackクラスのCoverageは低いですね。
カバーされなかったコードを確認してみましょう。エディタでBoundStackクラスを開くとカバーされた行は緑、カバーされなかった行は赤が表示されます。
リファクタリングで作成したNull Objectのテストに漏れがあったことがわかりました。