ヒトデさんの以下のツイートを目にして便利そうだと思ったので fish + peco + vim でやってみることにした。

以下のような fish 関数を追加した上でショートカットキーを bind しておいた。

function peco_gitlsfiles_vim
  git ls-files | peco --query "$LBUFFER" | read selected
  if [ $selected ]
    vim $selected
  end
end
function fish_user_key_bindings
  fish_vi_key_bindings
  bind \cg\cv peco_gitlsfiles_vim
  bind -M insert \cg\cv peco_gitlsfiles_vim
end

これまで一旦 vim を閉じてしまうとファイルを開きたいときには vim . して Unite で調べててたけど、いきなり git ls-files して peco して絞り込めるようになってとても便利になった。

2018-06-20 16.48.40.gif

Lokka の wysiwyg エディターは jQuery ベースの jwysiwyg といつやつが採用されている。

jwysiwyg/jwysiwyg

jwysiwyg/jwysiwyg

WYSIWYG jQuery Plugin. Contribute to jwysiwyg/jwysiwyg development by creating an account on GitHub.

github.com

しかしここ最近はメンテナンスされてなくて、最近の Chrome では利用できない状況だったりする。以前、ファイルアップロード機能を付けたときも jwysiwyg の不具合が原因で wysiwyg モードでの画像アップロードは断念した。

Lokka にファイルアップロード機能を付ける

Lokka にファイルアップロード機能を付ける

ブログの管理画面にファイルアップロード機能をつけてみた。 GitHub の Issue みたいにドラッグアンドドロップでアップロードしてくれる。こんな感じ。いまのところ埋め込みフォーマットは Markdown にしか対応してない。Lokka は heroku とか PaaS...

portalshit.net

最近のモダンな wysiwyg はどんなのがあるんだろうと調べてみたら Quill ってのが GitHub で 18000 くらいスター付いてて良さそうだった。

quilljs/quill

quilljs/quill

Quill is a modern WYSIWYG editor built for compatibility and extensibility. - quilljs/quill

github.com

Quill - Your powerful rich text editor

Quill - Your powerful rich text editor

Quill is a free, open source WYSIWYG editor built for the modern web. Completely customize it for any need with its modular architecture ...

quilljs.com

ちょっと試してみたところ wysiwyg で画像のアップロードもできるようになったので jwysiwyg からこいつに置き換えるのも悪くはなさそう。

ただ Quill は擬似 textarea なので実際の form には対応していない。 SPA で利用されることが想定されており、 Ajax 前提なつくりとなっている。サーバーサイドで HTML をレンダリングする昔ながらのウェブアプリで使うには一手間必要そうだった( form の onsubmit で texarea を createElement して Quill で入力したデータをぶっこむなど)。

追記

Pull Request 出した。

Replace jwysiwyg with quill by morygonzalez · Pull Request #237 · lokka/lokka

Replace jwysiwyg with quill by morygonzalez · Pull Request #237 · lokka/lokka

closes #233 Drop jwysiwyg, instead introduce Quill Remove unused css files Add npm kind of things Add rake task to build JavaScript fil...

github.com

さらに追記

↑の Pull Request 、 Merge されたんで Lokka の wysiwyg エディターは Quill となりました。

IMG_4652.jpg

302 Found

302 Found

blog.craftz.dog

↑の記事を読んで tmux でウィンドウを上下分割して、上でコード書いて下でテスト実行したりするの便利そうだなと思ってまねしてみてる。とてもよい。

ただ分割比を調整するために毎回何度かキー入力が発生するのがいまいちだなと思っていた。 tmux のセッションを開始するときにしかこのコマンドを実行することがないので面倒だなと思いながら都度手動で調整していた。

