- 投稿日:2021-02-28T23:46:59+09:00
Pythonからテスト用コンテナを手軽に作成する方法(testcontainers-python)
概要
pythonコード内でMySQLやPostgres、Seleniumなどのテストコンテナを作成することができる、testcontainersというライブラリがあるらしいというので試してみた結果です。
最初はDocker環境もいらないとかまさかないよね、と思いつつちょっと期待して試しましたが、予想通り、単にどこかにある既存のDockerホストに対して命令するだけだったのですが、それでも手動で上げ下げするよりは楽だと思われます。対応範囲も広く、以下に対応しています。
- MySQL
- MariaDB
- PostgreSQL
- ElasticSearch
- Oracle DB
- MongoDBMongoはなぜかReadmeに記載ないですが、以下のように対応しています。
https://testcontainers-python.readthedocs.io/en/latest/database.html#testcontainers.mongodb.MongoDbContainer前提
自分のPCにDocker Toolboxを使ってコンテナ実行環境を作成しています。WindowsなのでDocker Desktop for Windowsを使いたいところですが、Hyper-Vを有効化できないため、インストールできません。またWindowsのバージョンも縛りがあり、WSL2も不可。
そこで少し古い方法ですが、Docker Toolboxというツールを使用しています。
https://docs.docker.com/toolbox/toolbox_install_windows/Oracle Virtualboxという仮想化ソフトをHypervisorとして使用し、そのうえでLinuxを動かし、その上でDockerを起動する形になる。そのため、前提としてVirtualBoxをインストールする必要がある。(Toolboxをインストールする際に一緒にインストールすることもできる)
Host: Windows10(1803)
VirtualBox:6.0.12
Docker: 19.03.1またDocker-Toolbox自体は起動された状態にしておく必要がある。(testcontainersからDocker-Toolboxの起動はできない。たぶん)
ちなみにコンテナ起動環境は、Windowsの場合、DOCKER_HOSTという環境変数を元に探しているとのこと。
echo %DOCKER_HOST% tcp://192.168.99.100:2376
インストール
必要モジュールのインストールを実施。今回つかう範囲で必要だった上2つは追加で入れています。
pip install PyMySQL pip install sqlalchemy pip install testcontainers
公式にあるrequirementsファイルをpythonのバージョンに合わせて使用してもよいのですが、分量が多すぎたので個別にいれています。
https://github.com/testcontainers/testcontainers-python/tree/master/requirementsテスト
公式のテストコードそのままですが、以下のコードを記載したファイルを作成し、実行します。
app.pyimport sqlalchemy from testcontainers.mysql import MySqlContainer with MySqlContainer('mysql:5.7.17') as mysql: engine = sqlalchemy.create_engine(mysql.get_connection_url()) version, = engine.execute("select version()").fetchone() print(version) # 5.7.17イメージがない場合はpullしてきて起動してくれます。新規にコンテナを起動した場合は、コード実行後にコンテナも削除するようです。
テスト(MongoDBバージョン)
前提としてPyMongoをインストールしておきます。
pip install pymongo
以下のコードを実行。MySQL同様にバージョンを返しています。
app.pyfrom testcontainers.mongodb import MongoDbContainer with MongoDbContainer("mongo:4.4.4") as mongo: db = mongo.get_connection_client() version = db.server_info()['version'] print(version) # 4.4.4感想
今までは開発環境の操作とコーディング、テストは別々に行っていましたが、これをうまく使えば統合できそうだなと。ただまだそこまで至っていないので、まずはテストをしっかり書くところから取組みたいと思います。
- 投稿日:2021-02-28T22:21:12+09:00
fitbit senseの心拍データをリアルタイムで取得する
アプリケーション例: 心拍数に応じて強さを調整する五目並べAI
モンテカルロ木探索による五目並べAIにて紹介した五目並べAIの強さを、心拍数に応じて調整する。具体的には、心拍数が上がるとAIは弱くなり、逆に心拍数が下がるとAIは強くなる。目的
- fitbitデバイスを用いて、リアルタイムに心拍データを取得する
- ただし後述する「現時点での制限」のように、実行時におけるいくつかの制限がある。
- 主な使用用途
- 心拍データを用いたゲームのプロトタイプ作成
- 心理学的実験のための簡易心拍取得装置
環境
- 心拍の取得(身に付けるもの)
- fitbit sense (他のfitbitでもたぶん可)
- スマートフォンのfitbit app (ここで試したのはAndroid)
- 開発、実行の制御(開発するもの。また現時点では実行時にも必要なもの)
- windows 10 home (開発だけでなく実行の制御も現時点ではしている)
- docker (websocket serverの構築のために使用)
- websocket server(心拍データを受け取り、配信するもの)
- Google Cloud Platform (この中のContainer Registryでdocker imageを管理し、Cloud Runでそのimageを実行している)
構成
- 図の黒い矢印は開発時の関係、黄色の矢印は実行時のデータ経路を示す。
- fitbitのデバイスから直接、サーバ等に心拍データを送信することができないため、スマートフォンのfitbit appにデプロイしたcompanion(fitbit companion apiを用いて開発したfitbit appの中で動くもの)経由でサーバにデータを送信する。
- サーバへの接続方法は、http get/postとwebsocketが用意されている。ここでは通信効率の良いと思われるwebsocketを使用する。
- websocketのサーバには、googleのcloud runを使用する。cloud runは、dockerのイメージをそのまま実行できる環境である。dockerとは、OSを含めて実行環境をひとまとめにパックできるようなもので、ローカルPCのOSに依存せずにサーバの開発などができる。今回は、debianディストリビューションのlinuxをOSとしたpythonの環境をdocker hubのpython imagesから参照して、そこにpython上でASGI(asynchronous server gateway interface): 非同期サーバゲートウェイインターフェースを構築するためのuvicornによってwebsocketサーバを構築する。
現時点での制限
- 約2分間の実行後にfitbitデバイスのアプリが終了する。現時点では、fitbitデバイスをタップしてスリープさせないようにする必要がある。fitbit device apiのappTimeoutEnabledをfalseにしてアプリのタイムアウトを無効化しても、なぜか約2分でアプリは落ちる。fitbitデバイスの画面設定で「常に画面をON」をつけると、少し実行できる時間は伸びて、一度試してみたところ、3分半ぐらいになった。
- Websocketサーバとして使用するGoogle Cloud Runは、デフォルトで5分間で接続が切れる。Cloud Runのマニュアルには、非アクティブが5分続くとタイムアウトするとの説明があるが、Cloud Runのwebsocketでの使用に関するマニュアルにも示されているように、Websocketを通信し続けていても、5分で接続は切れる。Cloud Runの設定でタイムアウトを15分までなら伸ばすことができるとの説明があるが試してはいない。クライアントでwebsocketの接続断イベントを取得し、再接続を行うことによって疑似的な永続的接続を構築することは出来るが、再接続までの受信データをロストするなどの問題はある。
- スマホのfitbit appを立ち上げてスリープさせない方が良い。スリープ状態になると、websocketの通信が途絶えることがある。
fitbitデバイスとスマートフォンで動作するfitbit app内のアプリの開発
fitbit studioによるアプリ開発
fitbit studioにアクセスし、fitbitアカウントでログインする。
Starterテンプレートを用いて新規プロジェクトを作成する。
左側のappはfitbitデバイスで動作するもの、companionはスマートフォンのfitbit appの中で動作するもの。resourcesはスタイル等の設定である。
左側で app / index.js を選択して、以下のコードを入力する。index.js/* fitbitデバイスで動作する。 */ import * as messaging from "messaging"; import { HeartRateSensor } from "heart-rate"; import { me } from "appbit"; // fitbit appとの通信を開始する。 messaging.peerSocket.addEventListener("open", (evt) => { start_heartRateSensor(); }); messaging.peerSocket.addEventListener("error", (err) => { console.error(`Connection error: ${err.code} - ${err.message}`); }); // fitbit appと接続後に、心拍データの取得を行う。 function start_heartRateSensor(){ const hrm = new HeartRateSensor({ frequency: 1, batch: 1 }); hrm.addEventListener("reading", () => { sendMessage({ heartRate: hrm.heartRate ? hrm.heartRate : 0 }); }); hrm.start(); //なぜか効かない。 //me.appTimeoutEnabled = false; } // fitbit appに心拍データを送信する。 function sendMessage(data) { if (messaging.peerSocket.readyState === messaging.peerSocket.OPEN) { messaging.peerSocket.send(data); } }importされているmessagingは、スマートフォン fitbit appとの通信を行う、HeartRateSensorは心拍の取得、meはこのアプリのタイムアウトの設定を行うためのものである。ただし、meを用いたタイムアウトの設定はなぜか効果がないため、現在はコメントアウトして使用していない。
HeartRateSensorの生成時に frequencyで心拍取得の間隔を指定している。現在は1秒間隔で心拍情報を取得する。心拍情報とは、1分間に波打つ回数である。
index.js/* スマートフォンのfitbit app内で動作する。 */ import * as messaging from "messaging"; // websocketの接続先 const wsUri = "wss://_______________________________________/xx"; const websocket = new WebSocket(wsUri); // それぞれのリスナを設定する。 websocket.addEventListener("open", onOpen); websocket.addEventListener("close", onClose); websocket.addEventListener("message", onMessage); websocket.addEventListener("error", onError); function onOpen(evt) { console.log("CONNECTED"); websocket.send("connected") } function onClose(evt) { console.log("DISCONNECTED"); } function onMessage(evt) { console.log(`from websocket server: ${evt.data}`); } function onError(evt) { console.error(`ERROR: ${evt.data}`); } // デバイスからのデータを受信したときに、同データをwebsocketサーバに送信する。 messaging.peerSocket.addEventListener("message", (evt) => { if(websocket.readyState===WebSocket.OPEN){ const data = JSON.stringify(evt.data); websocket.send(data); } });import の messagingは、fitbitデバイスのアプリと通信を行う。wsUriにwebsocketのサーバのアドレスとエンドポイントを設定する。デバイスからの心拍情報を取得したら、そのままwebsocketでサーバに送信するというプログラムになっている。
最後に、package.jsonにて Heart RateとInternetのRequested Permissionsのチェックを付ける。アプリのデプロイと実行確認
スマートフォンでの設定
スマートフォンのfitbitアプリを起動して、左上のアカウントをタップする。fitbitデバイスでの設定
fitbit studioでの設定
fitbitデバイス、スマートフォンのfitbit appと接続するために、Select a phone Select a deviceメニューを開く。Phones Devicesが見えていなければ、Refreshを選択し、表示されるそれぞれのデバイスをクリックして接続する。
接続すると、それぞれのデバイス名の横に Connectedと表示される。実行確認
fitbit studioの Runボタンを押すと、コンソールに心拍情報が表示される。ただし、現時点での制限で述べたように約2分間で、アプリは終了する。その時には、もう一度 Runボタンを押す必要がある。
またfitbitデバイスには、fitbit studioのresourcesで設定したデフォルトのHello World!が表示されている。一度、fitbit studioでアプリをデプロイした後には、fitbitデバイスからアプリを起動することができる。この際に、スマートフォンのfitbit appは、起動しておいた方が良い。
fitbitデバイスで横にスクロールさせていくと、最後にデプロイしたアプリが表示される。
このアプリをタップすると、起動する。この時に、自動的にスマートフォンのfitbit app内の開発したアプリも起動する。dockerによるwebsocketサーバの構築
windows 10 homeへの dockerのインストール
Docker DesktopからDownload for Windowsをクリックしてダウンロード、インストールを開始する。インストールオプションは、基本デフォルトで行う。
インストール終了後にWSL 2のインストールができていないとのメッセージが表示されるので、そのメッセージにあるURLをクリックする。
表示されるサイトのx64マシン用 WSL2 Linuxカーネル更新プログラム パッケージをクリックし、インストールする。
Dockerが立ち上がるとチュートリアルの開始となるが、とりあえずスキップして問題はない。
インストールが完了する。左側のContainers / Appsは、それぞれのコンピューティング環境、Imagesはその環境を構築するための元(昔のインストールCDのようなもの)である。websocketサーバの構築
適当なフォルダを作成して、次に示す3つのファイルを作成する。
DockerfileFROM python:3.8-alpine WORKDIR /usr/src/app COPY requirements.txt ./ RUN apk add --no-cache build-base \ && pip install --no-cache-dir --trusted-host pypi.python.org -r requirements.txt \ && apk del build-base COPY . . CMD [ "uvicorn", "simple_main:app", "--reload", "--host", "0.0.0.0", "--port", "8080" ]Dockerによるコンピューティング環境構築用のDockerファイルであり、FROM python:3.8-alpineという指定によってdocker hubのpythonの上に構築している。このpythonは、DebianディストリビューションのlinuxをOSとし、pythonをインストールしてくれる。タグと呼ばれる3.8-alpineは、pythonのバージョンを3.8とし、alpineというタグ付けされたpythonの最小構成を作るものである。
最後のCMDで指定された uvicornの実行は、ポート番号を 8080 にする必要がある。これは、このwebsocketサーバを最終的にはgoogle cloud runで実行するために必要なことであり、cloud runはデフォルトで8080ポートを使用するために、これに合わせるものである。cloud runは、外向きのインターフェースは、https / wss の433であるが、そこに到達したデータは、内部の8080に転送されるという仕組みになっている。よって開発するサーバは、8080ポートで待っている必要がある。requirements.txtuvicorn[standard] fastapirequirements.txtファイルはDockerファイルから参照されて、必要なpythonコンポーネントを記述しておく。
simple_main.pyfrom fastapi import FastAPI from starlette.websockets import WebSocket, WebSocketDisconnect # fast app app = FastAPI() # client websockets list clients = [] # notify to all clients async def notify(msg:str): for websocket in clients: await websocket.send_text(msg) # notification generator async def notification_generator(): while True: msg = yield await notify(msg) notification = notification_generator() # websocket endpoint @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): await websocket.accept() clients.append(websocket) try: while True: data = await websocket.receive_text() await notification.asend(data) except WebSocketDisconnect: clients.remove(websocket) # startup (preparing notification generator) @app.on_event("startup") async def startup(): await notification.asend(None)Websocketサーバのプログラムで、fastapiと呼ばれるpythonのwebフレームワーク、uvicornというASGI(Asynchronous Server Gateway Interface) :非同期サーバーゲートウェイインターフェイスのインプリメンテーションを使用する。
Websocketの接続先であるエンドポイントは、 @app.websocket("/ws")で指定した /ws とし、そこに接続されるwebsocketをすべて保存し、あるwebsocketから送られてきたデータは、その送信元websocketも含めて、全ての接続先websocketへ配信するようになっている。docker build
Dockerfileのあるフォルダで、次のコマンドを実行してdocker imageを作成する。
docker build -t websocket_server .-tオプションのwebsocket_serverは作成するdocker imageの名前である。
ビルドが成功すると、Docket画面のimagesに、websocket_serverというイメージが作成されている。実行確認
次のコマンドを実行すると、websocket_server イメージからコンテナが作成されてコンピューティング環境が実行される。
docker run -p 80:8080 websocket_serverオプションの -p は、dockerの外であるwindowsPCからdockerの中にあるアプリに接続するためにポートを転送する設定であり、この場合は、windows pcの80ポートを、dockerの8080ポートに転送するとなる。
websocket_client_test.html<!DOCTYPE html> <!-------------------------- websocket client html ・フォームからサーバにデータを送信できる。 ・サーバから受信したデータを表示する。 --> <html> <head> <title>My Heart Rate</title> </head> <body> <label for="test_form">テストデータの送信</label> <form action="" onsubmit="sendMessage(event)" id="test_form"> <input type="text" id="messageText" autocomplete="off"/> <button>Send</button> </form> <ul id='messages'> </ul> <script> //const ws = new WebSocket("wss://_______________________________________/xx"); const ws = new WebSocket("ws://localhost/ws"); ws.onmessage = function(event) { const messages = document.getElementById('messages') const message = document.createElement('li') const content = document.createTextNode(event.data) message.appendChild(content) messages.appendChild(message) }; function sendMessage(event) { const input = document.getElementById("messageText") ws.send(input.value) event.preventDefault() } </script> </body> </html>localhostのwsに接続するhtml, javascriptを作成し、ブラウザで表示する。
サーバに送信したデータがループバックで戻ってきて、リスト表示される。同時に複数のブラウザを開けば、1つのブラウザから送信したデータは、全てのブラウザに戻ってくることが確認できる。
この時に、dockerのコンソールには、このような感じで表示されている。Google Cloud Runによるwebsocketサーバの公開
Google Cloud Platformでプロジェクトの作成
Google Cloud Platformのプロジェクト選択から新規プロジェクトを作成する。もちろん、既にあるプロジェクトに新たなサービスを追加する場合は、その既存プロジェクトを選択してもよい。おそらく課金の設定についてもする必要がある。
Container Registryの設定
直接、Cloud Runを操作してdocker imageからアプリを起動することも可能であるが、ここでは、docker imageのcontainer registryへのアップロード、そしてcontainer registryにあるimageからcloud runのアプリを生成、と段階を踏んで行っていくこととする。
docker tag [docker image id] gcr.io/[google cloud project id]/docker image name]dockerコマンドによって、先に作成したdocker imageにタグ付けを行う。image id, project id, image nameの確認方法は、次に示す。またgcr.ioは、とりあえずcloud runのデプロイ先をus-central1 (アイオア)にすることを前提としている。
google cloud project idの確認は、google cloud platformのプロジェクトの選択等で行うことができる。
docketのイメージ名、image idの確認は、docker画面の左側のimagesタブから行う。
docker imageへのタグ付けが成功すると、docker画面ではこのように表示されている。docker push gcr.io/[google cloud project id]/websocket_serverタグ付けされたimageをdocker pushでgoogleにアップしようとすると、以下のように権限が無いとのエラーが表示される。
次に、権限付与のためにGoogle Cloudの認証方法のサイトの説明に従ってまず、Google Cloud SDKのインストールを行う。
Cloud SDKのドキュメントに接続し、Google Cloud SDKのインストールを選択する。
windowsのタブを選択し、Cloud SDKをダウンロードしてインストールする。インストールは基本的にデフォルト設定のままで行う。
インストール後に、Start Google Cloud SDK ShellとRungcloud init
to configure the Cloud SDKのチェックを付けてFinishボタンを押す。
コンソールでログインを要求されるので、 y を入力して enterを押す。
ブラウザが表示されて許可を求めるためにGoogleアカウントの選択が表示される。使用するアカウントを選択する。
コンソールでは、プロジェクトを選択するところで止まっているので、先に作成したプロジェクトを選択する。次に、container registryにアップロードをするdockerにgoogleの権限を与えるために、gcloud 認証情報ヘルパーを参考に設定を進めていく。
gcloud auth logingcloudにログインするために、上記のコマンドを実行する。表示されるブラウザでは、先ほどと同様にgoogleアカウントを選択して、許可を押す。
gcloud auth configure-dockergcloudコマンドで、dockerを構成する。
docker push gcr.io/[google cloud project id]/[docker image name]あらためてdockerコマンドでdocker imageをcontent registryにアップロードする。
docker pushコマンドの結果として、このような感じで表示される。
Google Cloud Platformのcontainer regstryで確認すると、websocket_serverという名前のimageが追加されている。Cloud Runの設定
gcloud run deploy websocket-server --image gcr.io/[google cloud project id]/websocket_servergcloudコマンドによって、container regstryにアップしたdocker imageからサービスを構成する。deploy websocket-serverのwebsocket-serverはcloud run上のサービス名となる。このサービス名では _ (アンダースコア)が使用できないため、- (ハイフン) としている。
target platformは[1] Cloud Run (fully managed)を、projectを有効化するかには y を回答する。
リジョンには[18] us-central1を、unauthenticated invocationsには y を選択する。
google cloud platformのcloud runで確認すると、websocket-serverという新しいサービスが登録されている。この時点で既に動作している。URLがサービスのURLであるため、動作確認のために控えておく。実行確認
websocketの送受信
先に示したwebsocketクライアント用のhtmlファイルの接続先urlを、cloud runのサービスurlに変更し、プロトコルもwsからwssに変えてブラウザで表示する。
送信したデータが、全てのクライアントに返ってくることを確認する。また、クラウド上のwebsocketサーバなので、例えば、スマートフォンから参照しても同様の結果を得ることができる。心拍データの送受信
fitbit studioのcompanion / index.js の wsUriを cloud runのサービスURLに変更する。
fitbit studioでfitbitデバイス、fitbit appのcompanionを起動すると、websocketサーバ経由でリアルタイムに心拍データを取得することができる。Cloud Runのタイムアウト対策
次のは、websocketが5分でタイムアウトしたときにクライアント側から接続し直すjavascriptである。ついでに、受信時にリストを下に増やすのではなく、上に追加する形で最新が常に一番上になるように変更している。
websocket_client.html<!DOCTYPE html> <!-------------------------- websocket client html ・フォームからサーバにデータを送信できる。 ・サーバから受信したデータを表示する。 --> <html> <head> <title>My Heart Rate</title> </head> <body> <label for="test_form">テストデータの送信</label> <form action="" onsubmit="sendTextInput(event)" id="test_form"> <input type="text" id="messageText" autocomplete="off" /> <button>Send</button> </form> <ul id='messages'> </ul> <script> const url = "wss://_______________________________________/xx"; //const url = "ws://localhost/ws"; function sendTextInput(event) { const input = document.getElementById("messageText"); sock.send(input.value); event.preventDefault(); } const sock = new function () { const self = this; let ws = undefined; self.init = () => { if (ws && ws.readyState === WebSocket.OPEN) { ws.close(); } ws = new WebSocket(url); ws.onmessage = (event) => { const messages = document.getElementById('messages'); const message = document.createElement('li'); const content = document.createTextNode(event.data + " [" + (new Date()) + "]"); message.appendChild(content); messages.prepend(message); }; ws.onclose = (event) => { console.log("closed and re-init"); self.init(); }; } self.send = (msg) => { if (ws.readyState === WebSocket.OPEN) { ws.send(msg); } else { console.log("cannot send " + msg + " [" + ws.readyState + "]"); } }; self.init(); } </script> </body> </html>sock変数では、wsという名前のwebsocketのインスタンスを保持し、それをサーバに接続するinitという名称のメソッドを有する。そのinitメソッドは、websocketが接続断されたときのイベント onclose の中で再度呼び出される。
参考にさせて頂いたサイト
Windows 10 HomeへのDocker Desktop (ver 3.0.0) インストールが何事もなく簡単にできるようになっていた (2020.12時点)
- 投稿日:2021-02-28T20:16:40+09:00
DockerでPHPUnitを少し使ってみる(Composer使用)
はじめに
Dockerをある程度使える前提で書いていきます。
『DockerでPHPUnitを少しさわってPHPのテスト少し試してみたい!』方向けです。
よろしくお願いします!
★テスト記述する際のお決まり事項 ・Class という名前のクラスのテストは、ClassTest という名前のクラスに記述 ・ClassTest は、(ほとんどの場合) PHPUnit\Framework\TestCase ・テストは、test* という名前のパブリックメソッドとなります。(なのでtestSample()やtestA()という書き方になる) ・テストメソッドの中で assertSame() のようなアサーションメソッド (アサーション を参照ください) を使用して、期待される値と実際の値が等しいことを確かめます。 ★ユニットテストを書くそもそもの目的 バグの発見と修正や コードのリファクタリングを開発者がやりやすくすること。 ソフトウェアのドキュメントとしての役割を果たすこと。 私的には同じバグを繰り返さないために書くものという感じで理解しています。 ★どこをテストすべきか ひとつのユニットテストがカバーするのは、 通常はひとつの関数やメソッド内の特定のルートだけとなる 上記内容はほぼ以下URLからの参考です。 https://phpunit.readthedocs.io/ja/latest/writing-tests-for-phpunit.html様々なチェックメソッドは以下
https://phpunit.readthedocs.io/ja/latest/assertions.html#appendixes-assertionsでは早速
環境作成
以下のようにファイルを作成して、『docker-compose.yml』ある場所で
docker-compose up --build -d
を叩きます。
そうすると、<<ipアドレス>>:81以下のように表示されています。
※ポート(81の部分)は同じですがIpは各環境で違います。ちなみに私のipは$ docker-machine ip default
で調べられます。意味は『defaultとして作成したドッカーマシンのip』全体の構成参考画像
index.php<?php require_once 'sample.php'; $sample = new sample\Sample(); echo $sample->a();sample.php<?php namespace sample; class Sample { public function a() { return 'あ'; } public function i() { return 'い'; } }SampleTest.php<?php use PHPUnit\Framework\TestCase; require_once 'sample.php'; class SampleTest extends TestCase { public function testSample() { $sample = new sample\Sample(); //同じ値、同じ型でない場合エラー $this->assertSame($sample->a(), 'あ'); } }docker-compose.ymlversion: '3' services: apach: container_name: sample-apache #作成されていると出来ないので新しく作成する場合ここの名前を変更 build: ./ #Dockerfileを使用してコンテナ作成 ports: - 81:80 volumes: - ./html:/var/www/html # コンテナ var/www/html の中に/htmlの中身を入れるFrom php:7.4-apache RUN apt-get update \ && apt-get install -y \ zip \ unzip WORKDIR /var/www/html
※以下に最終完成バージョンも含めてあげているので、もしよかったら参考にしていただければと思います。
コンテナに入ってもう一仕事する
以下コマンド今使用しているコンテナIDの確認を行う
$ docker ps
コンテナ内に以下コマンドで入る、 ※『1999d283b4f9』は上記で出力したコンテナIDを使用してください
$ docker exec -i -t 1999d283b4f9 bash
コンテナ内で以下コマンドうちcomposerのインストール
$ curl -sS https://getcomposer.org/installer | php
composerのインストールが無事行えたら以下コマンドで『PHPUnit』のインストール
$ php composer.phar require --dev phpunit/phpunit
参考:https://phpunit.readthedocs.io/ja/latest/installation.html全ての準備は出来た!テストコードは書いてあるので、以下コマンドをコンテナ内で実行しテスト実行してみる
$ vendor/bin/phpunit SampleTest.php
以下のようになればテスト行えたことが分かる
テスト実行結果参考以下URL
https://phpunit.readthedocs.io/ja/latest/textui.html
表示 意味 . テストが成功した際に表示されます。 F アサーション失敗 E テストメソッドで何かしら失敗 I テストがふかんぜんまたは未実装 最後に
以上終わりです!
書き足すことあるかもですが最初に書いたものは削除しない予定です!参考
- 投稿日:2021-02-28T20:16:40+09:00
DockerでPHPUnitを少しさわってみる
はじめに
Dockerをある程度使える前提で書いていきます。
これはあくまで、
『DockerでPHPUnitを少しさわってPHPのテスト少し試してみたい!』方向けです。よろしくお願いします!
後、あ、これも!これも書いておきたい!っと思ったら書き足していく予定です!
では早速
環境作成
以下のようにファイルを作成して、『docker-compose.yml』ある場所で
docker-compose up --build -d
を叩きます。
そうすると、<<ipアドレス>>:81以下のように表示されています。
※ポート(81の部分)は同じですがIpは各環境で違います。ちなみに私のipは$ docker-machine ip default
で調べられます。意味は『defaultとして作成したドッカーマシンのip』
※以下に最終完成バージョンも含めてあげているので、もしよかったら参考にしていただければと思います。
index.php<?php require_once 'sample.php'; $sample = new sample\Sample(); echo $sample->a();sample.php<?php namespace sample; class Sample { public function a() { return 'あ'; } public function i() { return 'い'; } }SampleTest.php<?php use PHPUnit\Framework\TestCase; require_once 'sample.php'; class SampleTest extends TestCase { public function testSample() { $sample = new sample\Sample(); //同じ値、同じ型でない場合エラー $this->assertSame($sample->a(), 'あ'); } }docker-compose.ymlversion: '3' services: apach: container_name: sample-apache #作成されていると出来ないので新しく作成する場合ここの名前を変更 build: ./ #apachのDockerfileを使用してコンテナ作成 ports: - 81:80 volumes: - ./html:/var/www/html # コンテナ var/www/html の中に/apach/htmlの中身を入れるFrom php:7.4-apache RUN apt-get update \ && apt-get install -y \ zip \ unzip WORKDIR /var/www/htmlコンテナに入ってもう一仕事する
・以下コマンド今使用しているコンテナIDの確認を行う
$ docker ps
・コンテナ内に以下コマンドで入る、 ※『1999d283b4f9』は上記で出力したコンテナIDを使用してください
$ docker exec -i -t 1999d283b4f9 bash
・コンテナ内で以下コマンドうちcomposerのインストール
$ curl -sS https://getcomposer.org/installer | php
・composerのインストールが無事行えたら以下コマンドで『PHPUnit』のインストール
$ php composer.phar require --dev phpunit/phpunit
参考:https://phpunit.readthedocs.io/ja/latest/installation.html・全ての準備は出来た!テストコードは書いてあるので、以下コマンドをコンテナ内で実行しテスト実行してみる
$ vendor/bin/phpunit SampleTest.php
以下のようになればテスト行えたことが分かる
最後に
以上終わりです!
書き足すことあるかもですが最初に書いたものは削除しない予定です!参考
- 投稿日:2021-02-28T20:16:40+09:00
DockerでPHPUnitを少しさわってみる(Composer使用)
はじめに
Dockerをある程度使える前提で書いていきます。
これはあくまで、
『DockerでPHPUnitを少しさわってPHPのテスト少し試してみたい!』方向けです。よろしくお願いします!
後、あ、これも!これも書いておきたい!っと思ったら書き足していく予定です!
では早速
環境作成
以下のようにファイルを作成して、『docker-compose.yml』ある場所で
docker-compose up --build -d
を叩きます。
そうすると、<<ipアドレス>>:81以下のように表示されています。
※ポート(81の部分)は同じですがIpは各環境で違います。ちなみに私のipは$ docker-machine ip default
で調べられます。意味は『defaultとして作成したドッカーマシンのip』
※以下に最終完成バージョンも含めてあげているので、もしよかったら参考にしていただければと思います。
全体の構成参考画像
index.php<?php require_once 'sample.php'; $sample = new sample\Sample(); echo $sample->a();sample.php<?php namespace sample; class Sample { public function a() { return 'あ'; } public function i() { return 'い'; } }SampleTest.php<?php use PHPUnit\Framework\TestCase; require_once 'sample.php'; class SampleTest extends TestCase { public function testSample() { $sample = new sample\Sample(); //同じ値、同じ型でない場合エラー $this->assertSame($sample->a(), 'あ'); } }docker-compose.ymlversion: '3' services: apach: container_name: sample-apache #作成されていると出来ないので新しく作成する場合ここの名前を変更 build: ./ #apachのDockerfileを使用してコンテナ作成 ports: - 81:80 volumes: - ./html:/var/www/html # コンテナ var/www/html の中に/apach/htmlの中身を入れるFrom php:7.4-apache RUN apt-get update \ && apt-get install -y \ zip \ unzip WORKDIR /var/www/htmlコンテナに入ってもう一仕事する
以下コマンド今使用しているコンテナIDの確認を行う
$ docker ps
コンテナ内に以下コマンドで入る、 ※『1999d283b4f9』は上記で出力したコンテナIDを使用してください
$ docker exec -i -t 1999d283b4f9 bash
コンテナ内で以下コマンドうちcomposerのインストール
$ curl -sS https://getcomposer.org/installer | php
composerのインストールが無事行えたら以下コマンドで『PHPUnit』のインストール
$ php composer.phar require --dev phpunit/phpunit
参考:https://phpunit.readthedocs.io/ja/latest/installation.html全ての準備は出来た!テストコードは書いてあるので、以下コマンドをコンテナ内で実行しテスト実行してみる
$ vendor/bin/phpunit SampleTest.php
以下のようになればテスト行えたことが分かる
最後に
以上終わりです!
書き足すことあるかもですが最初に書いたものは削除しない予定です!参考
- 投稿日:2021-02-28T19:40:04+09:00
コマンドライン(command line)からDockerのコンテナ(container)をvscodeで直接開く
containerを直接vscodeで開く
ちょっと感動したので記載。
起動したdocker containerをvscodeで開く場合、コマンドパレットやサイドバーから開くことが一般的だと思いますが、container名さえわかれば、下記コマンドでコマンドラインから直接開くことができます。
code --folder-uri=vscode-remote://attached-container+{container_hex}{path_to_open_folder}上記のcontainer_hexの計算方法は以下(例としてbashです。)
text="{\"containerName\":\"/${1}\"}" container_hex=printf $text | od -A n -t x1 | tr -d '[\n\t ]'${1}には起動したいコンテナの名前を指定してください。
これでdocker containerをvscodeで直接開くことができます。
開発環境にscript化しておいておけば手間が省けるかもしれません。参考
script化してくれてる方がいらっしゃいます。
https://github.com/geircode/vscode-attach-to-container-script
- 投稿日:2021-02-28T19:12:49+09:00
docker内でコマンド実行すると「-bash: apt-get: コマンドが見つかりません」と怒られる
例えば、psコマンドを使いたい場合に以下を実行すると、
$ docker-composer exec container_name apt-get update && apt-get install -y procps以下のようなメッセージが表示される
-bash: apt-get: コマンドが見つかりませんbash経由で実行したらできた。
$ docker-compose exec container_name bash -c 'apt-get update && apt-get install -y procps'
- 投稿日:2021-02-28T18:02:58+09:00
Vagrant+VirtualBoxでのDocker環境
目的
WindowsでDocker環境を構築するに際して「Docker Desktop for Window」を利用してみようとしましたが、
諸々の環境依存の問題に当たることがあり、過去の経験上で比較的問題のでることが少なかった
「Vagrant+VirtualBox」の環境上にDocker環境を作成したものです方針
- Windowsの依存問題を避けるため、VMはLinux系のもの
- VM/コンテナを両方起動することの無駄には目をつぶる
Vagrant起動スクリプト
Vagrant.configure("2") do |config| config.vm.box = "centos/7" config.ssh.insert_key = false config.vm.network "forwarded_port", guest: 80, host: 80 # HTTP config.vm.network "forwarded_port", guest: 443, host: 443 # HTTPS config.vm.network "forwarded_port", guest: 3306, host: 3306 # MySQL config.vm.network "forwarded_port", guest: 6379, host: 6379 # Redis config.vm.synced_folder "./workDir", "/home/vagrant/mnt/workDir" # Docker config.vm.provision :docker, run: "always" # docker-compose config.vm.provision :docker_compose, compose_version: "1.27.4", run: "always" end個別の補足
config.vm.box = "centos/7" config.ssh.insert_key = false今回はCentOS7を採用
2行目はSSHのキーを省略する設定、ローカル環境でのみ使用するためこの設定としていますが、
あまり好ましくない設定と思われるので、本記事を参考にする際はきちんと調べて適切に設定してくださいconfig.vm.network "forwarded_port", guest: 80, host: 80 # HTTPVirtualBoxのポートフォワーディングに関する設定
開発環境に必要なポートを定義していますconfig.vm.synced_folder "./workDir", "/home/vagrant/mnt/workDir"ホストマシンとゲストマシンのディレクトリの同期設定です
「/home/vagrant/mnt/」は「Vagrant+VirtualBox+CentOS7」の環境のマウント用パスですconfig.vm.provision :docker, run: "always" (※中略) config.vm.provision :docker_compose,Dockerおよびdocker-composeをインストールするための設定です
あとがき
かなりザックリすぎに記事を書いた自覚はあるので、後日時間があれば追記修正する気持ちはあります……
- 投稿日:2021-02-28T17:03:18+09:00
Windows10 Home に Docker Desktop をインストールしてMySQLサーバを起動してSQLで動作確認するまでの手順2021/02
Windows を最新にする
バージョンが降るとインストールできない場合があるので、最新かどうか確認します。
Windows キーを押して設定ボタン
を押します。
表示された設定画面の右上の Windows Update 部分をクリックします。
Docker ホームページを開く
下記 URL の Docker 公式ページを開きます。(英語ページです)
https://www.docker.com
インストーラーをダウンロード
Docker Desktop の Download for Windows ボタンを押します。
インストーラーを実行
Docker Desktop Installer.exe がダウンロードされますので、
ダウンロードが完了したら実行します。
終了したら Close and restart ボタンを押して再起動します。
カーネル更新プログラムのインストール
再起動後以下の画面が出たら https://aka.ms/wsl2kernel のリンクを開きます。
※この画面は後で使うので消さないでおいておきます。
x64 マシン用 WSL2 Linux カーネル更新プログラムパッケージ のリンクをクリックします。
保存ボタンを押してダウンロードします。
ダウンロードした wsl_update_x64.msi を実行します。
この画面は今回特に使わないので閉じます。
MySQLで動作確認
まずバージョン情報を見てみます。
cmd と入力して OK ボタンを押します。
Docker バージョン情報表示
docker version
と入力し Enter キーを押して実行します。
つぎに、試しにMySQLを動かしてみます。
以下のコマンドを入力して実行します。MySQLサーバのDockerコンテナを起動
MySQLサーバの起動docker run -e MYSQL_ROOT_PASSWORD=password mysql
ずらずらと表示が出てきて ready for connections. という行が表示されれば起動完了です。
docker run
で docker コンテナという仮想サーバ的なものを起動します。
mysql
というのが立ち上げるコンテナの種類になります。
-e MYSQL_ROOT_PASSWORD=password
は環境変数の設定で MySQL の root ユーザのパスワードを指定しています。次にサーバに入ってみます。
Windows+R キーでもう一つ別のコマンドプロンプトを起動します。
以下のコマンドで動作中のコンテナ一覧が確認できます。
Dockerコンテナ一覧表示
実行中Dockerコンテナ一覧を表示docker psこの一覧にある mysql の CONTAINTER ID をコピーしておきます。
※IDは毎回変わります。今回は 11576ddb996a です。そのID を指定して以下のコマンドを実行します。
Dockerコンテナのシェルに入る
Dockerコンテナのシェルに入るdocker exec -it 11576ddb996a bashそうすると
root@11576ddb996a:/#
が表示されます。
これは mysql サーバ内のシェルに入った状態です。次に以下のコマンドでMySQLクライアントを起動します。
MySQLクライアント起動
MySQLクライアント起動mysql -uroot -pパスワードを聞かれるので環境変数で指定した password を指定します。
以下のように、mysql> が表示されればOKです。
MySQLクライアント起動後root@11576ddb996a:/# mysql -uroot -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 10 Server version: 8.0.23 MySQL Community Server - GPL Copyright (c) 2000, 2021, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql>データベース一覧
データベース一覧を表示してみます。
データベース一覧表示show databases;データベース一覧表示後mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | sys | +--------------------+ 4 rows in set (0.01 sec)データベース作成
データベースを作成してみます。
データベース作成create database testdb;データベース作成結果mysql> create database testdb; Query OK, 1 row affected (0.04 sec)データベース切り替え
testdbに入ります。
データベース切り替えuse testdb;データベース切り替え結果mysql> use testdb; Database changedテーブル作成
テーブルを作ってみます。
テーブル作成create table test_users(id int auto_increment primary key, name varchar(10), birthday date);テーブル作成結果mysql> create table test_users(id int auto_increment primary key, name varchar(10), birthday date); Query OK, 0 rows affected (0.04 sec)レコード挿入
レコードを挿入してみます。
レコード挿入insert into test_users(name,birthday) values('tanaka', '1990-01-02');レコード挿入結果mysql> insert into test_users(name,birthday) values('tanaka', '1990-01-02'); Query OK, 1 row affected (0.04 sec)テーブル内容確認
テーブルの内容を確認します。
テーブル内容確認select * from test_users;テーブル内容確認結果mysql> select * from test_users; +----+--------+------------+ | id | name | birthday | +----+--------+------------+ | 1 | tanaka | 1990-01-02 | +----+--------+------------+ 1 row in set (0.00 sec)問題なく使えますね。(日本語を使っていないのは実は現状では扱えないためです。この後日本語も扱う方法を紹介します。)
後片付け
mysql>
が表示されている状態で exit を実行し、MySQL クライアントを終了します。
root@11576ddb996a:/#
が表示されている状態で exit を実行し、シェルを終了します。コンテナ停止
次に以下のコマンドでMySQLサーバを停止します。
※ 11576ddb996a は コンテナID です。docker ps で確認できるものです。Dokcerコンテナ停止docker stop 11576ddb996a停止されているか docker ps で確認します。
実行中コンテナ一覧C:\Users\nakaz>docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES一覧が空になっているのでコンテナが停止したことが確認できました。
コンテナの起動
再度起動したいときは以下のコマンドを実行します。
コンテナ起動docker start 11576ddb996a起動したいときにコンテナID がわからなくなった場合は、以下のコマンドで確認できます。
停止中も含めたコンテナ一覧docker ps -a
停止中も含めたコンテナ一覧実行結果C:\Users\nakaz>docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 11576ddb996a mysql "docker-entrypoint.s…" 41 minutes ago Exited (0) 4 minutes ago naughty_curranコンテナ削除
もしコンテナが不要になって消したい場合は以下のコマンドを実行します。
コンテナ削除docker rm 11576ddb996a
コンテナ削除結果C:\Users\nakaz>docker rm 11576ddb996a 11576ddb996aコンテナ削除後確認
再度
docker ps -a
を実行すると消えていることが確認できます。コンテナ削除後確認結果C:\Users\nakaz>docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESMySQL で日本語入力に対応する
先ほどのやり方だと MySQL で日本語が正しく扱えない状態ですので、日本語対応の環境を作ってみます。
また、タイムゾーンも世界標準時(UTC)となっているので日本時間にしてみます。メモ帳を開いて以下の内容を入力します。
Dockerfile 作成
FROM mysql RUN apt-get update && apt-get install -y locales-all ENV LC_ALL="ja_JP.UTF-8" TZ="Asia/Tokyo"
FROM mysql
は先ほど使っていた mysql のイメージを元にするという意味です。
RUN...の行は世界で使われている言語設定をインストールしています。その中に日本語も含まれています。
ENV...の行は言語設定(ロケール)を日本、タイムゾーンを東京に設定しています。ファイル名は Dockerfile と入力してファイルの種類は「すべてのファイル」としてユーザフォルダに保存します。
Docker ファイルから Docker イメージを作成
コマンドプロンプトを開いて dir コマンドで Dockerfile があることを確認します。
以下のコマンドを実行して、Dockerfile から Docker イメージを作成します。
※もともと用意されているイメージ(mysql)に追加でインストールや設定を行ったもので新しいイメージを作ることができます。Dockerイメージ作成docker build -t mysqlj .※
-t mysqlj
で作成するイメージに名前を付けることができます。(ここでは mysqlj という名前を付けています。)Dockerイメージ作成結果C:\Users\nakaz>docker build -t mysqlj . [+] Building 0.2s (6/6) FINISHED => [internal] load build definition from Dockerfile 0.1s => => transferring dockerfile: 306B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/library/mysql:latest 0.0s => [1/2] FROM docker.io/library/mysql 0.0s => CACHED [2/2] RUN apt-get update && apt-get install -y locales-all 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:9708290e1f00c169ec210361aa1dcebe60744b694b8eb3325bf7cf862e8af7b5 0.0s => => naming to docker.io/library/mysqlj 0.0s作成したイメージを使ってコンテナを起動
作成したイメージからコンテナを起動します。
日本語対応MySQLサーバ起動docker run --name=mysqlj1 -e=MYSQL_ROOT_PASSWORD="password" mysqlj mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci※--name=mysqlj1 でコンテナに名前を付けることができます。ここでは mysqlj1 という名前を付けています。
※mysqld 以降は MySQL で日本語を扱う設定にするためにつけています。日本語動作確認
日本語が入力できるか確認してみます。
MySQLクライアント起動docker exec -it mysqlj1 mysql -uroot -p※コンテナIDの代わりにコンテナ起動時につけた名前を指定することができます。
※bash の部分を mysql にすることでいきなりクライアントを起動することができます。日本語が扱える設定かどうか確認します。
文字コード設定確認
文字コード設定確認show variables like '%chara%';文字コード設定確認結果mysql> show variables like '%chara%'; +--------------------------+--------------------------------+ | Variable_name | Value | +--------------------------+--------------------------------+ | character_set_client | utf8mb4 | | character_set_connection | utf8mb4 | | character_set_database | utf8mb4 | | character_set_filesystem | binary | | character_set_results | utf8mb4 | | character_set_server | utf8mb4 | | character_set_system | utf8 | | character_sets_dir | /usr/share/mysql-8.0/charsets/ | +--------------------------+--------------------------------+ 8 rows in set (0.02 sec)ちなみに、最初の日本語が扱えない時の設定は以下のようになっていました。
mysql> show variables like '%chara%'; +--------------------------+--------------------------------+ | Variable_name | Value | +--------------------------+--------------------------------+ | character_set_client | latin1 | | character_set_connection | latin1 | | character_set_database | utf8mb4 | | character_set_filesystem | binary | | character_set_results | latin1 | | character_set_server | utf8mb4 | | character_set_system | utf8 | | character_sets_dir | /usr/share/mysql-8.0/charsets/ | +--------------------------+--------------------------------+ 8 rows in set (0.03 sec)※latin1 になっていると日本語が文字化けしたりします。
日本語レコード作成
データベース一覧を確認します。
データベース一覧確認結果mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | sys | +--------------------+ 4 rows in set (0.00 sec)さっき作った testdb がないことがわかります。
これは、コンテナごとに別々のデータ領域が作られるためです。
コンテナは docker run を実行するごとに作成され、 docker ps -a で一覧を確認できます。先ほどと同じように、データベースとテーブルを作って日本語のレコードを挿入してみます。
データベース作成結果mysql> create database testdb; Query OK, 1 row affected (0.04 sec)データベース切り替え結果mysql> use testdb; Database changedテーブル作成結果mysql> create table test_users(id int auto_increment primary key, name varchar(10), birthday date); Query OK, 0 rows affected (0.04 sec)日本語でレコードを挿入してみます。
レコード挿入insert into test_users(name,birthday) values('山田太郎', '1990-01-02');レコード挿入結果mysql> insert into test_users(name,birthday) values('山田太郎', '1990-01-02'); Query OK, 1 row affected (0.04 sec)日本語レコード確認
挿入したデータを確認してみます。
テーブル内容確認select * from test_users;テーブル内容確認結果mysql> select * from test_users; +----+--------------+------------+ | id | name | birthday | +----+--------------+------------+ | 1 | 山田太郎 | 1990-01-02 | +----+--------------+------------+ 1 row in set (0.00 sec)正しく表示されました。
- 投稿日:2021-02-28T17:01:08+09:00
負荷試験でlocustを使ってみる
locustについて
- シナリオをPythonで書ける
- テストの状況をリアルタイム表示することができるWeb画面がある
- 冗長構成にすることができる
- https://locust.io/
- https://docs.locust.io/en/stable/index.html
試してみる
前提
- Docker、docker-composeがインストール済み
環境構築
locustのコンテナと、試しに負荷をかけてみるコンテナを作ります。
docker-compose.ymlversion: '3.7' services: master: image: locustio/locust ports: - "8089:8089" volumes: - ./:/mnt/locust # 負荷をかける環境のドメインを指定する command: -f /mnt/locust/main.py -H http://test_web:80 # お試し用の負荷をかけられるコンテナ test_web: image: httpd tty: trueテストシナリオ
テストシナリオを作ります。
main.pyfrom locust import HttpUser, task, constant_pacing class StressTestUser(HttpUser): wait_time = constant_pacing(1) @task def top_page_access(self): # 負荷をかける対象のパスを指定する self.client.get("/")ユーザクラスを作ります。ユーザクラスは1人のユーザーが行う行動をwait_timeやtaskなどで定義していきます。
https://docs.locust.io/en/stable/writing-a-locustfile.html#wait-time-attribute起動
コンテナを起動します。
docker-compose up
実行
http://localhost:8089
をブラウザで開くNumber of total users to simulate
に同時アクセスを行う最大ユーザ数を入力するSpawn rate
にユーザ数の増加速度を入力するStart swarming
をクリックするテスト状況
- 投稿日:2021-02-28T15:26:09+09:00
コンテナの検証環境をためしてみる~ ネコンテナ3回
コンテナの検証環境
コンテナを勉強していくには検証環境が必要である。今回はローカルマシンでつかうコンテナ検証環境を比較してみる。
コンテナの検証環境
- docker
- minikube
- crc (code ready container)
難しいので今回は確実に覚えなくてもいいが、コンテナオーケストレーションはAIO環境という1台で構成される環境であり、外部から参照するのが難しい環境のため、商用リリース用で考えるものではない。
比較内容
種類 何の検証環境 windows環境 windows pro環境 mac linux 最低必要SPEC docker docker 仮想環境を使えば導入可能 導入可能 導入可能 導入可能 考慮必要なし minikube kubernetes 導入可能 導入可能 導入可能 導入可能 cpu:2,memory:2GB,20GB disk CRC Openshift4 導入不可能 導入可能 導入可能 導入可能 cpu:4v,memory:8GB,35GB disk ※windows home では昔はdockerインストールできなかったが、202102時点では対応されている情報アリ
※最低必要スペックのためのセルアプリケーションのリソースは追加で必要。1. docker
いろんな書籍で聞くdockerです。RHEL(centos)環境ではdocker.systemで動かすものです。クライアントもdocker、サービスもdockerなので初めての人だと言葉が混在してしまいます。
デファクトスタンダードではありますが最近下火になっているdockerです。2022年を生き延びるのは難しいかもしれません。
docker -> kubernetes には移行はありますが、逆はないに等しいです。2.minikube
商用利用したり、コンテナ開発の優位性を生かすためのオーケストレーションのkubernetesの検証環境がminikubeです。kubernetesは管理ツールの役目が強いため、いきなり商用を使うと外部設定の箇所で躓いてしまします。
少し前までは最初からyamlファイルを書かないとアプリケーションのテストもできなかったため、最初の『Hello,world』までに時間がかかって嫌われてきました。kubectl create 'kind'kindはdeployment(runみたいなもの)やservice(ネットワーク紐づけ)等を使うと、簡単に最初のデプロイができます。
crc (code ready container) 1押し(コンテナのメリットを考えるなら)
kubernetesにコンテナの有用機能をたっぷり付けたOpenshiftのローカル検証用、優秀なGUIがついているもの。
商用運用はredhatのわかりにくく高い料金体系を考えないといけませんが、コンテナ初期学習において一番わかりやすいです。ただ動作環境がかなり縛られているため、最初の初期構築で悩んでしまいます。
※低スペックPCで試してはいけない。
※現バージョンはvirtualBOXでは動きません。
- crcを使ったデプロイ例(S2Iデプロイ)
- 下記のようなカタログから自分の環境を選ぶ
- githubにソースをアップロード
- GUIの手順に従ってデプロイ
CRCはデプロイのみ考えると、こんなに簡単なんです。
基盤の設計はアーキテクトに詳しい人に任せてコンテナ開発をCRCを使って遊んでください。
商用利用は商用利用の時に考えればいい、コンテナのメリットを考えるなら一押し最後に
コンテナの開発環境も各種クラウドマネージドサービスでたくさん出ています。そちらを使って最初からやる方法もありです。どのサービスでもいいので使いやすいのを使っていただければと思います。今回はあくまで費用の掛からないローカル環境での構築3つを上げさせていただきました。
コンテナ開発は一度きりの開発ではないため、初期の学習コストが低いからdockerで勉強したが、最終的にk8s環境の勉強をしないといけないという無駄勉強が発生することがあります。そうならないように注意してください。
- 投稿日:2021-02-28T15:15:14+09:00
LaradockでDuskやろうとしたらちょっと手間が必要だった件
そもそもの話
最近、Laradock触ってないなぁ、と思ったので、試しにDuskを動かそうと思ったことから始まりました。
環境はすでに作成しているものを使っています。やってみたいこと
- Laravelをサブディレクトリとして構築
- Duskのテストが通る
とりあえずの前提条件
- Webサーバのrootとしては、
/var/www/public
を指定- Laravelのrootは
/var/www/public/lara1
、URLもhttp://localhost/lara1
でアクセスできるようにする- 他の設定は標準(Seleniumアクセスのポートは4444で、など)
Laradock起動コマンド
Webサーバはnginx、データベースはmysqlを使います。
Chromeはseleniumコンテナに入っているので同時に起動します。
あとは適当にdocker-compose up workspace nginx mysql seleniumDusk導入まで
やり方は2つの参考記事をもとに対応。
参考1: https://qiita.com/amymd/items/0a5f2705e29972d0d22e
参考2: https://qiita.com/hrkbyc/items/755e9799c205c092c323コマンドは以下の順番で処理していきます。
cd /var/www/public composer create-project laravel/laravel lara1 --prefer-dist cd lara1 composer install composer require --dev dusk composer update # 念の為 npm install npm run dev php artisan dusk:installキモは、
AppServiceProvider.phpの修正
Duskをプロバイダに登録するコードを追加します。
AppServerProvicer.php<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use Laravel\Dusk\DuskServiceProvider; // Duskの参照を追加 class AppServiceProvider extends ServiceProvider { /** * Register any application services. * * @return void */ public function register() { // Duskのプロバイダを登録 if ($this->app->environment('local', 'testing')) { $this->app->register(DuskServiceProvider::class); } } /** * Bootstrap any application services. * * @return void */ public function boot() { // } }DuskTestCase.php の修正
baseUrlを所定のURLに、driverをSeleniumのサーバーに変更します。
DuskTestCase.php<?php namespace Tests; use Facebook\WebDriver\Chrome\ChromeOptions; use Facebook\WebDriver\Remote\DesiredCapabilities; use Facebook\WebDriver\Remote\RemoteWebDriver; use Laravel\Dusk\TestCase as BaseTestCase; abstract class DuskTestCase extends BaseTestCase { use CreatesApplication; // baseUrlは別サーバのサブディレクトリになるため明記 protected function baseUrl() { return 'http://nginx/lara1'; } /** * Prepare for Dusk test execution. * * @beforeClass * @return void */ public static function prepare() { if (! static::runningInSail()) { static::startChromeDriver(); } } /** * Create the RemoteWebDriver instance. * * @return \Facebook\WebDriver\Remote\RemoteWebDriver */ // seleniumコンテナのChromeを使用する protected function driver() { return RemoteWebDriver::create( 'http://selenium:4444/wd/hub', DesiredCapabilities::chrome() ); } /** * Determine whether the Dusk command has disabled headless mode. * * @return bool */ protected function hasHeadlessDisabled() { return isset($_SERVER['DUSK_HEADLESS_DISABLED']) || isset($_ENV['DUSK_HEADLESS_DISABLED']); } }さあテスト開始…ところが!?
php artisan duskところが、エラーを出してしまうわけです。
Duskはちゃんと動いているようですが、どうやらLaravelのアプリがまともに動いていないと予想しました。
そこで、参考記事を元にnginxの設定をいじることにしました。参考:https://qiita.com/saba1024/items/2d50fb8284d699924360
対策とってみた
default.confの設定変更
vi /etc/nginx/sites-available/default.conf先の記事で参照していたnginxの設定ではサブディレクトリを考慮していなかったので、色々設定を変えました。
最終的にはこういう形になりました。
特にハマったのはSCRIPT_FILENAME。
index.phpの場所が変わっているのにすぐに気づきませんでした…。
パスの指定を現在の配置に合わせました。fastcgi_param SCRIPT_FILENAME $document_root/public/index.php;あと、fastcgi_passの指定。
もともとあったLaravel向けの設定を読んでみて、php-upstreamコンテナを指定すれば良いことがわかりました。fastcgi_pass php-upstream;あとは、locationやtry_filesの設定に注意して出来上がりました。
default.conflocation ~ ^/lara1((/)?(.+))?$ { root /var/www/public/lara1; try_files $1 /lara1/index.php?$query_string; location ~ ^/lara1/index.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root/public/index.php; fastcgi_param PATHINFO $fastcgi_path_info; fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass php-upstream; fastcgi_index index.php; } }そして、nginxの再起動…エラーなし。
nginx -s reloadこれでLaravelが動くだろう…世の中そんなに甘くありませんでした。
書き込み権限の変更
…Dockerのコマンドラインではrootなのに、nginx上ではnginxユーザで動かしているのをすっかり忘れていました。
storageディレクトリに書き込み権限を付記します。cd /var/www/public/lara1 chmod -R a+w storageさあリベンジ!
ブラウザでは無事Laravelのアプリが表示されました。
続いてDuskでは…root@6580d8cd10f5:/var/www/public/lara1# php artisan dusk PHPUnit 9.5.2 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 00:10.353, Memory: 20.00 MB OK (1 test, 1 assertion)動いたー! やったー!
まとめ
過去の知見となんとか駆使すればなんとかなるということです、はい。
- 投稿日:2021-02-28T12:21:50+09:00
Flutter Webの単体テストはdart:htmlライブラリを含まないように作る【CI構築】
Flutter Webで動画プレイヤーアプリを作成したのですが、PUSHする毎に単体テストを動かすCIを構築するときに、Riverpodの使い方でひと工夫必要だったので記事にしました。
dart:htmlライブラリを含むと単体テストが難しい
アプリの概要
スプラトゥーン2のプレイヤー向けのシーン自動頭出し機能付きの動画プレイヤーアプリです。自動検出された特定シーンの時刻ボタンを押すと、その場所に飛びます。
※ 左上の動画は任天堂株式会社のスプラトゥーン2からの引用です。
単体テストの例
秒数をコロン区切りの時刻テキストに変換するutil系メソッドの単体テストがあります。
例: 192.0 → 3:12実装
util.dartimport 'package:sprintf/sprintf.dart'; /// 秒数を時間テキストにする String toTimeString(double seconds) { int hour = seconds.toInt() ~/ 3600; int minute = (seconds.toInt() - hour * 3600) ~/ 60; int second = seconds.toInt() % 60; if (hour >= 1) { return sprintf("%d:%02d:%02d", [hour, minute, second]); } else { return sprintf("%d:%02d", [minute, second]); } }単体テスト
util_test.dartvoid main() { test('test toTimeString', () async { expect(toTimeString(0.0), '0:00'); expect(toTimeString(59.9), '0:59'); expect(toTimeString(60.0), '1:00'); expect(toTimeString(9 * 60 + 59.9), '9:59'); expect(toTimeString(10 * 60.0), '10:00'); expect(toTimeString(59 * 60.0 + 59.9), '59:59'); expect(toTimeString(3600.0), '1:00:00'); expect(toTimeString(3600 + 59 * 60.0 + 59.9), '1:59:59'); expect(toTimeString(7200.0), '2:00:00'); }); }単体テストの実行
これだけならば、このコマンドで単体テストを実行することができます。
flutter test
しかし、実は同じファイルにHTMLのvideo要素を作成、取得するメソッドが同居していました。なので、
dart:html
ライブラリがインポートされています。util.dartimport 'dart:html'; // ↑ これが問題 import 'package:sprintf/sprintf.dart'; String toTimeString(double seconds) { int hour = seconds.toInt() ~/ 3600; int minute = (seconds.toInt() - hour * 3600) ~/ 60; int second = seconds.toInt() % 60; if (hour >= 1) { return sprintf("%d:%02d:%02d", [hour, minute, second]); } else { return sprintf("%d:%02d", [minute, second]); } } /// 1つしか無いvideo要素 VideoElement _ikutVideoElement; /// 1つしかないvideo要素を取得する VideoElement getVideoElement(BuildContext context) { if (_ikutVideoElement == null) { final videoElement = VideoElement(); videoElement.controls = true; // リスナ設定などがあるが省略 _ikutVideoElement = videoElement; } return _ikutVideoElement; }その場合は
flutter test
コマンドでテスト実行するとエラーになってしまいます。lib/util/util.dart:1:8: Error: Not found: 'dart:html' import 'dart:html';
--platform chrome
引数を付けるとエラーになりません。flutter test --platform chrome
dart:html
ライブラリの動作にはGoogle Chromeが必要なようです。Google Chrome入りFlutter Webビルド環境をDockerで作成する
CIでの動作を前提に、Dockerイメージを作成します。DockerHubで一番使われているFlutterのイメージcirrusci/flutterをベースに作成しました。
FROM cirrusci/flutter # Setup Flutter Web RUN flutter channel beta RUN flutter upgrade RUN flutter config --enable-web # Install Google Chrome RUN apt-get update RUN apt-get install -y fonts-liberation libgbm1 xdg-utils RUN wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb RUN dpkg -i google-chrome-stable_current_amd64.debdocker-composeを使ってビルドして
flutter doctor
を実行します。docker-compose.ymlflutter: build: . # プロジェクトディレクトリを読み書きできるようにする volumes: - /your_project_directory:/work working_dir: /workdocker-compose build docker-compose run flutter flutter doctor一見良さそうに見えます。
[✓] Flutter (Channel beta, 1.26.0-17.6.pre, on Linux, locale en_US.UTF-8) [✓] Android toolchain - develop for Android devices (Android SDK version 30.0.2) [✓] Chrome - develop for the web [!] Android Studio (not installed) [✓] Connected device (1 available)Docker環境でGoogle Chromeを使用した単体テストを実行する(失敗)
docker-composeを使って実行しました。freezedを使っているので、そのためのビルドコマンドを含んでいます。
docker-compose run flutter sh -c "flutter pub get && flutter pub run build_runner build && flutter test --platform chrome"しかし10分以上待たされてエラーになってしまいました。
Downloading Web SDK... 12:00 +0 -1: compiling /work/test/presenter/battle_list_provider_test.dart [E] TimeoutException after 0:12:00.000000: Test timed out after 12 minutes.原因はよく分かりませんでした。
そこでGoogle Chrome入りのDockerイメージを使って
flutter test --platform chrome
コマンドを実行する方針から、dart:html
ライブラリに依存しないようにアプリの設計とテストコードの作成を行う方針に変更しました。単体テストにdart:htmlライブラリを含まないようにしてCIを作成
dart:htmlライブラリを含むメソッド等は別ファイルに移動する
- toTimeStringメソッド(dart:html不使用、テスト対象)はutil.dartファイルに設置。
- getVideoElementメソッド(dart:html使用、テスト対象ではない)はweb_util.dartファイルに設置。
CIの設定を行う
DockerファイルはGoogle Chromeを含めないで作成しました。
FROM cirrusci/flutter RUN flutter channel beta RUN flutter upgrade RUN flutter config --enable-web私は日頃CIにAWS CodeBuildを使っているので、その設定を行いました。DockerイメージはElastic Container RegistryにPUSHしてbuildspec.ymlはこのようにしました。
buildspec.ymlversion: 0.2 phases: build: commands: - flutter pub get - flutter pub run build_runner build - flutter testできあがり
別のdart:htmlライブラリを含むProviderを使っているStateNotifierProviderの単体テストを作成する
このケースの場合、Riverpodの使い方に工夫が必要だったので、それを紹介します。
該当部分の仕様
該当アプリには設定画面があり、頭出しされたシーンの再生は何秒前から開始するかや、スキップするときの秒数をスライダーを左右にドラッグすることで設定出来ます。設定はブラウザローカルに保存されて、次回同じブラウザでアプリを開いたときは復帰されます。
単体テスト指針
- 設定画面を構築するためのStateNotiferをテストする。
- StateNotifierは設定をブラウザローカルに保存する担当オブジェクトを持っている。それをモック化して呼び出され方を確認する。
アプリの実装
設定のモデルはこのようになっています。(freezedで作成)
ikut_config.dart@freezed abstract class IkutConfig with _$IkutConfig { /// 設定情報 /// [skipTime] スキップで進んだり戻ったりする秒数 /// [deathBeforeTime] やられたシーンはN秒前から見たい /// [endBeforeTime] 試合終了へのスキップはN秒前から見る /// [slowRatio] スロー再生のレシオ(分母) /// [fastRatio] 早送り再生のレシオ(分子) factory IkutConfig(int skipTime, int deathBeforeTime, int endBeforeTime, int slowRatio, int fastRatio) = _IkutConfig; }endBeforeTimeフィールドとfastRatioフィールドは固定値を入力して使っています。
それを使ってこのようにWidgetを構築しています。横にスライドして値を変更する部品にはSliderを使用しています。
config.dartHookBuilder _buildConfigContent() { return HookBuilder(builder: (context) { // 現在設定値 final config = useProvider(ikutConfigStateNotifierProvider.state); // このStateNotifierのメソッドを呼び出して設定値を更新する final configStateNotifier = useProvider(ikutConfigStateNotifierProvider); // 見た目の定義 final nameTextStyle = TextStyle(fontSize: 14, color: IkutColors.blackText); final valueTextStyle = TextStyle( fontSize: 14, color: IkutColors.blackText, fontWeight: FontWeight.bold); final sliderWidth = 400.0; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // 進むときと戻るときの秒数 SizedBox(height: 16), Text(sprintf(Strings.configSkipMinutes, [config.skipTime]), style: nameTextStyle), Container( width: sliderWidth, child: Slider( activeColor: Colors.pinkAccent.shade400, inactiveColor: Colors.pinkAccent.shade100, value: config.skipTime.toDouble() /* 現在値設定 */, min: 1.0, max: 10.0, divisions: 9, label: config.skipTime.toString() + Strings.seconds, onChanged: (value) { // スライダーが移動したときのイベント configStateNotifier.setSkipTime(value.toInt()); }), ), // あと2つの設定は省略 ]); }); }StateNotifierと、そのProviderはこのようになっています。
ikut_config_state_notifier_provider.dartclass IkutConfigStateNotifier extends StateNotifier<IkutConfig> { IkutConfigStateNotifier(this._dataStore, IkutConfig config) : super(config); /// Window.localStorageで設定値を読み書きする担当オブジェクト LocalStorageDataStore _dataStore; /// 進むときと戻るときの秒数を設定する void setSkipTime(int skipTime) { state = state.copyWith(skipTime: skipTime); _dataStore.setInt(Strings.configNameSkipTime, skipTime); } /// やられたシーンは何秒前から見るを設定する void setDeathBeforeTime(int deathBeforeTime) { state = state.copyWith(deathBeforeTime: deathBeforeTime); _dataStore.setInt(Strings.configNameDeathBeforeTime, deathBeforeTime); } /// スロー再生速度を設定 void setSlowRatio(int slowRatio) { state = state.copyWith(slowRatio: slowRatio); _dataStore.setInt(Strings.configNameSlowRatio, slowRatio); } } // ignore: top_level_function_literal_block final ikutConfigStateNotifierProvider = StateNotifierProvider((context) { // Window.localStorageで設定値を読み書きする担当オブジェクトを取得 final dataStore = context.read(localStorageDataStoreProvider); // 現在設定値読込 int skipTime = dataStore.getInt(Strings.configNameSkipTime, 5); int deathBeforeTime = dataStore.getInt(Strings.configNameDeathBeforeTime, 4); int slowRatio = dataStore.getInt(Strings.configNameSlowRatio, 2); // 設定更新用のStateNotiferを作成する return IkutConfigStateNotifier(dataStore, IkutConfig(skipTime, deathBeforeTime, 3, slowRatio, 2)); });設定値はWindow.localStorageを使い、ブラウザローカルに保存しています。しかしそのためには問題の
dart:html
ライブラリのインポートが必要です。よって、インターフェースと何もしない実装を作り、それを提供するProviderを作成します。local_storage_data_store_provider.dart/// Window.localStorageからデータを出し入れする機能のインターフェース abstract class LocalStorageDataStore { /// int型の値を取得する /// [name] 保存キー /// [defaultValue] 未設定の時の返却値 int getInt(String name, int defaultValue); /// 設定を保存する /// [name] 設定名 /// [intValue] 設定値 void setInt(String name, int intValue); } /// 上記のなにもしない実装 class LocalStorageDataStoreNoop implements LocalStorageDataStore { @override int getInt(String name, int defaultValue) { return defaultValue; } @override void setInt(String name, int intValue) {} } /// 上記の何もしない実装を提供する /// ここで本番実装を提供すると、単体テストでdart:htmlが取り込まれてしまう。 // ignore: unnecessary_cast final localStorageDataStoreProvider = Provider((_) => LocalStorageDataStoreNoop() as LocalStorageDataStore);こちらが
dart:html
ライブラリを使う、本番で使われる実装です。local_storage_data_store_impl.dartimport 'dart:html'; import 'package:ikut/localDataStore/local_storage_data_store.dart'; /// Window.localStorageからデータを出し入れする機能の本番実装 class LocalStorageDataStoreImpl implements LocalStorageDataStore { /// int型の値を取得する /// [name] 保存キー /// [defaultValue] 未設定の時の返却値 @override int getInt(String name, int defaultValue) { String value = window.localStorage[name]; if (value == null) { return defaultValue; } else { return int.parse(value); } } /// 設定を保存する /// [name] 設定名 /// [intValue] 設定値 @override void setInt(String name, int intValue) { window.localStorage[name] = intValue.toString(); } }こうすることで、単体テストでは間接的にも問題の
dart:html
を取り込みません。単体テストの実装
単体テストはこのようになります。モックオブジェクトの作成のために、mockitoを使用しました。
ikut_config_state_notifier_provider_test.dartclass MockLocalStorageDataStore extends Mock implements LocalStorageDataStore {} void main() { test('test ikutConfigStateNotifier', () async { // Window.localStorageへの保存をモック化したオブジェクト final dataStore = MockLocalStorageDataStore(); // 呼ばれるメソッドの実装を定義する when(dataStore.getInt(Strings.configNameSkipTime, 5)).thenReturn(5); when(dataStore.getInt(Strings.configNameDeathBeforeTime, 4)).thenReturn(4); when(dataStore.getInt(Strings.configNameSlowRatio, 2)).thenReturn(2); // LocalStorageDataStoreの実装をモックに差し替える final container = ProviderContainer(overrides: [ localStorageDataStoreProvider.overrideWithValue(dataStore) ]); // StateNotifierを取得する final configStateNotifier = container.read(ikutConfigStateNotifierProvider); // 更新された状態を取得するラムダ final getState = () => container.read(ikutConfigStateNotifierProvider.state); // 初期値を確認 expect(getState(), IkutConfig(5, 4, 3, 2, 2)); // skipTime設定を更新する configStateNotifier.setSkipTime(3); // dataStoreが適切に呼び出されたことを確認 verify(dataStore.setInt(Strings.configNameSkipTime, 3)); // 状態も更新されたことを確認 expect(getState(), IkutConfig(3, 4, 3, 2, 2)); // 以下略 }); }本番用実装のオーバーライド
localStorageDataStoreProviderがデフォルトで何もしない実装を返却するようになっているので、アプリを起動するときには、本番実装を返却するようにRiverpodのProviderScopeクラスのoverridesフィールドを設定します。
main.dartvoid main() { runApp(ProviderScope( overrides: [ /// 本番実装を返却するようにProviderの返却値を上書きする。 localStorageDataStoreProvider.overrideWithValue(LocalStorageDataStoreImpl())], child: IkutApp())); }まとめ
dart:html
ライブラリを含んだメソッド等の、CI環境での単体テストは上手くいきませんでしたが、Riverpodのoverridesを使い実装の差し替えを行うことでdart:html
ライブラリを含まない実装を作り、単体テストを行うことができました。
- 投稿日:2021-02-28T09:24:37+09:00
dockerでexpressとmysqlをつなげたい
お品書き
1.はじめに
2.expresss-generatorでアプリの準備
3.Dockerfileとdocker-composeを準備
4.mysqlのコンテナを追加する
5.おわりに1. はじめに
dockerを使ってexpressとMySQLをつなげようとしたときにハマってしまったのでメモしておきます。
dockerを使うと簡単に環境構築ができまた潰すのも簡単です。
つまり初学者こそdockerを使いいろんな環境を構築すべきなんです!
しかしdockerと聞くと難しそうな技術だなと尻込みしてしまいます。
そんな人のためにこの記事を書きました。
難しいことはわかりませんが、dockerを使って開発できるようになりましょう。1-1 目標
dockerを使ってexpress(のコンテナ)とMySQL(のコンテナ)をつなげる
完成コードはこちら1-2 そもそもdockerってなに
パソコン内に仮想環境を作る技術の1つで最近人気になっています。
dockerの良いところは気軽に様々な環境をつくることができることです。
コンテナという仮想環境を作ってそこでやりたい放題できる。
コンテナ内に好きなOSを入れてそれを好きなように操作するって感じです。
基本的は軽量のlinuxベースのもが多いのでlinuxコマンドを知っていると便利です。
そして要らなくなったらそのコンテナを捨てばOKってな感じです。1-3 dockerがわかりにくい理由
とはいったものの、dockerを初めて触ったときに???がいっぱい浮かびました。
仮想環境やコンテナなどの概念などはわかるのですが、実際にどう使えばいいの??的な。
なぜならdockerを扱う上で似たようなコマンドが出てくるし、オプションも多いからです。
特にdockerとdocker-composeが初学者にはややこしい。
一通り調べた結果、とりあえずdocker-composeを使えばなんとかなりそうという結論になりました。(笑)
こちらの動画がかなり参考になりました。
docker-compose入門をチェックしてください。
難しいことは考えずに気軽に開発環境をつくっていきましょう。1-4 今回使うdocker-composeのコマンドは2つだけ
- docker-compose up 複数のコンテナを起動する
- docker-compose down 起動しているコンテナを停止する とりあえずこれだけ知っていれば今回は大丈夫です。 その他のコマンドは後で覚えればOKです。
2. expresss-generatorでアプリの準備
まずはお好きな方法でディレクトリーを作ってください。
今回はdocker_express_mysqlという名前にしておきます。terminalmkdir docker_express_mysql今回はexpress-generatorを使ってサクッとアプリを作っていきます。
アプリの名前はdockerExpressにします。
express-generatorの使い方はこちらを参考にしてください。
express-generatorを使いたいterminal#ディレクトリーへの移動 cd docker_express_mysql #express-generatorを使ってdockerExpressというアプリを作る今回はテンプレートエンジンにejsをつかいます。 express --view=ejs dockerExpress #dockerExpressに移動し立ち上げまで cd dockerExpress npm install npm start次にdockerExpressの中身をいじっていきます。
することは
- nodemonのインストール 保存時に自動で更新してくれるスグレモノ
- mysql2のインストール mysqlに接続するのに必要
- sequelizeのインストール クエリーを簡単にするためのもの。ORMとよばれるものterminalnpm install nodemon mysql2 sequelizepackage.json"scripts": { // "start": "node ./bin/www" <-を下のように変える "start": "nodemon ./bin/www" }これで準備が整いました。
3. Dockerfileとdocker-composeを準備
これはめっちゃ簡単です。というかvscodeの"Docker"というエクステンションを使えば簡単にできます。
詳しいことは省略しますが、Dockerfileはコンテナのもとになるものを作るときに必要なものです。
docker-composeは複数のコンテンなを立ち上げるときに必要になるものです。
この辺の書き方が、初心者には難しいと感じるので、是非本記事で感覚だけでも掴んでください。
とりあえず最低限必要な設定のみにしています。
拡張機能のDockerについて詳しくはこちら3-1 vscode拡張機能"Docker"を導入する
"Docker"という拡張機能を使って作業します。もしvscodeを使っていな人や拡張機能を使いたくない人はあとに出てくるDockerfileとdocker-compose.ymlを自分で作成し、コードをコピーしてください。
導入方法は至って簡単です。
拡張機能から"Docker"と検索してインストールするだけです。
3-2 拡張機能を使う
インストールできたら、拡張機能を使いましょう。
vscodeでcmd+shift+pを押してひたすらenterをおしてください。(詳しく知りたい人はこちら)
そうすると自動でDockerfileとdocker-composeのファイルをができます。
これらの自動生成されたファイルを多少変更していきます。3-3 Dockerfileの編集
まずDockerfileから編集していきます。
いらない設定を削除していきます。DockerfileFROM node:12.18-alpine # ENV NODE_ENV=production ←を削除 WORKDIR /usr/src/app COPY ["package.json", "package-lock.json*", "npm-shrinkwrap.json*", "./"] # RUN npm install --production --silent && mv node_modules ../ ←を下のよう変更する RUN npm install COPY . . EXPOSE 3000 CMD ["npm", "start"]3-4 docker-compose.ymlを編集
続いてdocker-compose.ymlを編集していきます。
こちらは不要な設定を消しておきます。docker-compose.ymlversion: '3.4' services: dockerexpress: image: dockerexpress build: context: dockerExpress dockerfile: ./Dockerfile #environment: ←を削除 #NODE_ENV: production ←を削除 ports: - 3000:3000 #接続するポート番号。お好きな番号でいけます。これでdockerを使う準備が整いました。
3-5 コンテナを立ち上げてみる
試しにコンテナを動かして見ましょう。
注意点はdocker-compose.ymlがあるディレクトリーでdocker-compose upというコマンドを打つことです。terminal#docker-compose.ymlが入っているフォルダに移動してから #コンテナを立ち上げるコマンド docker-compose up
問題なければhttp://localhost:3000/にアクセスできるはずです。
*もしdocker-compose up
をしてエラーが出た場合はコードを確認してdocker-compose build
を行ってからdocker-compose up
を行ってください。ここまででdockerを使ってnodeのアプリをコンテナ内に立ち上げることができました。
4. mysqlのコンテナを追加
ここまで来たらあとはmysqlのコンテナを立ち上げるだけです。
基本的にすることはdocker-compose.ymlにmysqlのコンテナの設定を書くだけです。
今回は最小限の設定でコードを書いていきます。
dockerでコンテナをつくるときには1つのコンテナにすべての機能を入れるのではなくて、最小限の機能に分けて作るのがベストプラクティスらしいです。
なので今回はアプリ部分のコンテナ(expressで作ったもの)とDB部分のコンテナ(今から作るmyslqのもの)に分けて作っています。4-1 docker-compose.ymlにmysqlのコンテナ設定を書く
docker-compose.ymlに次のように追記してください。
ポイントはDBのデータを永続化させるために第3の場所にデータの置き場を作ることです。
詳しくは別の記事で説明は別の記事でできれば。。docker-compose.ymlversion: '3.4' services: # ここから my_mysql: image: mysql #もととなるイメージ。ここではmysql environment: #最小限の記述にしています。他にも環境設定は可能です。 MYSQL_ROOT_PASSWORD: root #rootパスワード。今回はrootにしました。 MYSQL_DATABASE: my_mysql_db #作成するデータベース。今回はmy_mysql_dbにしました。 ports: - '3300:3300' #接続するポート番号。お好きな番号でいけます。 volumes: - my_volume:/var/lib/mysql #第3の場所my_volumeをコンテナ内に同期するコード # ここまで追記 下の部分も忘れずに追記してください。 dockerexpress: image: dockerexpress build: context: dockerExpress dockerfile: ./Dockerfile ports: - 3000:3000 #第3の場所にデータを置いておく場所をつくる。こちらも忘れずに追記してください。 volumes: my_volume:これでmysqlのコンテナを起動する準備ができたので起動させてみましょう。
terminal# docker-composeがあるディレクトリーで docker-compose upこれでエラーがでなかれば起動しているはずです。
4-2 mysqlコンテナ内に入って操作してみよう
一応myslqのコンテナが起動しているか確認しましょう。
イメージとしては先程立ち上げたコンテナ内に入ってmysqlを使うというかたちです。
ここがちょっとややこしいです。とりあえずコマンドを打ちましょう。別のterminalを開く# my_mysqlコンテナ内に入るためのコマンド docker-compose exec my_mysql /bin/bashmy_mysqlのコンテナ内--プロンプトが変わったらコンテナ内に入れた証拠です。続いてmysqlを起動させていきます。 mysql -u root -p --docker-composeで設定したパスワードを入力my_mysqlのコンテナ内show databases;
データベースが作られていることを確認!
*他にもdocker-composeでいろんな指定ができますが省略します。
あとはdockerexpressからmy_mysqlにデータを取りにいけるようにしましょう。4-3 コンテナ間の接続を確認する
expressのコンテナとmysalのコンテナはそれぞれ別のコンテナとして立ち上げました。
なので今は2つのコンテナ立ち上がっているわけです。
それらのコンテナをつなげることに挑戦します。とはいってもとても簡単です。
host名はローカルではlocalhostになっていますが、これをdocker-compose.ymlで設定したDBの名前(今回はmy_mysql)にするだけです。
これを知らなくてかなり時間を無駄にしてしまいました。。。mysql2を使ってクエリを書いても確認できますが、今回はsequelizeを使ってみます。
sequelizeの本家サイトはこちらusersのページに遷移したときにdbにつながっているかをしらべてみます。
なのでroutes内のusers.jsを開き次のようにsequelizeの設定と確かめるためのコードを追記しましょう。users.jsvar express = require('express'); var router = express.Router(); // sequelizeの設定を追加 const { Sequelize } = require('sequelize'); // databaseやuser, passwordをdcoker-compose.ymlで設定したものを使う↓ const sequelize = new Sequelize('my_mysql_db', 'root', 'root', { host: 'my_mysql', // hostの名前をdocker-compose.ymlで設定したmy_mysqlに変更する dialect: 'mysql', }); /* GET users listing. */ router.get('/', async (req, res, next) => { // 忘れずに上に"async"を追加する。 // my_mysqlに接続されているかテスト try { await sequelize.authenticate(); console.log('Connection has been established successfully.'); } catch (error) { console.error('Unable to connect to the database:', error); } res.send('respond with a resource'); }); module.exports = router;URLをhttp://localhost:3000/usersとして、usersページに遷移したときに以下のようなメッセージがターミナルに出たら成功です。
terminaldockerexpress_1 | Executing (default): SELECT 1+1 AS result dockerexpress_1 | Connection has been established successfully. dockerexpress_1 | GET /users 304 45.706 ms - -
とりあえずこれでdockerを使ってexpressとmyqlのコンテナ同士をつなげることができました。
あとはアプリを作り込んだり、DBにデータをどんどん追加していくだけです!5. おわりに
githubにコードを載せておきます。
初学者のため、不十分な理解やコードのミス等があるとおもいます。もしあれば優しく教えてくいただければ幸いです。sequelizeの使い方に関しては後日別の記事を書きたいと思います。
もう少しい詳しいdockerの解説も後日別の記事を書きたいと思います。
- 投稿日:2021-02-28T02:40:20+09:00
マルチステージビルドしたらStreamlitが動かなくなった
Streamlitはpythonのアプリケーションフレームワークで、簡単にデータの可視化や機械学習モデルのアプリを作成できるて便利。
今回はStreamlitをDocker上で構築して動かしていたのですが、機械学習のDockerイメージはどうしてもイメージサイズがデカくなってしまうのでマルチステージビルドで軽くしちゃおうと思ったら動かなくなりました。というお話。
修正前
requirements.txtstreamlit plotly pandas scikit-learnFROM python:3.7 USER root EXPOSE 8501 WORKDIR /streamlit-docker COPY requirements.txt /streamlit-docker RUN pip install -r requirements.txt CMD streamlit run app.py ENV LC_ALL=C.UTF-8 ENV LANG=C.UTF-8 RUN mkdir -p /root/.streamlit RUN bash -c 'echo -e "\ [general]\n\ email = \"\"\n\ " > /root/.streamlit/credentials.toml' RUN bash -c 'echo -e "\ [server]\n\ enableCORS = false\n\ " > /root/.streamlit/config.toml'この状態でもしっかり動くのですが、今回はdockerイメージサイズを減らすために変更しました。
マルチステージビルド
FROM python:3.7-buster as builder COPY requirements.txt /streamlit-docker RUN pip install -r requirements.txt FROM python:3.7-slim-buster as runner COPY --from=builder /usr/local/lib/python3.7/site-packages /usr/local/lib/python3.7/site-packages COPY --from=builder /usr/local/bin/streamlit /usr/local/bin/streamlit WORKDIR /streamlit-docker USER root EXPOSE 8501 CMD streamlit run app.py ENV LC_ALL=C.UTF-8 ENV LANG=C.UTF-8 RUN mkdir -p /root/.streamlit RUN bash -c 'echo -e "\ [general]\n\ email = \"\"\n\ " > /root/.streamlit/credentials.toml' RUN bash -c 'echo -e "\ [server]\n\ enableCORS = false\n\ " > /root/.streamlit/config.toml'pythonのマルチステージビルドは以下の記事を参考にしました。
・Pythonの機械学習用Docker imageのサイズ削減方法の紹介
・仕事でPythonコンテナをデプロイする人向けのDockerfile (1): オールマイティ編この状態でコンテナを立ち上げるとエラーが出ます。
/bin/sh: 1: streamlit: not foundどこで引っかかってるかというと、Dockerfileのこの部分です。
CMD streamlit run app.py要するにpathを通せてないよって言われてます。
解決法
streamlitの通すパスを見つけるためにマルチステージビルド前の動くコンテナを使って調べます。
whichコマンドでコマンドのフルパスを表示させます。>> which streamlit /usr/local/bin/streamlit通すパスがわかったので、dockerfileを修正します。
1. ビルドステージから/usr/local/bin/streamlitをコピー
2. ENVコマンドでPATHを通す
これで動きました。FROM python:3.7-buster as builder WORKDIR /streamlit-docker COPY requirements.txt /streamlit-docker RUN pip install -r requirements.txt FROM python:3.7-slim-buster as runner COPY --from=builder /usr/local/lib/python3.7/site-packages /usr/local/lib/python3.7/site-packages # 追加 COPY --from=builder /usr/local/bin/streamlit /usr/local/bin/streamlit ENV PATH $PATH:/usr/local/bin/streamlit WORKDIR /streamlit-docker USER root EXPOSE 8501 CMD streamlit run app.py ENV LC_ALL=C.UTF-8 ENV LANG=C.UTF-8 RUN mkdir -p /root/.streamlit RUN bash -c 'echo -e "\ [general]\n\ email = \"\"\n\ " > /root/.streamlit/credentials.toml' RUN bash -c 'echo -e "\ [server]\n\ enableCORS = false\n\ " > /root/.streamlit/config.toml'
- 投稿日:2021-02-28T01:39:18+09:00
マルチステージビルドした時のMeCabエラーを回避する
概要
MeCabを使ったマルチステージビルドで詰まったのでメモを残します。
今回はasariというパッケージを使いたかったためmecab-python3のバージョンが低いです。
今度時間があればバージョン上げても動くか検証してみます。環境
python 3.7
mecab-python3 0.7修正前
DockerfileFROM python:3.7-buster as builder WORKDIR /work RUN apt-get update \ && apt-get install -y mecab mecab-utils libmecab-dev \ && pip install -U pip COPY requirements.txt /work RUN pip install -r requirements.txt FROM python:3.7-slim-buster as runner COPY --from=builder /usr/local/lib/python3.7/site-packages /usr/local/lib/python3.7/site-packages WORKDIR /work CMD python sample.pysample.pyの中身はmecabを使用したファイルとします
sample.pyimport MeCab実行するとimportの際にエラーが出ます。
Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/local/lib/python3.7/site-packages/MeCab.py", line 26, in <module> _MeCab = swig_import_helper() File "/usr/local/lib/python3.7/site-packages/MeCab.py", line 22, in swig_import_helper _mod = imp.load_module('_MeCab', fp, pathname, description) File "/usr/local/lib/python3.7/imp.py", line 242, in load_module return load_dynamic(name, filename, file) File "/usr/local/lib/python3.7/imp.py", line 342, in load_dynamic return _load(spec) ImportError: libmecab.so.2: cannot open shared object file: No such file or directory原因
エラー文にある通りlibmecab.so.2がないのが原因っぽいです。
一度マルチステージビルドしていない正しく動いたコンテナでどこにlibmecab.so.2があるのか調べてみます>> find / | grep libmecab /usr/share/doc/mecab/libmecab.html /usr/share/doc/libmecab-dev /usr/share/doc/libmecab-dev/changelog.Debian.gz /usr/share/doc/libmecab-dev/copyright /usr/share/doc/libmecab2 /usr/share/doc/libmecab2/changelog.Debian.gz /usr/share/doc/libmecab2/copyright /usr/lib/x86_64-linux-gnu/libmecab.so.2.0.0 /usr/lib/x86_64-linux-gnu/libmecab.a /usr/lib/x86_64-linux-gnu/libmecab.so.2 /usr/lib/x86_64-linux-gnu/libmecab.so /var/lib/dpkg/info/libmecab-dev.md5sums /var/lib/dpkg/info/libmecab-dev.list /var/lib/dpkg/info/libmecab2:amd64.triggers /var/lib/dpkg/info/libmecab2:amd64.conffiles /var/lib/dpkg/info/libmecab2:amd64.md5sums /var/lib/dpkg/info/libmecab2:amd64.list /var/lib/dpkg/info/libmecab2:amd64.shlibs色々出てきましたが、とりあえずlibmecab.so.2は/usr/lib/x86_64-linux-gnu/libmecab.so.2にあることがわかったので、これをbuild用のコンテナからコピーすれば行けそうです
修正後
DockerfileFROM python:3.7-buster as builder WORKDIR /work RUN apt-get update \ && apt-get install -y mecab mecab-utils libmecab-dev \ && pip install -U pip COPY requirements.txt /work RUN pip install -r requirements.txt FROM python:3.7-slim-buster as runner COPY --from=builder /usr/local/lib/python3.7/site-packages /usr/local/lib/python3.7/site-packages # 追加部分 COPY --from=builder /usr/lib/x86_64-linux-gnu /usr/lib/x86_64-linux-gnu WORKDIR /work CMD python sample.pyこれで動きました。