ansibleでディスクを拡張してみる

こんにちは。インフラの奥村です。

最近、業務でインフラ運用業務の改善活動をさせて頂いています。
僕が行った改善活動の一つ
「ディスク拡張のコマンド化」について書いていこうと思います。

以前までは

  1. Web GUIで仮想マシンのディスクを追加し
  2. 対象のマシンにSSH等でアクセスし
  3. コマンドを打ち込む。

という作業を行なっていました。この作業を短縮するべくansibleを書きました。

環境

  • 対象: vSphere仮想マシン

必要な工程

  1. 仮想マシンのディスク拡張(デバイス追加)
  2. パーティションの拡張
  3. LVM領域の拡張
  4. ファイルシステムの拡張

以上4工程をansible化しました。shellの利用が多くなっているのは妥協してしまっています。

必要無さそうな条件分岐等がありますが、これはディスク拡張のパターンとして

  • デバイスを追加するパターン
  • ディスクの容量を拡張するパターン

があるためです。 今回はデバイスを追加するパターンにフォーカスを当てて書きますので、余分な部分はカットしています。

ファイル群

  • ディレクトリ構造
.(DISK_CHANGE_ROOT)
├── ansible
│   ├── disk_change.yml
│   └── roles
│        └── disk
│             ├── defaults
│             │   └── main.yml
│             ├── tasks
│             │    └── disk_add.yml
│             └── templates
│                  └── disk_add_cmdline.j2
├── govc_commands
│   └── disk_add
└── scripts
     └── exec_disk_add.py

playbook

  • disk_change.yml
---
 - hosts: default
   become: True
   vars:
     execute_disk_add: True
   roles:
     - disk
  • main.yml
- name: generate govc command  #テンプレートからgovcのコマンドを生成する
  become: False
  template:
    src: disk_add_cmdline.j2
    dest: "{{ playbook_dir }}/../govc_commands/disk_add"
    mode: 0755
  delegate_to: 127.0.0.1
  when: ip is defined

- name: include task for disk_add
  include: disk_add.yml
  when: execute_disk_add == True
  • disk_add.yaml
---
- name: execute disk_add  #govcコマンドを実行する
  become: False
  local_action: shell ./disk_add
  args:
    chdir: "{{ playbook_dir }}/../govc_commands/"
  run_once: True

- name: print disk info #現在のデバイスの状態をファイルとして書き出しておく
  parted:
    device: /dev/{{ device_name }}
    state: info
    unit: KiB
  register: result

- name: save disk info to file
  copy:
    content: "{{ result }}"
    dest: /home/okumura/export.txt

- name: create partition
  parted:
    device: /dev/{{ device_name }}
    number: 1
    flags: ["lvm"]
    state: present
    part_end: 100%
  ignore_errors: True

- name: exec parted command because ansible module failer  #partedモジュールでうまくいかなかったのでpartedコマンドで対応
  shell: parted -s -m -a optimal /dev/{{ device_name }} set 1 lvm
  ignore_errors: True

- name: set already_pv flag  #追加するデバイスがすでにPhysical Volumeにないか確認する
  shell: pvdisplay | grep {{ device_name }}1
  register: already_pv
  ignore_errors: True

- block:
  - name: pvcreate
    shell: pvcreate /dev/{{ device_name }}1

  - name: resize volume group
    shell: vgextend VolumeGroup /dev/{{ device_name }}1

  - name: resize logical volume
    shell: lvextend -l +100%FREE /dev/VolumeGroup/LogicalVolume

  - name: resize filysystem
    filesystem:
      fstype: ext4
      dev: /dev/VolumeGroup/LogicalVolume
      resizefs: yes
    when:
      - ansible_distribution == "CentOS"
      - ansible_distribution_major_version == "6"

  - name: resize filysystem
    filesystem:
      fstype: xfs
      dev: /dev/VolumeGroup/LogicalVolume
      resizefs: yes
    when:
      - ansible_distribution == "CentOS"
      - ansible_distribution_major_version == "7"
  when: already_pv.rc == 1

テンプレートファイル

  • disk_add_cmdline.j2
{% for host in ip %}  #呼び出すときのIPの数でループ
govc vm.disk.create -vm.ip={{ host }} -size {{ disk_size }}G -name={{ device_name }} -ds=datastore
{% endfor %}