しかし tmux には select-layout という機能があって、 Control
+b + Alt+1 とか入力するといい感じに pane を配置し直してくれることを思い出した。上下分割は C-b + M-3main-horizontal というスタイルが割り当てられており、上の pane をメインにして上下分割してくれる。このとき main-pane-height という設定項目に任意の値を設定しておくと分割した pane の高さを指定できるようだった。デフォルトだとメインの pane の高さは小さめなので自分は set-window-option -g main-pane-height 60 にしておいた。これで tmux を起動してウィンドウを水平分割したあと C-b + M-3 とやるだけで 8:2 くらいで二つの pane に水平分割されるようになった。ちなみに左右で分割したときの値を調整したい場合は set-window-option -g main-pane-width 230 などとしておけばよい。便利。

GW 中、十分インスタンスを用意しておいたが想定を超えるアクセスがあって負荷が高まり、 Alert が飛んでくる事態となった。車を運転中に iPhone をカーステにつないでいたところ Slack がピコピコ鳴り、嫁さんから「休みなのか仕事なのかハッキリしろ!」と言われたので Alert が上がらないようにオートスケールを仕込むことにした。 いみゅーたぶるいんふらすとらくちゃー諸兄からしたら「そんなの常識じゃん」みたいな話ばかりだけど、自分でやってみて得られた知見をまとめておきます。

なおここで言っているのは EC2 インスタンスのオートスケール( EC2 Auto Scaling )であり、 AWS の様々なリソースを包括的にオートスケールする AWS Auto Scaling とは異なります。

Amazon EC2 Auto Scaling

Amazon EC2 Auto Scaling

aws.amazon.com

Auto Scaling (仮想サーバー処理能力の自動拡張・縮小機能) | AWS

Auto Scaling (仮想サーバー処理能力の自動拡張・縮小機能) | AWS

AWS Auto Scaling が、アプリケーションをモニタリングし、安定した予測可能なパフォーマンスを可能な限り低コストで維持するよう自動的に容量を調整する仕組みを確認してください。

aws.amazon.com

オートスケールをやるにあたって必要なこと

1. インスタンス起動時に最新のコードを pull してきてアプリケーションを起動させる

オートスケールしてきたインスタンスだけコードが古いとエラーが発生する。

2. インスタンス停止時にアプリケーションのログファイルをどっか別のところに書き出す

Auto Scaling Group のインスタンスは Stop ではなく Terminate されるため、インスタンス破棄後もログを参照できるように S3 に上げるとかして永続化させる必要がある。 Fluentd や CloudWatch Logs に集約するのでも良い。

3. AMI を定期的にビルドする

オートスケール対象のアプリケーションは枯れていて今更新しいミドルウェアが追加されたりすることはなくてソースコードを git clone してくるだけで十分なのだが、 Gemfile に変更があった場合を想定して少しでもサービスインを早めるため( bundle install を一瞬で終わらせるため)、 master ブランチへの変更が行われなくなる定時間際のタイミングで Packer でビルドして AMI にプッシュするようにしている。

Packer by HashiCorp

Packer by HashiCorp

Packer is a free and open source tool for creating golden images for multiple platforms from a single source configuration.

www.packer.io

4. Launch Configuration を自動作成

AMI のプッシュが成功したら最新の AMI を利用する Launch Configuration を作成し、 Auto Scaling Group も最新の Launch Configuration を参照するように変更する。 AWS CLI でできるので自動化してある。

5. Auto Scaling Group の設定をいい感じにやる

Auto Scaling Policy を決め( CPU 使用率が一定水準を超えたらとか、 Load Balancer へのリクエスト数が一定以上になったらとか)、時間指定で Desired Count や Minimum Count を指定したければ Schedule をいい感じに組む。 AWS Management Console 上でポチポチするだけでよい。

6. deploy 対象の調整を頑張る

当初は Auto Scaling Group のインスタンスには deploy を行わない(業務時間中はオートスケールしない、夜間と土日だけオートスケールさせる)つもりだった。

8bd51139504996f811a14c1dd04e4c25.jpeg

