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

まとめ

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

参考文献

【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スマホのモバイル回線テザリングでも可能です。 しかし、本格的に楽しむためにはやはりモバイル回線ではなく、家庭の高速で安定した固定回線が必要になってくると思いました。

個人用mastodonサーバーを構築する

f:id:s2terminal:20170423131210p:plain

ニンジャスレイヤーのmastodonサーバーができたようですが、私はmastodonアカウントを持っていないのでフォローできませんでした。

diehardtales.com

上記記事より引用

あなた自身でサーバーを立てて象になることもできますが、それにはある程度のUNIX知識が求められます。

ではせっかくなので、個人用にサーバーを立てて象になってみました。

なおUNIX知識が求められるそうですが普通にLinux(CentOS7)で構築します。

用意したもの

mastodonAWS S3に対応しているそうなので、特に理由が無ければサーバーもAWS EC2で用意するのが無難だと思います。 真面目にやるならAWS ECS+S3+ElastiCache+PostgreSQL on RDSみたいな構成になるのではと思いますが、今回は個人用なので適当です。

サーバーにはdockerやnginxなど必要なものを入れておきます。

アプリケーションサーバーの準備

jtwp470.hatenablog.jp

上記記事などに沿ってdockerイメージのビルド、RailsのアセットプリコンパイルとDBマイグレーションまで終わらせます。 ちょっと時間がかかりますが、気長に待ちます。

最終的に、$ docker-compose up -d すればOKです。

SSL証明書の取得

qiita.com

この作業のときにサーバーの443番ポートをパブリックに開放する必要があります。 (逆に、このとき以外はファイアウォールで必要なアクセス元以外を閉めておけば個人利用には問題ありません)

Let'sEncryptの証明書は3ヶ月で切れますが、3ヶ月も経てば飽きて辞めるか本格運用のためイチから作り直すと思うので自動更新とかはしなくて良いと思います。

メールサーバーの設定

個人利用ならばSMTPサーバーは用意しなくてもDBを直接操作でOKみたいですが、今回はせっかくなのでSendGridを使ってメール送信設定をします。

s2terminal.hatenablog.com

SendGridのアカウント情報を.env.productionに書いておきます。

シングルユーザーモードに設定

qiita.com

上記を参考に、自分のアカウントに管理者権限を付けて、 .env.productionSINGLE_USER_MODE=trueをコメントインし、docker-composeの再ビルドを実行します。

ファイアウォールの設定

今回は個人の検証用途なので、443番ポートは個人のIPアドレス以外に対して基本的に閉じています。IDCFクラウドIPアドレス仮想ルーターファイアウォールで設定しています。

他に、フォローしたいアカウントのあるサーバーからのアクセスを許可する必要があります。今回紹介したようなシンプルな構成だとドメインのAレコードIPとゲートウェイIPアドレスは一緒だと思うので、たとえばニンジャスレイヤーをフォローするには$ dig a dhtls.netした結果をファイアウォールの設定に貼り付けていますが、この運用には限界があります。

本格的に使いたいならば443番ポートをパブリックにできる環境を用意する必要があると思います。

参考