アニメ制作の進行とソフトウェア開発のマネジメントの話

結論から言えば、非常に似ている…というのは10年以上前から思っているのだけど、アニメ制作会社の「進行」が具体的にどんな仕事をこなしているのかわからなかったのだが、TVアニメ「SHIROBAKO」公式サイト を見るとよくわかる。というか、ソフトウェア開発のマネジメント(主にアジャイル開発のほう)で、どのようなプランニングと調節/折衝を行えばよいのかが、よく描かれている。

プランニング

何を作るのかが決まると、概略のスケジュールを立てる。スケジュールを立てるには、あらかじめ必要なタスクが必要になる。

image

ソフトウェア開発は複雑怪奇なのでプランニングがうまくいかないという話を昔からよく聞くが、実は、大まかな単位(要件定義、設計、実装、試験などの古来のウォーターフォール開発の工程)と関わる人はあまり変わらない。

だから、超概算見積もり的には、ざっくりと開発者の人数と人月を出してしまって、超概算のスケジュールを立てる。最初のプランにあうかどうかは分からないし、実際には合わない。しかし、プロジェクトが進めば「確実性が増す」ので、その都度プランを変えていく。

どちらのせよ、最初にタスクの総量の概算を出す必要がある。

変更に対応する

それぞれのメンバの思惑があって、ステークホルダー(利害関係者)があるので、横槍なり変更がはいる。

image

過度な変更を排除するのもマネジメント手法のひとつ(スクラムマスターの役割とか)だけど、やむ得ない変更の場合は、現在の進行を変える必要がでてくる。

image

現場の進行全体を止める必要はなく、進められる部分は勧めてしまう。いわゆるタスクを並行に動かして依存関係をチェックする。

大抵のマネージャはガントチャートにそれを求めてしまうが、変更しにくいガントチャートのソフトを使うことによって、現場の進行を変更できないというドツボにはまってしまう。なので、ガントチャートは過信せず、単なるチケットの羅列でもよい場合が多い。

残件を確認する

全てのタスクをこなしたら終了になるとすれば、すべてのタスクがどれほどあるのか?を知っておかないといけない。

image

トラブルが発生したら、タスクが増える。何かを整理したらタスクが減る。プロジェクト開始時に未知だった部分も、プロジェクトが進むと既知になってくる。タスクを抽出して、消化したタスクを消していけば、残りのタスクが分かる。

そうすると、あとどれだけのタスクをこなせばプロジェクトが終わるのかが判明する。

なので、マネージャはあとどれだけ仕事をこなしたら、プロジェクトが終わるのか?をバーンダウンチャートなどを使いながら常に頭に入れておく必要がある。

合意形成

ソフトウェア開発において、顧客との合意形成(場合によってはメンバに対しても)することは、営業の範疇なのかマネジメントの範疇なのかが問われるけど、単純な契約書だけでは済まないものが、合意形成である。

image

理不尽な要求ははねのけるのだけど、どうしても受け入れざるを得ない要求変更というものがある。そこは、プロジェクト予算とか保険とかマージンとかで賄うものなのだけど(あるいは、工数単位で請求するとか)、納期が変わらないというパターンが一番面倒である。

これも、日本のソフトウェア開発の事情と、欧米の開発事情、中国や東南アジアでの開発事情もあるので、それぞれの文化における「合意形成」のラインを引いておく必要がある。こちらの常識はあちらでは通じないし、同時にあちらの常識をこちらに押し通してくる場合もある。

合意形成の失敗によって、被害をこうむりそうな場合、いくつかの対処のパターンがある。そう、対処が数パターンあることを覚えておくとよい。

  • 違約金を払ってでも撤退する
  • 自腹で、予算を変えても、変更を飲み込む
  • 予算を変更せずに、できればスケジュール(納期)を変更する。
  • 納期は変えずに、追加費用を請求する
  • 未作成の部分と追加要求を交換する
  • 変更要求を、納期後に飲み込む
  • 変更要求を、受け付けない。

どのパターンを取ってもよいのだが、いちばんやってはいけないのは「変更要求をのみ込んで、予算も納期も変えない。機能も変えない」というパターンだ。これだと、いくらでも変更を突っ込まれてしまう。何かを詰め込めば、何がはみ出るというパターンに必ずしておく。

ひとまず、ざっと箇条書き的に。全話見たら、また追記しましょうか。

カテゴリー: プロジェクト管理 | アニメ制作の進行とソフトウェア開発のマネジメントの話 はコメントを受け付けていません

少しシビアにプロジェクトの金額見積もりをしてみよう

「シビアに」というのは、社員ではなくても、派遣/契約/準契約などなど、なんでもいいんだが「社員以外」の人がプロジェクトに入って開発を行う場合を考える。

例えば、

  • マネージャ1名
  • 派遣3名
  • 開発期間が3か月

というパターンを考えてみる。一見、プロジェクトの費用的には、マネージャと派遣社員の4名だけが関わっているように見えるが、実は、

  • 契約を取ってきた営業
  • 派遣している派遣会社

がある。他にも会社の間接部門だとか設備費用とか諸々があるけど、ひとまず人としてかかわってくる2名(派遣会社は組織だけど)も加える。

こうなると、プロジェクトの予算を、マネージャ、派遣3名、営業、派遣会社の6名で分け合うことになる。

割合を決める

営業費用、マネジメント費用、派遣会社の斡旋費用を割合で決定する。

  • 営業費用 = プロジェクト費用の20%
  • マネジメント = プロジェクト費用の10%
  • 斡旋料 = 派遣単価の20%

この割合が、だいたい現実的なところ。大きなプロジェクトであればマネージャがべったりつくこともできるけど、5人以下の小規模のプロジェクトの場合はフルという訳にはいかない。ので、費用的に10%程度にしてある。

営業費用、歩合制も含めて20%と考えてみる。営業の場合、足で稼がないと駄目な場合もあれば引き合いの場合もあって色々なので、実際は結構かかっている。

仮の予算を決める

最初に、概算予算を決める。

3か月のプロジェクトなので、

  • 派遣社員の単価を50万円として、3×3人月で 450万円
  • 営業費用が2割として、概算で90万円
  • マネジメントが1割として、概算で50万円

合計で600万円程度ではないか?と初期予算を決める。

image

諸々計算すると、630万円になるので、ちょっとたりない。なので、調節する。

image

概算を、650万円にすると、合計が似た感じになるので、この予算でいけることがわかる。

対外的には、派遣社員の3名しか見えないので、お客に示す単価は 72万円/月程度になる。

割合を見る

総金額の取り分を円グラフにしてみると、こんな感じになる。

image

総取り分の半分位を、営業職、マネージャ、派遣会社が取っていることになる。この割合が妥当かどうかは、各自が判断することだが、少なくともアニメ業界よりもマシだけど、それに近い感じになっていることが分かる。

派遣Aのみに外注する

ためしに、3人月のプロジェクトを派遣Aのひとりだけに外注してみる。

image