しかしメトリクスを確認すると朝の通勤時間帯や平日の昼休み時間帯などにもアクセス数が多いことがわかったので一日中 Auto Scaling Group インスタンスを稼働させることにした。となると deploy 対象が動的に増減する、ということなので Capistrano の deploy 対象もいい感じに調整しないといけない。 AWS SDK Ruby で稼働中の EC2 インスタンスの情報はわかるので、 deploy 時には動的に deploy 対象を判定するようにした。

auto-scaling-all-day.png

本当は push 型 deploy をやめて pull 型 deploy にするのがナウでヤングなのだろうが、レガシーアプリケーションに対してそこまでやるのは割に合わない。そのうちコンテナで動くもっとナウでヤングなやつに置き換えるのでこういう雑な対応でお茶を濁すことにした。

注意点

冒頭に書いているけどあくまで上記は EC2 インスタンスの Auto Scaling であり、周辺のミドルウェアは Scaling されない。例えば RDS を使っていたとして、 RDS インスタンスの方は拡張されないので Connection 数が頭打ちになったり、 CPU を使うクエリが沢山流れたりしたらそこがボトルネックになって障害になってしまう。周辺ミドルウェア、インフラ構成に余力を持たせた状態で行う必要がある場合は AWS Auto Scaling の方を使うことになると思う。

所感

1 と 2 のステップはすでに実現できていたので、自分は 3 、 4 、 5 、 6 をやった。オートスケール、めっちゃむずかしいものというイメージを持っていたけど、まぁまぁすんなり行った(二日くらいで大枠はできて、連休後半には実戦投入した)。負荷に応じて EC2 インスタンスがポコポコ増えて、週末の夜にパソコンを持たずに出かけられるようになった。これで家庭円満です。

A summer storm

問題点

Rails でデファクトスタンダードとなっているページネーション gem に Kaminari というのがある。

kaminari/kaminari

kaminari/kaminari

kaminari - ⚡ A Scope & Engine based, clean, powerful, customizable and sophisticated paginator for Ruby webapps

github.com

めっちゃ最高便利で大好きなのだけど、巨大なテーブルに対して COUNT 文を投げると遅いという問題にぶち当たった。このような巨大なテーブルで Kaminari を使うために COUNT 文を発行しない without_count というメソッドが用意されている( Kaminari 1.0.0 でやってくる 5 つの大きな変更 - Qiita )が、これを使うと next_pageprev_pagetotal_pages が取れなくなる(当たり前)。次のページがあるかどうかはばくち状態になってしまう。

本当は DB のスキーマを見直すべき(インデックスがちゃんと効くようにスキーマ変更するべき)だが、 Rails からもレガシーアプリからも同時に同じ DB にアクセスしており、並行運用しているような状況ではなかなか大胆な変更は実行できない。

DB 構造をなおせないとなるとキャッシュを思いつく。 HTML も Rails でレンダリングするのであれば partial cache などでページャー部分だけをキャッシュすれば良いが、 API 選任野郎と化した Rails ではビューのキャッシュはできない。

どうしたか

total_count をキャッシュする。公開範囲を設定できるようなリソースだと全員同一のキーでキャッシュするわけにはいかないのでユーザーごとにキーを作ってキャッシュする必要あり。全ユーザーの全リクエストでスロークエリになってたやつが 5 分に一回スロークエリになるくらいだったら何とか許容できる。

例えば以下のようなコントローラーがあったとする。 HeavyModel には数千万レコードあって、普通に COUNT 文を投げると遅い。 Paginatable という名前でモジュールを定義して、 render メソッドを上書きし、ページネーションを間に挟み込む。

class HeavyModelController < ApplicationController
  include Paginatable

  before_action :login_required

  def index
    resources = HeavyModel.all
    render json: resources, paginate: true
  end
end

モジュールはこんな感じ。車輪の再発明をしている感はあるが、 COUNT 文の結果が current_user 、コントローラーのクラス名、アクション名のそれぞれをつなげたものをキーにしてキャッシュされる。

