LoginSignup
10
12

More than 3 years have passed since last update.

【RailsAPI×Docker】シェルで手軽に環境構築 & Flutterでも動作確認

Last updated at Posted at 2020-09-13

初心者の方でも進められるよう手順は丁寧してあります。

やること

  • RailsAPI × Docker での環境構築
  • RailsAPIを軽く実装
  • APIクライアントからAPIを叩く(PostmanやFlutterアプリから)※Flutterは任意で

スクリーンショット 2020-09-13 13.42.28.png

APIコンテナ起動のシェルだけ見たい方 → こちら
一応 RailsAPIのソースコードFlutterのソースコードも用意(※いじり倒してる可能性あり)

手順

  • API側のDockerコンテナ起動まで
    • ディレクトリ作成
    • シェルでRailsAPIコンテナビルド
    • コンテナ起動&確認
  • RailsAPI実装
    • User モデル作成&DBテーブル用意
    • User のコントローラ作成
    • ルーティング設定
  • PostmanでAPIの動作を確認
    • Postmanインストール
    • GETでDBのUserリソースを取得
    • POSTでDBのUserリソースを追加
  • FlutterでAPIを叩く
    • プロジェクト作成
    • Http通信を行えるようにする
    • main.dartを編集

前提

  • AndroidStudioでFlutterアプリケーションを起動した状態にする
    • SDKなどをダウンロードしてシミュレータを起動できる
    • ※ Flutterセットアップはこちらの記事で基本全部いけました(3~40分はかかります)
  • Docker, docker-compose のコマンドが使える($ docker-compose -vが動けばOK)

1. API側のDockerコンテナ起動まで

RailsAPIのDockerコンテナを作っていきます。

ただ、1からコマンドを打っていくのは非効率なので、基本的なところはシェルにまとめてあります。

1-1. ディレクトリ作成

app_nameにプロジェクト名を当てはめてディレクトリを用意します。

$ mkdir app_name  # APIアプリを置くディレクトリ用意
$ cd app_name

1-2. シェルでRailsAPIコンテナビルド

以下がAPIコンテナのビルドまでやってくれるシェルです。(参考にした記事)

サンプルAPI立ち上げ用のシェル
set_up_rails.sh
#!/bin/bash

#config setting#############
APP_NAME="app_name"            # ← 自由に変更してください(ディレクトリ名と一緒がいいかも)
MYSQL_PASSWORD="password"      # ← 自由に変更してください
###########################

echo "docker pull ruby2.6.6"
docker pull ruby:2.6.6

echo "docker pull mysql:5.7"
docker pull mysql:5.7

echo "docker images"
docker images

echo "make Dockerfile"

cat <<EOF > Dockerfile
FROM ruby:2.6.6
ENV LANG C.UTF-8
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
#yarnのセットアップ
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
ENV PATH /root/.yarn/bin:/root/.config/yarn/global/node_modules/.bin:\$PATH
# 作業ディレクトリの作成、設定
RUN mkdir /${APP_NAME}
ENV APP_ROOT /${APP_NAME}
WORKDIR \$APP_ROOT
# ホスト側(ローカル)のGemfileを追加する
ADD ./Gemfile \$APP_ROOT/Gemfile
ADD ./Gemfile.lock \$APP_ROOT/Gemfile.lock
# Gemfileのbundle install
RUN bundle install
ADD . \$APP_ROOT
# gem版yarnのuninstall rails6でエラーになるため
RUN gem uninstall yarn -aIx
#webpackerの設定
#RUN rails webpacker:install
EOF

echo "make Gemfile"
cat <<EOF > Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem 'rails', '~> 6.0.3.2'
EOF

echo "make Gemfile.lock"
touch Gemfile.lock

echo "make docker-compose.yml"
cat <<EOF > docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD}
      MYSQL_DATABASE: root
    ports:
      - '3306:3306'
  api:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/${APP_NAME}
    ports:
      - '3000:3000'
    links:
      - db
EOF

echo "docker-compose run api rails new . --api --force --database=mysql --skip-bundle"
docker-compose run api rails new . --api --force --database=mysql --skip-bundle

echo "docker-compose run api bundle exec rails webpacker:install"
docker-compose run api bundle exec rails webpacker:install

docker-compose build