こんな感じです。

流れを説明すると

  1. disk_add_cmdline.j2というテンプレートファイルをもとにして、localhost宛にgovcのスクリプトを作成する。
  2. パーティションを切る
  3. すでにデバイスがないか判断する
  4. 無い場合はphysical volumeを作成し、Volume Groupの拡張、Logical Volumeの拡張を行なう
  5. ディストリビューションを判断し、デフォルトのファイルシステムで拡張

という流れです。

192.168.0.1のホストのディスクサイズを20GB拡張したいときに呼び出すansibleは

ansible-playbook -i inventory/hosts --extra-vars '{"ip": [192.168.0.1], "disk_size": 20, "device_name": sdb}' ansible/disk_add.yml

!?

長すぎるじゃないですか!シングルクウォートとダブルクォートが混在しているコマンドを誰が好んで実行するんですか! ってなりますよね。私もなりました。

ということで、今回はこのコマンドを呼び出すためのスクリプトも作ってみました。 内容としては 受け取った引数を元にansibleのコマンドを作り出し、それを実行するというまぁ、うん、なスクリプトです。

  • exec_disk_add.py
#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys
import subprocess
import os
import argparse

def create_inventory(iplist,env_path):
    f = open("" + env_path + "/inventory/pre_hosts","w")
    inventory_text = "[default]\n"
    for ip in iplist:
        inventory_text = inventory_text + ip + "\n"
    f.write(inventory_text)
    f.close()

def create_cmd_args_for_add(iplist,size,device):
    cmd = '{"ip": [' + ",".join(iplist) + '], "disk_size": ' + size+ ', "device_name": ' + device + '}'
    return cmd

def create_cmd(cmd_args,env_path):
    cmd = "ansible-playbook -i " + env_path + "/inventory/pre_hosts --extra-vars '" + cmd_args + "' " + env_path + "/ansible/disk_add.yml"
    return cmd

def main():
    parser = argparse.ArgumentParser(description='this script extends the disk.')
    parser.add_argument("-i","--ip",      help = "target ips. This option is required",       required=True, nargs="*")
    parser.add_argument("-s","--size",    help = "target disk size. This option is required", required=True)
    parser.add_argument("-d","--device",  help = "choice device. This option is required",    required=True)
    parser.add_argument("-p","--printif", help = "print command",                             action="store_true", default=False,)

    cmd_arg = parser.parse_args()

    create_inventory(cmd_arg.ip,os.environ.get('DISK_CHANGE_ROOT'))

    cmd = create_cmd(create_cmd_args_for_add(cmd_arg.ip,cmd_arg.size,cmd_arg.device),os.environ.get('DISK_CHANGE_ROOT'))

    print(decided_cmd) if cmd_arg.printif == True else subprocess.call(decided_cmd, shell=True)

if __name__ == "__main__":
    main()

上のディレクトリ構造の「.」の部分を「DISK_CHANGE_ROOT として環境変数に登録すれば

./exec_disk_add.py -i 192.168.0.1 -s 20 -d sdb

これで呼び出せるようになりました。

まとめ

最近「Yak Shaving(ヤクの毛を刈る)」という言葉を知りました。

「ある問題を解決しようとしたら、別の問題に直面し、その問題を解決しようとしたら、また別の問題に直面し・・・」

というようになることだそうです。(気になる方は調べてください)
改善活動をしていると、こういう状況に出くわすことが多いと思います。

ですが、改善活動の場合は、出てきた問題を一つ一つ潰していくのが理想だと私は思います。

最後までご覧頂き、ありがとうございます。

スクラムにおける4つのイベント

はじめまして!入社2年目のエンジニアまっちゃんです!
もともとチームとしてスクラム開発を行っていたものの、うまく回せていないという課題がありました。
スクラム改善として、新しくチームにジョインしてくださったスクラムマスターのT先輩から「スクラムの事再度調べてこい」とチーム全員に課題を出されたので、スクラムのイベントについてチームメンバーと認識合わせしたものをまとめてみました。

まとめるにあたって、下記のものを読みました。

SCRUM BOOT CAMP THE BOOK

SCRUM BOOT CAMP THE BOOK