module Paginatable
  def render(*args)
    options = args.extract_options!
    resources = options[:json]
    if options[:paginate]
      resources, meta = paginate(resources, cache_total_count: options[:cache_total_count])
      options[:json] = resources
      options[:meta] ||= {}
      options[:meta].merge!(meta)
    end
    args << options
    super(*args)
  end

  def paginate(resources, options = {})
    parse_params_for_pagination
    paginator = Paginator.new(
      resources:         resources,
      page:              @page,
      per:               @per,
      cache_total_count: options[:cache_total_count],
      cache_key:         total_count_cache_key
    )
    [paginator.resources, paginator.meta]
  end

  def total_count_cache_key
    @total_count_cache_key ||= "#{current_user&.id}_#{self.class.name}_#{action_name}_count"
  end

  class Paginator
    attr_reader :cache_total_count

    UNCOUNTABLE = -1

    def initialize(resources:, page:, per:, cache_total_count: false, cache_key: nil)
      @_resources        = resources
      @page              = page.to_i
      @per               = per.to_i
      @cache_total_count = cache_total_count
      @cache_key         = cache_key
    end

    def resources
      @resources ||= if cache_total_count?
                       @_resources.page(page).per(per).without_count
                     else
                       @_resources.page(page).per(per)
                     end
    end

    def page
      @page.zero? ? 1 : @page
    end

    def per
      @per.zero? ? Kaminari.config.default_per_page : @per
    end

    def meta
      {
        current_page: current_page,
        next_page:    next_page,
        prev_page:    prev_page,
        total_pages:  total_pages,
        total_count:  total_count
      }
    end

    alias cache_total_count? cache_total_count

    def paginatable?
      !cache_total_count? && resources.respond_to?(:total_count)
    end

    def current_page
      resources.current_page || page
    end

    def next_page
      paginatable? ? (resources.next_page || UNCOUNTABLE) : next_page_fallback
    end

    def next_page_fallback
      return UNCOUNTABLE if page < 1
      return UNCOUNTABLE if per > resources.length
      total_count_fallback > current_page * per ? current_page + 1 : UNCOUNTABLE
    end

    def prev_page
      paginatable? ? (resources.prev_page || UNCOUNTABLE) : prev_page_fallback
    end

    def prev_page_fallback
      return UNCOUNTABLE if page < 2
      (total_count_fallback.to_f / per).ceil >= page ? current_page - 1 : UNCOUNTABLE
    end

    def total_pages
      paginatable? ? resources.total_pages : (total_count.to_f / per).ceil
    end

    def total_count
      paginatable? ? resources.total_count : total_count_fallback
    end

    def total_count_fallback
      @total_count_fallback ||=
        begin
          cached_total_count = Rails.cache.read(@cache_key)
          if cached_total_count
            cached_total_count
          else
            real_total_count = @_resources.page(page).total_count
            Rails.cache.write(@cache_key, real_total_count, expires_in: 5.minutes)
            real_total_count
          end
        end
    end
  end
end

これで 5 分間はキャッシュが効くようになる。

以前、 AWS ECS で試験運用したことがあったので Docker 化自体は済んでいた。 ECS などマネージドコンテナサービスを使わずに Docker 運用ができないか試してみた。

🚨 Docker & ECS 化追跡 24 時

🚨 Docker & ECS 化追跡 24 時

ブログを Docker 化して AWS ECS で運用するようにした。なぜ Docker 化したか仕事で Docker を使う機会が増え知見がたまってきた仕事では Production 投入はできていないので個人ブログで Production 投入して知見を得ておきたかった...

portalshit.net

動機

関連記事の更新処理、諸々障害があって自動化できておらず、 DB を clone してきて手元で実行してサーバーにエクスポートするという運用が続いていた。これを自動化したかった。

⛓TF-IDF で関連エントリーを表示

⛓TF-IDF で関連エントリーを表示