トータルだと、220万円ぐらいでいけることがわかる。

image

同じように円グラフにすると、割合はこんな感じになる。派遣Aの手取りが、妥当になるように調節していくと、人月単位での総予算が計算できる(あまり、手取りが低いと、家賃と税金で精いっぱいになってしまうので、生活苦になりITの勉強ができない)。これを、規模見積もりの概算と比較していくと、予算が妥当かどうかがわかる。

ツール

Excel で作った「超概算金額見積もりツール」はこちら

https://1drv.ms/x/s!AmXmBbuizQkXgpt507RUtRIS3BY9yw

カテゴリー: プロジェクト管理 | 少しシビアにプロジェクトの金額見積もりをしてみよう はコメントを受け付けていません

VB6用ステップカウンタを使い、移行見積もりする

実にベタなツールではあるけれど、VB6 用のステップカウンタを作ったので github moonmile/vb6check: VB6移行の規模見積もりツール に公開しておきます。

使い方

使い方は簡単で、.vbp ファイルか、フォルダを指定して VB6プロジェクトのファイル(.bas, *.frm)を拾ってきてステップカウントします。

こんな風に、全コード/コードのみ/空行抜きで取得できるので、Excel に出力してから、フィルターを掛けたり sum を使ってカウントすれば ok です。

  • 全コードは、単なるファイルの行数
  • コードのみは、*.frm の先頭にあるデザイン部分を抜いた行数
  • 空白抜きは、所謂コメントや空行を抜いたときの行数

これ自体で何ができるという訳ではないけれど、つまりは VB6 から C# へ移行するときのコード調査として使います。

使いどころ

ここからが本格的な使い方で、本来の目的は VB6 から C# へ移行するときに「どの程度の規模が発生するのか?」の目安にする。

現システムが VB6 を使っていると、いよいよ Windows 7 の期限切れが発生してきて、Windows 10 に OS を移行しないといけない。しかし、既存の VB6 の業務ツールがあって簡単には移行できない。予算が潤沢にあれば、えいっと業務改善も含めて Windows 10 へ移行&クラウド利用、などに変えてしまう方法もあるのだが、なんらかの理由があってそれができない。

消極的な理由としては「予算がない」のだろうけど、実は、ある程度予算があったとしても「積極的に移行しない」理由というのが存在する。

  • 現状のシステムが、会社の業務に最適化されていてシステム自体を変更する必要がない

というパターンだ。IT システムの場合、現在の AI やらクラウドやらで、最新技術を使ったほうがよいような気がしていたのでが、実はそうでもないことが分かってきた。業務をするときの「慣れ」ではないか?という理由を自分はつけて来たが、もっと良い例を最近思いついた。

明治や大正時代の鋳物で作られたような工場の機械が、現在でも丁寧に使われていると同じ理由ではないだろうか?

IT 自体はどんどん進化するし、モノの工作機械もどんどん進化する。けれども、作られるもの自体は必ずしも変化するとは限らない(伝統工芸とか昔から変わらない製品とか)。だから、昔のよいものを引き続き作る場合には、昔と同じ方法を使う必要があるし、プラス進化したものも必要になる。だから、古い工場で鋳物の工作機械が動いていて、最新のWEBやスマホで受注ができたり、工程をAIで管理/観測できたりする。そういう新旧混在した場合もある。

なので、まるごと最新にする必要はないし、場合によっては古いものをそのまま使っても良い。RPA もそのひとつの解答だろう。

無料RPAで「ソリティア」に挑んでみた 業務自動化でゲームも自動化できるか (1/2) – ITmedia NEWS https://www.itmedia.co.jp/news/articles/1903/03/news033.html

そんな訳で、RPA とは別に、手作業で VB6 から C#/WPF/MVVM に移行するパターンをいくつか実践している。

VB6 から C#/WPF への移行パターン

VB6 で作成した業務システムの移行先として、WEB アプリがあるのだけど、WEB に移行できない&デスクトップアプリである必要があるパターンがある。

  • 他のデスクトップアプリと連携している
  • 印刷ものが多い
  • 複数の画面を同時に開いて作業することが多い
  • キーレスポンスが早いことが求められる

最近は、JavaScript/Node.js/React のパターンで、画面のレスポンスは早くなったのだが、問題は「VB6 時代のベタな UI をブラウザ上で実現するのが難しい」ってこともある。UI を変えない、Look & feel を変えない場合は、ベタなデスクトップアプリのほうが忠実に作れたりする。

これは以前書いたかもしれないが、業務 VB6 にはいくつかの要素があって、

  • データベース接続(ODBC, RDO, ADO)がある。
  • 帳票出力(クリスタルレポート、スプレッドシート)がある
  • データグリッドを使ったリスト表示がある
  • 3Dコントロールのボタンがついている
  • エンターキーによる項目移動が実装されている
  • 数値や文字制限などが独自実装されている

このあたりを、WPF/MVVM で実装する。

  • データベースは EF/LINQ を使う
  • 帳票出力は Excel 出力に切り替える、あるいはクリスタルレポート
  • グリッド表示は DataGrid を使い、データバインドする
  • 3Dコントロールは、通常のボタンに戻す
  • エンターキー移動は、独自ライブラリを作った
  • 数値、文字制限は、コンバーターで実装可能

というパターンに切り替える。このあたり、VB6 のときはデータベース接続やテキストボックス内の操作でコードが肥大していたが、C# の MVVM パターンを使うことにより、1/4 から 1/3 程度のコード量に減らすことができる。

このあたりの話は後日。

カテゴリー: 開発 | VB6用ステップカウンタを使い、移行見積もりする はコメントを受け付けていません

Laravel で wordpress を読み込む(自前MVC)

ひとまず、Laravel に慣れるために MVC パターンを直接扱ってみる。先に InfyOm を使って自動作成してみたのだが、エラーが頻発してよくわからん。ので、そういうときは手作業でやってみるに限る。

