おふくろさまより愛をこめて

mmmpa ふとしです。誠実なプログラミングを心がけたい。

Recent entries

依存性の逆転について

現代のプログラミングでは Plugin という仕組みや Polymorphism が一般的になっているため + 動的型付け言語を使用してきたため、依存性の逆転 が具体的になにを解決しているのかよくわかっていませんでした。なにしろ雰囲気でプログラミングをやってきたので。

現在 Clean Architecture を少しずつ読んでいます。読みながら極端に悪い依存性を持つ状態をイメージし、そこに依存性の逆転が持ち込まれると何が解決されるのかを見ることによってようやく理解できました。

以下は自分が理解するために書いたものです。

実行処理の上流の module が下流の module の実装に依存する状態

実行処理の流れがそのまま依存の流れになっている (依存先の詳細を知らなければならない) 状態です。A -> B -> C という順に処理が呼び出される場合、A は B に依存し、B は C に依存するという構造です。

例では Processor は上流からの呼び出しに従い任意の device を使って readwrite を行います。

極端に悪い例

MicTabletTerminal が公開しているメソッド名が統一されていないため、使う device によって処理を切り替える必要があります。「依存先の詳細を知らなければならない」状態の悪さがわかると思います。

import Mic from './Mic';
import Tablet from './Tablet';
import Terminal from './Terminal';

export default class Processor {
  device: Mic | Tablet | Terminal;

  constructor (device: Mic | Tablet | Terminal) {
    this.device = device;
  }

  read (): string {
    switch (this.device.constructor) {
    case Mic:
      return (this.device as Mic).listen();
    case Tablet:
      return (this.device as Tablet).write();
    case Terminal:
      return (this.device as Terminal).in();
    default:
      return '';
    }
  }

  write (s: string) {
    switch (this.device.constructor) {
    case Mic:
      (this.device as Mic).say(s);
      return;
    case Tablet:
      (this.device as Tablet).display(s);
      return;
    case Terminal:
      (this.device as Terminal).out(s);
      return;
    default:
      //
    }
  }
}

Plugin 化された例

メソッド名を統一することで随分すっきりしたコードになりました。

import Mic from './Mic';
import Tablet from './Tablet';
import Terminal from './Terminal';

export default class Processor {
  device: Mic | Tablet | Terminal;

  constructor (device: Mic | Tablet | Terminal) {
    this.device = device;
  }

  read (): string {
    return this.device.in();
  }

  write (s: string) {
    this.device.out(s);
  }
}

しかし、未だに下流の module に依存している状態です。MicTabletTerminal のいずれかが変更されると、Processor のコンパイルしなおしが必要になってしまいます。

依存性の逆転

そこで MicTabletTerminalProcessor 用の Interface IDevice に依存するように変更します。つまり、下流の module が上流の module の実装に依存するように、処理の流れとは逆にするということです。

export default interface IDevice {
  out (s: string): void
  in (): string
}
import IDevice from './IDevice';

export default class Processor {
  device: IDevice

  constructor (device: IDevice) {
    this.device = device;
  }

  // snip
}
import IDevice from './IDevice';

export default class Mic implements IDevice {
  out (s: string): void {
  }

  in (): string {
  }
}

(他の Device は省略)

これですべての module が独立してコンパイルできるようになりました。また何か新しい Device が導入されたとしても、その Device は IDevice を実装すれば Processor にはなんの変更もせず使用することができます。

Architecture の観点から

この依存性の逆転はソースコードの依存性を一方向性を解決し、依存の方向をどちらへも向けることを可能にします。

Clean Architecture では、システム/ビジネスのポリシーやルールに基づいて、Open-Close の原則の方向を決めます。

そこでこのソースコードレベルの依存性によらない自由な依存の方向が必要になります。

まとめ

実際、Ruby などの動的型付け言語では plugin 化の段階ですでに依存性の逆転は達成されています。Duck Typing を満たす限りどんな Device も Processor で使用できるからです。

依存性の逆転について、粗結合に優れるコードを書くための概念としては把握していました。しかし今回、極端に悪い例と実際に逆転を意識しないと実現できないコードを知ることにより、具体的に何を解決していたかを知ることができました。

IBM Bluemix (IBM Cloud - Cloud Foundry) をローカルで動作するように環境を準備する

Bluemix は IBM が提供する PaaS で、Heroku に似た仕組みを持つ Cloud Foundry をベースとして動作しています。専用の cli を使ってコードを push すると、コードにあわせて buildpack が選択され、コードに合わせた環境が準備されて自動的にアプリが起動します。

push から動作までが非常に速やかで簡潔な反面、環境がすべて自動で準備されているため、動作における不具合の調査とその調整が少ししづらい印象があります。幸い、Croud Foundry にはローカルで動作させるための PCF Dev (P は開発元であった Pivotal 社の頭文字) という plugin が用意されていますので、それを今回はインストールしました。