cho45 さんの以下の記事を参考に関連記事を表示するようにしてみた。 TF-IDFとコサイン類似度による類似エントリー機能の実装 | tech - 氾濫原 lowreal.net ほとんど ...

portalshit.net

二つ問題があって、以下の通りだった。

  1. 関連記事の更新処理時に日本語の分かち書きをする必要があるが、 VPS インスタンスのメモリ上限があり MeCab の拡張辞書をサーバー上でインストールできない
  2. VPS 上で SQLite の算術計算を行うためには追加拡張が必要で、そのためには SQLite をソースコードからコンパイルする必要がある

1 は Docker イメージにして手元でイメージをビルドすれば解決できた。 2 の問題も Docker のなかでコンパイルを行うことで解決できた。

どうやるか

  • nginx.conf の修正
  • コンテナの create
    キャッシュのためにファイルシステムを利用しているのでホストとコンテナで public ディレクトリを共有する必要があった。
docker create \
  -e DATABASE_URL=db_url \
  -e RACK_ENV=production \
  -v /home/morygonzalez/sites/portalshit/public:/app/public \
  -p 3001:3001 --name portalshit -it morygonzalez/portalshit bundle exec puma -p 3001
  • コンテナの起動
docker start portalshit

結果どうだったか

サイトを Docker で公開することはできたが、 docker create して docker start するまでの間、ダウンタイムが発生する。

ダウンタイムなしで deploy するためには deploy のタイミングで Nginx conf を書き換えて service nginx reload する必要が出てくる。個人のブログレベルでそこまでやりたくない。

コンテナを管理するサービス( AWS ECS や Kubernetes )があるんだったら Nginx conf の書き換えなどしなくてもいい感じに deploy できると思うが、こちらも個人のブログレベルで使うものではないと思った。

結論

  • サイトの deploy はこれまで通り cap で行い、 puma はホスト OS で普通に動かす(コンテナ化しない)
  • 関連記事表示のバッチ処理のみコンテナ化することにした

前書いてた記事の続き。

🏭 Docker を Production 投入するメリットを考える

🏭 Docker を Production 投入するメリットを考える

仕事で開発中のシステムで、 master ブランチに Pull Request が Merge されると自動的に AWS ECS に構築した社内向けの確認環境にデプロイが行われるような仕組みを導入した。自動テスト、コンテナイメージのビルド、デプロイには CircleCI を...

portalshit.net

Kaizen Platform 時代は Naoya Ito さんの以下の記事にあるような感じで deploy してた。 Slack 上で hubot に話しかけると deploy 用の Pull Request が作られていい感じに deploy フローが始まる。

GitHub 時代のデプロイ戦略 - naoyaのはてなダイアリー

GitHub 時代のデプロイ戦略 - naoyaのはてなダイアリー

少し前までアプリケーションのデプロイと言えば capistrano などをコマンドラインから叩いてデプロイ、み..

d.hatena.ne.jp

これがめっちゃ良くて、現職場でも導入したいと思ってたので今週ちょっとやってみたところ deploy できるようになった。

実際のデプロイフロー

まず Slack で hubot ( 山の会社なので tengu という名前にしてる)に話しかける。すると hubot で GitHub の API を叩いて deploy 対象の Pull Request を取得し、それぞれの Pull Request ごとに commit をグルーピングして、 deploy 対象の Pull Request の Author にメンションするかたちで master ブランチから deployment/production ブランチへの Pull Request が作成される。

tengu deploy 1

最近 Slack の GitHub Integration がアップデートされて、 Webhook の通知がいい感じに飛んでくるようになったので Slack 上でどんな内容が deploy されるのかが一目瞭然となる。

実際に作成される Pull Request は以下のような感じ。この Pull Request を Merge することで CircleCI 上で deploy 用のビルドが走る。その辺は Naoya さんの記事で書いてあるのと同じ。

tengu deploy 2