# fix config/database.yml
echo "fix config/database.yml"
cat config/database.yml | sed "s/password:$/password: ${MYSQL_PASSWORD}/" | sed "s/host: localhost/host: db/" > __tmpfile__
cat __tmpfile__ > config/database.yml
rm __tmpfile__

echo "docker-compose run api rails db:create"
docker-compose run api rails db:create

エディタからプロジェクト直下に新規ファイルとして作成&↑をコピペし、set_up_rails.shなどとして保存しましょう。「自由に変更してください」となっているapp_nameなどは合わせて編集してください。

保存が終わったら以下のようにシェルの権限を変更し、実行してみましょう(5分くらいかかります)。

$ chmod 755 set_up_rails.sh  # 権限の変更
$ ./set_up_rails.sh          # セットアップのシェル実行
docker pull ruby2.6.6
2.6.6: Pulling from library/ruby
57df1a1f1ad8: Pull complete
71e126169501: Pull complete
1af28a55c3f3: Pull complete
・
・
・

シェルの実行が終わったら、$ docker-compose psでコンテナの状態を確認してみましょう。以下のようになっていれば問題ないです。

$ docker-compose ps
     Name             Command       State       Ports
----------------------------------------------------------
app_name_db_1     docker-           Up      0.0.0.0:3306->
                  entrypoint.sh             3306/tcp,
                  mysqld                    33060/tcp

1-3. コンテナ起動 & 確認

次に、シェルでビルドしたコンテナを起動します。

$ docker-compose up
rails-api-handson_db_1 is up-to-date
Creating rails-api-handson_api_1 ... done
Attaching to rails-api-handson_db_1, rails-api-handson_api_1
db_1   | 2020-09-12 08:27:00+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.31-1debian10 started.
・
・
・
api_1  | * Environment: development
api_1  | * Listening on tcp://0.0.0.0:3000
api_1  | Use Ctrl-C to stop    # ← これが出たらOK!

Dockerコンテナのログが大量に出ますが、最後のUse Ctrl-C to stopが出ればコンテナが起動しているのでOKです。

コンテナの起動が確認できたので、http://localhost:3000 にアクセスし、ちゃんとRailsとしてレスポンスを返してくれるか確認しましょう。以下のようになっていればAPIコンテナの動作は大丈夫です。

スクリーンショット 2020-09-12 17.38.44.png

2. RailsAPI実装

動作確認用なので単純なユーザデータにしましょう。今回は、

User ( id, name, email )

みたいなリソースで実装してみます。

2-1. Userモデル作成 & DBテーブル用意

Userモデルとテーブルを用意します。

ここだけマイグレーションを使う方法Ridgepoleを使う方法に分けて紹介します(どちらも結果として同じモデル、スキーマが反映されます)。

Ridgepoleは、マイグレーションを使わずにスキーマを管理できるツールです。僕はこちらで進めていきますが、普段からマイグレーションを使っている方であればそちらで問題ありません。

パターン1) マイグレーションでモデル&テーブル作成

コンテナに入り、ジェネレータでUserモデルを作成。

$ docker-compose exec api bash     # コンテナに入る
# rails g model user name:string email:string
# ↑ 出来なければ以後はBundle経由でやってみましょう( # bundle exec rails .... みたいな )exit                             # コンテナから出る(以後はコンテナの出入りは明記しません。
                                   #              コマンドラインの「$」や「#」で区別します。)
$

あとはマイグレーションをかけるだけです。

# rake db:migrate

パターン2) Ridgepoleでモデル&テーブル作成

マイグレーションをスキップしてモデルを作成。

# rails g model user --skip-migration

プロジェクト直下にあるGemfilegem 'ridgepole', '>= 0.8.0'を追記します。mysql2の下とかで問題ないでしょう。

Gemfile
gem 'mysql2', '>= 0.4.4'     # 元々ある
gem 'ridgepole', '>= 0.8.0'  # これを追加

追記したgemをインストール。

# bundle install

次にdb/のなかに、Schemafile.rbというファイルを作成します(Ridgepoleではスキーマをここで一元管理する)。

db/Schemafile.rbには以下のように書き、保存します。

db/Schemafile.rb
create_table "users", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
  t.string "name"
  t.string "email"
  t.timestamps
end