目的としては、

  • wordpress のような既存のデータベースを読み込む
  • マスター管理のような CRUD ベースのページを自動作成する
  • 場合によっては、CRUD な Web API も自動生成する。
  • 場合によっては、クライアントから呼び出すコード(C#, nodejsとか)も生成してしまう。

まずは、xampp で wordpress をインストールして、mysql 部分だけアクセス可能にしておく。

composer でインストール&動作確認

適当なフォルダーを作成して、Laravel をインストール


composer create-project --prefer-dist laravel/laravel blog

データベースの設定

/.env を修正。wordpress のデータベースを読み込ませる。


DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=wordpress DB_USERNAME=wordpress DB_PASSWORD=wordpress

リスト表示を作成

いわゆるトップにアクセスして、一覧を表示させる。http://localhost:8000/WpPost/ にアクセスする。wordpress の記事は wp_posts にあるのだが、これを Model クラスでは WpPost として使う。

Model の作成

Model クラスを作る。対象のテーブル名を記述していないが、この手の MVC パターンの命名規約に

  • テーブル名は複数形、モデルクラスは単数形
  • モデルクラスは、キャメルパターンが多い
  • テーブル名はスネークパターンが多い
  • 「_」が単語の区切りになる。

ということで、自動的に WpPost から wp_posts を推測させている。wordpress の場合プレフィックスは「wp_」とは限らないので、このあたりは明示的にテーブル名を指定したほうがよいかも。


php artisan make:model Models/WpPost

/app/Models/wp_posts.php が生成される


<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class WpPost extends Model { // }

Model クラスの中身が空っぽで不安になるが、これで ORM されている。ただし、条件があって、

  • プライマリーキーは「id」の int 型である。
  • 作成日時に「create_on」を使う。
  • 更新日時に「update_on」を使う。

が必要になる。これを満たすテーブルの場合はそのまま使えるし、新規にテーブルを作るときはこの規約に合わせるほうが便利(テーブルの複数形もあわせて)。

ただ、今回は既存の wordpress のテーブルを使うので、この条件が少し違っている。これは後で修正することになる。

Controller の作成

コントローラーは、モデル名 + 「Controller」となる。


php artisan make:controller WpPostController

/app/Http/Controllers/WpPostController.php が生成される


<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class WpPostController extends Controller { // }

これも中身が空っぽで不安になるのだが、ここは手作業で作ることになる。別途、自動生成することもできるのだけど、今回は手動で作ってみる。

リスト表示する index アクションだけ追加する。


<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Models\WpPost; class WpPostController extends Controller { public function index() { $items = WpPost::all(); return view('WpPost.index', ['items' => $items]); } }

データベースから全件拾ってくる all() を使うのと、ビューに渡すために items で参照できるようにしておく。

  • ASP.NET の EF のように、ent.WpPost.ToList() な感じで取得
  • ViewBag( “items” ) = items のようなもの

View を作成する

/resources/views/layouts/app.blade.php を作成する。いわゆる
全ページから参照するテンプレートで、@extends(‘layouts.app’) で参照できるようにする。


<!DOCTYPE html> <html> <head> <title> @yield('title') </title> </head> <body> <h1>@yield('title')</h1> <div class="content-wrapper"> @yield('content') </div> </body> </html>

各ページから入れ込む部分は「@yield()」で書いておく。各ページからは「@section()」で参照する。本来は CSS とか JS とかの読み込みが書いてるのだが、今回はシンプルに書いておく。

一覧表を表示するページはこっち。/resources/views/WpPost/index.blade.php を新規作成する。


@extends('layouts.app') @section('title','WpPost.index') @section('content') <table> <thead> <td>ID</td> <td>post_title</td> <td>post_date</td> </thead> <tbody> @foreach( $items as $item ) <tr> <td>{{ $item->ID }}</td> <td>{{ $item->post_title }}</td> <td>{{ $item->post_date }}</td> </tr> @endforeach </tbody> </table> @endsection

レイアウトのファイルを「@extends(‘layouts.app’)」で参照している。あと、コントローラーから渡された、$items を使ってリスト表示する。

ルート情報を追加

/routes/web.php に追加する


Route::get('WpPost', 'WpPostController@index');

ブラウザから呼び出すと、どこが呼び出されるかを記述する。「http://localhost:8000/WpPost」とすると、WpPostController クラスの index メソッドが呼び出される。

ブラウザで表示する.

簡易 HTTP サーバーを動かして、呼び出してみる。


php artisan serve

作成順序

MVC パターンを使うと、書き込むコードや設定があちこち散るのが大変なのだけど、手順を作っておくと便利だったりする。

  1. モデルクラスを作る /app/Models/wp_posts.php
  2. コントローラーを作る /app/Http/Controllers/WpPostController.php
  3. アクションを作る WpPostController::index
  4. ビューを作る /resources/views/WpPost/index.blade.php
  5. ルート情報を作る /routes/web.php

Laravel の場合は、Model -> Controller -> View -> ルート情報(web.php) の順で作っていくとわかりやすい。

詳細表示を作成

id を指定して詳細表示をするページを作る。http://localhost:8000/WpPost/1 のように ID を指定してページを開く。

Controller の作成

/app/Http/Controllers/WpPostController.php を編集


<?php class WpPostController extends Controller { ... public function show($id) { $item = WpPost::find($id); return view('WpPost.show', ['item' => $item]); } }

プライマリ―キーの id での検索は find() を使う。

View を作成する

/resources/views/WpPost/show.blade.php を新規作成する。


@extends('layouts.app') @section('title','WpPost.show') @section('content') <table> <tr> <td>ID</td> <td> {{ $item->ID }}</td> </tr> <tr> <td>post_title</td> <td> {{ $item->post_title }}</td> </tr> <tr> <td>post_date</td> <td> {{ $item->post_date }}</td> </tr> </table> @endsection

ルート情報を追加

/routes/web.php に追加する


Route::get('WpPost/{id}', 'WpPostController@show');

ブラウザで表示する.

http://localhost:8000/WpPost/1

MVC パターンの関係はこうなっている。

Create ページを追加

新規作成のページを作る。

  • 新規入力をする create
  • データを登録する store

に分かれる

Model を修正

/app/Models/wp_posts.php を修正

  • ID が自動インクリメントなので除外する
  • create_at, update_at を除外する
  • デフォルト値を補う

class WpPost extends Model { // IDは自動インクリメント protected $guarded = array('ID'); // 作成日、更新日はなし const CREATED_AT = null; const UPDATED_AT = null; public function __construct() { // insert 時にデフォルト値を補っておく $this->post_excerpt = ''; $this->to_ping = ''; $this->pinged = ''; $this->post_content_filtered = ''; } }

Controller を追加

/app/Http/Controllers/WpPostController.php を編集


class WpPostController extends Controller { ... /// 新規作成(入力) public function create() { $item = new WpPost(); return view('WpPost.create', ['item' => $item]); } /// 新規入力(登録) public function store( Request $request ) { $item = new WpPost(); $form = $request->all(); unset($form['_token']); $item->fill($form); // 現在時刻を入れておく $item->post_date = date('Y-m-d H:i:s'); $item->save(); return redirect('/WpPost'); } }

View を作成する

/resources/views/WpPost/create.blade.php を新規作成する。


@extends('layouts.app') @section('title','WpPost.create') @section('content') <form action="/WpPost/create" method="post" > {{ csrf_field() }} <table> <tr> <td>post_title</td> <td><input type="text" name="post_title" value="{{ $item->post_title }}" /></td> </tr> <tr> <td>post_content</td> <td><input type="text" name="post_content" value="{{ $item->post_content }}" /></td> </tr> </table> <input type="submit" /> </form> @endsection

ルート情報を追加

/routes/web.php に追加する

  • create と store は、show の前に入れる

Route::get('WpPost', 'WpPostController@index'); Route::get('WpPost/create', 'WpPostController@create'); Route::post('WpPost/create', 'WpPostController@store'); Route::get('WpPost/{id}', 'WpPostController@show');

ブラウザで表示する.

http://localhost:8000/WpPost/create

エラーがなければ、一覧へ戻る

編集画面を作る

  • 更新情報を入力する画面
  • 更新処理をする

の2つのアクションの分かれる

Model の修正

/app/Models/wp_posts.php を修正

  • プライマリ―キーが 「id」以外の時は $primaryKey で設定

class WpPost extends Model { // IDは自動インクリメントなので除外 public $incrementing = true ; protected $guarded = array('ID'); // プライマリーキー名を変更 protected $primaryKey = 'ID'; // 作成日、更新日はなし const CREATED_AT = null; const UPDATED_AT = null; public function __construct() { // insert 時にデフォルト値を補っておく $this->post_excerpt = ''; $this->to_ping = ''; $this->pinged = ''; $this->post_content_filtered = ''; } }

Controller を作る

/app/Http/Controllers/WpPostController.php を編集


class WpPostController extends Controller { ... /// 更新(入力) public function edit($id) { $item = WpPost::find($id); return view('WpPost.edit', ['item' => $item]); } /// 更新(コミット) public function update(Request $request) { $item = new WpPost(); $form = $request->all(); $id = $form['ID']; unset($form['_token']); $item = WpPost::find($id); $item->fill($form); // 更新日時を入れておく $item->post_modified = date('Y-m-d H:i:s'); $item->save(); // 一覧へ戻る return redirect('/WpPost'); } }

View を作成する

/resources/views/WpPost/edit.blade.php を新規作成する。


@extends('layouts.app') @section('title','WpPost.edit') @section('content') <form action="/WpPost/edit" method="post" > {{ csrf_field() }} <input type="hidden" name="ID" value="{{ $item->ID }}" /> <table> <tr> <td>post_title</td> <td><input type="text" name="post_title" value="{{ $item->post_title }}" /></td> </tr> <tr> <td>post_content</td> <td><input type="text" name="post_content" value="{{ $item->post_content }}" /></td> </tr> </table> <input type="submit" /> </form> @endsection

ルート情報を追加

/routes/web.php に追加する

  • create と update は、show の前に入れる
  • update のほうに、id が含められないので、form から取得する

Route::get('WpPost/edit/{id}', 'WpPostController@edit'); Route::post('WpPost/edit', 'WpPostController@update'); Route::get('WpPost/{id}', 'WpPostController@show');

ブラウザで表示する.

http://localhost:8000/WpPost/create

エラーがなければ、一覧へ戻る

アイテムを削除する

  • 更新画面に「削除」ボタンを追加する
  • 削除処理をする

の2つのアクションの分かれる

Controller を作る

/app/Http/Controllers/WpPostController.php を編集


class WpPostController extends Controller { ... /// 削除 public function destroy( Request $request ) { $form = $request->all(); $id = $form['ID']; WpPost::find($id)->delete(); // 一覧へ戻る return redirect('/WpPost'); } }

View を編集する

/resources/views/WpPost/edit.blade.php に追記する

下のほうに「削除」ボタンを付ける。


@extends('layouts.app') @section('title','WpPost.edit') @section('content') <form action="/WpPost/edit" method="post" > {{ csrf_field() }} <input type="hidden" name="ID" value="{{ $item->ID }}" /> <table> <tr> <td>post_title</td> <td><input type="text" name="post_title" value="{{ $item->post_title }}" /></td> </tr> <tr> <td>post_content</td> <td><input type="text" name="post_content" value="{{ $item->post_content }}" /></td> </tr> </table> <input type="submit" /> </form> <script> function check(){ return window.confirm('削除してよろしいですか?'); } </script> <form action="/WpPost/delete" method="post" onSubmit="return check()" > {{ csrf_field() }} <input type="hidden" name="ID" value="{{ $item->ID }}" /> <input type="submit" value="削除" /> </form> @endsection

ルート情報を追加

/routes/web.php に追加する

  • destroy は、show の前に入れる
  • destroy に、id が含められないので、form から取得する

Route::post('WpPost/delete', 'WpPostController@destroy'); Route::get('WpPost/{id}', 'WpPostController@show');

ブラウザで表示する.

http://localhost:8000/WpPost/edit/10

「削除」ボタンを押したときに問い合わせのダイアログが出る。

カテゴリー: 開発, PHP | Laravel で wordpress を読み込む(自前MVC) はコメントを受け付けていません

Azure Functionsで初めてのPython関数を作ったら躓くの話

なんとはなしに、Function AppをPythonで作ってみるテストで、いきなり躓いたのでメモ。

Azure で初めての Python 関数を作成する | Microsoft Docs
https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-create-first-function-python

基本的な手順は、上記に書いてあるので、素直にコマンドを打てばいいのだけど、どうやら一カ所だけ「書いてない」ので、そこで頓挫してしまう。

仮想環境をアクティブにする


python3.6 -m venv .env
source .env/bin/activate

プロジェクトを作成する

Pythonの仮想環境で動かしているので、”–worker-runtime python” は必要なし。


func init MyFunctionProj

関数を作成する

プロジェクトのフォルダに移動してから、func new する


cd MyFunctionProj
func new

コード(__init__.py)を見る


import logging
import azure.functions as func
def main(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')
    name = req.params.get('name')
    if not name:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            name = req_body.get('name')
    if name:
        return func.HttpResponse(f"Hello {name}!")
    else:
        return func.HttpResponse(
             "Please pass a name on the query string or in the request body",
             status_code=400
        )

ローカルで実行する


func host start


http://localhost:7071/api/HttpTrigger

にブラウザでアクセスすればOK.

デプロイする

あらかじめ Azure に作成しておいた「azfunc-python」という名前のFunction Appにデプロイします。


func azure functionapp publish azfunc-python

こんな風に、エラー出ます。


Getting site publishing info...
pip download -r /home/masuda/azfunc/MyFunctionProj/requirements.txt --dest /tmp/azureworkeriyncp4zc
There was an error restoring dependencies.ERROR: ['pip', 'download', '-r', '/home/masuda/azfunc/MyFunctionProj/requirements.txt', '--dest', '/tmp/azureworkeriyncp4zc'] failed with exit code 1

何度もやっても駄目、環境を変えても駄目なんですが、どうやら「requirements.txt」の中にある「pkg-resources==0.0.0」を削除すればいいらしいのです。


azure-functions==1.0.0b3
azure-functions-worker==1.0.0b4
grpcio==1.14.2
grpcio-tools==1.14.2
pkg-resources==0.0.0	★ここを削除
protobuf==3.7.1
six==1.12.0

なんでテンプレートがこの行を作るのか、なぜこの行を消すとうまくいくのかよくわからんのですが、vi などで消します。
Git あたりで見ると、去年の暮れあたりでバグっぽい雰囲気があるのですが、解決されたのかどうかよくわかりません。クローズはされているけど。

Azure Portalで見てみる

ポータルで見ると、無事アップロードできました。

めでたしめでたし。

カテゴリー: 開発 | Azure Functionsで初めてのPython関数を作ったら躓くの話 はコメントを受け付けていません

Access形式でバックアップしてSQL Serverに手軽に戻すC#な方法

SQL Server Management Studio の「データのエクスポート」を使うと、SQL Server から手軽にバックアップが取れる。バックアップというよりも、どこかに移動したいときに使うわけだが、Access 形式で取るのが一番手軽だと思う。

image

問題

顧客の SQL Server なデータを、開発用の SQL Server に吸い上げて、検証環境を作ることはよくやる。確実な方法は、SQL Server のバックアップをコピーするのがベストなのだけど、残念ながらバージョンが違うとこれがうまくいかない。バージョンが違うのだから、バージョンを揃えた環境を整えるのが筋なんだけど、古めのデータベース(SQL Server 7 とか)とか微妙な差異があって、いろいろなバージョンを揃えておくのも以外と面倒くさい。できれば、開発環境の SQL Server をそのまま使っておきたい。

SSMS の「データのインポート」がそのまま動けばいいのだが、Access 形式にエクスポートしたときにカラムの型が違ってしまっていて、インポート時にエラーになる。それに PK の扱いもややこしくて、ちまちま設定をしながらインポートをしないといけない。ひとつふたつのテーブルならば、手作業でもいいのだけど、

  • 大量のテーブル(20以上ある)をインポートするのに、いちいち手作業は大変
  • 何度もインポートするので手作業は大変

解決方法

ということで、緩い感じで、Access から SQL Server へインポートできないものか?と考えていた。で、結局 C# でスクリプト的なものを書くことにする。

顧客 SQL Server → Access → 開発 SQL Server

という流れなので、SQL Server 同士の型は一緒になっている。最初の「顧客 SQL Server → Access 」は問題がないので、後の「Access → 開発 SQL Server」をスクリプト化する。


public class DBBackup
{
    public string AccessMdbPath { get; set; }
    public string SqlServerConnectionString { get; set; }

    public bool Restore( string tableName )
    {
        // Accessから読み込み
        OleDbConnection cn = new OleDbConnection();
        cn.ConnectionString = @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + AccessMdbPath; // MDB名など
        var dt = new DataTable();
        var da = new OleDbDataAdapter($"select * from {tableName}", cn);
        da.Fill(dt);
        // SQL Severへ書き出し
        var cnn = new SqlConnection(SqlServerConnectionString);
        // 指定テーブル内を消去
        var cmd = new SqlCommand($"TRUNCATE TABLE {tableName}", cnn);
        cmd.Connection.Open();
        cmd.ExecuteNonQuery();
        cmd.Connection.Close();
        // バルクコピーで挿入
        var bc = new SqlBulkCopy(cnn);
        cnn.Open();
        bc.DestinationTableName = tableName;
        bc.WriteToServer(dt);
        cnn.Close();

        return true;
    }

}

こんな感じに

  • OleDb で Access MDB ファイルから読み込む
  • SqlBulkCopy を使って、SQL Server にバルクコピーする

バルクコピーが一番早いし、こうじゃないと実行時に時間が掛かってしまう。

使い方

テーブル名は Access でも SQL Server でも一緒なので、DBBackup::Restore を使ってがしがしと戻していく。テーブル名は自動で取得できるけど、今回の場合は、移行不要なテーブルもあるので、必要なものだけピックアップしている。


private void clickLoadAccess(object sender, RoutedEventArgs e)
{
    var dbb = new DBBackup()
    {
        AccessMdbPath = @"営業管理_20190304.mdb",
        SqlServerConnectionString = "Data Source=.;Initial Catalog=営業管理2;Integrated Security=True;",
    };

    var tables = new List()
    {
        "タイトルファイル2",
        "タイトルファイル属性",
        "プログラム一覧",
        "パスワード",
        "ユーザマスタ",
...
    };

    foreach ( var name in tables )
    {
        dbb.Restore( name );
        System.Diagnostics.Debug.WriteLine("テーブル: " + name);
    }
    MessageBox.Show("保存しました");
}
カテゴリー: 開発, C# | Access形式でバックアップしてSQL Serverに手軽に戻すC#な方法 はコメントを受け付けていません

サブドメインでアクセスしてDockerコンテナの指定ポートに割り当てる

ほどよく、Azure Functions と Docker の組み合わせがうまくいくことが分かったので、実運用でのパターンを考えてみる。Docker は内部のポートを外部のポートへ -p オプションを使ってフォワードして、外部から http://servername.com:8000/ のようにアクセスできる。だが、こうすると Docker を動かして機能を増やすたびにポートを開放しないといけなくて、結構リスキーだったりする。

ならば、host://func.servername.com/ のようにサブドメイン名をしておいて、これを http://servername.com:8000/ にフォワードしてくれるように設定すれば、開放するポートは 80 番だけで済む。これを実現するのが「リバースプロキシ」なんだけど、Apache や Nginx でその機能がある。

Apacheでリバースプロキシ – Qiita https://qiita.com/tac0x2a/items/1ad05f204ac5ab679fb2

ひとまず、Apache を使って動作を確認してみよう。

目的

リバースプロキシを使って、サブドメインを指定すると、内部で起動している Docker コンテナに接続できるようにする。

実現方法

  • ローカルで動作している Linux サーバー「luna.local」を使う。
  • Docker コンテナを -p 8000:80 で起動しておく
  • Windows 10 から azfunc.luna.local にアクセスすると、luna.local:8000 とアクセスしたと同じように設定する

設定手順

リバースプロキシの設定

Linux サーバーの luna.local に apache2 をインストールして、


/etc/apache2/sites-available/azfunc.luna.local.conf


を設定する。


<VirtualHost *:80>
  ServerAdmin webmaster@azfunc.luna.local
  ErrorLog /var/log/apache2/error.log
  CustomLog /var/log/apache2/access.log combined
  LogLevel warn
  ProxyPreserveHost On
  ServerName azfunc.luna.local
  ProxyPass        / http://localhost:8000/
  ProxyPassReverse / http://localhost:8000/
</VirtualHost>

これでリバースプロキシを使って、azfunc.luna.local のアクセスが http://localhost:8000/ にフォワードされる。

proxy 用のコマンドを動かして、apache2 をリブートする。


$ sudo a2enmod proxy
$ sudo a2enmod proxy_http
$ sudo a2ensite azfunc.luna.local.conf
$ sudo service apache2 restart

Docker の起動


sudo docker run -it --rm -p 8000:80 moonmile/sample-azure-docker

あらかじめ、hub.docker.com にアップロード済みの moonmile/sample-azure-docker をダウンロードして実行する。

sample-azure-docker は、func init と func new して作った .NET 環境の Azure Function である。

Windows 10 の hosts 設定

実は、luna.local は、DNS を使っているのではなくて、ubuntu 上で avahi-daemon を起動している。なので、Windows 10 から luna.local は見れるけど、azfunc.luna.local は見れない。

これを見れるようにするため、Windows 10 の hosts に以下の行を追加する。IP は Docker が動いている Linux サーバーの IP になる。この場合は、luna.local と同じ値になる。


192.168.1.11 azfunc.luna.local

ping azfunc.luna.local として通知できているか確認しておく。

ブラウザからアクセス

ブラウザから http://azfunc.luna.local/ へアクセスすると、無事 Function App の最初のページが表示される。これでうまく通達ができていることがわかる。

image

再びブラウザで、URLアドレス http://azfunc.luna.local/api/HttpTrigger1?name=masuda にアクセスすると、「Hello, masuda」と表示されることがわかる。

image

これでうまくローカルのFunction Appが動いていることが確認できた。

まとめ

構造的にはこんな形になっている。

image

  1. ブラウザからサブドメイン付きで呼び出す
  2. Apache2 は 80 番で受ける
  3. リバースプロキシで、指定サブドメインは 8000 番へ送る
  4. Dockerは 8000 番で受けたものを、コンテナの 80 番へ送る
  5. .NET Core + Function App が動作する

いわゆる、本番の Azure 上で sample-azure-docker.azurewebsites.net のようにアクセスしている部分を、Azure 無し&ローカル環境で構築できるというわけだ。当然のことながら、Azure の機能は使えないけど、Azure Functions だけの機能を使って試したいというのは結構簡単に実現できることが分かる。特に、本番の Azure Functions で Linux 環境+Docker の組み合わせにしようとすると、Linux の VM が必要になってしまい、月額5,000円位かかることになる。これはちょっと高い。

なので、一般的な VPS を借りてきて、そこの中でサブドメイン付きで作れれば、.NET Core + Azure Functions な環境を Docker で動かすのも悪くないかなと思うのだがどうだろうか。たぶん、Docker の運用系の使い方としては、こういうサブドメインを経由させるほうが一般的だと思う。

実際は認証やSSLなどは、HTTPヘッダの部分をリバースプロキシのところで設定しないといけないのだが。まあ、ひとまずローカルな Linux サーバーで確認がとれたといことで。

あとで、nginx のリバースプロキシも確認してみる。

カテゴリー: Azure, Docker, NET Core | サブドメインでアクセスしてDockerコンテナの指定ポートに割り当てる はコメントを受け付けていません

Docker Toolbox で Azure Functionsを動かす

Hyper-V が動かない環境の場合、Docker Toolbox を使えばよいので、使ってみる。内部で VirtualBox を動かしているので、仮想OSを VMWare で作っていると変わらないのだが、UI で操作できる「Kitematic」が意外と便利なので。

Docker Toolboxのインストール:Windows編 – Qiita https://qiita.com/maemori/items/52b1639fba4b1e68fccd

ツールをインストールすると、3つのアイコンが作られる。Docker Quickstart Terminal は通常のコマンドライン、Kitematic (Alpha) が GUI ツールである。

image

Kitematic を起動して Docker Hub のユーザー名とパスワードを入力する。ここから Docker イメージをダウンロードする。

image

あらかじめ、Azure Functions で作った Dockerfile があるので「moomile」で検索する。公開済みの moonmile/sample-azure-docker を Create する

image

うまく起動ができると、80 番ポートで待っている状態になる。このままではアクセスできないので、

image

Docker の 80 番ポートを、8000番にフォワードする。いわゆる –p 80:8000 ということ。

これで、http://192.168.99.100:8000 でアクセスができるようになる。Docker コンテナをリブートしておく。

image

Docker の IP アドレス(192.168.99.100固定なのか?)を確認して、ブラウザから、http://192.168.99.100:8000 でアクセスする。Function App のトップページが開かれている。内部としては、Azure のエミュレータが動いている形になる。

image

Function App の HttpTrigger1 関数を呼び出すためにブラウザで、http://192.168.99.100:8000/api/HttpTrigger1?name=masuda を指定すると「Hello, masuda」で返される。

image

これで、Linux 環境がなくても、自由に Azure Function + Docker が試せますね。本家の Azure Function の Linux&Docker は結構高いので、ローカルで動かせると便利だろうということで。

あとで、サブドメインを使ってポートフォワードする方法を合わせて試してみる。

カテゴリー: Azure, NET Core | Docker Toolbox で Azure Functionsを動かす はコメントを受け付けていません

Azure FunctionsのDocker版をローカルで起動する

Azure Functions で「なでしこ」が動くとか「COBOL」が動くとかという記事があったハズなのだが、これ難しいのでは?とは思っていたが、実は内部で Docker が動いているだけだった、という話。

Azure でサーバーレスなFunction Appを作るときに、現状ではWindows環境にすると「dotnet」と「node」、「java」が選べるようになっている。それぞれ VM 環境を Azure 側で用意してそれの上で動かすのだから、このあたりの環境は Azure = Microsoft 社が用意するんだろうなぁというのが想像できる。

image

もうひとつ、Linux 環境というのがあって、こっちは .NET と JavaScript と python が選べる。Windows のほうに python のがないのは何故?とか PHP は何処で動かすのか?と不思議に思っていたのだが、実は PHP のようなほかの言語は、Linux 環境での「Docker イメージ」のほうに作ることになる。

image

Function App は内部的には App Service と同じなので、つまりは VM 上で動いている。App Service のほうは、HTTP プロトコルの受け口から作るけど、Function App の場合は MVC でいうところの Controller のメソッドが直接呼び出される形になっている、と思えばいい。

となると、この「Docker イメージ」というやつは、

  • 内部で、HTTP サーバーが起動している
  • クライアントHTTP リクエストをすると、内部の MVC の Controller らしきものが起動される。
  • Function Appの「関数」として、static なコードが呼び出される。

というスタイルになればいい。実は「内部の MVC の Controller らしきもの」のように分離されてなくても、単純に HTTP リクエストで呼び出されたときにレスポンスを返す、という通常の HTTP サーバーがあればいいのだ。

Ubuntu で func init してみる

Ubuntu 上で、Function Appを作るために「func init」することができる。このときに docker スイッチを付けると、Dockerfile を作ってくれる。

func init –docker

作成された Dockerfile を見てみると、


FROM microsoft/dotnet:2.1-sdk AS installer-env

COPY . /src/dotnet-function-app
RUN cd /src/dotnet-function-app &amp;amp;&amp;amp; \
mkdir -p /home/site/wwwroot &amp;amp;&amp;amp; \
dotnet publish *.csproj --output /home/site/wwwroot

FROM mcr.microsoft.com/azure-functions/dotnet:2.0
ENV AzureWebJobsScriptRoot=/home/site/wwwroot

COPY --from=installer-env ["/home/site/wwwroot", "/home/site/wwwroot"]

となっている。これは .NET 環境(C#)なのだが、なるほど、dotnet コマンドでビルドして publish しているだけだ。*.csproj なプロジェクトは所謂 ASP.NET MVC プロジェクトみたいなものなので、ここで適当な関数が呼びだされていることになる。

試しに func start host してみる

func start host してみよう。

image

のように、普通に azure function のエミュレータが起動する。つまり、Docker の中身は azure function と同じなわけだ。まあ、同じだからいいんだけど。

docker build して docker run する

Dockerfile があるのだから、ローカルな Linux 環境でも実行ができるはずだ。Ubuntu に docker を入れた状態で、

sudo docker build -t moonmile/azure-docker:ver0.2 .

のようにビルドをすると、ビルドができる。Nuget で関連するアセンブリも持ってくる。

image

次のように docker で実行する。

sudo docker run -it -p 8000:80  moonmile/azure-docker:ver0.2

image

すると、http://localhost:8000/api/HttpTrigger1?name=masuda でアクセスできるようになる。

注意しないといけないのは、関数が呼び出されるときに AuthorizationLevel.Function のままだと、ユーザー名/パスワードを要求されるので(Basic認証なのか?)、AuthorizationLevel.Anonymous に設定して、匿名でもアクセスできるようにする。

自前の Dockerfile が作れる?

そんな訳で、.NET 環境の場合は、AzureWebJobsScriptRoot=/home/site/wwwroot なところでルートが指定されているから、このあたりで外側から指定の関数を呼び出されているんだろうなあ、ということが想像できる。

となると、PHP とか他のプログラム言語で構築する場合も、HTTP リクエスト受け付けるように Dockerfile を作ってやれば、Azure Functionsの中身を「自分で使いやすい言語」に切り替えられるだろう、ってことが分かる。Linux 環境に限られるのだが。

カテゴリー: 開発, Azure | Azure FunctionsのDocker版をローカルで起動する はコメントを受け付けていません

開発環境を仮想OS上に保管する実験

最近の開発環境と言えば、Docker で構築しておいてビルド環境を配布、というのが主流なのだが、Visual Studio 絡みで開発環境を用意するとそうもいかない。.NET Core で作っておけば Docker のコマンドラインで済むのだけど、Visual Studio でビルドということになると(*.sln を呼び出して msbuild しても状況は変わらない?Windows の GUI が必要だから)、なんらかの形で OS ごとまるっと開発環境を残すことになる。

クライアントアプリの開発となると、GUI が必須になるので、

  • 顧客の主たる OS(検証機込み)
  • 開発環境としての Visual Studio
  • 動作環境としての SQL Server 諸々

ということになって、OS/Visual Studio/SQL Server はそれぞれ当時のバージョン込みで用意しておくことになる。顧客の主に動作している OS ってのは、Windows 7 だったり、それ以前だったりすることもあるので、場合によっては、検証機/開発機の2つの環境を残すことになる。クライアントアプリの場合、デバッグ実行で動作確認することが多いので、大抵の場合「検証環境のOS」=「開発環境のOS」となることが多い。古い IE とかを動かす場合は別途検証機をいくつか用意することになる。

ちなみに IE の検証環境は、https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/ から、各 IE が入った仮想環境をダウンロードできる。動作チェックをしたい場合は、ここから逐次ダウンロードすればよいだろう。たぶん、将来的にも残っていくものと思われる。

どの仮想環境を使うか?

いくつかの仮想環境があるが、

  • Hyper-V
  • VMWare Player
  • QEMU

を検討してみた。VirtualBox もあるのだけど、これはちょっと慣れていなくて試していない。主に VMWare Workstation で仮想環境を構築してきたので、こっちのほうが慣れているというのもある。

Windows 10 上で構築するので、Hyper-V のほうが良さそうに見えるのだが、実は Hyper-V で開発環境を残してしまうと、

  • Linux をホストにできない
  • Android のエミュレータと競合しがち

という欠点がある。Android エミュレータのほうは、Hyper-V のほうを使えばいいじゃないか?という話もあるのだが、ホストが Windows に固定されてしまうのが微妙に痛い。手元で主に VM 環境を動かしているのが Ubuntu ということもあり(Windows Update のリブートに嫌気がさしてしまったので)、Linux でホストするという点も考慮しておきたい。もっとも、最近では、Hyper-V の vhdx なファイルを VMWare の vmdk にしてくれるツール StarWind V2V Converter もあるので、Hyper-V で作成しておいて、後からコンバートするという方法もある。コンバートツールには、QEMU-img を使う方法もある。

そんな訳で、Windows 上の Hyper-V じゃなくて、VMWare の Workstation Player を使って構築してみたのだが、さて、この VMWare をお客の環境に入れていいものかどうか?(最終的には、開発環境自体をお客自身が管理できる状態にしておきたい)というのが疑問になった。顧客のサーバー環境の片隅にでも、開発環境が VM 上で動いていれば、それはそれで便利なのだ。

建前上ではあるけれど、受託開発の場合は開発が済んだら一切合切を、開発会社のPC から消し去ってしまうのだ。仕様書とか試験のデータとかも、ISO9000的な保管の意味からすると、漏洩リスクを持つよりも「消して」しまうのが一番よい。実際、契約上も仕事が終わったら「消す」ことになっている。だが、実際のところは、瑕疵とか修正とかの問題があって、開発環境諸々を PC に残すことになる。不具合の発生や要求が発生するたびに、お客に開発環境を要求するのも大変だし、再び開発環境を整えるのは大変だ。だから、建前上は「消した」ことになっているのだが、なんらかの形で開発環境は残る。かなり矛盾しているが、仕事の効率上仕方がないところがある。

だが、できることならば「消す」なり、なんらかの形でバックアップで保存という形にしておきたい。大容量の HDD の片隅(場合によっては社内の大量ストレージ用のサーバー)の置くのもいいのだけど、バックアップに時間がかかるし、そもそも HDD が故障したときにそれらの開発環境がごっそり消えてしまうことになる。本来ならば「消した」ものであっても、実際改修などが起これば消してしまうと不便なわけで、じゃあ、何処に残しておけばよいか?という問題が残ってくる。

なので、残しておくは、お客さんの運用環境のどこかに置いておくのがベターだ。保守契約を結んでいれば、なんらかの社内ストレージに残しておけばよいのだが、とびとびの改修案件ということになると、継続的なバックアップ環境は顧客自身が持っておくほうがよい。かつては、実機 PC で保存しておくのがよいのだが、じゃあ、仮想環境においておくのがよいのでは?と思っている。

このとき、運用環境をできるだけ弄らないで、開発環境を仮想化しておきたい。となると、運用環境に VMWare のソフトウェアを入れるのはどうなのか?って話になる。固いこと言わなければまあいいじゃん、ってことになるけど、ちょっと考えてみる。

なので、QEMU ではどうか?ってのを思いついた。ストレージも顧客サーバーの HDD/SSD 上に置くのではなくて、USB メモリに置けばいいのではないか?QEMU だったらソフトウェアのエミュレートなので、ソフトウェアを Windows に入れなくて済む。必要なときは、USB メモリを差して使えばいいのでは?と考えてみた。

SD カードのアクセススピード

実は、いきなり USB メモリ(SD カード)上に VMWare の仮想ファイルを作ってアクセスしたのだが、条件によっては実用に耐えない。どうも、やたらに遅いので CrystalDiskMark を使ってアクセススピードを確認してみる。

上から、SSD, HDD, USB2.0+SDカード, USB3.0+SDカードの順になる。実際に利用したいのは「USB3.0+SDカード」のパターンで、高速な USB3.0 ならば安価なSDカード(USBメモリ)でも HDD の変わりになるのではないか?と思ったからだ。SD カードは東芝の128GBを購入して確認している。値段が3,000円弱なので、高価なSSDを買うより断然安く済む。

SSD TS480G
image

HDD WD20EZRX
image

Toshiba UHS-I microSDXC 128GB + ELECOM USB 2.0 MR-K011
image

Toshiba UHS-I microSDXC 128GB + iBUFFALO USB 3.0 BSCR27U3BK
image

結果を見るとわかるのだが、

  • HDD WD20EZRX
  • Toshiba UHS-I microSDXC 128GB + iBUFFALO USB 3.0 BSCR27U3BK

が、ほぼ同じになる。ということは、通常の物理HDD に仮想ストレージを用意したときと同じぐらいのスピードが SDカード+USB3.0 の組み合わせでは出るのではないか?という想定である。ただし、SDカードのほうが、シーケンシャルの書き込み(Seq Q32T1の Writeのほう)が5倍位遅いので、なんらかの大量書き込みをしたときは遅い、という予想が立つ。

実際、SDカードに仮想領域を作って VMWare Player で動かしてみると、それなりに Windows 10 が立ち上がってくる。SSD に仮想領域を作ったときよりは遅いが、まあ、なんとかなるという程度だ。しかし、Visual Studio 2017 を起動したときが非常に遅い。起動しきれば、なんとかなるのだが、起動が相当遅い。これは、仮想に割り当てるメモリを4GB程度にしても変わらない(メモリを2GBにすると、仮想上の Windows 10 の起動自体がかなり危うくて遅い)。あと、VMWare をサスペンドするときにもかなり遅い。これは、シーケンシャルな書き込みが HDD よりも SDカードのほうがずっと遅いことが原因である。Visual Studio の起動が遅いのは、おそらくスワップファイルの書き込みが大量発生しているからだろう。ビルド自体は問題ない(小さなファイルの書き込みなので)ので、スワップファイルのような大きなファイルの書き込みに遅さが際立つと思われる。

VMWare Player で作るファイルは一括ファイルと分割ファイルがあるのだが、SD カード上では「一括ファイル」じゃないと重くて動かない。どうやら、分割ファイルだと読み込みを遅い領域に入ってしまうらしく使い物にならない。

そんな訳で、SD カード時に仮想な Windows 10 を入れるのはちょっと大変、っぽいことが分かる。ちなみに、Windows 10 + Visual Studio + SQL Server + Excel の組み合わせだと、使用済みの容量が32GB 程度で収まる。先行きの Windows Update 等を考えても 64 GB あれば十分だろう。これが、小さめのノートPC(Surface も含む)に Visual Studio 入れられない理由でもある。Visual Studio Code ならばなんとかなるけど。

ラズパイの場合、Raspbian が SD カードに入るわけだが、Linux から大量書き込みが発生すると OS が重たくなるのはこの現象だろう。Linux の場合は、アクセス方法がちょっと違うかもしれないが。

あと、試しに Windows 7 で開発環境を SD カード上に作ってみたが、比較的 Winodws 10 よりも軽くなる。これは、Windows 10 が高速な SSD に最適化されているのだが、Windows 7 は低速な HDD に最適化されていた、からだろうと思う。HDD が遅めの場合は、あえて Windows 7 上に開発環境を構築するのも良いかもしれない。

QEMU で Windows の開発環境を動かせるか?

VMWare Player だと顧客PCに対して「インストール」が必要になるので、どの PC でも良い、という訳にはいかなくなる。そうなると、インストール不要な QEMU を使ったらいいんじゃないか?と思って考えてみた。

無料のWindows仮想マシンをQEMUで動かす – Qiita https://qiita.com/Hiroki_Kawakami/items/b18f6c50df4ce215e40d

ここの記事を参考にすると、qemu 上でも Winodws 10 を動かせそうな感じではあるんだが、どうなのか?と思って、VMWare で作成した vmdk を変換して使ってみたのだが…結論から言えば使い物にならなかった。

  • ゲスト OS の Windows 10 から、外部のサーバー等が見えない。
  • 何故か、ゲスト OS 内で何か操作(ビルドとか)をした後に、2回目の起動をすると無反応になってしまう。

外部サーバー見えない問題は、ネットワークがホストのPCしか参照できない仕組みになっているので、QEMU で Raspberry Pi をエミュレートする でやったように、-net nic –net tapi,ifname=TAP32 のようにブリッジ接続を設定すればよいはずなのだが、どうもこれを指定した時点で起動できなくなってしまう。また、なんか操作をしたあとに、もう一度立ち上げると、ハングアップしてしまうのは致命的だ。そのほかにもマウスポインタの位置がずれたままで操作できなくなってしまう。たぶん、ゲスト OS の相性の問題になるのだと思うけど、開発環境を残す意味として「途中でなんかの原因で立ち上がらなくなってしまう」のは非常に困るので、この件に関しては QEMU は却下する。

VMWare 上から CrystalDiskMark してみる

Windows 10 の場合
image

Windows 7の場合
image

USB 3.0のSDカード上にあるVMファイルを使って起動して、Windows 10/7 でベンチマークを取ってみる。驚いたことに、SSD へのアクセス並みにアクセススピードが出ている。これが本当にスピードが上がったのか、計算上だけの問題なのかはわからないが、ゲスト OS 内から見たら HDD アクセススピードは結構なスピードでアクセスできる(というように見えるだけ?)ということになる。

ただし、VMWare 外から見たアクセススピードは下記のように断然遅いので、サスペンドのときとかは十分注意する必要がある。

Toshiba UHS-I microSDXC 128GB + iBUFFALO USB 3.0 BSCR27U3BK
image

結論

そんな訳で、現在の私の開発環境からいくと、

  • 顧客サーバーに VMWare Player をインストールする
  • 顧客サーバーの HDD に仮想ファイル64GBを置いて使う

のが開発環境を顧客の環境で動かすときの最適値らしい。USB 3.0 にメモリを差しこんで、開発環境を別に作ることもできるが、必要な仮想HDDの容量が、64GB 程度で済むならばそれでいいだろう。逆に言えば、安価なノートPCに開発環境を作るとき、ストレージが64GB程度あれば、Visual Studio+SQL Serverなどを入れた .NETの開発環境が作れるというわけだ。32GB 程度しかストレージがないときは、食いつぶしてしまうので駄目。なんらかの形で、SD カードに追い出さないといけない。

安価なノートPCで、DockerとかをSDカードに追い出した場合はどうなるのだろう。他にも、QEMU な環境でも Linux の場合だったら比較的動くのかもしれない。これは後で試してみよう。

カテゴリー: 開発 | 開発環境を仮想OS上に保管する実験 はコメントを受け付けていません