- 投稿日:2020-03-14T21:26:29+09:00
djangoとpostgresqlをDockerで接続しようとしたときに「django.db.utils.OperationalError: could not translate host name "db" to address: Name or service not known」というエラーが出る
エラー
Quickstart: Compose and Djangoを見ながらdockerでDjangoの開発環境を構築しようとして、
docker-compose up
したときに以下のエラーが出た。django.db.utils.OperationalError: could not translate host name "db" to address: Name or service not knownsetting.py
setting.pyDATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'postgres', 'USER': 'postgres', 'PASSWORD': 'postgres', 'HOST': 'db', 'PORT': 5432, } }修正前のdokcer-compose.yml
Docker-compose.ymlversion: '3' services: web: build: . command: python manage.py runserver 0.0.0.0:8000 volumes: - .:/code ports: - "8000:8000" depends_on: - db db: image: postgres修正後のdocker-compose.yml
docker-compose.ymlversion: '3' services: web: build: . command: python3 manage.py runserver 0.0.0.0:8000 volumes: - .:/code ports: - "8000:8000" depends_on: - db db: image: postgres ports: - "5432" environment: - POSTGRES_DB=postgres - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgresこれで、エラーが解消しました。
Docker内でpostgresを使用するには、データベースユーザー、パスワード、db-nameなどの情報を構成する必要があります。これは、修正後のdokcer-compose.yml
にあるenvironment
においてコンテナの環境変数を設定することにより行うことができる。
- 投稿日:2020-03-14T19:43:37+09:00
CakePHP4 を docker-compose で動くようにする。
CakePHP4 もリリースされて時間が立ちました。
そろそろどんなものか触っていこうと思います。でも、そのためには動かさないといけないので、まずは動かすところまでやってみます。
この記事でわかること
- CakePHP4を nginx・php-fpm・MySQL で Mac(ローカルPC)上で動かせるようになります。
- この記事内のソースは以下で公開しています。
事前準備
- Docker Desktop のインストール
僕の Mac に入っている Dockerの各種バージョンは以下です。
バージョン docker Docker version 19.03.5, build 633a0ea docker-compose docker-compose version 1.25.4, build 8d51620a CakePHP プロジェクトの作成
ローカル環境に依存したくないで docker を使って CakePHP のプロジェクトも作ります。
CakePHP4 のプロジェクトを作るディレクトリに入れて、一時的なコンテナを立ち上げて入ります。
一時的なコンテナが独自なのは、最低限必要なライブラリを入れているためです。
( https://github.com/katsuhiko/docker-php-fpm-base/blob/master/Dockerfile )php:7.4-fpm-buster で立ち上げて自身でライブラリ(zip, intlが最低限必要なライブラリかな!?)をインストールしてもOKです。
cd . docker run --rm -it -v "$(pwd):/home/app" -w /home/app katsuhikonagashima/php-fpm-base:7.4-buster /bin/bashここからはコンテナ内での作業になります。
composer を取得して、CakePHP4プロジェクトを作ります。
curl はすでに入っているはずです。
composer.phar は開発中に使うことになるので、作成したプロジェクトにコピーして含めるようにしています。curl -sS https://getcomposer.org/installer | php php composer.phar create-project --prefer-dist cakephp/app:4.* cakephp-vue-study cp composer.phar ./cakephp-vue-study/ exitここからは、作成したプロジェクト内での作業になるので移動します。
cd ./cakephp-vue-study/docker-compose の準備
どこで docker 系のファイルを準備するかですが、
僕は./docker/local/
配下に置くのが好きです。
本番にデプロイするファイルとわけることが多いのでlocal
というワンクッションをおいてます。
本当は本番・ローカルでファイルを別けなくないんだけど、ややこしくなっちゃうんですよね。。。この点は勉強中です。php-fpm の準備
./docker/local/php-fpm/Dockerfile
を作成します。zip, mysql, intl, gd を入れています。開発によって追加するライブラリがあると思うので、 php-fpm は Dockerfile を作ります。
FROM php:7.4-fpm-buster RUN apt-get update RUN apt-get install -y git libzip-dev zip unzip \ && docker-php-ext-install zip RUN docker-php-ext-install pdo_mysql RUN apt-get install -y libicu-dev \ && docker-php-ext-configure intl \ && docker-php-ext-install intl RUN apt-get install -y libfreetype6-dev libjpeg62-turbo-dev libpng-dev \ && docker-php-ext-configure gd --with-freetype --with-jpeg \ && docker-php-ext-install gdnginx の準備
./docker/local/nginx/default.conf
を作成します。Cookbook ( https://book.cakephp.org/4/en/installation.html#nginx )の内容そのままに近いです。
fastcgi_pass
のところのapp
は、 php-fpm のコンテナ名です。後ほど docker-compose.yml で指定します。server { listen 80; listen [::]:80; server_name _; root /var/www/html/webroot; index index.php; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; location / { try_files $uri $uri/ /index.php$is_args$args; } location ~ \.php$ { try_files $uri =404; include fastcgi_params; fastcgi_pass app:9000; fastcgi_index index.php; fastcgi_intercept_errors on; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }MySQL の準備
./docker/local/mysql/etc-mysql.cnf
を作成します。マルチバイトの対応しているのみです。昨今絵文字(?とか)も保存したいからね。
[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_general_ci [client] default-character-set=utf8mb4docker-compose.yml の作成
./docker-compose.yml
を作成します。
networks
の設定は趣味みたいな感じです。複数のアプリケーションでDBを共有したいっというときには役立つんですが、現時点では不要ですね。
container_name
を指定するとdocker exec
等でのコンテナ名指定が固定化されるので好んで指定しています。php-fpm を
app
としている点が先程の./docker/local/nginx/default.conf
の記述に関連しているところです。
nginx は、追加でライブラリを入れることはないのでコンテナサイズ重視で alpine にしています。
mysql が 5.7 なのは、実業務では AWS Aurora を使うことが多いためです。version: '3.5' networks: frontend: driver: bridge backend: driver: bridge volumes: db-data: driver: local services: web: image: nginx:1.16-alpine container_name: web ports: - 80:80 volumes: - ./docker/local/nginx/default.conf:/etc/nginx/conf.d/default.conf - ./:/var/www/html networks: - frontend - backend app: build: ./docker/local/php-fpm container_name: app volumes: - ./:/var/www/html networks: - backend db: image: mysql:5.7 container_name: db ports: - 3306:3306 environment: - MYSQL_DATABASE=default - MYSQL_USER=default - MYSQL_PASSWORD=secret - MYSQL_ROOT_PASSWORD=root volumes: - ./docker/local/mysql/etc-mysql.cnf:/etc/mysql/conf.d/etc-mysql.cnf - db-data:/var/lib/mysql networks: - backendDB 接続設定
./config/app_local.php
のDB接続情報を変更します。
host, username, detabase の箇所を変更しています。
./config/app_local.php
はソース管理のリポジトリには含めないファイルになるので./config/app_local.example.php
にも同じ変更を反映しておきましょう。'Datasources' => [ 'default' => [ 'host' => 'db', 'username' => 'default', 'password' => 'secret', 'database' => 'default', 'url' => env('DATABASE_URL', null), ], 'test' => [ 'host' => 'db', 'username' => 'default', 'password' => 'secret', 'database' => 'test_myapp', ], ],docker-compose の実行
初回の起動は時間がかかりますが、2回目からの起動からはイメージがキャッシュされるので速いです。
docker-compose up -d確認
http://localhost へ接続するといつもの CakePHP のトップページが表示されます。
これで開発の準備ができました。
補足) Github へアップする
git init git add --all git commit -m "create cakephp4 project." git remote add origin https://github.com/katsuhiko/cakephp-vue-study.git git push -u origin master補足) 別のローカル環境で始めるとき
先にコンテナを起動してから、composer install してライブラリを取り込むのがポイントです。
git clone https://github.com/katsuhiko/cakephp-vue-study.git cd ./cakephp-vue-study cp ./config/.env.example ./config/.env cp ./config/app_local.example.php ./config/app_local.php docker-compose up -d docker exec -it app php composer.phar install
- 投稿日:2020-03-14T19:43:37+09:00
CakePHP4 を docker-compose で動くようにする
CakePHP4 もリリースされて時間が立ちました。
そろそろどんなものか触っていこうと思います。でも、そのためには動かさないといけないので、まずは動かすところまでやってみます。
この記事でわかること
- CakePHP4を nginx・php-fpm・MySQL で Mac(ローカルPC)上で動かせるようになります。
- この記事内のソースは以下で公開しています。
事前準備
- Docker Desktop のインストール
僕の Mac に入っている Dockerの各種バージョンは以下です。
バージョン docker Docker version 19.03.5, build 633a0ea docker-compose docker-compose version 1.25.4, build 8d51620a CakePHP4 4.0.4 CakePHP プロジェクトの作成
ローカル環境に依存したくないで docker を使って CakePHP のプロジェクトも作ります。
CakePHP4 のプロジェクトを作るディレクトリに入れて、一時的なコンテナを立ち上げて入ります。
一時的なコンテナが独自なのは、最低限必要なライブラリを入れているためです。
( https://github.com/katsuhiko/docker-php-fpm-base/blob/master/Dockerfile )php:7.4-fpm-buster で立ち上げて自身でライブラリ(zip, intlが最低限必要なライブラリかな!?)をインストールしてもOKです。
※ Windows の場合、試してないですが PowerShell を使えば動くと思います(そのために "" で囲んでるんで)。動かない場合、
$(pwd)
の箇所を${pwd}
にすると良いのでないかなっと思います。たぶん。cd . docker run --rm -it -v "$(pwd):/home/app" -w /home/app katsuhikonagashima/php-fpm-base:7.4-buster /bin/bashここからはコンテナ内での作業になります。
composer を取得して、CakePHP4プロジェクトを作ります。
curl はすでに入っているはずです。
composer.phar は開発中に使うことになるので、作成したプロジェクトにコピーして含めるようにしています。curl -sS https://getcomposer.org/installer | php php composer.phar create-project --prefer-dist cakephp/app:4.* cakephp-vue-study cp composer.phar ./cakephp-vue-study/ exitここからはコンテナから出て、ホスト側で作成したプロジェクト内での作業になります。
まずは、プロジェクトのディレクトリへ移動します。cd ./cakephp-vue-study/
docker-compose の準備
どこで docker 系のファイルを準備するかですが、僕は
./docker/local/
配下に置くのが好きです。
本番へデプロイするファイルとわけることが多いのでlocal
というワンクッションをおいてます。
本当は本番・ローカルでファイルを別けたくないんだけど、ややこしくなっちゃうんですよね。。。この点は勉強中です。php-fpm の準備
./docker/local/php-fpm/Dockerfile
を作成します。zip, mysql, intl, gd を入れています。開発によって追加するライブラリがあると思うので、 php-fpm は Dockerfile を作ります。
buster(Debian) にしているのは、 apt を使うことに慣れているからだけです。ライブラリを追加するときに手間取りたくないからね。docker/local/php-fpm/DockerfileFROM php:7.4-fpm-buster RUN apt-get update RUN apt-get install -y git libzip-dev zip unzip \ && docker-php-ext-install zip RUN docker-php-ext-install pdo_mysql RUN apt-get install -y libicu-dev \ && docker-php-ext-configure intl \ && docker-php-ext-install intl RUN apt-get install -y libfreetype6-dev libjpeg62-turbo-dev libpng-dev \ && docker-php-ext-configure gd --with-freetype --with-jpeg \ && docker-php-ext-install gdnginx の準備
./docker/local/nginx/default.conf
を作成します。Cookbook ( https://book.cakephp.org/4/en/installation.html#nginx )の内容そのままに近いです。
fastcgi_pass
のところのapp
は php-fpm コンテナのサービス名です。後ほど docker-compose.yml で指定します。docker/local/nginx/default.confserver { listen 80; listen [::]:80; server_name _; root /var/www/html/webroot; index index.php; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; location / { try_files $uri $uri/ /index.php$is_args$args; } location ~ \.php$ { try_files $uri =404; include fastcgi_params; fastcgi_pass app:9000; fastcgi_index index.php; fastcgi_intercept_errors on; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }MySQL の準備
./docker/local/mysql/etc-mysql.cnf
を作成します。マルチバイトの対応をしているのみです。昨今絵文字(?とか)も保存したいですからね。
docker/local/mysql/etc-mysql.cnf[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_general_ci [client] default-character-set=utf8mb4docker-compose.yml の作成
./docker-compose.yml
を作成します。
networks
の設定は趣味みたいな感じです。複数のアプリケーションでDBを共有したいっというときには役立つんですが、現時点では不要ですね。
container_name
を指定するとdocker exec
等でのコンテナ名指定が固定化されるので好んで指定しています。php-fpm を
app
としている点が先程の./docker/local/nginx/default.conf
の記述に関連しているところです。
nginx は、追加でライブラリを入れることはないのでコンテナサイズ重視で alpine にしています。
mysql が 5.7 なのは、実業務では AWS Aurora を使うことが多いためです。docker-compose.ymlversion: '3.5' networks: frontend: driver: bridge backend: driver: bridge volumes: db-data: driver: local services: web: image: nginx:1.16-alpine container_name: web ports: - 80:80 volumes: - ./docker/local/nginx/default.conf:/etc/nginx/conf.d/default.conf - ./:/var/www/html networks: - frontend - backend app: build: ./docker/local/php-fpm container_name: app volumes: - ./:/var/www/html networks: - backend db: image: mysql:5.7 container_name: db ports: - 3306:3306 environment: - MYSQL_DATABASE=default - MYSQL_USER=default - MYSQL_PASSWORD=secret - MYSQL_ROOT_PASSWORD=root volumes: - ./docker/local/mysql/etc-mysql.cnf:/etc/mysql/conf.d/etc-mysql.cnf - db-data:/var/lib/mysql networks: - backendDB 接続設定
./config/app_local.php
のDB接続情報を変更します。
host, username, detabase の箇所を変更しています。
host にしているdb
は mysql コンテナのサービス名です。docker-compose.yml で記載しています。
./config/app_local.php
はソース管理のリポジトリには含めないファイルになるので./config/app_local.example.php
にも同じ変更を反映しておきましょう。config/app_local.php'Datasources' => [ 'default' => [ 'host' => 'db', 'username' => 'default', 'password' => 'secret', 'database' => 'default', 'url' => env('DATABASE_URL', null), ], 'test' => [ 'host' => 'db', 'username' => 'default', 'password' => 'secret', 'database' => 'test_myapp', ], ],docker-compose の実行
初回の起動は時間がかかりますが、2回目からの起動からはイメージがキャッシュされるので速いです。
docker-compose up -d
動作確認
http://localhost へアクセスするといつもの CakePHP のトップページが表示されます。
Database のところがきちんと緑色になって接続できていることを確認してください。
これで開発の準備ができました。補足) Github へアップする
git init git add --all git commit -m "create cakephp4 project." git remote add origin https://github.com/katsuhiko/cakephp-vue-study.git git push -u origin master補足) 別のローカル環境で始めるとき
先にコンテナを起動してから、composer install してライブラリを取り込むのがポイントです。
コンテナ名を app 指定にしたから、docker exec
するときスッキリしてるんじゃないかなっと思います。git clone https://github.com/katsuhiko/cakephp-vue-study.git cd ./cakephp-vue-study cp ./config/.env.example ./config/.env cp ./config/app_local.example.php ./config/app_local.php docker-compose up -d docker exec -it app php composer.phar install
- 投稿日:2020-03-14T19:25:01+09:00
envoyのgRPC-JSON transcoderをローカルで試す
はじめに
HTTP/JSONからgRPCへのtranscodingを行うには大まかに次の2種類の方法があります。
- gRPC gatewayを使用する。
- gRPC-JSON transcoderを使用する。
前者はプラグインとして提供されていてHTTPリバースプロキシを作成することになります。これはgolangにしか対応されていません。
後者についてはgrpc-httpjson-transcodingというライブラリがありIstioやGoogle cloud endpointで現在も使用されています。
envoy proxy単体でもgRPC serviceにHTTP/JSONインタフェースを適用することで実行可能です。今回はenvoy, gRPCサーバをDockerコンテナとして立ててgRPC-JSON transcoderのテストを行います。HTTPリクエストはGET/POSTを試しています。
コードはGitHubにあげてあります。
環境
macOS Mojave 10.14.1
docker 18.09.2
docker-compose 1.23.2実装
ビルド前に用意しておくファイルは次のようになります。envoyについては公式のDockerイメージを使用するので自分でDockerfileは作成しません。
├── docker-compose.yaml ├── pb │ └── helloworld.proto ├── server │ ├── Dockerfile │ └── main.go └── envoy └── envoy.yaml
まずはdocker-compose.yaml
です。こちらはgRPCサーバとenvoyを定義しています。
envoyとgRPCサーバの間はコンテナ間通信を行うのでlinks
を設定します。docker-compose.yamlversion: '3.7' services: server: build: ./server image: grpc-server:latest container_name: grpc-server volumes: - './server:/go/src/server' - './pb:/go/src/pb' expose: - 50051 command: - /go/src/server/bin/server envoy: image: envoyproxy/envoy:v1.13.1 volumes: - './envoy:/etc/envoy' - './pb:/etc/pb' expose: - 51051 ports: - 51051:51051 links: - server volumes: data: driver: 'local'
次にprotoファイルです。GETとPOSTの2つ分定義しています。pb/helloworld.protosyntax = "proto3"; package helloworld; import "google/api/annotations.proto"; service Greeter { rpc GetTest(HelloRequest) returns (HelloResponse) { option (google.api.http) = { get: "/hello" }; } rpc PostTest(HelloRequest) returns (HelloResponse) { option (google.api.http) = { post: "/hello" body: "*" }; } } message HelloRequest { string name = 1; } message HelloResponse { string message = 1; }
次にgRPCサーバのDockerfile
です。alpineをベースイメージにして必要となるものをインストールしています。server/DockerfileFROM alpine:3.8 RUN apk add --no-cache curl git go unzip musl-dev libc6-compat ENV PROTOBUF_VERSION 3.11.4 RUN curl -sL https://github.com/google/protobuf/releases/download/v${PROTOBUF_VERSION}/protoc-${PROTOBUF_VERSION}-linux-x86_64.zip -o /tmp/protoc.zip && \ unzip /tmp/protoc.zip -d /tmp && \ cp -r /tmp/bin /tmp/include /usr/local/ && \ rm -rf /tmp/* ENV GOPATH /go ENV PATH $PATH:$GOPATH/bin:/usr/local/go/bin RUN mkdir /go && \ go get -u google.golang.org/grpc && \ go get -u github.com/golang/protobuf/protoc-gen-go && \ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway && \ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
main.go
ではprotoファイルに書かれた内容を元にGETとPOSTの関数を定義します。
GETのときにはHello World
、POSTのときにはHello <NAME>
となるようにしました。
実行には次章で後述するpb.goファイルが必要となります。server/main.gopackage main import ( "log" "net" pb "pb" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/reflection" ) const ( port = ":50051" ) type server struct{} func (s *server) GetTest(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) { return &pb.HelloResponse{Message: "Hello World"}, nil } func (s *server) PostTest(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) { return &pb.HelloResponse{Message: "Hello " + in.Name}, nil } func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterGreeterServer(s, &server{}) reflection.Register(s) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
最後にenvoyの設定ファイルenvoy.yaml
です。
コンテナ間通信のため、host名(address)は宛先のコンテナ名であるgrpc-server
を指定しています。envoy/envoy.yamladmin: access_log_path: /tmp/admin_access.log address: socket_address: { address: 0.0.0.0, port_value: 9901 } static_resources: listeners: - name: listener1 address: socket_address: { address: 0.0.0.0, port_value: 51051 } filter_chains: - filters: - name: envoy.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager stat_prefix: grpc_json codec_type: AUTO route_config: name: local_route virtual_hosts: - name: local_service domains: ["*"] routes: - match: { prefix: "/" } route: { cluster: grpc, timeout: { seconds: 60 } } http_filters: - name: envoy.grpc_json_transcoder config: proto_descriptor: "/etc/pb/helloworld.pb" services: ["helloworld.Greeter"] print_options: add_whitespace: true always_print_primitive_fields: true always_print_enums_as_ints: false preserve_proto_field_names: false - name: envoy.router clusters: - name: grpc connect_timeout: 1.25s type: logical_dns lb_policy: round_robin dns_lookup_family: V4_ONLY http2_protocol_options: {} load_assignment: cluster_name: grpc endpoints: - lb_endpoints: - endpoint: address: socket_address: address: grpc-server port_value: 50051サーバ設定
envoyとgRPCサーバを起動する前にpbファイル作成とgolangファイルビルドを行います。
最終的なフォルダ構成は次のようになります。(*)が付いたファイルが新たに作成されるものです。
├── docker-compose.yaml ├── pb │ ├── helloworld.proto │ ├── helloworld.pb (*) │ └── helloworld.pb.go (*) ├── server │ ├── Dockerfile │ ├── main.go │ └── bin │ └── server (*) └── envoy └── envoy.yaml
まずはgRPCサーバをコンテナとして起動します。$ docker-compose run --rm server /bin/sh
protoファイルからpbファイルを作成します。これはenvoyで使用されるバイナリファイルです。# protoc \ -I $GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ -I /go/src/pb/ \ --include_imports \ --include_source_info \ --descriptor_set_out=/go/src/pb/helloworld.pb \ helloworld.proto
protoファイルからpb.goファイルを作成します。これはgRPCサーバで使用されます。# protoc \ -I $GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ --proto_path /go/src/pb \ --go_out=plugins=grpc:/go/src/pb \ helloworld.proto
次にgo buildを行いサーバ起動用のバイナリファイルを作成します。# cd /go/src/server && go build -i -v -o bin/server
exitします。コンテナ起動時のオプションに--rm
をつけていたためコンテナは自動で削除されます。# exitサーバ起動
docker-composeでenvoyとgRPCサーバ両方を起動します。
$ docker-compose up -d確認
まずはGETリクエストをenvoyに対して送ります。
Hello World
がレスポンスとして返ってくることが確認できます。$ curl http://localhost:51051/hello -v * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 51051 (#0) > GET /hello HTTP/1.1 > Host: localhost:51051 > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 200 OK < content-type: application/json < x-envoy-upstream-service-time: 8 < grpc-status: 0 < grpc-message: < content-length: 30 < date: Sat, 14 Mar 2020 07:31:18 GMT < server: envoy < { "message": "Hello World" } * Connection #0 to host localhost left intact
次にPOSTリクエストを送ります。リクエストボディに含めた{"name":"samskeyti88"}
がレスポンスに反映されることが確認できます。$ curl -X POST http://localhost:51051/hello -d '{"name":"samskeyti88"}' -v Note: Unnecessary use of -X or --request, POST is already inferred. * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 51051 (#0) > POST /hello HTTP/1.1 > Host: localhost:51051 > User-Agent: curl/7.54.0 > Accept: */* > Content-Length: 22 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 22 out of 22 bytes < HTTP/1.1 200 OK < content-type: application/json < x-envoy-upstream-service-time: 0 < grpc-status: 0 < grpc-message: < content-length: 36 < date: Sat, 14 Mar 2020 07:50:14 GMT < server: envoy < { "message": "Hello samskeyti88" } * Connection #0 to host localhost left intactおわりに
envoyのgRPC-JSON transcoderを使用してHTTPリクエストをgRPCに変換できることをDockerコンテナを用いて確認しました。
自分でこの変換を実装するよりははるかに楽にできることが分かります。参考
gRPC-JSON transcoder
How to build a REST API with gRPC and get the best of two worlds
JimmyCYJ/grpc-transcoding-experiment
- 投稿日:2020-03-14T19:25:01+09:00
envoyのgRPC-JSON transcoderをローカルでテストする
はじめに
HTTP/JSONからgRPCへのtranscodingを行うには大まかに次の2種類の方法があります。
- gRPC gatewayを使用する。
- gRPC-JSON transcoderを使用する。
前者はプラグインとして提供されていてHTTPリバースプロキシを作成することになります。これはgolangにしか対応されていません。
後者についてはgrpc-httpjson-transcodingというライブラリがありIstioやGoogle cloud endpointで現在も使用されています。
envoy proxy単体でもgRPC serviceにHTTP/JSONインタフェースを適用することで実行可能です。今回はenvoy, gRPCサーバをDockerコンテナとして立ててgRPC-JSON transcoderのテストを行います。HTTPリクエストはGET/POSTを試しています。
コードはGitHubにあげてあります。
環境
macOS Mojave 10.14.1
docker 18.09.2
docker-compose 1.23.2実装
ビルド前に用意しておくファイルは次のようになります。envoyについては公式のDockerイメージを使用するので自分でDockerfileは作成しません。
├── docker-compose.yaml ├── pb │ └── helloworld.proto ├── server │ ├── Dockerfile │ └── main.go └── envoy └── envoy.yaml
まずはdocker-compose.yaml
です。こちらはgRPCサーバとenvoyを定義しています。
envoyとgRPCサーバの間はコンテナ間通信を行うのでlinks
を設定します。docker-compose.yamlversion: '3.7' services: server: build: ./server image: grpc-server:latest container_name: grpc-server volumes: - './server:/go/src/server' - './pb:/go/src/pb' expose: - 50051 command: - /go/src/server/bin/server envoy: image: envoyproxy/envoy:v1.13.1 volumes: - './envoy:/etc/envoy' - './pb:/etc/pb' expose: - 51051 ports: - 51051:51051 links: - server volumes: data: driver: 'local'
次にprotoファイルです。GETとPOSTの2つ分定義しています。pb/helloworld.protosyntax = "proto3"; package helloworld; import "google/api/annotations.proto"; service Greeter { rpc GetTest(HelloRequest) returns (HelloResponse) { option (google.api.http) = { get: "/hello" }; } rpc PostTest(HelloRequest) returns (HelloResponse) { option (google.api.http) = { post: "/hello" body: "*" }; } } message HelloRequest { string name = 1; } message HelloResponse { string message = 1; }
次にgRPCサーバのDockerfile
です。alpineをベースイメージにして必要となるものをインストールしています。server/DockerfileFROM alpine:3.8 RUN apk add --no-cache curl git go unzip musl-dev libc6-compat ENV PROTOBUF_VERSION 3.11.4 RUN curl -sL https://github.com/google/protobuf/releases/download/v${PROTOBUF_VERSION}/protoc-${PROTOBUF_VERSION}-linux-x86_64.zip -o /tmp/protoc.zip && \ unzip /tmp/protoc.zip -d /tmp && \ cp -r /tmp/bin /tmp/include /usr/local/ && \ rm -rf /tmp/* ENV GOPATH /go ENV PATH $PATH:$GOPATH/bin:/usr/local/go/bin RUN mkdir /go && \ go get -u google.golang.org/grpc && \ go get -u github.com/golang/protobuf/protoc-gen-go && \ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway && \ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
main.go
ではprotoファイルに書かれた内容を元にGETとPOSTの関数を定義します。
GETのときにはHello World
、POSTのときにはHello <NAME>
となるようにしました。
実行には次章で後述するpb.goファイルが必要となります。server/main.gopackage main import ( "log" "net" pb "pb" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/reflection" ) const ( port = ":50051" ) type server struct{} func (s *server) GetTest(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) { return &pb.HelloResponse{Message: "Hello World"}, nil } func (s *server) PostTest(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) { return &pb.HelloResponse{Message: "Hello " + in.Name}, nil } func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterGreeterServer(s, &server{}) reflection.Register(s) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
最後にenvoyの設定ファイルenvoy.yaml
です。
コンテナ間通信のため、host名(address)は宛先のコンテナ名であるgrpc-server
を指定しています。envoy/envoy.yamladmin: access_log_path: /tmp/admin_access.log address: socket_address: { address: 0.0.0.0, port_value: 9901 } static_resources: listeners: - name: listener1 address: socket_address: { address: 0.0.0.0, port_value: 51051 } filter_chains: - filters: - name: envoy.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager stat_prefix: grpc_json codec_type: AUTO route_config: name: local_route virtual_hosts: - name: local_service domains: ["*"] routes: - match: { prefix: "/" } route: { cluster: grpc, timeout: { seconds: 60 } } http_filters: - name: envoy.grpc_json_transcoder config: proto_descriptor: "/etc/pb/helloworld.pb" services: ["helloworld.Greeter"] print_options: add_whitespace: true always_print_primitive_fields: true always_print_enums_as_ints: false preserve_proto_field_names: false - name: envoy.router clusters: - name: grpc connect_timeout: 1.25s type: logical_dns lb_policy: round_robin dns_lookup_family: V4_ONLY http2_protocol_options: {} load_assignment: cluster_name: grpc endpoints: - lb_endpoints: - endpoint: address: socket_address: address: grpc-server port_value: 50051サーバ設定
envoyとgRPCサーバを起動する前にpbファイル作成とgolangファイルビルドを行います。
最終的なフォルダ構成は次のようになります。(*)が付いたファイルが新たに作成されるものです。
├── docker-compose.yaml ├── pb │ ├── helloworld.proto │ ├── helloworld.pb (*) │ └── helloworld.pb.go (*) ├── server │ ├── Dockerfile │ ├── main.go │ └── bin │ └── server (*) └── envoy └── envoy.yaml
まずはgRPCサーバをコンテナとして起動します。$ docker-compose run --rm server /bin/sh
protoファイルからpbファイルを作成します。これはenvoyで使用されるバイナリファイルです。# protoc \ -I $GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ -I /go/src/pb/ \ --include_imports \ --include_source_info \ --descriptor_set_out=/go/src/pb/helloworld.pb \ helloworld.proto
protoファイルからpb.goファイルを作成します。これはgRPCサーバで使用されます。# protoc \ -I $GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ --proto_path /go/src/pb \ --go_out=plugins=grpc:/go/src/pb \ helloworld.proto
次にgo buildを行いサーバ起動用のバイナリファイルを作成します。# cd /go/src/server && go build -i -v -o bin/server
exitします。コンテナ起動時のオプションに--rm
をつけていたためコンテナは自動で削除されます。# exitサーバ起動
docker-composeでenvoyとgRPCサーバ両方を起動します。
$ docker-compose up -dテスト
まずはGETリクエストをenvoyに対して送ります。
Hello World
がレスポンスとして返ってくることが確認できます。$ curl http://localhost:51051/hello -v * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 51051 (#0) > GET /hello HTTP/1.1 > Host: localhost:51051 > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 200 OK < content-type: application/json < x-envoy-upstream-service-time: 8 < grpc-status: 0 < grpc-message: < content-length: 30 < date: Sat, 14 Mar 2020 07:31:18 GMT < server: envoy < { "message": "Hello World" } * Connection #0 to host localhost left intact
次にPOSTリクエストを送ります。リクエストボディに含めた{"name":"samskeyti88"}
がレスポンスに反映されることが確認できます。$ curl -X POST http://localhost:51051/hello -d '{"name":"samskeyti88"}' -v Note: Unnecessary use of -X or --request, POST is already inferred. * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 51051 (#0) > POST /hello HTTP/1.1 > Host: localhost:51051 > User-Agent: curl/7.54.0 > Accept: */* > Content-Length: 22 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 22 out of 22 bytes < HTTP/1.1 200 OK < content-type: application/json < x-envoy-upstream-service-time: 0 < grpc-status: 0 < grpc-message: < content-length: 36 < date: Sat, 14 Mar 2020 07:50:14 GMT < server: envoy < { "message": "Hello samskeyti88" } * Connection #0 to host localhost left intactおわりに
envoyのgRPC-JSON transcoderを使用してHTTPリクエストをgRPCに変換できることをDockerコンテナを用いて確認しました。
自分でこの変換を実装するよりははるかに楽にできることが分かります。参考
gRPC-JSON transcoder
How to build a REST API with gRPC and get the best of two worlds
JimmyCYJ/grpc-transcoding-experiment
- 投稿日:2020-03-14T18:37:43+09:00
DockerHub Dockerイメージのタグ一覧を取得する
Dockerイメージの検索は
docker search
コマンドが用意されているがイメージのタグ情報は検索できないようです...(困惑)タグを取得するAPIは提供されていたのでそれを使ってDockerイメージのタグ一覧を取得します。
環境
- Mac
Dockerイメージの検索
$ docker search ubuntu NAME DESCRIPTION STARS OFFICIAL AUTOMATED ubuntu Ubuntu is a Debian-based Linux operating sys… 10614 [OK] dorowu/ubuntu-desktop-lxde-vnc Docker image to provide HTML5 VNC interface … 404 [OK] rastasheep/ubuntu-sshd Dockerized SSH service, built on top of offi… 243 [OK] consol/ubuntu-xfce-vnc Ubuntu container with "headless" VNC session… 211 [OK] ubuntu-upstart Upstart is an event-based replacement for th… 106 [OK] ...前提条件
- curl, jq コマンドが必要です。
$ brew install jqイメージのタグを取得する
ubuntu イメージのタグは下記のAPIから取得できます。
イメージ名のところを引数にしてコマンドを作れば良さそうです。
.bash_profilefunction docker-tags { curl -s https://registry.hub.docker.com/v1/repositories/$1/tags | jq -r '.[].name' }自作した
docker-tags
コマンドを実行してみる。$ docker-tags ubuntu latest 10.04 12.04 12.04.5 12.10 13.04 13.10 14.04 14.04.1 14.04.2 14.04.3 14.04.4 14.04.5 ...良い感じです☺️
- 投稿日:2020-03-14T17:07:35+09:00
【Laradock環境構築】Dockerインストール〜Laravelデフォルトページ表示まで②
はじめに
こちらの記事では、Laradockの環境構築として、
Dockerのインストール〜Laravelのデフォルトページ表示までを載せていきます。
※本記事はパート①の続きになります。
↓
【Laradock環境構築】Dockerインストール〜Laravelデフォルトページ表示まで①
https://qiita.com/y-aimi/items/4fc3c65f85a103685238前提条件
パート①でDockerのインストールが終わっている状態
プロジェクト用のディレクトリを作成
開発するアプリケーション用のディレクトリを作成します。
本記事ではecsiteとします。
以下のコマンドを実行。
//mkdirでディレクトリを作成。cdでecsiteのディレクトリに移動 $ mkdir ecsite $ cd ecsiteちなみに、どこでこのコマンドを実行すればいいの?という方は、
// ~ はルートディレクトリを表している $ cd ~でルートディレクトに移動すれば、下記の場所にecsiteというフォルダができるので
そちらで問題ないかと思います。リポジトリのクローンを作成
LaradockのGithubのリポジトリをクローンします。
クローンしたらlaradockディレクトリに移動します。以下のコマンドを実行。
$ git clone https://github.com/Laradock/laradock.git $ cd laradock試しに、
https://github.com/Laradock/laradock.git
をクリックしてGithubを見てみてください。ここのクローンを先程作ったecsiteのディレクトリに作っているよー
ということですね!
そして、そのディレクトリに移動したということです。Laradockの設定ファイル準備
Laradockのenv-exampleを.envとしてコピーします。
/ecsite/laradock/で以下のコマンドを実行してください。
$ cp env-example .env(cp はコピーという意味です。まんまですね。)
以下のコマンドを実行し、/ecsite/laradock/直下に.envファイルがあることを確認。
$ ls次に作成した.envファイルをVimを使って編集していきます。
(Vimを使えば、いちいちフォルダを探して、ファイルを探して開いて編集・・・
をしなくてもターミナル上で編集できるのでラクです!)/ecsite/laradock/で以下のコマンドを実行。
$ vi .envするとターミナルでこんな画面がでます。
この画面は/ecsite/laradock/.envの編集画面です。
この画面上で i を押します。
下部に-- INSERT --と編集できるモードに切り替わるので、
この状態でファイルを編集していきます。/ecsite/laradock/.envを以下のように編集してください。
### Paths ################################################# # Point to the path of your applications code on your host # 次の行を編集 APP_CODE_PATH_HOST=../ecsite # Point to where the `APP_CODE_PATH_HOST` should be in the container APP_CODE_PATH_CONTAINER=/var/wwwAPP_CODE_PATH_HOSTの値が、
Laradockで動かすウェブアプリのディレクトリのパスになります。
(つまり、Laradockで動かす時にどのフォルダを使うの?それの場所をここに書いてね。ということです。)次にescキーを押してコマンドモードに移行し、:wqを押し、
enterキーでファイルへの変更を保存し終了。
(":w"は保存、"q"は終了のVimコマンド→合わせて":wq"コマンドというわけですね)次にウェブアプリのディレクトリを作っておきます。
以下のコマンドを実行。
$ mkdir ../ecsiteこれで、現在のディレクトリは下記のようになっているかと思います。
ecsite ├── ecsite ←今作ったウェブアプリ用のディレクトリ └── laradock ←序盤にクローンしたlaradock.gitのクローンmysqlの認証方法の設定
mysqlのバージョンによっては、認証方法がデフォルトでは
なくなってしまっていることがあるので、
※変更の必要ない人は必要ないです。
(ちなみに私も変更しなくても良かったです。)/ecsite/laradock/ディレクトリ内で以下のコマンドを実行し、vimを起動。
$ vi mysql/my.cnf先程と同じように、iを押してINSERTモードに切り替え、
/ecsite/laradock/mysql/my.cnfを以下のように編集。# The MySQL Client configuration file. # # For explanations see # http://dev.mysql.com/doc/mysql/en/server-system-variables.html [mysql] [mysqld] sql-mode="STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION" character-set-server=utf8 # この行を追加(この文が元々ある人もいるかも?) default_authentication_plugin=mysql_native_passwordescキーを押してコマンドモードに移行し、
:wqを入力しenterキーでファイルの変更を保存し終了。これでmysqlの準備は終了です。
Laradockのコンテナ起動
Laradockを使ってあらかじめ用意されている、
ウェブサーバー(nginx)とデータベースサーバー(mysql)のコンテナを起動します。/ecsite/laradock/ディレクトリ内で以下のコマンドを実行します。
$ docker-compose up -d nginx mysqlそして、下記のように全部"done"になれば無事起動されています。
これで、ローカルのウェブサーバーとデータベースが起動しました!
workspaceコンテナのパッケージアップデート
つぎにworkspaceコンテナに入り、パッケージのアップデートをしておきます。
workspaceコンテナは開発に必要なツール等がインストールされているコンテナらしいです。
(Laradockのコンテナを起動すると自動的にworkspaceコンテナも起動します)以下のコマンドでパッケージをアップデート。
$ docker exec -it laradock_workspace_1 bash root@hoge:/var/www# apt-get update(root@hogeの"hoge"は個人で違うかと思います。)
Laravelのセットアップ
最後にlaravelのセットアップをします。
workspaceコンテナに入ったまま、Laravelをインストール。//こちらを参考にプロジェクト名を入れて実行 root@hoge:/var/www# composer create-project laravel/laravel [プロジェクト名] //実例① root@hoge:/var/www# composer create-project laravel/laravel ECsite //実例②:laravelのバージョンを指定する際はこちらを参考に root@hoge:/var/www# composer create-project laravel/laravel ECsite "5.8.*"動作確認
下記のURLでLaravelのデフォルトページが表示されれば環境構築成功です!
エラー多発した!!
この環境構築、個人個人によって色んなエラーが出ることが多いです。
(エラーで苦戦して、やっぱLaradock使うのやめよう...と思った人もいるはず)ちなみに、個人的にあるあるなのが、
Nginx 404not found
でデフォルトページが表示されないエラー。おそらく原因は、/ecsite/laradock/.envで編集したこちらが正しくない可能性が高いです。
例えば、ディレクトリの構成を変えていたり、置く場所を変えていたりすると、
ここも変えなければいけません。重要!
あ、エラーになってしまった!
↓
よし、これを修正したら大丈夫なはず!!
↓
...え、また同じエラー??
↓
...え、しかも何かエラー増えてる?
↓
.........わからん。
↓
............オワタ。詰んだ。となった方いませんか?
僕がまさにこの状態になって抜け出せなかったので、対処法を!!以下のコマンドで、コンテナを再起動!!
$ docker-compose stop $ docker-compose up -d nginx mysqlそしたら、エラー解消しました。
エラー箇所を正しく修正したとしても、再起動しないとエラーのままだったりするので、
困ったら再起動してみましょう。
それでもエラーが出るなら、何か修正点があるかと思いますので、
ボス戦の攻略法を探すような感じで、ググりましょう!まとめ
環境構築って一番の壁だと思います。
わからないことだらけですし、これができないとその後の開発が何もできないし...
でも、個人的にはこの環境構築に苦戦して乗り越えると、
ただ開発するだけでは得られない知識が得られるので、
良い経験ができているなーと感じてます。開発中のエラーがボス戦だとしたら、
環境構築のエラーはシークレットボス戦みたいな感じですかね!笑
- 投稿日:2020-03-14T16:26:11+09:00
コーディング未経験のPO/PdMのためのRails on Dockerハンズオン vol.11 - Test coding -
はじめに
第11回目ですね。
前回はテストを自動化するためにRSpec、Selenium、Capybaraなどを導入しましたね。今日は今まで作ってきたアプリケーションに対してテストコードをコーディングしていきます。
本当はアプリをコーディングする前にテストをコーディングしてRedのフェーズにするべきなのですが、
まぁ最初なのでご愛嬌ということでGreenの状態から始めましょう。前回のソースコード
前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。
どういうふうにテストコード書いてくの?
ここは人によってやりやすいようにでいいと思うのですが、このハンズオンでは基本的には作りたい機能(ユーザーストーリー)ごとにテストファイルを分けて記述していきます。
例えば、今までだと「サインアップ」とか「サインイン」とかそういうやつです。例えば以下のようにテストシナリオを考えてみます。
1. ユーザーとして、ページにダイレクトアクセスしたい
- 未サインインのユーザーが、トップページにダイレクトアクセスしたとき、トップページが表示されること
- 未サインインのユーザーが、サインアップページにダイレクトアクセスしたとき、サインアップページが表示されること
- 未サインインのユーザーが、サインインページにダイレクトアクセスしたとき、サインインページが表示されること
- 未サインインのユーザーが、ユーザー詳細ページにダイレクトアクセスしたとき、ユーザー詳細ページが表示されること
- サインイン済のユーザーが、トップページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること
- サインイン済のユーザーが、サインアップページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること
- サインイン済のユーザーが、サインインページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること
- サインイン済のユーザーが、ユーザー詳細ページにダイレクトアクセスしたとき、ユーザー詳細ページが表示されること
2. ユーザーとして、ヘッダーリンクからページ遷移できること
- 未サインインのユーザーが、トップページでヘッダーのロゴをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、トップページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、トップページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること
- 未サインインのユーザーは、トップページでヘッダーに「Profile」リンクが存在しないこと
- 未サインインのユーザーは、トップページでヘッダーに「Sign out」リンクが存在しないこと
- 未サインインのユーザーが、サインアップページでヘッダーのロゴをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、サインアップページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、サインアップページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること
- 未サインインのユーザーは、サインアップページでヘッダーに「Profile」リンクが存在しないこと
- 未サインインのユーザーは、サインアップページでヘッダーに「Sign out」リンクが存在しないこと
- 未サインインのユーザーが、サインインページでヘッダーのロゴをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、サインインページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、サインインページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること
- 未サインインのユーザーは、サインインページでヘッダーに「Profile」リンクが存在しないこと
- 未サインインのユーザーは、サインインページでヘッダーに「Sign out」リンクが存在しないこと
- 未サインインのユーザーが、ユーザー詳細ページでヘッダーのロゴをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、ユーザー詳細ページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること
- 未サインインのユーザーが、ユーザー詳細ページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること
- 未サインインのユーザーは、ユーザー詳細ページでヘッダーに「Profile」リンクが存在しないこと
- 未サインインのユーザーは、ユーザー詳細ページでヘッダーに「Sign out」リンクが存在しないこと
- サインイン済のユーザーが、ユーザー詳細ページでヘッダーのロゴをクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
- サインイン済のユーザーは、ユーザー詳細ページでヘッダーに「Home」リンクが存在しないこと
- サインイン済のユーザーは、ユーザー詳細ページでヘッダーに「Sign in」リンクが存在しないこと
- サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Profile」リンクをクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること
- サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Sign out」リンクが存在すること
3. ユーザーとして、サインアップしたい
- 未サインインのユーザーが、トップページで「Sign up now!」ボタンを選択したとき、サインアップページに遷移すること
- サインアップページで「お名前」を入力できること
- サインアップページで「メールアドレス」を入力できること
- サインアップページで「パスワード」を入力できること
- サインアップページで「パスワード」はマスク化されること
- サインアップページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスにチェックをいれたとき、「パスワード」が表示されること
- サインアップページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスのチェックを外したとき、「パスワード」がマスク化されること
- サインアップページで「お名前」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「お名前」未入力のエラーメッセージが表示されること
- サインアップページで「お名前」を51文字以上入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「お名前」文字数超過のエラーメッセージが表示されること
- サインアップページで「メールアドレス」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」未入力のエラーメッセージが表示されること
- サインアップページで「メールアドレス」を256文字以上入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」文字数超過のエラーメッセージが表示されること
- サインアップページで「メールアドレス」を誤ったフォーマットで入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」フォーマットチェックエラーのエラーメッセージが表示されること
- サインアップページで「メールアドレス」がすでに登録済みのメールアドレスを入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」重複のエラーメッセージが表示されること
- サインアップページで「パスワード」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「パスワード」文字数不足のエラーメッセージが表示されること
- サインアップページで「パスワード」を5文字以下で入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「パスワード」文字数不足のエラーメッセージが表示されること
- サインアップページで「お名前」「メールアドレス」「パスワード」を正しく入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは成功し、そのユーザーのユーザー詳細ページにサインイン済状態で遷移すること
- サインアップに成功したユーザーは、遷移後のユーザー詳細ページで自分の入力した「お名前」を確認できること
- サインアップに成功したユーザーは、遷移後のユーザー詳細ページで自分の入力した「メールアドレス」を確認できること
- サインアップに成功したユーザーは、遷移後のユーザー詳細ページでウェルカムメッセージを確認できること
- サインアップに成功したユーザーは、遷移後のユーザー詳細ページをリロードしたとき、ウェルカムメッセージを確認できなくなること
- サインアップページで「登録済みの方はこちら」リンクを選択したとき、サインインページに遷移すること
4. ユーザーとして、サインインしたい
- サインインページで「メールアドレス」を入力できること
- サインインページで「パスワード」を入力できること
- サインインページで「パスワード」はマスク化されること
- サインインページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスにチェックをいれたとき、「パスワード」が表示されること
- サインインページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスのチェックを外したとき、「パスワード」がマスク化されること
- サインインページで「メールアドレス」を入力していないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること
- サインインページで「メールアドレス」として登録されていないメールアドレスを入力したユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること
- サインインページで「メールアドレス」は正しいが「パスワード」を入力していないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること
- サインインページで「メールアドレス」は正しいが「パスワード」が正しくないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること
- サインインページで「メールアドレス」「パスワード」に正しい値を入力したユーザーが、「Sign in」ボタンをクリックしたとき、サインイン済状態でそのユーザーのユーザー詳細ページに遷移すること
- サインインに成功したユーザーは、遷移後のユーザー詳細ページでサインイン成功メッセージを確認できること
- サインインに成功したユーザーは、遷移後のユーザー詳細ページをリロードしたとき、サインイン成功メッセージを確認できなくなること
- サインインページで「登録がまだの方はこちら」リンクを選択したとき、サインアップページに遷移すること
5. ユーザーとして、サインアウトしたい
- サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Sign out」リンクをクリックしたとき、未サインイン状態になりトップページに遷移すること
6. ユーザーとして、他のユーザーの情報を閲覧したい
- ユーザーが、存在するユーザーのユーザー詳細ページにアクセスしようとしたとき、そのユーザーの「お名前」「メールアドレス」を確認できること
- ユーザーが、存在しないユーザーのユーザー詳細ページにアクセスしようとしたとき、エラーが発生すること
ざっとあげただけでもこれだけのテストシナリオがあります。
こんなにコード書かなきゃいけないのかよ!と思うかもしれませんが、コードを書かないと少しのリファクタリングの度にこれら全てのテストを手動で行わなければ安心してデプロイできないという修羅の道を選ぶことになります。
今日でテストコードへのハードルを爆下げして気軽にリファクタできるエンジニアをめざしましょう!テストコードを書いていこう
ここから実際にテストコードを書いていきます。
上でナンバリングで章立てしてましたね。それごとにスペックファイルを作って管理します。1. ユーザーとして、ページにダイレクトアクセスしたい
ファイル名は
01_direct_access_spec.rb
にしておきましょう。$ mkdir spec/system/ $ touch spec/system/01_direct_access_spec.rb01_direct_access_spec.rbfeature "ユーザーとして、ページにダイレクトアクセスしたい", type: :system do background do @user1 = User.create(name: "John Smith", email: "john@sample.com", password: "john1234") @user2 = User.create(name: "Taro Tanaka", email: "taro@sample.com", password: "taro1234") end scenario "未サインインのユーザーが、トップページにダイレクトアクセスしたとき、トップページが表示されること" do visit root_path expect(current_path).to eq root_path end scenario "未サインインのユーザーが、サインアップページにダイレクトアクセスしたとき、サインアップページが表示されること" do visit sign_up_path expect(current_path).to eq sign_up_path end scenario "未サインインのユーザーが、サインインページにダイレクトアクセスしたとき、サインインページが表示されること" do visit sign_in_path expect(current_path).to eq sign_in_path end scenario "未サインインのユーザーが、ユーザー詳細ページにダイレクトアクセスしたとき、ユーザー詳細ページが表示されること" do visit user_path(@user1) expect(current_path).to eq user_path(@user1) visit user_path(@user2) expect(current_path).to eq user_path(@user2) end feature nil, type: :system do background do visit sign_in_path fill_in :user_email, with: @user1.email fill_in :user_password, with: @user1.password click_on :sign_in_button end scenario "サインイン済のユーザーが、トップページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること" do visit root_path expect(current_path).to eq user_path(@user1) end scenario "サインイン済のユーザーが、サインアップページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること" do visit sign_up_path expect(current_path).to eq user_path(@user1) end scenario "サインイン済のユーザーが、サインインページにダイレクトアクセスしたとき、そのユーザーのユーザー詳細ページが表示されること" do visit sign_in_path expect(current_path).to eq user_path(@user1) end scenario "サインイン済のユーザーが、ユーザー詳細ページにダイレクトアクセスしたとき、ユーザー詳細ページが表示されること" do visit user_path(@user1) expect(current_path).to eq user_path(@user1) visit user_path(@user2) expect(current_path).to eq user_path(@user2) end end endこのテストをパスさせるために、サインインページに少し細工をします。
app/views/sessions/new.html.erb- <%= form.submit "Sign in", class: "btn btn-primary form-control" %> + <%= form.submit "Sign in", class: "btn btn-primary form-control", id: :sign_in_button %>
id: :sign_in_button
を追記しました。これでid
属性を追加できます。まずはテストがパスするのを体感しましょうか!
$ docker-compose up -d $ docker-compose exec web ash# rspec spec/system/01_direct_access_spec.rb Capybara starting Puma... * Version 4.3.1 , codename: Mysterious Traveller * Min threads: 0, max threads: 4 * Listening on tcp://127.0.0.1:45497 ........ Finished in 11.85 seconds (files took 5.4 seconds to load) 8 examples, 0 failuresテストパスしましたね!
どんなテストが実行されたのか、ちょっと紹介させてください!構文
前回も紹介しましたが、RSpecのシステムテストの構文は
feature "test name", type: :system do scenario "test scenario" do # テストコード end endです。
scenario
は複数あります。
feature
内の全てのscenario
に適用する初期条件を記述する場合はbackground
を使います。feature "test name", type: :system do background do # 前提条件 end scenario "test scenario" do # テストコード end endまた、
feature
を入れ子にすることも可能です。これによって特定のscenario
たちにだけ前提条件をつけることも可能です。feature "test name", type: :system do scenario "test scenario" do # 前提条件が適用されない end feature "test detail name", type: :system do background do # 前提条件 end scenario "test scenario" do # 前提条件が適用される end end endまずはこの構文を身に付けましょう。
background
background
は前提条件を定義するためのブロックです。そのファイルのシナリオに共通して行われる処理をここで定義します。
よく使われる場面としては、今回のようにモデルを作っておく、とかですね。
モデルは今までのRubyコードと同じように、User.create
やUser.new
が使えます。また、インスタンス変数にしないとscenario
側では参照できないので注意してくださいね。visit
visit
は引数に名前付きルート(xxxx_path)やURLをとって、そこにアクセスします。visit root_pathこれで
root_path
、つまり/
にアクセスをしようとします。expect().to
expect().to
は()
内をto
以降と検証します。
例えばexpect().to eq xxxxx
のようにeq
と組み合わせることで()
内とxxxxx
が等しいことを検証します。
この検証がfalse
の場合はそのシナリオをFailureになります。current_path
current_path
は現在表示されているパス(/
とか/users/1
とか)を取得します。expect(current_path).to eq root_pathで、今表示されているページのパスがルートパス、つまりトップページであるかどうかを検証しているのです。
fill_in
fill_in
はinput
に文字を入力する操作を実行します。fill_in [id], with: [入力したい文字列]で、ページから
id
属性が[id]
のinput
に[入力したい文字列]
を入力してくれます。click_on
click_on
は<a>
または<button>
タグをクリックする操作を実行します。click_on [id]で、ページから
id
属性が[id]
の<a>
か<button>
タグをクリックしてくれます。ここまでを理解すると
background do visit sign_in_path fill_in :user_email, with: @user1.email fill_in :user_password, with: @user1.password click_on :sign_in_button endが、サインインページにアクセスして
@user1
でサインインしようとしていることがわかりますね?こんな感じで操作と検証を組み合わせてテストを自動化していきます!
ではどんどんテストコードを書いていきましょう!!2. ユーザーとして、ヘッダーリンクからページ遷移できること
ファイル名は
02_header_spec.rb
でいきましょうか。# touch spec/system/02_header_spec.rbまた少し
id
を仕込んでおきましょう。app/views/layouts/application.html.erb<div class="container"> - <%= link_to "sample app", root_path, class: "navbar-brand" %> + <%= link_to "sample app", root_path, class: "navbar-brand", id: :header_logo %> <ul class="navbar-nav"> <% if signed_in? %> <%# サインイン済みの場合のリンク %> - <li class="nav-item"><%= link_to "Profile", current_user, class: "nav-link" %></li> - <li class="nav-item"><%= link_to "Sign out", sign_out_path, method: :delete, class: "nav-link" %></li> + <li class="nav-item"><%= link_to "Profile", current_user, class: "nav-link", id: :header_profile_link %></li> + <li class="nav-item"><%= link_to "Sign out", sign_out_path, method: :delete, class: "nav-link", id: :header_sign_out_link %></li> <% else %> <%# 未サインインの場合のリンク %> - <li class="nav-item"><%= link_to "Home", root_path, class: "nav-link" %></li> - <li class="nav-item"><%= link_to "Sign in", sign_in_path, class: "nav-link" %></li> + <li class="nav-item"><%= link_to "Home", root_path, class: "nav-link", id: :header_home_link %></li> + <li class="nav-item"><%= link_to "Sign in", sign_in_path, class: "nav-link", id: :header_sign_in_link %></li> <% end %> </ul> </div>そしてテストシナリオを書きます。
spec/system/02_header_spec.rbfeature "ユーザーとして、ヘッダーリンクからページ遷移できること", type: :system do background do @user1 = User.create(name: "John Smith", email: "john@sample.com", password: "john1234") @user2 = User.create(name: "Taro Tanaka", email: "taro@sample.com", password: "taro1234") end scenario "未サインインのユーザーが、トップページでヘッダーのロゴをクリックしたとき、トップページに遷移すること" do visit root_path click_on :header_logo expect(current_path).to eq root_path end scenario "未サインインのユーザーが、トップページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること" do visit root_path click_on :header_home_link expect(current_path).to eq root_path end scenario "未サインインのユーザーが、トップページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること" do visit root_path click_on :header_sign_in_link expect(current_path).to eq sign_in_path end scenario "未サインインのユーザーは、トップページでヘッダーに「Profile」リンクが存在しないこと" do visit root_path expect(page).not_to have_selector "#header_profile_link" end scenario "未サインインのユーザーは、トップページでヘッダーに「Sign out」リンクが存在しないこと" do visit root_path expect(page).not_to have_selector "#header_sign_out_link" end scenario "未サインインのユーザーが、サインアップページでヘッダーのロゴをクリックしたとき、トップページに遷移すること" do visit sign_up_path click_on :header_logo expect(current_path).to eq root_path end scenario "未サインインのユーザーが、サインアップページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること" do visit sign_up_path click_on :header_home_link expect(current_path).to eq root_path end scenario "未サインインのユーザーが、サインアップページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること" do visit sign_up_path click_on :header_sign_in_link expect(current_path).to eq sign_in_path end scenario "未サインインのユーザーは、サインアップページでヘッダーに「Profile」リンクが存在しないこと" do visit sign_up_path expect(page).not_to have_selector "#header_profile_link" end scenario "未サインインのユーザーは、サインアップページでヘッダーに「Sign out」リンクが存在しないこと" do visit sign_up_path expect(page).not_to have_selector "#header_sign_out_path" end scenario "未サインインのユーザーが、サインインページでヘッダーのロゴをクリックしたとき、トップページに遷移すること" do visit sign_in_path click_on :header_logo expect(current_path).to eq root_path end scenario "未サインインのユーザーが、サインインページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること" do visit sign_in_path click_on :header_home_link expect(current_path).to eq root_path end scenario "未サインインのユーザーが、サインインページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること" do visit sign_in_path click_on :header_sign_in_link expect(current_path).to eq sign_in_path end scenario "未サインインのユーザーは、サインインページでヘッダーに「Profile」リンクが存在しないこと" do visit sign_in_path expect(page).not_to have_selector "#header_profile_path" end scenario "未サインインのユーザーは、サインインページでヘッダーに「Sign out」リンクが存在しないこと" do visit sign_in_path expect(page).not_to have_selector "#header_sign_out_path" end scenario "未サインインのユーザーが、ユーザー詳細ページでヘッダーのロゴをクリックしたとき、トップページに遷移すること" do visit user_path(@user1) click_on :header_logo expect(current_path).to eq root_path end scenario "未サインインのユーザーが、ユーザー詳細ページでヘッダーの「Home」リンクをクリックしたとき、トップページに遷移すること" do visit user_path(@user1) click_on :header_home_link expect(current_path).to eq root_path end scenario "未サインインのユーザーが、ユーザー詳細ページでヘッダーの「Sign in」リンクをクリックしたとき、サインインページに遷移すること" do visit user_path(@user1) click_on :header_sign_in_link expect(current_path).to eq sign_in_path end scenario "未サインインのユーザーは、ユーザー詳細ページでヘッダーに「Profile」リンクが存在しないこと" do visit user_path(@user1) expect(page).not_to have_selector "#header_profile_link" end scenario "未サインインのユーザーは、ユーザー詳細ページでヘッダーに「Sign out」リンクが存在しないこと" do visit user_path(@user1) expect(page).not_to have_selector "#header_sign_out_link" end feature nil, type: :system do background do visit sign_in_path fill_in :user_email, with: @user1.email fill_in :user_password, with: @user1.password click_on :sign_in_button end scenario "サインイン済のユーザーが、ユーザー詳細ページでヘッダーのロゴをクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること" do visit user_path(@user2) click_on :header_logo sleep 1 expect(current_path).to eq user_path(@user1) end scenario "サインイン済のユーザーは、ユーザー詳細ページでヘッダーに「Home」リンクが存在しないこと" do visit user_path(@user2) expect(page).not_to have_selector "#header_home_link" end scenario "サインイン済のユーザーは、ユーザー詳細ページでヘッダーに「Sign in」リンクが存在しないこと" do visit user_path(@user2) expect(page).not_to have_selector "#header_sign_in_link" end scenario "サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Profile」リンクをクリックしたとき、そのユーザーのユーザー詳細ページに遷移すること" do visit user_path(@user2) click_on :header_profile_link expect(current_path).to eq user_path(@user1) end scenario "サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Sign out」リンクが存在すること" do visit user_path(@user2) expect(page).to have_selector "#header_sign_out_link" end end endダイレクトアクセスのテストと似ているところが多いですが、新出のコードを紹介していきます!
page
expect(page)
という形で現れましたね。これは今表示されているページ全体のことです。not_to
expect().not_to
という表現がでてきましたね。
to
の反対で()
とnot_to
以降がアンマッチであることを検証します。
マッチした場合にfalse
になり、そのシナリオがFailureになります。have_selector
have_selector
は指定したタグや属性を持っているかを検証します。
例えば、以下のような要素があるとします。<h1 id="title" class="main-title">Title</h1>これをタグ、
id
属性、class
属性でそれぞれhave_selector
で検証しようとすると以下のようになります。# タグで検証 expect(page).to have_selector("h1") # id属性で検証 expect(page).to have_selector("#title") # class属性で検証 expect(page).to have_selector(".main-title")ページ内や子要素に特定の要素がないかを調べる時に使うので覚えておくとよしです!
sleep
sleep
は指定した秒数、次のコードの実行を待つコードです。sleep 1
であれば1秒待った後に次の行に進みます。
今回は、アプリケーション側でサインイン状態を確認した後リダイレクトする処理を入れていますが、自動化されたテストがそのままのスピードで検証を進めてしまうとリダイレクト処理が終わる前に検証が完了してしまい、思ったとおりの結果を得られないことがあります。
こういった自体を防ぐために、sleep
を挟むことで処理を待たせることも必要になります。ただし、
sleep
の使用は最低限にするべきです。なぜならそのせいでテストの実行時間が長くなってしまっては自動化した意味が失われかねないからです。3. ユーザーとして、サインアップしたい
ファイル名は
03_sign_up_spec.rb
でいきます!# touch spec/system/03_sign_up_spec.rbそして、今回も
id
を仕込みます。app/views/static_pages/home.html.erb- <%= link_to "Sign up now!", sign_up_path, class: "btn btn-lg btn-primary mt-5" %> + <%= link_to "Sign up now!", sign_up_path, class: "btn btn-lg btn-primary mt-5", id: :sign_up_link %>app/views/users/new.html.erb<%= form_with model: @user, url: create_user_path, local: true do |form| %> ... <div class="form-group mt-5"> - <%= form.submit "Sign up!", class: "form-control btn btn-primary" %> + <%= form.submit "Sign up!", class: "form-control btn btn-primary", id: :sign_up_button %> </div> <% end %> - <p class="text-center">登録済みの方は<%= link_to "こちら", sign_in_path %></p> + <p class="text-center">登録済みの方は<%= link_to "こちら", sign_in_path, id: :sign_in_link %></p>はい。ではテストコードです。
spec/system/03_sign_up_spec.rbfeature "ユーザーとして、サインアップしたい", type: :system do background do @user = User.new(name: "John Smith", email: "john@sample.com", password: "john1234") end scenario "未サインインのユーザーが、トップページで「Sign up now!」ボタンを選択したとき、サインアップページに遷移すること" do visit root_path click_on :sign_up_link expect(current_path).to eq sign_up_path end scenario "サインアップページで「お名前」を入力できること" do visit sign_up_path fill_in :user_name, with: @user.name expect(find("#user_name").value).to eq @user.name end scenario "サインアップページで「メールアドレス」を入力できること" do visit sign_up_path fill_in :user_email, with: @user.email expect(find("#user_email").value).to eq @user.email end scenario "サインアップページで「パスワード」を入力できること" do visit sign_up_path fill_in :user_password, with: @user.password expect(find("#user_password").value).to eq @user.password end scenario "サインアップページで「パスワード」はマスク化されること" do visit sign_up_path fill_in :user_password, with: @user.password expect(find("#user_password")[:type]).to eq "password" end scenario "サインアップページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスにチェックをいれたとき、「パスワード」が表示されること" do visit sign_up_path fill_in :user_password, with: @user.password check :visible_password expect(find("#user_password")[:type]).to eq "text" end scenario "サインアップページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスのチェックを外したとき、「パスワード」がマスク化されること" do visit sign_up_path fill_in :user_password, with: @user.password check :visible_password uncheck :visible_password expect(find("#user_password")[:type]).to eq "password" end scenario "サインアップページで「お名前」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「お名前」未入力のエラーメッセージが表示されること" do error_message = "お名前を入力してください" visit sign_up_path fill_in :user_name, with: "" fill_in :user_email, with: @user.email fill_in :user_password, with: @user.password click_on :sign_up_button expect(current_path).to eq sign_up_path expect(page).to have_text error_message end scenario "サインアップページで「お名前」を51文字以上入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「お名前」文字数超過のエラーメッセージが表示されること" do error_message = "お名前は50文字以内で入力してください" visit sign_up_path fill_in :user_name, with: "a" * 51 fill_in :user_email, with: @user.email fill_in :user_password, with: @user.password click_on :sign_up_button expect(current_path).to eq sign_up_path expect(page).to have_text error_message fill_in :user_name, with: "a" * 50 fill_in :user_password, with: @user.password click_on :sign_up_button expect(current_path).not_to eq sign_up_path expect(page).not_to have_text error_message expect(current_path).to eq user_path(User.find_by(email: @user.email)) end scenario "サインアップページで「メールアドレス」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」未入力のエラーメッセージが表示されること" do error_message = "メールアドレスを入力してください" visit sign_up_path fill_in :user_name, with: @user.name fill_in :user_email, with: "" fill_in :user_password, with: @user.password click_on :sign_up_button expect(current_path).to eq sign_up_path expect(page).to have_text error_message end scenario "サインアップページで「メールアドレス」を256文字以上入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」文字数超過のエラーメッセージが表示されること" do error_message = "メールアドレスは255文字以内で入力してください" visit sign_up_path fill_in :user_name, with: @user.name fill_in :user_email, with: "a" * 245 + "@sample.com" fill_in :user_password, with: @user.password click_on :sign_up_button expect(current_path).to eq sign_up_path expect(page).to have_text error_message fill_in :user_email, with: "a" * 244 + "@sample.com" fill_in :user_password, with: @user.password click_on :sign_up_button expect(current_path).not_to eq sign_up_path expect(page).not_to have_text error_message expect(current_path).to eq user_path(User.find_by(email: "a" * 244 + "@sample.com")) end scenario "サインアップページで「メールアドレス」を誤ったフォーマットで入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」フォーマットチェックエラーのエラーメッセージが表示されること" do error_message = "メールアドレスは不正な値です" visit sign_up_path fill_in :user_name, with: @user.name fill_in :user_email, with: "sample.com" fill_in :user_password, with: @user.password click_on :sign_up_button expect(current_path).to eq sign_up_path expect(page).to have_text error_message end scenario "サインアップページで「メールアドレス」がすでに登録済みのメールアドレスを入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「メールアドレス」重複のエラーメッセージが表示されること" do error_message = "メールアドレスはすでに存在します" @user.save visit sign_up_path fill_in :user_name, with: @user.name fill_in :user_email, with: @user.email.upcase fill_in :user_password, with: @user.password click_on :sign_up_button expect(current_path).to eq sign_up_path expect(page).to have_text error_message end scenario "サインアップページで「パスワード」を入力していないユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「パスワード」文字数不足のエラーメッセージが表示されること" do error_message = "パスワードは6文字以上で入力してください" visit sign_up_path fill_in :user_name, with: @user.name fill_in :user_email, with: @user.email fill_in :user_password, with: "" click_on :sign_up_button expect(current_path).to eq sign_up_path expect(page).to have_text error_message end scenario "サインアップページで「パスワード」を5文字以下で入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは失敗し「パスワード」文字数不足のエラーメッセージが表示されること" do error_message = "パスワードは6文字以上で入力してください" visit sign_up_path fill_in :user_name, with: @user.name fill_in :user_email, with: @user.email fill_in :user_password, with: "john1" click_on :sign_up_button expect(current_path).to eq sign_up_path expect(page).to have_text error_message fill_in :user_password, with: "john12" click_on :sign_up_button expect(current_path).not_to eq sign_up_path expect(page).not_to have_text error_message expect(current_path).to eq user_path(User.find_by(email: @user.email)) end feature nil, type: :system do background do @welcome_message = "サインアップありがとう" visit sign_up_path fill_in :user_name, with: @user.name fill_in :user_email, with: @user.email fill_in :user_password, with: @user.password click_on :sign_up_button end scenario "サインアップページで「お名前」「メールアドレス」「パスワード」を正しく入力したユーザーが、「Sign up!」ボタンをクリックしたとき、サインアップは成功し、そのユーザーのユーザー詳細ページにサインイン済状態で遷移すること" do expect(current_path).to eq user_path(User.find_by(email: @user.email)) expect(page).not_to have_selector "#header_sign_in_link" expect(page).to have_selector "#header_sign_out_link" end scenario "サインアップに成功したユーザーは、遷移後のユーザー詳細ページで自分の入力した「お名前」を確認できること" do expect(page).to have_text @user.name end scenario "サインアップに成功したユーザーは、遷移後のユーザー詳細ページで自分の入力した「メールアドレス」を確認できること" do expect(page).to have_text @user.email end scenario "サインアップに成功したユーザーは、遷移後のユーザー詳細ページでウェルカムメッセージを確認できること" do expect(page).to have_text @welcome_message end scenario "サインアップに成功したユーザーは、遷移後のユーザー詳細ページをリロードしたとき、ウェルカムメッセージを確認できなくなること" do visit current_path expect(page).not_to have_text @welcome_message end end scenario "サインアップページで「登録済みの方はこちら」リンクを選択したとき、サインインページに遷移すること" do visit sign_up_path click_on :sign_in_link expect(current_path).to eq sign_in_path end endまた、はじめましての書き方を紹介していきます。
find
モデルのときにつかった
find
とはまた別ですよ。
find()
でページの中から()
内で指定した要素を取得してくれます。1つ以上該当するものがあるとエラーになってしまうので、id
属性に対して使うのが好ましいでしょう。
例えば今回のテストコードでは、expect(find("#user_name").value).to eq @user.nameのように使っていますが、これで
id
属性がuser_name
に定義されている要素を取得します。value
find("#user_name").valueのように
value
を使っています。これはinput
にvalue
属性を取得しています。
value
属性にはinput type="text"
などの場合にはテキストボックスにデフォルトで入力しておきたい文字列を入力しておいたりしますが、Capybaraではvalue
属性を取得することで今入力されている文字列を取得することができます。[:type]
find("#user_password")[:type]のように使っています。Capybaraでは
value
とtext
は要素と.
でつなぐことで取得できるのですが、それ以外の属性は[:attribute_name]
の形式で取得します。[:type]
だとtype
属性を取得してきていることになりますね。
今回はpassword
のtype
属性をjavascriptでtext
とpassword
を切り替えているので、これでチェックができます。text
はマスク化なし、password
はマスク化ありはHTML5の仕様なので、今回のテストではtype
属性が正しく指定されているかを検証しました。check
check
はチェックボックスにチェックする操作です。
今回はcheck :visible_passwordの形式で
id
属性がvisible_password
のチェックボックスにチェックを入れています。uncheck
uncheck
はcheck
の反対でチェックボックスからチェックを外す操作です。have_text
have_text
は指定した文字列が存在するかどうかを検証するために使います。expect(page).to have_text xxxxxxxxxxと記述することでページのどこかにでも
xxxxxxxxxx
の文字列が存在しないかを検証します。
page
の箇所をfind()
などで限定した要素にすることで、その要素内にxxxxxxxxxx
の文字列が存在するかどうかを検証するように範囲を狭めることもできます。visit current_path
以前お話したように
current_path
は現在のパスです。そこにvisit
しているということは...そう!これはリロード操作ですね。
はい。今回のテストコードで新しく出てきた表現はこのくらいではないでしょうか。
そろそろ慣れてきましたか?一回書き始めると案外それらの組み合わせだけでいろいろなテストを実行できることがわかってきたんじゃないかと思います。
それでは次はサインインのテストコードを記述していきましょう!4. ユーザーとして、サインインしたい
まずはシナリオファイルの作成から。
# touch spec/system/04_sign_in_spec.rbそして、必要な箇所に
id
属性を振ります。app/views/sessions/new.html.erb- <p class="text-center">登録がまだの方は<%= link_to "こちら", sign_up_path %></p> + <p class="text-center">登録がまだの方は<%= link_to "こちら", sign_up_path, id: :sign_up_link %></p>以下、テストコードです。今回は目新しい表現はないので、下のコードを見ずに書いてみてもらっても面白いかもしれないです。
spec/system/04_sign_in_spec.rbfeature "ユーザーとして、サインインしたい", type: :system do background do @user = User.create(name: "John Smith", email: "john@sample.com", password: "john1234") end scenario "サインインページで「メールアドレス」を入力できること" do visit sign_in_path fill_in :user_email, with: @user.email expect(find("#user_email").value).to eq @user.email end scenario "サインインページで「パスワード」を入力できること" do visit sign_in_path fill_in :user_password, with: @user.password expect(find("#user_password").value).to eq @user.password end scenario "サインインページで「パスワード」はマスク化されること" do visit sign_in_path fill_in :user_password, with: @user.password expect(find("#user_password")[:type]).to eq "password" end scenario "サインインページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスにチェックをいれたとき、「パスワード」が表示されること" do visit sign_in_path fill_in :user_password, with: @user.password check :visible_password expect(find("#user_password")[:type]).to eq "text" end scenario "サインインページで「パスワード」を入力したユーザーが、「パスワードを表示する」チェックボックスのチェックを外したとき、「パスワード」がマスク化されること" do visit sign_in_path fill_in :user_password, with: @user.password check :visible_password uncheck :visible_password expect(find("#user_password")[:type]).to eq "password" end scenario "サインインページで「メールアドレス」を入力していないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること" do error_message = "メールアドレスまたはパスワードをもう一度確認してください。" visit sign_in_path fill_in :user_email, with: "" fill_in :user_password, with: @user.password click_on :sign_in_button expect(current_path).to eq sign_in_path expect(page).to have_text error_message end scenario "サインインページで「メールアドレス」として登録されていないメールアドレスを入力したユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること" do error_message = "メールアドレスまたはパスワードをもう一度確認してください。" visit sign_in_path fill_in :user_email, with: "dummy@sample.com" fill_in :user_password, with: @user.password click_on :sign_in_button expect(current_path).to eq sign_in_path expect(page).to have_text error_message end scenario "サインインページで「メールアドレス」は正しいが「パスワード」を入力していないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること" do error_message = "メールアドレスまたはパスワードをもう一度確認してください。" visit sign_in_path fill_in :user_email, with: @user.email fill_in :user_password, with: "" click_on :sign_in_button expect(current_path).to eq sign_in_path expect(page).to have_text error_message end scenario "サインインページで「メールアドレス」は正しいが「パスワード」が正しくないユーザーが、「Sign in」ボタンをクリックしたとき、サインイン失敗のエラーメッセージが表示されること" do error_message = "メールアドレスまたはパスワードをもう一度確認してください。" visit sign_in_path fill_in :user_email, with: @user.email fill_in :user_password, with: @user.password + "a" click_on :sign_in_button expect(current_path).to eq sign_in_path expect(page).to have_text error_message end feature nil, type: :system do background do @sign_in_message = "サインインしました。" visit sign_in_path fill_in :user_email, with: @user.email fill_in :user_password, with: @user.password click_on :sign_in_button end scenario "サインインページで「メールアドレス」「パスワード」に正しい値を入力したユーザーが、「Sign in」ボタンをクリックしたとき、サインイン済状態でそのユーザーのユーザー詳細ページに遷移すること" do expect(current_path).to eq user_path(@user) expect(page).not_to have_selector "#header_sign_in_link" expect(page).to have_selector "#header_sign_out_link" end scenario "サインインに成功したユーザーは、遷移後のユーザー詳細ページでサインイン成功メッセージを確認できること" do expect(page).to have_text @sign_in_message end scenario "サインインに成功したユーザーは、遷移後のユーザー詳細ページをリロードしたとき、サインイン成功メッセージを確認できなくなること" do visit current_path expect(page).not_to have_text @sign_in_message end end scenario "サインインページで「登録がまだの方はこちら」リンクを選択したとき、サインアップページに遷移すること" do visit sign_in_path click_on :sign_up_link expect(current_path).to eq sign_up_path end end5. ユーザーとして、サインアウトしたい
次はサインアウトについてですね。
# touch spec/system/05_sign_out_spec.rbそしてコーディング。これも今までのコードの組み合わせで表現可能ですね。
spec/system/05_sign_out_spec.rbfeature "ユーザーとして、サインアウトしたい", type: :system do scenario "サインイン済のユーザーが、ユーザー詳細ページでヘッダーの「Sign out」リンクをクリックしたとき、未サインイン状態になりトップページに遷移すること" do user = User.create(name: "John Smith", email: "john@sample.com", password: "john1234") visit sign_in_path fill_in :user_email, with: user.email fill_in :user_password, with: user.password click_on :sign_in_button visit user_path(user) click_on :header_sign_out_link expect(current_path).to eq root_path expect(page).to have_selector "#header_sign_in_link" expect(page).not_to have_selector "#header_sign_out_link" end endコメントアウトなどで説明文を書いたりは省いていますが、今までの内容が理解できていれば何をしているのか想像できると思います。
サインインページにアクセスしてサインインし、ユーザー詳細ページでヘッダーのサインアウトリンクをクリックし、トップページにリダイレクトされたと同時に未サインイン状態になっていることを検証していますね。6. ユーザーとして、他のユーザーの情報を閲覧したい
まずはシナリオファイルです。
# touch spec/system/06_show_user_info_spec.rb今回のテストでは、NotFoundのユーザーのユーザー詳細ページを表示しようとした時に、NotFoundのページが表示される、という項目があります。
Railsでは、NotFoundの例外が発生した場合、production
環境の場合はデフォルトでNotFound用のページが表示されるようになっています。
test
環境でも同じようにNotFoundページが表示されるようにconfigを変更します。config/environments/test.rb- config.consider_all_requests_local = true + config.consider_all_requests_local = false - config.action_dispatch.show_exceptions = false + config.action_dispatch.show_exceptions = trueこれで準備完了です。
NotFoundの場合、public/404.html
が表示されるようになります。
中身を見ると、<h1>The page you were looking for doesn't exist.</h1>と記述されているので、この文字列があるかどうかをチェックするようにします。
ではテストコードです。
spec/system/06_show_user_info_spec.rbfeature "ユーザーとして、他のユーザーの情報を閲覧したい", type: :system do background do @user1 = User.create(name: "John Smith", email: "john@sample.com", password: "john1234") @user2 = User.create(name: "Taro Yamada", email: "taro@sample.com", password: "taro1234") end scenario "ユーザーが、存在するユーザーのユーザー詳細ページにアクセスしようとしたとき、そのユーザーの「お名前」「メールアドレス」を確認できること" do # Before sign in visit user_path(@user1) expect(page).to have_text @user1.name expect(page).to have_text @user1.email expect(page).not_to have_text @user2.name expect(page).not_to have_text @user2.email visit user_path(@user2) expect(page).not_to have_text @user1.name expect(page).not_to have_text @user1.email expect(page).to have_text @user2.name expect(page).to have_text @user2.email # After sign in visit sign_in_path fill_in :user_email, with: @user1.email fill_in :user_password, with: @user1.password click_on :sign_in_button visit user_path(@user1) expect(page).to have_text @user1.name expect(page).to have_text @user1.email expect(page).not_to have_text @user2.name expect(page).not_to have_text @user2.email visit user_path(@user2) expect(page).not_to have_text @user1.name expect(page).not_to have_text @user1.email expect(page).to have_text @user2.name expect(page).to have_text @user2.email end scenario "ユーザーが、存在しないユーザーのユーザー詳細ページにアクセスしようとしたとき、エラーが発生すること" do not_found_message = "The page you were looking for doesn't exist." not_found_id = @user2.id + 1 expect{User.find(not_found_id)}.to raise_exception(ActiveRecord::RecordNotFound) # Before sign in visit user_path(not_found_id) expect(page).to have_text not_found_message # After sign in visit sign_in_path fill_in :user_email, with: @user1.email fill_in :user_password, with: @user1.password click_on :sign_in_button visit user_path(not_found_id) expect(page).to have_text not_found_message end end基本的には今までと変わりありませんね。
一つだけexceptionの検証の仕方だけ新出があるのでそれの説明を。expect{}.to raise_exception()
今までと違うのは
expect
の検証ターゲットを()
ではなく{}
でかこっていることですね。
そしてraise_exception
の後に期待する例外を記述します。
今回は@user2
のid
に+1
したid
のユーザーを検索しています。id
はシーケンシャルに払い出されるので@user2
よりも大きいid
を持っているユーザーはいないはず。
なのでUser.find(not_found_id)
はActiveRecord::RecordNotFound
の例外が発生するはずです。はい。ここまでで今のところ考えられる全てのテストケースをコーディングしてみました。
テストを実行してみましょう!# rspec Capybara starting Puma... * Version 4.3.1 , codename: Mysterious Traveller * Min threads: 0, max threads: 4 * Listening on tcp://127.0.0.1:46237 ...................................................................... Finished in 1 minute 21.27 seconds (files took 5.82 seconds to load) 70 examples, 0 failures全てパスしてますね!
もしパスしないテストケースがある場合は、もう一度アプリのコードかテストコードを見返してみてくださいね。また、あえてエラーになるようにテストコードを書き直してみてエラーになることを確認してみるのも面白いと思います。
さて、では本日はここまでにしましょう!
まとめ
今日は今まで作ってきたアプリに対してテストコードをコーディングしてみました。
これによって今後リファクタリングの都度自動テストを回すだけで全ての動作を確認することができるようになりましたね。テスト自動化、楽しいですよね??
テスト自動化は新しい機能を作ったり、アプリの仕様自体を変更する時にテストコードも記述する必要があるのでその分稼働が必要になることもあります。
しかし、リファクタリングや新機能開発時のデグレテストを簡略化でき、自動テストをパスしていればデプロイを自信をもって行える安心感を得ることができます。特にアプリが大きくなっていくと、これは稼働以上に嬉しい恩恵です。今後はこのハンズオンでもTDDで開発を進めていきます!
では、次回も乞うご期待!ここまでお読みいただきありがとうございました!
後片付け
では後片付けしていきますー。
前回もお話した通り、RSpecのシステムテストはテストが終わるとDBを勝手にリセットしてくれます。
ので、コンテナを落として終了ですね。# exit$ docker-compose down本日のソースコード
Reference
- 【Rails】こわくない!TDD/BDD・テスト自動化はじめの一歩ハンズオン! - Qiita
- 使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」 - Qiita
- Class: Capybara::Node::Element — Documentation for jnicklas/capybara (master)
- 【FeatureSpec】404 / 500ページを表示させる - Qiita
Other Hands-on Links
- 投稿日:2020-03-14T14:45:18+09:00
docker run のオプション
doxker runのオプションメモ
docker run --help Run a command in a new container Options: --add-host list Add a custom host-to-IP mapping (host:ip) -a, --attach list Attach to STDIN, STDOUT or STDERR --blkio-weight uint16 Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0) --blkio-weight-device list Block IO weight (relative device weight) (default []) --cap-add list Add Linux capabilities --cap-drop list Drop Linux capabilities --cgroup-parent string Optional parent cgroup for the container --cidfile string Write the container ID to the file --cpu-period int Limit CPU CFS (Completely Fair Scheduler) period --cpu-quota int Limit CPU CFS (Completely Fair Scheduler) quota --cpu-rt-period int Limit CPU real-time period in microseconds --cpu-rt-runtime int Limit CPU real-time runtime in microseconds -c, --cpu-shares int CPU shares (relative weight) --cpus decimal Number of CPUs --cpuset-cpus string CPUs in which to allow execution (0-3, 0,1) --cpuset-mems string MEMs in which to allow execution (0-3, 0,1) -d, --detach Run container in background and print container ID --detach-keys string Override the key sequence for detaching a container --device list Add a host device to the container --device-cgroup-rule list Add a rule to the cgroup allowed devices list --device-read-bps list Limit read rate (bytes per second) from a device (default []) --device-read-iops list Limit read rate (IO per second) from a device (default []) --device-write-bps list Limit write rate (bytes per second) to a device (default []) --device-write-iops list Limit write rate (IO per second) to a device (default []) --disable-content-trust Skip image verification (default true) --dns list Set custom DNS servers --dns-option list Set DNS options --dns-search list Set custom DNS search domains --domainname string Container NIS domain name --entrypoint string Overwrite the default ENTRYPOINT of the image -e, --env list Set environment variables --env-file list Read in a file of environment variables --expose list Expose a port or a range of ports --gpus gpu-request GPU devices to add to the container ('all' to pass all GPUs) --group-add list Add additional groups to join --health-cmd string Command to run to check health --health-interval duration Time between running the check (ms|s|m|h) (default 0s) --health-retries int Consecutive failures needed to report unhealthy --health-start-period duration Start period for the container to initialize before starting health-retries countdown (ms|s|m|h) (default 0s) --health-timeout duration Maximum time to allow one check to run (ms|s|m|h) (default 0s) --help Print usage -h, --hostname string Container host name --init Run an init inside the container that forwards signals and reaps processes -i, --interactive Keep STDIN open even if not attached --ip string IPv4 address (e.g., 172.30.100.104) --ip6 string IPv6 address (e.g., 2001:db8::33) --ipc string IPC mode to use --isolation string Container isolation technology --kernel-memory bytes Kernel memory limit -l, --label list Set meta data on a container --label-file list Read in a line delimited file of labels --link list Add link to another container --link-local-ip list Container IPv4/IPv6 link-local addresses --log-driver string Logging driver for the container --log-opt list Log driver options --mac-address string Container MAC address (e.g., 92:d0:c6:0a:29:33) -m, --memory bytes Memory limit --memory-reservation bytes Memory soft limit --memory-swap bytes Swap limit equal to memory plus swap: '-1' to enable unlimited swap --memory-swappiness int Tune container memory swappiness (0 to 100) (default -1) --mount mount Attach a filesystem mount to the container --name string Assign a name to the container --network network Connect a container to a network --network-alias list Add network-scoped alias for the container --no-healthcheck Disable any container-specified HEALTHCHECK --oom-kill-disable Disable OOM Killer --oom-score-adj int Tune host's OOM preferences (-1000 to 1000) --pid string PID namespace to use --pids-limit int Tune container pids limit (set -1 for unlimited) --privileged Give extended privileges to this container -p, --publish list Publish a container's port(s) to the host -P, --publish-all Publish all exposed ports to random ports --read-only Mount the container's root filesystem as read only --restart string Restart policy to apply when a container exits (default "no") --rm Automatically remove the container when it exits --runtime string Runtime to use for this container --security-opt list Security Options --shm-size bytes Size of /dev/shm --sig-proxy Proxy received signals to the process (default true) --stop-signal string Signal to stop a container (default "15") --stop-timeout int Timeout (in seconds) to stop a container --storage-opt list Storage driver options for the container --sysctl map Sysctl options (default map[]) --tmpfs list Mount a tmpfs directory -t, --tty Allocate a pseudo-TTY --ulimit ulimit Ulimit options (default []) -u, --user string Username or UID (format: <name|uid>[:<group|gid>]) --userns string User namespace to use --uts string UTS namespace to use -v, --volume list Bind mount a volume --volume-driver string Optional volume driver for the container --volumes-from list Mount volumes from the specified container(s) -w, --workdir string Working directory inside the container
- 投稿日:2020-03-14T14:44:23+09:00
【MySQL・Docker】cryptography is required for sha256_password or caching_sha2_password
DockerでMySQL8.0のイメージを使用した際、「cryptography is required for sha256_password or caching_sha2_password」エラーが発生したのでその回避策。
原因
デフォルトの認証がcaching_sha2_passwordになっていたのですが、これだと暗号化がうまくいかないのか、復号の際の検証がうまくいかないのか、エラーが発生しているようです。
参考
・アプリ制作時に悩まされたエラー集
・MYSQL8.0におけるデフォルトの認証プラグインの変更回避策
認証方式をMySQL8.0.4以前のmysql_native_passwordに戻しましょう。CMDで変更することも可能だと思いますが、今回はmy.cnfを自作して、起動したコンテナにマウントします。
my.cnf[musqld] default_authentication_plugin= mysql_native_passworddocker-compose.yaml・・・略 mysql: image: mysql:8.0 volumes: - ./my.cnf:/etc/mysql/conf.d/my.cnfmy.cnfをマウントする方式にしておくことで文字コード問題(utf8,latin問題)などが発生した場合も設定を書いてあげるだけなので簡単に対応することができるようになります。
- 投稿日:2020-03-14T14:12:46+09:00
Dockerで構築したUbuntu18.04でip系のコマンドが実行できない場合の対処
Dockerで起動したUbuntuでrootユーザーであるにも関わらず、ip系コマンドを入力して、以下のように実行できない場合。。。
root@d7a6f6a495dd:/# ip netns add helloworld mount --make-shared /var/run/netns failed: Operation not permitteddocker runのオプションである
--privileged
を付与すると権限が与えられるdocker run --privileged -it -d --name ubuntu-test ubuntu:18.04
これでOK
root@d7a6f6a495dd:/# ip netns add helloworld root@d7a6f6a495dd:/# ip netns list helloworld
- 投稿日:2020-03-14T13:57:00+09:00
dockerでwordpress(SSL)を速攻で立ち上げる
なんでこれを書こうと思ったのか
既にgithubで公開されているものを使ってサクっとやれないのかを探してみると、意外と国内の記事では自力でdocker-compose.ymlを書いていたり、versionが古かったりしていたので、備忘録がてらに残しておきます。
対象読者
- サーバ1台でwordpressを運用したい個人向け
- dockerが使える環境にあること
さて、やってみるよ
$ mkdir ~/docker && cd ~/docker $ git clone https://github.com/evertramos/docker-compose-letsencrypt-nginx-proxy-companion.git nginx-proxy $ cd nginx-proxy $ mv .env.sample .env $ ./start.sh$ cd ~/docker $ git clone https://github.com/evertramos/docker-wordpress-letsencrypt.git wordpress-letsencrypt $ cd wordpress-letsencrypt $ mv .env.sample .env $ vim .env
- 変更したいところは適当に変えていく。特にDBの接続情報や、letsencryptの設定(FQDN/E-Mail)
- コンテナ名は複数のwordpressを立ち上げたい場合はユニークにする必要がある
- コメントで丁寧に書かれているのでここでは省略
$ docker-compose up -dこれでおしまい。あとは少し待ってからブラウザで、上で指定したFQDNにアクセスしてあげれば、wordpressの設定画面が表示されます。
日本語化したい場合
ブラウザからログインまで出来るようにした後にwp-cliを使って設定をします。
$ chmod 777 ~docker/data/site/wp-content $ alias wp="docker-compose run --rm wpcli" $ wp core language install ja --activateこれでOKです。wp-contentのディレクトリパーミッションを変更しておかないと、languageディレクトの作成に失敗するようです。
- 投稿日:2020-03-14T12:53:09+09:00
DockerでMacのDiskがいっぱいになった
現象
気づかぬうちに、MacのDiskが蝕まれていた
Check
homeの下をチェック
sudo du -x -h -d 2 /Users/masato-naka/ | sort -h -r | head -30 ... 109G /Users/masato-naka/ 80G /Users/masato-naka//Library 54G /Users/masato-naka//Library/Containers 17G /Users/masato-naka//Library/Caches ...なぜかLibrary/Containersの下が54Gもある
sudo du -x -h -d 2 /Users/masato-naka//Library/Containers/com.docker.docker/Data | sort -h -r | head -30 50G /Users/masato-naka//Library/Containers/com.docker.docker/Data/vms/0 50G /Users/masato-naka//Library/Containers/com.docker.docker/Data/vms 50G /Users/masato-naka//Library/Containers/com.docker.docker/Data 60M /Users/masato-naka//Library/Containers/com.docker.docker/Data/log 41M /Users/masato-naka//Library/Containers/com.docker.docker/Data/log/vm 19M /Users/masato-naka//Library/Containers/com.docker.docker/Data/log/host 20K /Users/masato-naka//Library/Containers/com.docker.docker/Data/tasksDockerの仕業だった
以下のコマンドでチェックしてみるとVolumeが大量にありそうだった
docker system df -v解決方法
以下のコマンドでvolumeをprune
docker volume prune -f ... Total reclaimed space: 44.06GBこれでもまだ 50Gは消えていない
[Reset disk image] を押してすべて消す
sudo du -x -h -d 2 /Users/masato-naka//Library/Containers/com.docker.docker/ | sort -h -r | head -30 70M /Users/masato-naka//Library/Containers/com.docker.docker//Data 70M /Users/masato-naka//Library/Containers/com.docker.docker/ 63M /Users/masato-naka//Library/Containers/com.docker.docker//Data/log 164K /Users/masato-naka//Library/Containers/com.docker.docker//Data/vms 20K /Users/masato-naka//Library/Containers/com.docker.docker//Data/tasks開放できた!
df で確認するとほぼ全部なくなってる!
docker system df -v Images space usage: REPOSITORY TAG IMAGE ID CREATED SIZE SHARED SIZE UNIQUE SIZE CONTAINERS k8s.gcr.io/kube-controller-manager v1.15.5 1399a72fa1a9 5 months ago 158.8MB 42.32MB 116.5MB 0 k8s.gcr.io/kube-proxy v1.15.5 cbd7f21fec99 5 months ago 82.41MB 42.32MB 40.09MB 0 k8s.gcr.io/kube-apiserver v1.15.5 e534b1952a0d 5 months ago 206.9MB 42.32MB 164.6MB 0 k8s.gcr.io/kube-scheduler v1.15.5 fab2dded59dd 5 months ago 81.11MB 42.32MB 38.79MB 0 k8s.gcr.io/coredns 1.3.1 eb516548c180 14 months ago 40.3MB 0B 40.3MB 0 k8s.gcr.io/etcd 3.3.10 2c4adeb21b4f 15 months ago 258.1MB 0B 258.1MB 0 k8s.gcr.io/pause 3.1 da86e6ba6ca1 2 years ago 742.5kB 0B 742.5kB 0 Containers space usage: CONTAINER ID IMAGE COMMAND LOCAL VOLUMES SIZE CREATED STATUS NAMES Local Volumes space usage: VOLUME NAME LINKS SIZE Build cache usage: 0B CACHE ID CACHE TYPE SIZE CREATED LAST USED USAGE SHARED参考
- 投稿日:2020-03-14T12:24:40+09:00
zmq4でFizzBuzzする
初めに
Goのすごい人がついったーで
gophernotes の pure go zeromq 対応が入った。
とツイートしてた。
すごい人が言ってるんだから使っておいて損無いハズ。5分ぐらい考えた結果、FizzBuzzすることに決めた。
プログラマならFizzBuzzとか数列とか使いたくなるんだからしょうがない。できたもの
斬新なFizzBuzz結果
zeromqとは
ブローカーなしのメッセージング。ブローカー不要が何より嬉しい。
どうせ通信相手そんなにいないのに、いちいちサーバ立てたくない。
詳しくはZeroMQ参照。FizzBuzzとは
よくやる奴。割愛。
【非検証】gophernotesを使おうとしたけど使えなかったので手順だけまとめる
gophernotesとは、Jupyter NotebookのGo言語版的な奴。詳しくはgophernotesを参照。
諸般の事情(Windowsしか持ってない)があるので、Dockerを利用する必要がある。
が、今現在Dockerが利用できる端末がない。
なので環境構築の順番だけでもまとめる。簡単に利用するためにgopherdata/gophernotesをベースとして自分の使いやすいイメージを作る。
Dockerfile
FROM gopherdata/gophernotes:latest RUN go get github.com/go-zeromq/zmq4 \ && mkdir -p /etc/jupyter COPY ./entrypoint.sh /entrypoint.sh COPY ./jupyter_notebook_config.py /etc/jupyter/jupyter_notebook_config.py ENTRYPOINT [ "/entrypoint.sh" ]
entrypoint.sh
jupyter notebook --no-browser --allow-root --ip=0.0.0.0 --config=/etc/jupyter/jupyter_notebook_config.pyconfigはgopherdata/gophernotesのイメージをいったん走らせてから、コンテナに吐いてもらう。
# コンテナ走らせて中に入る docker run -it -d gopherdata/gophernotes docker exec -it ${gophernotesのid} /bin/sh # config吐く jupyter noteboopk --generate-config mv /path/to/jupyter_notebook_config.py /tmp/ exit # ローカルに落とす docker cp ${gophernotesのid}:/tmp/jupyter_notebook_config.py ./手元で試すだけなら、jupyter_notebook_config.pyのtokenを書き変える。
後はコンテナをビルドして実行するだけdocker build -t miyatamagophernotes:1.0.0 . docker run -it -p 8888:8888 miyatamagophernotes:1.0.0zmq4
まずはpub側から
publish側
1秒おきに100までのFizzBuzzを垂れ流す。
package main import ( "fmt" "time" "context" "sync" "github.com/go-zeromq/zmq4" ) func main() { err := startPublisher() if err != nil { fmt.Printf("%v", err) } } func startPublisher() error { pub := zmq4.NewPub(context.Background()) defer pub.Close() err := pub.Listen("tcp://*:5563") if err != nil { return err } for { publishFizzBuzz(pub) if err != nil { return err } time.Sleep(time.Second) } return nil } func publishFizzBuzz(publisher zmq4.Socket) { getFizzBuzzText := func(num int) string { if (num % 15) == 0 { return "FizzBuzz" } if (num % 3) == 0 { return "Fizz" } if (num % 5) == 0 { return "Buzz" } return fmt.Sprintf("%d", num) } wg := &sync.WaitGroup{} for i := 0; i < 100 ; i++ { wg.Add(1) go func(number int) { defer wg.Done() msg := zmq4.NewMsgFrom( []byte("FizzBuzz"), []byte(getFizzBuzzText(number)), ) publisher.Send(msg) }(i + 1) } wg.Wait() }続いてsub
Subscribe側
受け取って表示するだけ
package main import ( "context" "fmt" "github.com/go-zeromq/zmq4" ) func main() { err := startSubscribe() if err != nil { fmt.Printf("%v", err) } } func startSubscribe() error { sub := zmq4.NewSub(context.Background()) defer sub.Close() err := sub.Dial("tcp://localhost:5563") if err != nil { return err } err = sub.SetOption(zmq4.OptionSubscribe, "FizzBuzz") if err != nil { return err } for { msg, err := sub.Recv() if err != nil { return err } fmt.Printf("%s\n", msg.Frames[1]) } }ふりかえり
今回も様々な知見を得られた
- FizzBuzzをgophernotes上のZeroMQでやる必要は全くない
- gophernotesが利用できなかったので単純にzmq4でFizzBuzzしただけになった
- 並列処理したおかげでFizzBuzzの結果が正しいかよく分からなくなった
- 投稿日:2020-03-14T11:49:36+09:00
既存の開発環境にDockerを導入する
はじめに
今まではAWSのcloud9でrailsアプリを開発していましたが、実際の開発現場で使われているというDockerを開発環境に導入しようと思います。
※導入する際には、以下の記事を参考にさせていただきました。
丁寧すぎるDocker-composeによるrails5 + MySQL on Dockerの環境構築(Docker for Mac)環境
Ruby 2.5.3
Rails 5.2.4
MySQL 5.7
MacBook Pro1 自分のアプリのディレクトリ直下にDockerfileを作成する
DockerfileFROM ruby:2.5.3 RUN apt-get update -qq && apt-get install -y vim nodejs default-mysql-client COPY . /fishingshares ENV APP_HOME /fishingshares WORKDIR $APP_HOME RUN bundle install ADD . $APP_HOME2 docker-compose.ymlを作成する
docker-compose.ymlversion: '3' services: db: image: mysql:5.7 volumes: - ./db/mysql_data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: root ports: - "3306:3306" web: build: . command: rails s -p 3000 -b '0.0.0.0' volumes: - .:/fishingshares ports: - "3000:3000" depends_on: - db links: - db3 database.ymlを修正する
database.ymldefault: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password socket: /var/lib/mysql/mysql.sock host: db4 dockerコンテナを起動
terminal$ docker-compose build #コンテナを立ち上げる(立ち上げるのに少し時間かかります) $ docker-compose up #コンテナを起動するコンテナを起動して、ターミナルにこんなログが出てきたらうまくいってます。
5 データベースを構築する
terminal$ docker-compose run web rails db:create #コンテナ上にDBを作成する $ docker-compose run web rails db:migrate # コンテナ上のDBにマイグレーションファイルを反映させる6 ブラウザでlocalhost:3000にアクセスする
メモ1
コンテナを立ち上げた後に、以下のコマンドできちんとコンテナを止める必要があるみたいです。
terminal$ docker-compose downこのコマンドでコンテナを止めないと、次にコンテナを起動したときに、ブラウザでうまく表示されません。
コンテナを起動してから、ブラウザに表示されない時は、以下のコマンドを実施してください。
terminal$ rm tmp/pids/server.pidこのコマンドを実施してから、もう一度コンテナを起動すると、ブラウザにうまく表示されると思います。
メモ2
ローカルからMySQLコンテナに接続するコマンド
terminal$ mysql -u root -p -h localhost -P 3306 --protocol=tcp終わりに
dockerについては学習を始めてから、まだ数週間で慣れない点も多いですが、使いこなせるように勉強をしていきます。
- 投稿日:2020-03-14T11:41:31+09:00
【Docker】コンテナ一括削除のコマンド
はじめに
Dockerのコンテナを削除しようとして調べてたら色々なコマンドが出たので調べてみた。
検索してたら出てきたコマンド
コンテナ一括削除
$ docker rm -f `docker ps -a -q` $ docker rm `docker ps -a -q` $ docker rm -f `docker ps -aq` $ docker ps -aq | xargs docker rm $ docker container ls -aq | xargs docker container rm -f $ docker container prune概ね似たような感じ。
オプションの有無やxargsを使ってるかどうかが違うところか。
pruneは古いバージョンだと使えない。xargs
「コマンドA | xargs コマンドB」で、コマンドAの実行結果を引数にしてコマンドBを実行する。公式のドキュメントだとどうなってるか
コンテナの一括削除その1
$ docker rm $(docker ps -a -q)()の中で全てのコンテナのIDを取得してrmで削除。
-a:全てのコンテナを取得
-q:コンテナのIDを取得コンテナの一括削除その2
$ docker container pruneversionが1.25以上だと使える。$docker versionで確認。
- 投稿日:2020-03-14T00:00:31+09:00
【Laradock環境構築】Dockerインストール〜Laravelデフォルトページ表示まで①
はじめに
こちらの記事では、Laradockの環境構築として、
Dockerのインストール〜Laravelのデフォルトページ表示までを載せていきます。
このページでは、パート①ということで、Dockerのインストール方法を載せていきます。
(Mac版しか載せていません)次回のパート②はこちら↓
https://qiita.com/y-aimi/items/a6b3ee863ec67c8d4b97Dockerインストール
下記リンクからインストールできます。
https://hub.docker.com/editions/community/docker-ce-desktop-macSign Upをクリック
全て入力し、Sign Upをクリック
(プロフィール登録などが出ますが、今回は必要ないので無視)
すると、登録したメールアドレスに確認のメールが届いているのでチェック。[Verify email address]をクリック。
すると、Sign Inの画面が表示されるのでSign Inします。Docker.dmg がインストールされたかと思いますので、こちらを開く。
上記のような画面が出るので、左のクジラのイラストをApplicationsにドラッグ&ドロップ。
(このような画面が出なくてもApplicationに入れれば大丈夫です。)アプリケーションのDockerを起動。
すると、初回は下記のアラートが出ます。これは、
「ネットからインストールしたアプリだから確認のためにパスワード教えてくれない?」
ということです。問題なければOKをクリック。
そのままMacのパスワードを入力。すると、このような画面が表示されるので、Sing Inします。
[Docker Desktop is now up and running!] に変われば大丈夫です。
無事起動されているかのチェックは画面の上にこののマークがあれば大丈夫です。
(クジラさんがきちんとコンテナを積んでくれていますね...!!)まとめ
これで、Dockerのインストールが無事終わりました!
次回は、エラーが起きまくって困ったLaradockの環境構築に行きたいと思います。次へ
Laradock環境構築】Dockerインストール〜Laravelデフォルトページ表示まで②
https://qiita.com/y-aimi/items/a6b3ee863ec67c8d4b97