ridgepoleのコマンドでスキーマファイルを反映。

# bundle exec ridgepole --config ./config/database.yml --file ./db/Schemafile.rb --apply
Apply `./db/Schemafile.rb`
-- create_table("users", {:options=>"ENGINE=InnoDB DEFAULT CHARSET=utf8"})
   -> 0.0716s

2-2. User のコントローラ作成

こちらの記事を参考に、URIにAPIのメジャーバージョン(v1.3なら1の部分)を含めます。ただこの記事だと、URIにapiというネームスペースを使っています。

これは僕個人の意見ですが、URIにapiと明記する方法ではなく、サブドメインなどで示すことを想定して実装します。

example.com/api/vi/users  # URIでapiだと示す
api.example.com/vi/users  # サブドメインで示す

では本題のコントローラ作成に戻ります。

コントローラの前に、メジャーバージョンを示すv1/を作成し、その中でusersコントローラを作成します。

$ mkdir app/controllers/v1
$ rails g controller v1::users

コントローラファイルの中身は以下の感じです。

*/v1/users_controller.rb
class V1::UsersController < ApplicationController
  before_action :set_user, only: [:show, :update, :destroy]

  def index
    users = User.order(created_at: :desc)
    render json: { status: 'SUCCESS', message: 'Loaded user', data: users }
  end

  def show
    render json: { status: 'SUCCESS', message: 'Loaded the user', data: @user }
  end

  def create
    user = User.new(user_params)
    if user.save
      render json: { status: 'SUCCESS', data: user }
    else
      render json: { status: 'ERROR', data: user.errors }
    end
  end

  def destroy
    @user.destroy
    render json: { status: 'SUCCESS', message: 'Deleted the user', data: @user }
  end

  def update
    if @user.update(user_params)
      render json: { status: 'SUCCESS', message: 'Updated the user', data: @user }
    else
      render json: { status: 'SUCCESS', message: 'Not updated', data: @user.errors }
    end
  end

  private

  def set_user
    @user = User.find(params[:id])
  end

  def user_params
    params.require(:user).permit(:name, :email)
  end
end

2-3. ルーティング設定

config/routes.rbを以下のようにします。

config/routes.rb
Rails.application.routes.draw do
  namespace 'v1' do
    resources :users
  end
end

↑が終わると、rake routesコマンドでルーティングが確認できるはずです。以下のようなルーティングが反映されていればOKです。

# rake routes
  Prefix Verb    URI Pattern               Controller#Action
v1_users GET     /v1/users(.:format)       v1/users#index
         POST    /v1/users(.:format)       v1/users#create
 v1_user GET     /v1/users/:id(.:format)   v1/users#show
         PATCH   /v1/users/:id(.:format)   v1/users#update
         PUT     /v1/users/:id(.:format)   v1/users#update
         DELETE  /v1/users/:id(.:format)   v1/users#destroy

3. PostmanでAPIの動作を確認

Postmanとは、WebAPIのテストクライアントサービスのひとつです。APIの動作確認にはこれを使います。

3-1. Postmanインストール

公式サイトで、Postmanをインストールします。前にインストールしたのであまり覚えていませんが、登録して進めていけばOKだった気がします...。

Postmanインストール後、まずはコンソールで適当にUserのレコードを用意しておきましょう。以下のような感じでいいかと思います。

# rails c
Loading development environment (Rails 6.0.3.3)
irb(main):001:0> User.create(name: "hoge", email: "hoge@example.com")

3-2. GETでDBのUserリソースを取得

Postmanで以下のようにGETメソッドURIを指定し、Sendボタンを押します。

すると、画面下のBodyで返ってきたレスポンスのBodyが確認できます。ここまでできていればAPIの動作確認としては成功です!

スクリーンショット 2020-09-12 21.15.09.png

3-3. POSTでDBのUserリソースを追加

今度は以下のように、POSTメソッドを指定し、リクエストBodyの中にもデータを含めてみましょう。この時、形式がJSONになっているか確認しましょう(画像の"JSON"とオレンジになっているところ)。

成功すれば、登録されたUserのレコードがレスポンスBodyから確認出来ます。

スクリーンショット 2020-09-12 21.21.14.png

ここで先ほどのGETメソッドでのアクセスをもう一度行ってみると、POSTメソッドで追加したレコードも一緒に確認できます。