Scrum Guide 2016

※用語が分からない方はwikipedia参照
スクラム (ソフトウェア開発) - Wikipedia

スプリントプランニング

計画会です。
スプリントで開発チームが何を行うのかを決めます。

登場人物
  • プロダクトオーナー
    • プロダクトバックログの内容について説明
  • 開発チーム
    • プロダクトバックログの内容を確認  
    • タスクを洗い出す
  • スクラムマスター
    • ファシリテート
    • サポート
手順
  • 第1部
    • プロダクトオーナーがプロダクトバックログの上から順番にどこまで実現して欲しいかを開発チー ムに伝える
    • 何を実現すればいいのか確認。プロダクトオーナーから開発チームにそれぞれの項目について説明
    • 項目が達成できそうかをプロダクトオーナーと開発チームで相談
  • 第2部
    • 開発チーム全員で必要なタスクを洗い出す
    • タスクの見積もりを行う
時間
  • 2週間スプリント…4時間
  • 4週間スプリント…8時間
成果物
  • ポイントが見積もられたプロダクトバックログ
  • タスクが洗い出されたスプリントバックログ

デイリースクラム(デイリースタンドアップ)

よく朝会と呼ばれているものです。
これを行うだけでスクラムやっている感がでます。

手順
  • 開発チームが毎日、同じ時間・場所で開催する
  • 開発チームのメンバーが以下のことを説明
    • 開発チームがスプリントゴールを達成するために、私が昨日やったことは何か?
    • 開発チームがスプリントゴールを達成するために、私が今日やることは何か?
    • 私や開発チームがスプリントゴールを達成するときの障害物を目撃したか?
  • 開発チームまたは一部のチームメンバーは、デイリースクラムの終了直後に集まり、スプリント残作業について詳細な議論・適応・再計画を行う事もある
登場人物
  • 開発チーム
    • デイリースクラムを開催する責任を持つ
  • スクラムマスター
    • 開発チームにデイリースクラムに開催してもらうようにする
    • デイリースクラムを15分間で終わらせるよう開発チームに伝える
    • デイリースクラムには開発チームのメンバーしか参加できないというルールを厳守する
時間
  • 15分
成果物
  • スプリントゴールとスプリントバックログの作業の進捗を検査

スプリントレビュー

実際に成果物を見せて、意見をもらう貴重な場です。

手順
  • 参加者(スクラムチームと重要な関係者)はプロダクトオーナーが招待
  • プロダクトオーナーは、プロダクトバックログアイテムの「完成」したものと「完成」していないものについて説明
  • 開発チームは、スプリントでうまくいったこと・直面した問題点・それをどのように解決したかを議論
  • 開発チームは、「完成」したものをデモして、インクリメント(成果物)に対する質問に答える
  • プロダクトオーナーは、現在のプロダクトバックログを審議する。(必要であれば)現在の進捗から完了日を予測
  • グループ全体で次に何をするかを議論し、次のスプリントプランニングに価値のあるインプットを提供できるようにする
  • プロダクトの市場や今後の利用状況についてレビューした場合、次に行う最も価値の高いことが変更されることもある
  • プロダクトの次のリリースに対するスケジュール・予算・性能・市場をレビュー
  • 完了の定義について考える。プロダクトオーナーが求めてるもの。スクラムチーム内での合意を得る
    • デモ手順の通りに動作する
    • publicメソッドのテストコードがある
    • 調査した内容はWikiにまとめてある
    • 最新の仕様がWikiにまとめてある
    • リポジトリからいつでも最新のでも可能でテスト済みのソフトウェアが取得できる
登場人物
  • プロダクトオーナー
    • 関係者を招待
    • プロダクトバックログについて説明
    • プロダクトバックログの審議
  • ステークホルダ
    • 「完成」したものへの質問
    • プロダクトの市場や今後の利用状況についてレビュー
  • 開発チーム
    • スプリント内の良いところと悪いところ、悪いところをどのように解決したか議論
    • 「完成」したものを動かし、質問に答える
  • スクラムマスター
    • ファシリテート
    • サポート
時間
  • 2週間スプリント…2時間
  • 4週間スプリント…4時間
