backstage

合唱音源の新着情報の舞台裏

PHPカンファレンス2017でレガシーシステム対応の話を聞いてきた

※1週間経って資料が多数追加されたので、記事を半分くらい加筆修正しました

f:id:s2terminal:20171008122237j:plain:w300

PHPカンファレンス2017に行ってきました。

phpcon.php.gr.jp

聞いてきたセッション

今回は レガシーなシステムといかに向き合うか をテーマにセッションを選んできました。 なので、選んだのはすべて事例発表セッションです。

似たような題材のセッションを6つも選んだ都合上、近い話が多かったので、どんな話があったのか以下に簡単にまとめてみます。

レガシーなシステムがあると何が悪いのか?

  • セキュリティ が担保されない
  • アプリケーションの パフォーマンス の向上に限界
  • 新しい機能が使えず 生産性 が上がらない
  • 開発に関する 情報収集 が困難になる
  • 技術者が集まらず 採用不利

などなど、数あるデメリットを差し引いても、レガシーコードは残り続けます。 直接的に利益や価値を生み続けているからです。

ならば私たちにできるのは、「レガシーシステムをメンテナンスし続ける」か、それとも「新しい環境に移管する」かの2択になります。

VOYAGEさんのセッションのフレーズを借りると 「コードを自分たちの手に取り戻す」 事です。

コードを自分たちの手に取り戻す

レガシーなコードとは、何を足したら壊れるか分からない、何を引いても壊れるか分からない、 ジェンガ のようなものだと、VOYAGEさんのセッションでは喩えられていました。

  • ソースコードの全体像を誰も把握しておらず、読みづらい
  • 使われていないと思う箇所があるが、消す勇気が無い
  • エラーログがたくさん出ているが、だれも見ていない
  • 開発環境構築方法が確立されていない。たとえば、ひとつの大きなサーバにみんなでsshして開発している
  • KPI改善・機能追加のタスクが常に最優先で依頼されるため、リファクタリングなど品質担保に割く工数が無い
  • テストコードは無いか、メンテされていない
  • 前任者は退職しておりコードの意図は分からない
  • 不具合があるまま長らく放置され、それが仕様になってしまっており修正できない
  • そんなよく分からないシステムだが 動いており、利益を生み出し続けている

弁護士ドットコムさんのセッションの言葉を借りると カウボーイスタイルの開発 です。

プロジェクトのステージによって、考えるべきものは異なってきます。 明日には撤退するかもしれないようなシード期であれば、いかにサービスを良くするか?だけに注力するべきです。

そこから事業が成長するにつれて、後回しにしてきたソースコードの品質、サービスのスケーラビリティ、タスクの優先順位のようなプロジェクトマネジメントの重要性が上がっていきます。 こうなっていくと、 レガシーコードの対処 も必要なフェーズになってくるわけです。

https://image.slidesharecdn.com/20160901awsstartupsecuritytalkshkiriyam-160912001259/95/aws-startup-security-talks-aws-7-638.jpg?cb=1481444401

レガシーコードと付き合っていくために必要な事の例としては

  • レガシーコードを減らす
    • 適切な処理共通化や、地道なgrep等で、不要なコードをできるだけ削除していく
    • Vagrant,Chef,Ansible,Docker等で、開発環境構築を容易にし、ローカル開発可能にする
    • チーム全員にPhpStormを導入して静的コード分析することで、内容の把握できないレガシーコードを極力減らす
    • Redashを入れることで、不要になった管理機能をどんどん削除していく
  • これ以上レガシーコードが増えないようにする
    • git運用フローを決め、コードレビューをルール化する
    • コーディングガイドラインを制定し、ペアプロで浸透させ、チームで生産するコードの品質を向上する
    • スクラムを導入し、適切な優先度判断ができる開発スタイルを確立する

などが挙げられていました。

新しいシステムに移行する

一方で、PHP5系→7系のように、新しいシステムに移行した話も多く聞いてきました。

  • 継続的テスト環境を用意する
    • 簡易でもいいのでE2Eテストを
    • fluentd等でエラーログを収集しリアルタイムで通知できるように
  • リソース(人員、工数)を確保する
    • アプリ担当とインフラ担当の数名で、数か月以上かけて、というチームが多かった印象
    • QAチームがいない場合はチーム全員で検証するための時間を確保しよう、という話も
    • アーリーな事業フェーズではKPI改善や機能追加優先。サービスが成熟して安定黒字を出すようになったら移管の話を
    • パフォーマンス改善などユーザーメリットや、サーバー費用削減のような、具体的なメリットを打ち出す
  • 諦める事を決める
    • レスポンス改善は目指さない、デザインは変えない、deprecated警告は対応しない、など
    • これをしないと、やることが増え続けて何年経っても終わらなくなってしまう
  • 移行する順番を決める
    • HTTPエンドポイント単位やマイクロサービス単位などに分割して、少しずつ移行する。システムすべてを一気に移行することは難しい。
    • 簡単なもの、影響度の少ない部分、行数の少ない箇所から順番に進める
    • パスによってサーバごと切り分ける等で、新旧の両バージョンを同居させる