OS は Debian stretch で、手順は Introduction | Try PCF on your Local Workstation | Pivotal に従っています。

(なお windows と mac では cloudfoundry-incubator/cfdev: A fast and easy local Cloud Foundry experience on native hypervisors, powered by LinuxKit with VPNKit で簡単に準備できようです)

インストール

cf-cli のインストール

PCF Dev plugin は cf-cli 上で動作します。また、動作している Cloud Foundry へアクセスするためにも必要になるので、いずれにしてもインストールが必要になります。

# apt-get の https アクセスなどに必要なパッケージをインストール
sudo apt-get update
sudo apt-get install -y wget gnupg gnupg1 gnupg2 apt-transport-https

wget -q -O - https://packages.cloudfoundry.org/debian/cli.cloudfoundry.org.key | sudo apt-key add -
echo "deb https://packages.cloudfoundry.org/debian stable main" | sudo tee /etc/apt/sources.list.d/cloudfoundry-cli.list
sudo apt-get update
sudo apt-get install cf-cli

VirtualBox をインストール

仮想環境として内部的に VirtualBox を使用しています。VirtualBox から環境に合わせたパッケージをダウンロードしてインストールします。

素の Debian stretch だと多くの依存パッケージが足らず virtualbox-6.0 depends on Foo; however: Package Foo is not installed. といったエラーメッセージが複数行にわたって表示されます。したがって依存パッケージを先にインストールしなければなりません。

# 依存パッケージのインストール
sudo apt-get install -y libcurl3 libdevmapper1.02.1 libgl1-mesa-glx libgl1 libopus0 libpng16-16 libqt5core5a libqt5gui5 libqt5opengl5 libqt5printsupport5 libqt5widgets5 libqt5x11extras5 libsdl1.2debian libvpx4 libx11-6 libxcb1 libxcursor1 libxext6 libxml2 libxmu6 libxt6 psmisc 

sudo dpkg -i virtualbox-6.0_6.0.0-127566~Debian~stretch_amd64.deb

PCF Dev をインストール

Download PCF Dev — Pivotal Network からダウンロードします。ダウンロードにはアカウントが必要なので作成します。

plugin のインストールは cf-cli に対して行うので以下のようになります。

cf install-plugin ./pcfdev-v0.30.0+PCF1.11.0-linux

補記

チュートリアルのように $ ./pcfdev-v0.30.0+PCF1.11.0-linux とした場合は Your cf CLI version is too old. Please install the latest cf CLI. とエラーになりインストールできませんでした。

起動

以下のコマンドを実行するだけです。

cf dev start

ただし初回はまず仮想環境イメージのダウンロードで時間がかかり、2 回目以降も起動自体にものすごい時間がかかります。それを知らないと固まったような印象を受けますが、気長に待つのが肝要です。

起動時ログ例

$ cf dev start

Less than 4096 MB of free memory detected, continue (y/N): > y
Please sign in with your Pivotal Network account.
Need an account? Join Pivotal Network: https://network.pivotal.io

Email> 

Password> 
Downloading VM...
Progress: |====================>| 100%  # まず仮想環境イメージのダウンロードが長い
VM downloaded.
Allocating 4096 MB out of 32140 MB total system memory (1371 MB free).
Importing VM...
Starting VM...
Provisioning VM...
Waiting for services to start... # 2 回目以降もここからの起動が長い
7 out of 58 running
7 out of 58 running
7 out of 58 running
7 out of 58 running
40 out of 58 running
56 out of 58 running
58 out of 58 running            # 気長に待ちましょう

 _______  _______  _______    ______   _______  __   __
|       ||       ||       |  |      | |       ||  | |  |
|    _  ||       ||    ___|  |  _    ||    ___||  |_|  |
|   |_| ||       ||   |___   | | |   ||   |___ |       |
|    ___||      _||    ___|  | |_|   ||    ___||       |
|   |    |     |_ |   |      |       ||   |___  |     |
|___|    |_______||___|      |______| |_______|  |___|
is now running.
To begin using PCF Dev, please run:
   cf login -a https://api.local.pcfdev.io --skip-ssl-validation
Apps Manager URL: https://apps.local.pcfdev.io
Admin user => Email: admin / Password: admin
Regular user => Email: user / Password: pass

アクセス

cf-cli

以下のコマンドで、cf-cli のアクセス先を立ち上がった PCF Dev に設定します。なお、*.local.pcfdev.io192.168.11.11 にポイントされているので、きちんとローカルの Cloud Foundry にアクセスすることになります。

cf login -a api.local.pcfdev.io --skip-ssl-validation

EmailPassword が要求されるので、起動時のログに表示されているものを使用します。