成果物
  • 次のスプリントで使用するプロダクトバックログアイテムが含まれた 改訂版のプロダクトバックログ
  • 新たな機会に見合うように、プロダクトバックログを全体的に調整

スプリントレトロスペクティブ(スプリント振り返り)

今回のスプリントをもとに、次のスプリントがより良くなるか考えます。

手順
  • 人・関係・プロセス・ツールの観点から今回のスプリントを検査
  • うまくいった項目や今後の改善が必要な項目を特定・整理
  • スクラムチームの作業の改善実施計画を作成
登場人物
  • スクラムマスター
    • イベントが確実に開催されるようにする
    • 参加者に目的を理解してもらう
    • スクラムチームにタイムボックスを守るように伝える
    • スクラムプロセスを説明するためにチームメンバーとしてイベントに参加
  • 開発チーム
時間
  • 2週間スプリント…1.5時間
  • 4週間スプリント…3時間
成果物
  • 改善実施計画

まとめ

T先輩からの課題は大変ながらも無事にこなす事ができました!
が、本番はここからだと思います。
現在はそれぞれのイベントに対して、チームに合った実施方法をスクラムチーム全員で考え、議論していきたいです!

ansibleでCentOS7にmysql5.7系のインストールしてみた

社内ツールを作ったり壊したりしている永井です。
名前は覚えなくていいので「スプラトゥーン2は楽しい」ということだけ覚えておいて下さい。

お仕事でmysql5.7の環境を構築する機会がありました。
今更ですがせっかくなのでAnsibleでいい感じに入れてみるかーと思いAnsibleで構築してみたところ詰まったり詰まったりしたので完成品と共に紹介したいと思います。

環境

  • デプロイ元
    • ansible
      • 2.3.1.0
  • デプロイ先
    • CentOS
      • 7.2

要件

  • インストール
  • validate_passwordのアンインストール
  • 任意のrootパスワードに変更

完成品

できたものがこちらです。

---
- name: install mysql repository
  yum:
    state: present
    name: http://dev.mysql.com/get/mysql57-community-release-el7-7.noarch.rpm
    validate_certs: no

- name: install necessary libraries
  yum:
    state: present
    name: "{{ item }}"
  with_items:
    - yum-utils
    - MySQL-python
    - mysql-community-server

- name: running mysqld
  service:
    name: mysqld
    state: started
    enabled: yes

- name: chack root login
  shell: mysql -uroot -pany_password
  register: root_login_result
  ignore_errors: True

- name: get temporary password
  shell: cat /var/log/mysqld.log | grep "temporary password" | awk '{print $11}'
  register: mysql_default_password
  when: root_login_result.rc != 0

- name: change root user password for updating expiration date
  shell: |
    mysql -u root -p'{{ mysql_default_password.stdout }}' --connect-expired-password -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'Do_You_Love_MySQL5.7?';"
  when: root_login_result.rc != 0

- name: uninstall validate_password
  shell: |
    mysql -u root -p"Do_You_Love_MySQL5.7?" --connect-expired-password -e "uninstall plugin validate_password;"
  when: root_login_result.rc != 0

- name: change root user password
  shell: |
    mysql -u root -p'Do_You_Love_MySQL5.7?' --connect-expired-password -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'any_password';" # 任意のパスワード
  when: root_login_result.rc != 0

とりあえずインストールしたい方は、これをピーッとコピーしてペッとrolesの中やplaybookのtasks内にでも貼って貰えれば動くと思います。多分。

パスワードは今回は直打ちにしてありますが、セキュリティのため環境変数にすると良いと思います。

ざっくり解説

インストール自体はyumでサクッとできます。リポジトリを追加してからyum installでmysqlをインストールします。

しかしパスワード周りが少し厄介です。インストール直後のmysql5.7は仮のrootパスワードが設定されています。この仮パスワードは有効期限が切れていて、真っ先にこれを変更しないと大体何もできません

仮rootパスワードは/var/log/mysqld.logに記録されているので読み取ってログインしパスワードを変えます。

パスワードの縛りも激しく、8文字以上で英大文字小文字数字記号の4種類を含む必要があります。これはmysql5.7.8以降に標準で有効になっている"validate_password"というプラグインの働きによるものです。