また、New Relicでアプリケーションをモニタリング するとか、後方互換性を維持し、新旧両方のPHPバージョンで動くようにして同じテストを通す とかは複数の事例で言及されていたので、鉄板の手法のようです。

こういったPHPバージョンアップやレガシーコードとの付き合い方のようなセッションが多く、どのセッションもとても人が集まっていました。 会議室にとても入りきらないので急遽サテライト会場が用意されたり、話を聞いている人たちの反応が「あるある~」といった感じだったりして、レガシーシステム対応」を課題に思っている人はとても多いのを実感しました。

その他のセッションの資料等については、TECH PLAYレバテックさんのブログにまとまっています。

聞くことができなかったセッションの中では Docker 系の話が多かった印象です。「レガシーなシステムと向き合うために、開発環境を整えよう」という文脈でも、Dockerが話題に上がったセッションもありました。

その他雑記

PHPカンファレンスはネットが快適でいつも助かっています。

あと、昨年と違って今年はお弁当の販売は無かったようで、昼休憩でちょっと路頭に迷いました。

s2terminal.hatenablog.com

今年は懇親会には行かなかったのですが、懇親会LTには当日申し込み枠が設けられており、すべて埋まっているようでした。

参考文献

【memo】WindowsでPackageManagerを使う

自分用メモ

まずはPowerShellを管理者権限で立ち上げ、スクリプトの実行許可をつけておきます。

PS > Set-ExecutionPolicy RemoteSigned
PS > Get-ExecutionPolicy
RemoteSigned

続いて一般権限でPowerShellを立ち上げなおし、Chocolateyから必要な物を持ってきます。

PS > Get-PackageProvider Chocolatey,PowerShellGet -ForceBootstrap
PS > find-package    git, virtualbox, paint.net, github, mysql.workbench, winscp, vagrant, google-chrome-x64, 7zip, slack
PS > install-package git, virtualbox, paint.net, github, mysql.workbench, winscp, vagrant, google-chrome-x64, 7zip, slack -providername chocolatey

以上です。

これで正常終了したのですが、なぜかVirtualBox,WinSCP,7zipは上手くインストールされていないようでした。 調べてもよく分からなかったので諦めました。

もし他に必要なものがあるかどうか調べるならば、PS > find package | moreとかで確認できると思います。すごい時間かかりますが。

参考

dev.classmethod.jp

rcmdnk.com

kanonji.info

【Azure Cognitive Services】画像認識でalt属性の説明文を自動生成してみた

引越して1ヶ月半、ようやく家にネットが通ったので何かコードを書こうと思いました。

Microsoft Azure Cognitive Servicesには便利そうなたくさんAPIがあります。 そのうちComputer Vision APIを使うと、画像を解析して説明文などを自動生成できます。

参考:過去の記事 s2terminal.hatenablog.com

Computer Vision APIが吐き出す情報は英語か中国語です。 今回はさらにこれをTranslator Text APIで日本語化し、imgタグにして吐き出すプログラムを書いてみました。

f:id:s2terminal:20170814035609p:plain:w360

Public Domain Picturesから適当にいくつか画像を取ってきて、説明文を付けてもらいました。

「木からぶら下がって緑のバナナの束」

木からぶら下がって緑のバナナの束

<img alt="木からぶら下がって緑のバナナの束" src="http://www.publicdomainpictures.net/pictures/130000/velka/banana-bunch-1442837481wI7.jpg#.WZCXAvp82Mc.link">

画像のURLを入力するだけで、こういったalt属性のテキストを自動で書いてくれます。 精度はなかなか良い感じです。

「フェンスの横にゼブラ立って」

フェンスの横にゼブラ立って

<img alt="フェンスの横にゼブラ立って" src="http://www.publicdomainpictures.net/pictures/230000/velka/zebra-at-groenkloof-picnic-spot.jpg#.WZCX2R_ZqTY.link">

フランス料理の添え物みたいな言い回しが気になりますが、内容は完璧です。

「白砂のビーチに立っている人」

白砂のビーチに立っている人