Regular user => Email: user / Password: pass

GUI

ブラウザからは https://apps.local.pcfdev.io でコンソールにアクセスできます。EmailPassword はおなじく起動時ログで出てきたものを使用できます。

PCF Dev の動作確認

単純な ruby を起動してみましょう。Cloud Foundry ではなく Bluemix から配給されているものですが IBM-Cloud/ruby-sinatra-helloworld: Sample Ruby application for Bluemix which uses the Sinatra framework. が簡単に起動します。

git clone https://github.com/IBM-Cloud/ruby-sinatra-helloworld
cd ruby-sinatra-helloworld
cf push

問題がなければ以下のようなログが最終的に出力されます。経路 に示された sample-ruby-sinatra-brave-okapi.local.pcfdev.io にアクセスして動作を確認しましょう。

$ cf push
user としてマニフェストから組織 pcfdev-org / スペース pcfdev-space にプッシュしています...

# build のログが続く

名前:                   sample-ruby-sinatra
要求された状態:         started
インスタンス:           1/1
使用:                   256M x 1 インスタンス
経路:                   sample-ruby-sinatra-brave-okapi.local.pcfdev.io
最終アップロード日時:   Mon 31 Dec 07:39:34 JST 2018
スタック:               cflinuxfs2
ビルドパック:           ruby 1.6.37
開始コマンド:           bundle exec rackup config.ru -p $PORT

     状態   開始日時               cpu    メモリー       ディスク       詳細
#0   実行   2018-12-30T22:39:58Z   0.0%   456K of 256M   6.7M of 512M 

まとめ

これで Cloud Foundry をローカルで動作できるようになりました。

実は IBM Bluemix に乗せる平易なアプリケーションを作成していて、メモリを 64MB に設定すると落ちてしまう事象がありました。128MB にすると落ちるときと落ちない時があり、それのチューニングをする or チューニングの問題ではないのか調査するにあたって都度 push したのでは追いつかなくなってしまいました。

幸いローカル環境構築はきちんと提供されていたので大いに助かりました。 Pivotal 社及び「ローカル環境あるよ」 と教えてくださった Toshiaki Maki(@making)さん には感謝の言葉しかありません。

hosts でサブドメインをワイルドカードで指定したいができないので dnsmasq で対応する。

サブドメインをなんらかの key としたサービスがあります。その開発においてはローカルの開発環境においてもサブドメインを付与したアクセスが必要になります。しかしいわゆる /etc/hosts にあたるファイルではワイルドカードを指定できないため、key ごとに書き加えていく必要があります。

127.0.0.1       foo.example.com
127.0.0.1       bar.example.com
127.0.0.1       *.example.com # 機能しない

しかし、これは明らかに手間です。そこでローカルマシン上で DNS としての機能を提供する dnsmasq を用いて *.example.com へのアクセスを 127.0.0.1 にリダイレクトさせることで対応できます。

/etc/dnsmasq.conf

インストールは各環境に合わせて行い、設定ファイルにリダイレクト設定を書きます。

記法は単純で address=/a/b で a にマッチするアクセスがあった場合に b へリダイレクトします。これは必要なリダイレクト分だけ書けます。

address=/.example.com/192.168.0.1
address=/.o296.com/192.168.0.1

詰まりどころ

以下はわたしが dnsmasq を設定するにあたって詰まった部分です。比較的長い時間を溶かしたので、同じような状況に陥ったどなたかの助けになれれば幸いです。

Operation not permitted

起動時に以下のエラーが出る場合は /etc/dnsmasq.confuser=root を書き加えます。

dnsmasq: setting capabilities failed: Operation not permitted

リダイレクトされない

dnsmasq は DNS として振る舞います。どの DNS でドメインを解決するかというのは /etc/resolv.conf 書かれていて、大体の場合以下のようになっているようです。

nameserver 192.168.0.1
nameserver 2001:268:fd07:4::1
nameserver 2001:268:fd08:4::1

192.168.0.1 は自分自身に向いていることも多いのですが、わたしの場合はマシンの ip は 192.168.0.6 でした。これは確実に自分に向くように 127.0.0.1 に変更します。

nameserver 127.0.0.1
nameserver 2001:268:fd07:4::1
nameserver 2001:268:fd08:4::1

書きかえた /etc/resolv.conf がもとに戻ってしまう

NetworkManager が動作している場合 recolv.conf は再起動のたびに上書きされてしまいます。書き換えを防止するために、以下のように追記します。

[main]
plugins=ifupdown,keyfile
dns=none  # 追加 

まとめ

これで無限にサブドメインアクセスができるようになりました。

業務でちょうどサブドメインを key とするサービスの作業を担当していたのでもっと早く dnsmasq を知れれば、都度 /etc/hosts を編集することもなかったのにな……と思いました。(その業務からは年内で担当をはずれました)