少し厳しすぎるのもあり、いの一番にそいつを消してやりたいところですが期限の切れたrootパスワードを変えない限り消すこともできません。パスワードを変えないといけない、縛りがキツい、縛りを取りたい、パスワードを変えないといけない、縛りがk(ry。

任意のパスワードに変える流れは以下のとおりです。

  1. /var/log/mysqld.logから初期パスワードを取得しログイン
  2. 8文字以上で英大文字小文字数字記号の4種類を含む文字列を使ったパスワードに変更
  3. validate_passwordをアンインストール
  4. 任意のパスワードに変更する。

まとめ

mysql5.7はセキュリティのレベルが高い反面初見殺しが増えております。
ただ最初の方はrootパスワードさえ突破してやれば問題なく使うことができると思います。

新機能も増え、3倍速くなったと言われるmysql5.7を活用しましょう!(もうmysql8とか出てるけど)ちなみ自分の運用しているサービスで走っているあるクエリは90倍速くなりました。

ではこのへんで、ありがとうございました。

ソースコードを読んでも理解できない。そんなときモデリングしてみるのはどうでしょう。

はじめまして、飯沼です。

社内ツールの開発を行う部署に所属しています。

チームに合流するときにまず、業務理解、システムの現状把握をしますが ソースコードを読んでも理解できないとき、役立ったことを紹介したいと思います。

  • UMLなどのモデリングでモデルの関係性と構造を理解する
  • ユビキタス言語(用語集)でチーム内の共通認識を作る

UML、モデリングについてツールのインストール、使い方の例は、 前回のE野氏のわかりやすい記事がありますのでこちらをどうぞ。

blog.engineer.adways.net

記事中に、E野氏がおっしゃっています。

設計書が無い。

(コードが全てじゃ

はい。

私はチームに入ったとき、

  • 開発途中からチームに加わった(現状把握が必要)
  • 業務知識が足りていない(業務理解が必要)
  • 慣れない開発環境(初めて触れる技術)

という状況でした。

ですがソースコードをみればなんとなくわかるだろうと思っていました。

コードを見て、

なるほど

なるほど

わからん。

これをループしてました。

プロジェクトに参加して気づいたことです。

私はコードを見ただけで仕様を理解できる人間ではなかった。

こりゃやばい

モデリングで関係性と構造を理解する

そこで、現状のプログラムの構成を理解するためモデリング(UML)してみることにしました。

始めるにあたり課題がありました。

  • UMLの学習コスト
  • UML描画のツールが高価、使いづらい(と思っていた)
  • 見せる相手もUMLを理解している必要がある

過去、設計時にUMLを書いた経験はあります。(良い思い出はない)

ツールは検索してPlantUMLがすぐに見つかりました。(簡単に使えました)

チームは学習意欲が高いメンバーばかりなのでUMLを理解してくれる。(のはず)

ということで早速やってみました。

  • クラス図を作ることで、システム内の登場人物の相関がわかる
  • シーケンス図から、プログラムの流れがわかるようになる
  • ユースケース図から、そのシステムに関わる人、システムで解決する課題が見えてくる

これらがやってみてわかったことです。

ソースコードからは見えなかったものが、

モデリングすることで見えるようになりました。

UMLを書くときのコツとして、 目的は仕様の理解とメンバーとのコミュニケーションとしてのツールと考え あまりUMLの形式にこだわらないようにしています。

一つの図の中に書き込みすぎない。 あれもこれも書きたくなるのですが、一枚の図に目的を1つにしぼるのがいいと思います。 クラス図の関連なんか書きすぎるとスパゲティになります。

今は新規機能開発やリファクタリング時にも活用しています。

ちなみに私は、Atom + PlantUML + MacOS でモデリングをしていました。

その後インフラチームが gitlab 上でも PlantUML を使えるように設定してくれまして ブラウザ上でUMLを埋め込めるようになりました。 マジリクのコメントなんかにシーケンス図を差し込むことが気軽にできるようになりました。 インフラチームすげー、オープンソースすげー、です。

ユビキタス言語(システムの用語集として)

業務知識が不足している解決策として、ユビキタス言語は有効だと思います。

ユビキタス言語とは、

Eric Evansが『Domain Driven Design』において、開発者とユーザーとの間で共通の厳格な意味を持つ用語を構築するというプラクティスを表すために使用した用語である。ユビキタス言語はソフトウェアにおけるドメインモデルに基づいている。ソフトウェアは曖昧さをうまく扱うことができないため、厳格さが必要となるのである。

ユビキタス言語とは、チームで共有する言語のことで、 プロジェクトチーム全体で、一つの言語を共有するツールになります。

例えば、次のように用語が登録されていたとします。

用語 システム名称 概要
ダック duck あひる。水鳥のカモ科のマガモのこと。このシステムではワンとなく。

これがあると、チーム内の会話の中で “ダック” という用語が出てきたときに、 ワンとなくあひるのことだなとイメージできます。

システム名称というのはプログラムでそのまま使われる言葉で、duckというクラスを見たら同様に それが何を指してどのような振る舞いをするのかが予測できるようになります。

クラス図を見たときも、クラス名からそれが何を指すのかイメージでき、 関連するクラスもわかりやすくなります。

言葉は抽象化された表現です。 1つの用語でも具体的なところまで意味を持たせると チーム内のコミュニケーションがはかどります。

ユビキタス言語を厳密に作ろうと思うとチームの協力が必要ですが、 まずは個人で用語集を作ってみるのがいいかもしれません。

詰まったら抽象で捉えてみる

サービスやシステム開発は、解決したい課題(抽象)から 何を使ってどう解決するか(具体)に落としていくのですが、 今回はシステムを理解するために具象(ソースコード)から 抽象(モデリングやユースケース)に視点を上げる作業をしました。

このように抽象と具象の視点を切り替えることで視界が広がり わからなかったことがわかるようになる というのを理解できた経験でした。

何か問題にぶつかった時、視点を変え抽象度を上げて捉えてみると 問題解決のヒントがひらめくかもしれません。

まとめ

モデリングやユビキタス言語を使うと、業務理解、システムの現状把握を することに役立ちましたという話をさせていただきました。

何かのお役に立てば幸いです。

golangでダックタイピングをしてみよう

f:id:AdwaysEngineerBlog:20170711181010j:plain:w500

こんにちは、久保田です。
最近はインフラdivに移動し、専らgolangで日々の業務を行っております。

そんな僕は以前までRubyばかりを書いている日々でした。

なのでgoを書いていると、いけないと思いながらもこんな思いが生まれてしまうわけです。
「Rubyみたくカジュアルに書きたい。文字列からメソッドの実行がしたい。」と。

何事もダメだダメだと言われるとやりたくなるのが人間の性。「禁断の」ほど熱く燃え上がるものですね。

ということでRubyのメインコンセプトであるダックタイピングをgolangで実践してみようと思います。一緒に熱く燃え上がりましょう。

ダックタイピング

「もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルである」

Rubyやったことがある人ならば一度は聞いたことがある言葉ですね。
これを説明するとオブジェクト指向の話とかが難しいので、ものすごく簡単に言いますが、

「大切なのは振る舞い」

ってことですね。

オブジェクトがどんな形をしているかではなく、何ができるかにフォーカスを当てた考え方です。
アヒルのように扱えるならアヒルとして扱えるし、猫のように扱えるなら猫なんです。猫だと思っていたらそれは虎でも別にいいんです。

Rubyではこの考え方があるので、型を意識せず色々と自由な書き方ができるんですね。いいか悪いかは別として。

golangでダックタイピング

さて、では本題のgolangでダックタイピングをやってみようかなと思います。

ここで登場する大切なgolangのキーワードはinterfaceです。
interfaceを使うとダックタイピングを実装できます。

golangでは、
interfaceを実装している型はそのinterfaceの型として扱うことができます。

なんだか聞いたことのあるの響きですね。

例えば、こんなことができます。

package main

import "fmt"

type animal interface {
    bark() string
}

type dog struct {
}

type cat struct {
}

func (d dog) bark() string {
    return "bow"
}

func (c cat) bark() string {
    return "mew"
}

func main() {
    animals := []animal{dog{}, cat{}}

    for _, animal := range animals {
        fmt.Println(animal.bark())
    }
}

mainの中に、dog型とcat型を同じanimal型のsliceに入れています。
本来、異なる型のsliceには要素を入れることができないのですが、
dog型cat型animal型を実装しているので、こんなことができてしまいます。
interfaceが持っているメソッドが重要なのです。

複数の動作をするコマンドを実装してみる。

さて、基本的なところがわかったところで実践的な実装をしてみます。
golangといえばコマンドが簡単に作れるのが魅力だと思うので、
引数によって複数の動作をするコマンドをダックタイピングを用いて実装してみます。

動物の名前を渡したら鳴き声を出させましょうか。

例えばRubyであれば、こんなインターフェイスを持つコマンドは簡単に、簡潔に書けますよね。

$ ruby animal.rb dog
=> bow
$ ruby animal.rb cat
=> mew
$ ruby animal.rb bird
=> you can use [dog|cat]

animal.rbdogcatメソッドを実装し、sendで実行してあげれば文字列をそのままメソッドとして使えるので、タイプ数が減りますね。
ないものはmethod_missingなんかでhelpを出せるようにする感じですかね。

しかしgolangでは文字列実行などない。(と思います)

これをgolangで実装しようと思うと、こんな感じになってしまっていました。

package main

import (
    "fmt"
    "os"
)

func main() {
    commandName := os.Args[1]

    var r string

    switch commandName {
    case "dog":
        r = dog()
    case "cat":
        r = cat()
    default:
        r = help()
    }

    fmt.Println(r)
}

func dog() string {
    return "bow"
}

func cat() string {
    return "mew"
}

func help() string {
    return "you can use [dog|cat]"
}

嫌ですね。。。

これだと動物を増やすたびにswitchの中を増やさなければいけませんね。
それに、golangではできればmain packageの中に書きまくりたくないので、できればそれぞれの動物はpackage化してしまいたいです。

こういうのはできればコマンド引数の文字列から実行かつpackage化をしたい。。。

というわけで以下のようにしてみました。

.
├── cli
│   └── cli.go
├── animal
│   ├── animal.go
│   ├── cat
│   │    └── bark.go
│   └── dog
│        └── bark.go
└── main.go
  • main.go
package main

import (
    "fmt"

    _ "./animal/cat"
    _ "./animal/dog"

    "./cli"
)

func main() {
    fmt.Println(cli.Run())
}
  • cli/cli.go
package cli

import (
    a "../animal"
    "os"
)

var animals = map[string]a.Animal{}

// Register sets command to commands
func Register(key string, animal a.Animal) {
    animals[key] = animal
}

// Run calls commands
func Run() string {
    animal, ok := animals[os.Args[1]]
    if ok {
        return animal.Bark()
    }
    return help()
}

func help() string {
    return "you can use [dog|cat]"
}
  • animal/animal.go
package animal

// Animal is interface that has Bark function.
type Animal interface {
    Bark() string
}
  • animal/dog/bark.go
package dog

import (
    "../../cli"
)

// Dog implements cli.Animal
type Dog struct {
}

func init() {
    cli.Register("dog", Dog{})
}

// Bark returns string
func (d Dog) Bark() string {
    return "bow"
}
  • animal/cat/bark.go
package cat

import (
    "../../cli"
)

// Cat implements cli.Animal
type Cat struct {
}

func init() {
    cli.Register("cat", Cat{})
}

// Bark returns string
func (b Cat) Bark() string {
    return "mew"
}

main.goが動くと、animal以下のdog, cat がimportされます。
_ で importされるので、init関数が実行されるだけです。
それぞれの動物packageのinit関数が実行されると、cli.Registerが動き、cliのcommands変数に構造体が初期化されて入ります。

そしてその構造体をコマンド引数をmapのkeyとして、取り出し、animalインターフェイスで宣言されたそれぞれのBark関数を実行しているわけです。

animalインターフェイスはcli.goでimportしているので、DogとCatはanimalインターフェイスをimplementedだと判断されます。

コマンドが増えた時も、animal以下に動物を増やし、animalインターフェイスを実装しmain.goでimportすれば簡単に増やせます。

Bark関数が重要となり、ダックな世界が完成しましたね。

今回は以上です。

animal以下のpackageがcliを読み込んでいるところが若干複雑になっていますが、、まぁ仕方ないとしましょう。

このパターンは、以下のライブラリを真似していますので、皆さんもぜひ実装してください。

https://github.com/vmware/govmomi