<img alt="白砂のビーチに立っている人" src="http://www.publicdomainpictures.net/pictures/30000/velka/drink-on-beach.jpg#.WZCRVmnOXuE.link">

心霊写真でしょうか。

実装

ReactTypeScriptwebpackという、ほとんど私に馴染みのない技術の組み合わせで作りました。よく調べもせずノリで技術選定したので、生産性の低さが半端無いです。

こんな感じで環境が用意できます。準備は簡単です。

$ echo "{}" > package.json
$ npm install --save-dev webpack typescript awesome-typescript-loader source-map-loader
$ npm install --save react react-dom @types/react @types/react-dom
$ npm install --save superagent @types/superagent

実装時間の8割は、ReactでAPIキーみたいなセキュアな設定情報をどこにどう書けば良いのか悩んでいました。

残りの2割はドラゴンクエスト11でマジスロを回しながらコードを書いたので我ながらかなり適当です。はぐれメタルヘルムがもう1個欲しいので仕方ないです。

メイン処理になるmain.tsxはこんな感じです。

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as SuperAgent from 'superagent';
import * as config from './config';

interface ImageFormProps {
}
interface ImageFormState {
  image_src: string;
  textarea_value: string;
  header: string;
}

class ImageForm extends React.Component<ImageFormProps, ImageFormState> {
  constructor(props) {
    super(props);
    this.state = { image_src: '', textarea_value: '', header: '画像のURLを入力して下さい' };
    this.handleChangeURL = this.handleChangeURL.bind(this);
  }
  handleChangeURL(event) {
    var image_url = event.target.value;
    this.setState({image_src: image_url,header: "(解析しています...)"});
    SuperAgent
      .post(config.computer_vision.api_url)
      .set('Ocp-Apim-Subscription-Key', config.computer_vision.api_key)
      .set('Content-Type', 'application/json')
      .send({url:image_url})
      .end(function(error, response){
        if (error) { return this.setState({header: response.text}); }
        var desc = JSON.parse(response.text).description.captions[0].text;

        this.setState({header: `(翻訳APIのアクセストークンを取得しています...)`});
        SuperAgent
          .post(config.translator_text.issue_token_url)
          .set('Content-Type', 'application/json')
          .set('Accept', 'application/jwt')
          .set('Ocp-Apim-Subscription-Key', config.translator_text.api_key)
          .send()
          .end(function(error, response_token){
            if (error) { return this.setState({header: response_token.text}); }
            var token = response_token.text;
            this.setState({header: `(「${desc}」を翻訳しています...)`});

            SuperAgent
              .get(config.translator_text.api_url)
              .query({
                'appid': 'Bearer ' + token,
                'text': desc,
                'to': 'ja',})
              .set('Accept', 'application/xml')
              .end(function(error, response_trans){
                if (error) { return this.setState({textarea_value: response_trans.text}); }
                var parser = new DOMParser();
                desc = parser.parseFromString(response_trans.text, 'text/xml').firstElementChild.textContent;

                this.setState({header: desc, textarea_value: `<img alt="${desc}" src="${image_url}">`});
              }.bind(this));
          }.bind(this));
      }.bind(this));
  }
  render() {
    return (
      <div>
        <h1>{this.state.header}</h1>
        <form>
          <input
            type="url" placeholder="http://www.example.com/" size={40}
            onBlur={this.handleChangeURL}
          />
        </form>
        <textarea
          placeholder="ここにimgタグが出力されます" rows={5} cols={40}
          value={this.state.textarea_value}
        />
        <img src={this.state.image_src} width="100%" />
      </div>
    );
  }
}

ReactDOM.render(<ImageForm />, document.querySelector('#app'));

handleChangeURL()のコールバックをみると残念な気持ちになってきます。 納品用コードはちゃんとPromiseか何か使ってきれいに書いたほうが良いと思います。

ソースコードの全文はGitHubに上げました。

github.com

まとめ

これくらいなら人間の手で説明文を書いたほうが早いと思いました。

参考文献

【スプラトゥーン2】イカリング2の戦績データをPCブラウザで無理矢理閲覧する

スプラトゥーン2での戦績を閲覧できるイカリング2ですが、 Nintendo Switch Onlineというスマホアプリ内でしか見ることができません。

www.nintendo.co.jp

前作では普通のブラウザ上で閲覧できたので、戦績データをスクレイピングしてクラウドに保存していたのですが、アプリとなると専門外です。

そこで、プロキシを経由して無理矢理に中身を拝見しました。

アプリの通信内容を解析する

今回はプロキシサーバにmitmproxyを使って、イカリングを串焼きにしようと思います。

