ASP.NET MVC から Redmine+MySQL を操作する

以前 独自にポートフォワード作成し、ラズパイの MySQL へ LINQ で接続する で SSH ポートフォワードを使って MySQL に接続したのだが、これをもうちょっと活用すると、.NET Core で作成した ASP.NET MVC から Redmine の MySQL を閲覧できるようになる。

主な手順

  1. Redmine テーブルの EF を用意する
  2. ASP.NET MVC のひな形を .NET Core で作る
  3. ASP.NET MVC に EF をコピーする
  4. ASP.NET MVC のスキャフォード機能を使って、Controller と 各種の View を自動生成する。
  5. 動作時は、SSH ポートフォワードで、実運用の Redmine + MySQL に接続させる

ASP.NET MVC で利用する EF は直接 MySQL から作成してもよいのだけど、何かと面倒なので手作業で作るか、あらかじめ SQL Server で作ったものを用意する。

EF のモデルクラスを作成する

Visual Studio から EF を作成するほうが楽なので、”ADO.NET Entity Data Model” を追加して、SQL Server 上に作成した Redmine のテーブルから、モデルクラスを作成する。

必要なのは、RedmineModel.tt 配下にある *.cs のクラスで、たとえばチケットを管理している issues クラスはこんな感じになる。




public partial class issues { public int id { get; set; } public int tracker_id { get; set; } public int project_id { get; set; } public string subject { get; set; } public string description { get; set; } public Nullable<System.DateTime> due_date { get; set; } public Nullable<int> category_id { get; set; } public int status_id { get; set; } public Nullable<int> assigned_to_id { get; set; } public int priority_id { get; set; } public Nullable<int> fixed_version_id { get; set; } public int author_id { get; set; } public int lock_version { get; set; } public Nullable<System.DateTime> created_on { get; set; } public Nullable<System.DateTime> updated_on { get; set; } public Nullable<System.DateTime> start_date { get; set; } public int done_ratio { get; set; } public Nullable<double> estimated_hours { get; set; } public Nullable<int> parent_id { get; set; } public Nullable<int> root_id { get; set; } public Nullable<int> lft { get; set; } public Nullable<int> rgt { get; set; } public bool is_private { get; set; } public Nullable<System.DateTime> closed_on { get; set; } }

ASP.NET MVC Core のひな形を作る

コマンドラインから、以下のように作成する。




dotnet new mvc -n redmine.web

VSCode を使ってもよいのだけど、スキャフォード機能を使う場合、Visual Studio 2019 を使ったほうがよいので、*.sln ファイルも作っておく。

EF のモデルクラスをコピーする

先のプロジェクトで作成したモデルクラスを ASP.NET MVC の Models フォルダーにコピーする。

注意しないといけないのは、Redmine では tinyint(1) をフラグ代わりに使っているので、これを bool 型で扱うか? int 型で扱うかという問題がある。本来はチェックボックスにしたいので bool 型にしたいところなのだが、ひとまず int 型にしておく。




public partial class issues { public int id { get; set; } public int tracker_id { get; set; } public int project_id { get; set; } public string subject { get; set; } public string description { get; set; } public Nullable<System.DateTime> due_date { get; set; } public Nullable<int> category_id { get; set; } public int status_id { get; set; } public Nullable<int> assigned_to_id { get; set; } public int priority_id { get; set; } public Nullable<int> fixed_version_id { get; set; } public int author_id { get; set; } public int lock_version { get; set; } public Nullable<System.DateTime> created_on { get; set; } public Nullable<System.DateTime> updated_on { get; set; } public Nullable<System.DateTime> start_date { get; set; } public int done_ratio { get; set; } public Nullable<double> estimated_hours { get; set; } public Nullable<int> parent_id { get; set; } public Nullable<int> root_id { get; set; } public Nullable<int> lft { get; set; } public Nullable<int> rgt { get; set; } public int is_private { get; set; } public Nullable<System.DateTime> closed_on { get; set; } }

データベースの接続情報やデータアクセスを LINQ で使えるようにするため、DbContext を継承した RedmineContext クラスを作る。




namespace redmine.web.Data { public class RedmineContext : DbContext { public RedmineContext(DbContextOptions<RedmineContext> options) : base(options) { } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); } } }

NuGetで MySql.Data.EntityFrameworkCore を入れた後、
Startup.cs を開いて、MySQL への接続文字列を ConfigureServices メソッド内に書く。