いま作ってるやつは AWS ECS で運用しようとしてるので、 cap deploy ではなく手製のシェルスクリプトで以下のことをやっている。

  1. deploy 用のコンテナイメージをビルド
  2. AWS ECR にコンテナイメージをプッシュ
  3. プッシュしたイメージを利用する Task Definition を追加し、 ECS のサービスを更新 ecs-deploy というシェルスクリプトでやる

以前の記事にも書いたが「 CircleCI が落ちてたら deploy できないじゃん?」というツッコミが入ったため CircleCI が落ちていても deploy できるようにシェルスクリプト化してあるので、手元からおもむろに bin/deploy production とかやっても deploy できる。

ちなみにこのフローを実現する .circleci/config.yml は以下のような感じ。

jobs:
  deploy:
    docker:
      - image: docker:17.05.0-ce-git
    steps:
      - checkout
      - setup_remote_docker:
          docker_layer_caching: true
      - run:
          name: Install dependencies
          command: |
            apk add --no-cache py-pip=9.0.0-r1 jq curl curl-dev bash
            pip install docker-compose==1.18.0 awscli==1.14.38
            curl -s https://raw.githubusercontent.com/silinternational/ecs-deploy/ac2b53cb358814ff2cdf753365cc0ea383d7b77c/ecs-deploy | tee -a /usr/bin/ecs-deploy && chmod +x /usr/bin/ecs-deploy
      - run:
          name: Execute deployment (Docker image build, push to ECR, create new Task and replace container)
          command: |
            case ${CIRCLE_BRANCH} in
              "deployment/dev" | "master" )
                DEPLOY_ENV="dev" ;;
              "deployment/production" )
                DEPLOY_ENV="production" ;;
            esac
            bin/deploy ${DEPLOY_ENV}

workflows:
  version: 2
  production-deploy:
    jobs:
      - deploy:
          filters:
            branches:
              only:
                - deployment/production

Chat deploy のよさ

deploy フロー・ deploy 状況が可視化され、民主化されることがよい。昔ながらのローカルからの capistrano による deploy の問題点は deploy の特権化を招いてしまうことだと思う。 ○×さんしか deploy 用の踏み台サーバーに ssh できないので一々○×さんに deploy をお願いしないといけない、というような状況はよく分からない遠慮や序列を招きがち。 deploy フローが自動化されていることでチームに入ったばかりの人でもさくっと deploy が行えるというメリットもある。

deploy の履歴が Slack 上と CircleCI 上、また GitHub 上に Pull Request として残るのもよい。ひとくちに deploy といっても schema 変更が伴う場合は作業ログの共有やコミュニケーションをどこかで行う必要があり、その場所として GitHub の Pull Request が使えるのがとてもよい。 YAMAP で作った deploy スクリプトではそこまでやってないが、 Kaizen Platform の deploy スクリプトには deploy 用の Pull Request 本文に動作確認用のチェックボックスを作って、チェックボックスにチェックが入れられるまで cronbot が二時間おきに deploy 対象の commit author に Slack 上で動作確認を促す、というような仕組みまであった。

今後 YAMAP でもどんどん deploy フローを改善していって Merge ボタンを押したあと寿司を食ってれば良いような状態1にしていきたい。


ちなみに上記の chat deploy を実現するためには GitHub App を作っていろいろやる必要があって、その辺は Kaizen Platform で同僚だった t32k さんの以下の記事が参考になった。

チャットデプロイしたい2018 - MOL

チャットデプロイしたい2018 - MOL

タイトルの通り、チャットデプロイしたい。

t32k.me

書いてあるフローはほとんど Kaizen Platform のやつと同じでちょっとウケた。いやでもそのくらい完成されてる仕組みだと思う。この割とイケてる deploy フローを体験してみたい人は僕が勤めてる YAMAP の Wantedly をご覧下さい。資金調達しており割と積極的に採用中です。


  1. Terraform + GitHub + CircleCI + Atlasを利用してAWSの操作を自動化した - Glide Note http://blog.glidenote.com/blog/2015/02/18/terraform-github-circleci-atlas-aws/