適当なCentOSホスト上にプロキシサーバを立てました。ファイアウォールの設定で8081番ポート(自由)を自分のIPアドレスに対して公開しておきます。

mitmproxyはpipでインストールできますが、mitmproxy公式のDockerイメージがあったので、それを使って立ち上げました。 良い時代になりました。

$ docker run --rm -it -p 8081:8080 mitmproxy/mitmproxy

プロキシサーバが立ち上がったら、Android/iOSでプロキシの設定をしておいて、 http://mitm.it にアクセスして証明書をインストールし、NintendoSwitchOnlineアプリを開きます。

mitmproxyの画面に https://app.splatoon2.nintendo.net/ というURLへのGETリクエストが出るので、中身を開きます。 iksm_session(イカスミ?)というキーのcookieがあるので、これを控えておきます。 この値を使えば、アプリ上のhttpリクエストが他の環境でも再現できます。いわゆるセッションハイジャックを自身のスマホに対して行います。

ここでPCに戻り、GoogleChromeEditThisCookieを使って、iksm_sessionに先程と同じ値を書き込んで、 https://app.splatoon2.nintendo.net/home にアクセスすればOKです。

f:id:s2terminal:20170723201214p:plain

無事にPCでイカリング2を閲覧できました。

開発者ツールのNetworkタブを覗いてみると、「records」「stages」「active」「timeline」という4つのJSONデータを取得しており、ここさえ叩くことができれば戦績のログデータが手に入ることが分かります。

認証処理を解析してiksm_sessionを手動生成することができれば、スクレイピングでデータを取得することができそうです。 (誰かやって下さい)

参考文献

【IIJmio×mineo×WiMAX】モバイル回線3種でスプラトゥーン2してみた

f:id:s2terminal:20170716150553j:plain

先日、スプラトゥーン2 前夜祭が開催されました。 データが本編には引き継がれず勝敗が関係ないので、試しにモバイル回線を使ってみることにしました。

格安SIMであるmineoIIJmioテザリング、およびUQ WiMAXモバイルルーターを使って、それぞれ3試合ずつしてみました。

回線のスペック

- WiMAX2+ mineo IIJmio
下り 14.25 Mbps 2.09 Mbps 1.42 Mbps
上り 6.66 Mbps 3.38 Mbps 4.03 Mbps
ping 67 ms 108 ms 113 ms

(測定環境: 大阪市内 休日 日中 RBB SPEED TESTを利用)

注目すべきはping応答速度で、WiMAXの67msに対して、テザリングのmineoとIIJmioは2倍近い差をつけられています。 WiMAXなら比較的ラグの少ない環境で通信対戦できると言って良いと思います。

ナワバリバトル結果

f:id:s2terminal:20170716150612j:plain

3回線での合計9試合中、WiMAXで2勝、残りは全敗という散々な結果に終わりました。

単純に、私の得意武器が使えずスプラシューターコラボを使うしかなかったのもありますが、ちょっとひどい戦績です。

f:id:s2terminal:20170716150659j:plain

プレイ中、ゲームが止まったりワープするといった、ラグによるあからさまな異常は起きませんでした。 安定した通信さえできていれば、最低限のマナーを守って遊ぶことができると言えます。

シビアな格闘戦を制することもでき、スペシャルウェポンのジェットパックを使えば次々とキルを取ることができ、普通に楽しむことはできました。 しかし塗りやダメージの反映がちょっと遅れている感じはして、その結果相打ちになったりはしました。ある程度は枷になっていると思います。

またIIJmio利用中に1度だけ、マッチング中に通信エラーが起きてしまい対戦できない時がありました。

通信量について

1回のナワバリバトルに使用する通信量は約10MB/3分程度でした。 秒間に直すと55KB程度なので、通信速度は大抵の4G回線ならば問題ないと思います。

100試合で1GBと考えると、ちょっとずつ遊ぶ分にはモバイル回線の月間通信量が大きく不足することは無いでしょう。 ただしフェスでカンストまで進めたり、ガチマッチをやり込んだりすると、足りなくなると思います。

まとめ

f:id:s2terminal:20170716150815j:plain

  • モバイル回線でも遊べないことはない
  • やり込むなら固定回線必須

NintendoSwitchのスプラトゥーン2は前作WiiUと異なり携帯モードで遊ぶこともでき、必ずしも自宅など固定回線がある環境でなくてもプレイができます。 出先でちょっと遊ぶくらいなら4Gスマホのモバイル回線テザリングでも可能です。 しかし、本格的に楽しむためにはやはりモバイル回線ではなく、家庭の高速で安定した固定回線が必要になってくると思いました。