その他の手段

適当なドメインを所有している場合は *.local.o296.com などを 127.0.0.1 にポイントしておくという手もあります。サブドメインを使用するサービスでローカル開発環境を提供しているような場合、その会社がそういうローカル用のドメインを用意している場合もあるようです。

今年の洗濯回数は 8 回でした。

過去録

1 回目 2 月

2 回目 3 月

3 回目 5 月

4 回目 6 月

5 回目 7 月

6 回目 9 月

7 回目 11 月

8 回目 12 月

まとめ

今年は大規模な入れ替えもなかったので安定した洗濯生活でした。ただ、あたらしいボクブリを結構おろしたせいか、後半戦にはボクブリはあるのに T シャツの在庫がきれるという逆転現象が起こっていました。

来年は 6 億円を当てて自宅に 30kg 級の乾燥機を設置したいですね。

Ansible で rbenv をインストールする role を書いた

インフラ構成をできるだけ属人性を低くしつつ再現性を高くしたくて Ansible を触っています。Ansible は知識がない状態だと設定ファイルについて全くもって全景がとらえにくい === とてもとっかかりづらいという問題があります。しかし、そこを過ぎた処理設定自体はものすごく平易に書けるので素敵なツールだと感じています。

そこで、Ansible を使い慣れるのも兼ねて、rbenv がすでにある場合や ruby-version がすでにインストールされている場合はスキップ、ない場合は rbenv の update や ruby-version のインストールを行う role を書いてみました。

わたしの従来の rbenv インストール

rbenv について、従来は以下のような粗雑な shell ファイルを用意しインストールを行っていました。そしてバージョンの update が必要な場合は ssh で接続し、ruby-build のバージョンを上げ、ビルドしなおすという運用をしていました。

sudo yum -y install bzip2 gcc* openssl-devel readline-devel zlib-devel

git clone https://github.com/rbenv/rbenv.git ~/.rbenv
cd ~/.rbenv && src/configure && make -C src

echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(rbenv init -)"' >> ~/.bash_profile

git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build

~/.rbenv/bin/rbenv install 2.5.3

rbenv/tasks/main.yml

上記 shell ファイルを単純にそのまま Ansible role に書き下すのは、shell モジュールを用いて置き換えればいいだけなので非常に簡単です。しかしこれを何度も行った場合は少し問題があります。rbenv は都度ビルドされ、export PATH="$HOME/.rbenv/bin:$PATH" がどんどん書き足されていってしまいます。

そこで Check rbenv Check installed ruby version といった tasks で状態を把握して処理を切り替えます。

- name: Check rbenv
  shell: ~/.rbenv/bin/rbenv --version
  register: rbenv_exists
  changed_when: False
  ignore_errors: yes

- name: Check installed ruby version
  shell: ~/.rbenv/bin/rbenv versions | grep {{ ruby_version }}
  register: has_ruby_version
  changed_when: False
  ignore_errors: yes

- name: Check ruby version
  shell: ~/.rbenv/bin/rbenv version | grep {{ ruby_version }}
  register: is_ruby_version
  changed_when: False
  ignore_errors: yes

- name: Install ruby requiring packages
  yum:
    name:
      - bzip2
      - gcc*
      - openssl-devel
      - readline-devel
      - zlib-devel
  become: yes
  become_method: sudo
  when: has_ruby_version is failed

# install rbenv

- git:
    repo: https://github.com/rbenv/rbenv.git
    dest: ~/.rbenv
  when: rbenv_exists is failed

- git:
    repo: https://github.com/rbenv/ruby-build.git
    dest: ~/.rbenv/plugins/ruby-build
  when: rbenv_exists is failed

- name: Update rbenv
  shell: git pull
  args:
    chdir: ~/.rbenv
  when: has_ruby_version is failed

- name: Update ruby-build
  shell: git pull
  args:
    chdir: ~/.rbenv/plugins/ruby-build
  when: has_ruby_version is failed

- name: Make rbenv
  shell: src/configure && make -C src
  args:
    chdir: ~/.rbenv
  when: has_ruby_version is failed

- name: Register rbenv
  shell: echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
  when: rbenv_exists is failed

- name: Register rbenv
  shell: echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
  when: rbenv_exists is failed

# install ruby

- name: Install Ruby
  command: ~/.rbenv/bin/rbenv install {{ ruby_version }}
  when: has_ruby_version is failed

- name: Set default ruby version
  shell: ~/.rbenv/bin/rbenv global {{ ruby_version }}
  when: is_ruby_version is failed

まとめ

register changed_when ignore_errors でフラグを設定し when で切り替えるという単純な組み合わせで十分な role を書くことができました。ruby-version のダウングレードについても一瞬で終わるので便利ですね。