public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.Configure<CookiePolicyOptions>(options => { // This lambda determines whether user consent for non-essential cookies is needed for a given request. options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services.AddDbContext<redmine.web.Data.RedmineContext>(options => options.UseMySQL(@"server=localhost;user id=redmine;password=redmine;database=redmine;port=3306;sslmode=None"); }); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); }

スキャフォード機能で Controller と View を作る

ひとまずローカルの MySQLに接続して確認する

Visual Studio からデバッグ実行すると、デバッグ用のコマンドラインが表示されて、localhost:5000 へ接続できるようになる。

https://localhost:5001/Issues のようにアクセスすると、MySQL の中身が表示される。Issue の一覧になるので、すべてのプロジェクトの Issue がまとめて表示されてしまうが、これは独自にルーティングを使ってプロジェクト内のみ表示させればよい。

SSH ポートフォワードを使う

options.UseMySQL で記述した接続先を SSH ポートフォワード用に変更すればよいのだが、ひとまず SSH で繋がるかどうかを MySQL Workbench で接続してみる

Connection Method を “Standard TCP/IP over SSH” に変更して SSH 経由で接続できることを確認しておく。

その後で、Ubuntu on Windows で、localhost の 190000番を openccpm.com 内部にある MySQL(3306) に通すようにする。




ssh -L 19000:localhost:3306 masuda@openccpm.com

Setup.cs の該当箇所を書き換える




services.AddDbContext<redmine.web.Data.RedmineContext>(options => { options.UseMySQL(@"server=localhost;user id=redmine;password=redmine;database=redmine;port=19000;sslmode=None"); });

Visual Studio 2019 からデバッグ実行できれば ok.

カテゴリー: 開発 | ASP.NET MVC から Redmine+MySQL を操作する はコメントを受け付けていません

京都アニメーションの被害者に哀悼を込めて

実は、アニメ制作の進行とソフトウェア開発のマネジメントの話 | Moonmile Solutions Blog の追記をしようと思った矢先に、京都アニメーションの放火事件があり、心理的に続きが書きづらくなってしまった。ので、1時間ほど哀悼を込めて綴っておこう。

プログラミングのバックグラウンド映像にアニメを流している位に「アニメファン」ではあるのだが、実は京アニ「だけ」に思い入れがある訳ではなく、どちらかといればそれ以前のアートランドあたりからスタートしている「アニメマニア」であったりする。

先に書いた通り、アニメーション制作の進行係にはプロジェクト管理として興味を持っていて、その周辺の技術(動画とか作画とか諸々)はセルアニメの頃の知識で止まっていたりするので、実は「なつぞら」あたりのほうがよく知っていたりする。知識としてだけだが。

「なつぞら」と「SHIROBAKO」の進行を比べたり、三鷹のジブリ美術館に行ってみたり、ディズニーの絵コンテを買ってみたりするのは、趣味というのもあるけれど、絵を描くことと、プログラムを書くことは「手を動かす」という意味で同じジャンルにあるだろう(あるとしたい)という気持ちがある。まあ、大学の頃に漫研に居たというのもあるし、なんとなく絵を描き続けられなくてプログラムに逃げてしまった?という経緯もあって、いまだにちょっとだけ「憧れ」がある。とはいえ、50歳も過ぎれば、憧れは憧れのままにしておく手法もあり、どこに時間を費やすかといえば、プログラム&文章に費やしたりしてしまう。技術書をちまちまと書きおろしているのはそのためでもある。

いまさら、アニメーションが人々の心に影響が…という話でもないと思うのだが、「けいおん!」がアニメマニアではない高校生に大きな影響を及ぼしたのは間違いないらしい。間違いないらしいというのは、実は「けいおん!」はリアルタイムで見てはいなくて、時期的に言えば就職して暫くテレビを見てなかった頃だったので、後からレンタル屋さんで借りてきてみている。私の高校時代といえば、「うる星やつら」とか「ガンダム」とか「みゆき」とかが午後7時台に放映されたいた頃なので、アニメを見るのは子供でもありつつ高校生でもありつつ、ちょっとマニアックな大学生でもあった時代だった。「おたく」がテレビに認知されつつも、宮崎事件があった頃でもある。そこから、だいぶん下ったところに「けいおん!」があって、放映後にしばらくたった時期ではあるけれど「けいおん!」が放映されてときの同時代性というのものは、当時の高校生にとって非常に大きかったことを伺い知れる。

いわゆる、いじめが陰湿化してしまった後に、学校への不登校とクラスになじめない自分とのはざまの中で、疑似的にではあっても「けいおん!」が高校生活を体験させてくれたというブログ記事がちらほらみられるのが興味深い。それと同時に「けいおん!」を見て軽音を始めた高校生もいて、このアニメが影響を与えた層というものは、従来のアニメファンだけではなくく、一般的に理解されるであろう(おそらく当時の親の層からも理解されたかもしれない)層までも「アニメ」を広げたという感じがしている。おそらく、かつて「金八先生」をクラスで話題にしても恥ずかしくなかったぐらいに、「けいおん!」をクラスの友達と話してもハズか少なかった位にも浸透したのではないだろうか?という気がしている。

同じ現象は、ジブリアニメでも起きているわけで「天空の城ラピュタ」を誰もが知っているように、「ジョジョの不思議な冒険」を知っているように、そこの登用人物や科白を使っても恥ずかしくはない程度に「アニメ」は浸透したのだと思う。

実は京アニ自体に特別な思い入れはないのだけど、あらためて「響け!ユーフォニアム」や「たまこまーけっと」を見返すと、節に復活を願う。当然のことながら、「日常」と「らき☆すた」は、作業用BGMの定番である。

ちなみに、仕事中にアニメをBGMで流しておくのは、

  • タイマー替わり
  • 過集中を避ける

の2つの意味合いがある。タイマーのほうは、艦これでもよいのだが、TVアニメの場合24,5分単位で終わるので時間が測りやすい。よって、映画ではなくてTVアニメの各話を流し続ける。

あと、プログラミング自体に集中しすぎると最終的には効率が悪い。職業柄、大量なサンプルコードを書くことが多いので適度な休憩が必要になる。その休憩のタイミングをTVアニメの各話に挟めるというメリットがある。また、過度に集中するよりも、ある程度気を散らしたほうが疲れないというのもある。このあたり、ラジオが良い人もあれば、音楽が良い人もいる。あるいは、静かなほうが効率が良い場合もある。それぞれだ。

カテゴリー: 雑談 | 京都アニメーションの被害者に哀悼を込めて はコメントを受け付けていません

チケット駆動のチケット数を概算見積もりする

うまいチケット駆動でのプロジェクト管理を見たのは、20年ほど前のアジャイル協議会のTLだったと思う。ライントレーサーのロボットを作るのに、チケット駆動とPSP(パーソナル・ソフトウェア・プロセス)を組み合わせて、Excelで進捗チェックをするひとりプロジェクトだ。

複数名のチケット駆動というと、壁に付箋を貼るか、チケット駆動のツール(Redmineとか、Backlogとか)を使うことが多いのだろうが、何もツールの全ての機能を使わなくてもよい。というか、全ての機能を使おうとすると何かしらうまくいかないことが多い。最大公約数で機能を詰め込んであるのでオーバースペックなのだ。

で、

チケット駆動の最大の難点なのは、最終的にプロジェクトがいつ終わるのか?が見えないことだ。スクラムのバックログ方式も同じだけど、アジャイル方式で開発を行うとどうも終わりが見えなくて困る。当然のことながら終わりが見えないからアジャイル方式を使うのだけど、じゃあ、いつ終わるのか?という概算的な時間見積もり≒納期はどのあたりに定めればよいのだろうか?

プロジェクトが終わった時点から見れば、チケット駆動であれスクラムのバックログであれ、消化したチケット数/タスク数は決定的である。当たり前だ。プロジェクトは完成して終わったのだから、目の前にできたものに費やした時間は「記録さえしてあれば」計算できることになる。

なので、実はチケット駆動でも概算的な規模見積もりは可能であったりする。

ソフトウェア見積り 人月の暗黙知を解き明かす
https://www.amazon.co.jp/dp/B00KR96M6K

FP とかも含めて、いろいろと試してきたけれど、私の場合マコネル氏のこの本に同意する。マコネル氏はこの前後に計量的にタスクの時間見積もりをするツールを売っている(今はなくなっている…はず)けど、細かなタスクあるいはチケットを確率的に求めなくても、もっと概算で十分だろう、ってところに今の私は落ち着いている。年初あたりに確率統計を使って計算する試みをしてみたのだが、正確なシミュレーションをしたとしても途中のタスクの増減の影響が大きいのでプロジェクトを進める間は、その正確さは重要ではなくなってくる。大規模プロジェクトの計画段階(予算と期間を決めるという意味で)は重要になっているのだが、高々数百万のプロジェクト、期間が4か月程度のものに最初の正確さを求めても意味がないことが多い。それよりも、プロジェクトを運営する上での「やりくり=本当の意味でのマネジメント」を考えたほうが安全にプロジェクトを終わらせることが可能である。

期間を概算する

会社にいた頃は、機能を洗い出して機能ごとに時間単位あるいは日単位で見積もりを出して積み上げていたものだが、今はやっていない。超概算見積もりで人月を計算した後に、Excelで機能に割り振る、あるいはチケットに割り振ってしまう。

正確に言えば、

  • 超概算見積もりで、人月で算出する
  • 顧客の懐具合で、予算を算出する
  • 機能の数で、人月で算出する

と3つの人月を出す。大体において、派遣なり契約社員なり社員なりに支払うお金は「月単位」になることが多いので、週単位で出してもあまり意味はない。細かい単位でずれを修正しても仕方がないし、そもそもスポンサー≒お客の懐具合に予算を合わせないといけない。

ならば、月単位の単価をベースにして、超概算してしまうのがよい。細かいところはプロジェクトを進めながら合わせていく。

チケット数を概算する

期間と人数が決まれば、おのずと動いている時間が決まってくる。当然だが、20日/月、160h/月で考える。22日で計算すると祝日や別な割り込みの分を勘定できないし、そもそも計算が面倒くさい。2日≒10%程度の余裕が最初からあるほうがよい。

1日で消化するチケットする数は3つ程度と決めておく。期間が長ければ1日1チケットでもよい。しかし、1週間1チケットみたいなのは駄目。スクラムのスプリント期間に近くなってしまうし、1つのチケットが1日よりも長くなる場合は、チケットを分割するか、複数のチケットに同じタイトルで割り当てたほうが良い。いつまでも進行中のまま終わらないからだ。

チケットの作業時間の粒をできるだけ揃えておくのがよい。そうするとバーンダウンチャートで先行きが見えやすくなる。

プロジェクトの開発期間と人数が分かれば、全体のチケット数が概算できる。

既知の開発をチケットに割り振る

プロジェクトの開始時点(規模見積もりをしている時点)で、ある程度の既知の開発チケットがあるはずだ。そうじゃないと、どうやって規模/金額見積もりをしたんだ?という話になる。大まかな機能と、おおまかな時間(1日なのか3日なのか)を適当に決めて、チケットに割り当てる。

見積もったチケット数をすべて書き込む必要はない。いや、むしろ書き込んではいけない。プロジェクト開始時のチケット数はあとから増える可能性が大なので、空白のチケットを残しておく。これは保険だ。あるいは、未知なるチケットだ。

あるいは、チケットが全て埋まってしまう状態になったら、それはそもそもが全体のチケット数が足りない → プロジェクト期間が短すぎる、ことを意味している。先の超概算に戻って、再確認する。

残チケットを消化する

プロジェクトが進む中で、さまざまな要望が増えていくる。チケットを消化しつつも、チケットが増えてしまうのがチケット駆動の難点ではあるが、あらかじめ全体のチケット数が分かっていれば≒概算してあれば、プロジェクトが進んだとして大幅にチケット数が増えることはないだろう(それでも予想を超えて増えてしまうのが常なんだけど)。

私の場合、チケットの消化記録は、Excel か Redmine を使っている。先に書いた通り、Redmine を全ての機能を使うと多機能すぎてオーバースペックになる。なので、

  • チケットは新規/進行中/終了しか使わない。進捗率は使わない
  • 作業時間はいらない
  • ガントチャートも使わない
  • 優先度も使わない
  • チケットはあらかじめ概算時点で大量に置いておく。順々に消化する。

という非常にシンプルな使い方をする。

ひとりプロジェクトだと Excel ベースで十分なのだけど、2名以上だとファイル競合がでるので Redmine を使う。最初に大量のチケットを投入する必要があるので、現在ツールを作って投入中。ClosedXML を使って、Excel へのマージを作っている。これ、Redmine の CSV のインポート機能でいいんじゃないだろうか?と気づいたのだがどうだろう。あとで試してみる。

追記

CSV からインポートすると常に新規になるので、二重にチケットができてしまう。更新版を自作しないと駄目。初期チケットの大量投入は CSV を使うと便利。

カテゴリー: プロジェクト管理 | チケット駆動のチケット数を概算見積もりする はコメントを受け付けていません

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

結論から言えば、非常に似ている…というのは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コンテナの指定ポートに割り当てる はコメントを受け付けていません