スクリーンショット 2020-09-12 21.22.33.png

4. FlutterでAPIを叩く

結果こんな感じ

  • アプリを起動したらDBにあるユーザのデータを表示
  • 右下のAddボタンでユーザデータの登録

動作確認なのでミニマムな感じです。

スクリーンショット 2020-09-12 23.28.18.png

4-1. プロジェクト作成

File > New > New Flutter Project... からFlutterプロジェクトを作成します。iPhoneやAndroidのシミュレータ(エミュレータ)などが起動できていればOKです。

4-2. Http通信を行えるようにする

デフォルトだとhttpを行うパッケージのimportでエラーを起こすので少し操作が必要になります。

まず、pubspec.yamldependencies:に以下のように追記します。

pubspec.yaml
# ~ 省略 ~
dependencies:
  http: ^0.12.0   # ← 追加する
  flutter:
    sdk: flutter
# ~ 省略 ~

すると、Android Studioのエディタの上の方に、以下のようなFlutterコマンドが出てきます。今回はPub getを選択します。

もしこの先の作業でHttp周りのエラーが出るようであれば、僕が参考にしたこちらの記事も見てみるといいかも知れません。

スクリーンショット 2020-09-12 22.35.18.png

4-3. main.dartを編集

動作確認をしたいだけであれば、lib/main.dartを以下のように書き換えれば動きます。

なお、僕はFlutterは初歩の初歩なので物申すことがあればコメントにお願いします。

lib/main.dart
import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  MyApp({Key key}) : super(key: key);

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  Future<GetResults> res;
  @override
  void initState() {
    super.initState();
    res = getUsers();
  }

  // postUser()でUserを登録。setState内で再度User一覧を取得
  void _postUser() {
    postUser();
    setState(() {
      res = getUsers();
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'APIをPOSTで呼び出しJSONパラメータを渡す',
      theme: ThemeData(
        primarySwatch: Colors.yellow,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('RailsAPI x Flutter'),
        ),
        body: Center(
          child: FutureBuilder<GetResults>(
            future: getUsers(),
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return Text(
                    snapshot.data.message.toString()
                );
              } else if (snapshot.hasError) {
                return Text("${snapshot.error}");
              }
              return CircularProgressIndicator();
            },
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: _postUser,
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

class GetResults {
  final String message;
  GetResults({
    this.message,
  });
  factory GetResults.fromJson(Map<String, dynamic> json) {
    var datas = '';
    json['data'].forEach((item) => datas += 'id: ' + item['id'].toString() + ', name: ' + item['name'] + ', email: ' + item['email'] + '\n');
    return GetResults(
      message: datas,
    );
  }
}

class PostResults {
  final String message;
  PostResults({
    this.message,
  });
  factory PostResults.fromJson(Map<String, dynamic> json) {
    return PostResults(
      message: json['message'],
    );
  }
}

class SampleRequest {
  final String name;
  final String email;
  SampleRequest({
    this.name,
    this.email,
  });
  Map<String, dynamic> toJson() => {
    'name': name,
    'email': email,
  };
}

Future<GetResults> getUsers() async {
  var url = 'http://127.0.0.1:3000/v1/users';
  final response = await http.get(url);
  if (response.statusCode == 200) {
    return GetResults.fromJson(json.decode(response.body));
  } else {
    throw Exception('Failed');
  }
}

Future<PostResults> postUser() async {
  var url = "http://127.0.0.1:3000/v1/users";                             // APIのURI
  var request = new SampleRequest(name: 'foo', email: 'foo@example.com'); // Userのパラメータ。自由に変更してOK
  final response = await http.post(url,
      body: json.encode(request.toJson()),
      headers: {"Content-Type": "application/json"});
  if (response.statusCode == 200) {
    return PostResults.fromJson(json.decode(response.body));
  } else {
    throw Exception('Failed');
  }
}

シミュレータを起動すると、、登録されたUserがきちんと返ってきてますね!

スクリーンショット 2020-09-12 21.30.54.png

右下のAddボタンを押すと、、main.dartで指定したUserデータが登録され、取得できているのも確認できました!

スクリーンショット 2020-09-12 21.58.37.png

終わり

質問やご指摘があればコメントにお願いします。

10
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
12