devひよこのあしあと

いつでもひよっこな気持ちで学びと挑戦を

Railsプルリクのぞき見 - 2018/10/10

RubyRailsにちょっとでも貢献したいんですが、まだまだ若輩者なのでRubyRailsのプルリクなどを半年ROMっていようかと。

継続できるように、雑に眺めて内容がわかったものをピックアップして紹介してみる。

nilClassのtryを高速化

github.com

メソッドの引数定義を def try(*args) から def try(method_name = nil, *args) ってするだけで処理速度が上がるみたい。

nilClassだけなのはなんでかな?と思ったら、他のクラスは元々第一引数を method_name としてあったんですねー。nilClassはどうせnil返すだけで引数使わないからって*argsだけにしてたらRubyがメソッド呼び出すのに時間がかかってしまうようですね。

Rubyがメソッドの可変長引数を解釈しているところはどこだろう。そのコードまで追えるかな...

Rubyでの引数の扱い

techracho.bpsinc.jp

↑の記事によると、Rubyには8種類の引数(パラメーター)がある。

Rubyの中では↓の構造体で表現しているもよう。

struct args_info {
    /* basic args info */
    VALUE *argv;
    int argc;
    const struct rb_call_info_kw_arg *kw_arg;

    /* additional args info */
    int rest_index;
    VALUE *kw_argv;
    VALUE rest;
};

ruby/vm_args.c at ruby_2_5 · ruby/ruby · GitHub

たぶん、argv argc が必須な引数を表していて、kw_argvはキーワード引数だろうから、restがsplat部分かな。

同ファイルの setup_parameters_complex っていう関数でなんかごにょごにょしてるあたりかなー。

    if (iseq->body->param.flags.has_opt) {
    int opt = args_setup_opt_parameters(args, iseq->body->param.opt_num, locals + iseq->body->param.lead_num);
    opt_pc = (int)iseq->body->param.opt_table[opt];
    }

    if (iseq->body->param.flags.has_rest) {
    args_setup_rest_parameter(args, locals + iseq->body->param.rest_start);
    }

args_setup_rest_parameterの中でarg_copy関数使ってsplatパラメーターをコピーしたりしてるっぽい。

ということは、def try(*args)にたいして xxx.try(:hoge)と呼び出すと引数データのコピーとかが実行されるってことかな。 それを def try(:method_name = nil, *args) にするとargs_setup_opt_parametersの方が使われて、そのなかではコピーはしてせずにargv argcにセットしているもよう。

っていう感じで実行速度に差がでる... ってことでいいのかな?

AssetSyncでAzure BlobにRailsの静的ファイルを自動的に保存する

AzureでRailsアプリを運用されている皆様に朗報です。

アセットプリコンパイルで自動的にクラウドストレージに静的ファイルをアップロードしてくれるAssetSync gemがAzure Blobにも対応しました!! AssetSync PR#363

とりあえず結論

Gemfileに下記を追加

gem 'asset_sync', '~> 2.4.0'
gem 'fog-azure-rm'
$ bundle install
$ bundle exec rails g asset_sync:install --use-yml --provider=AzureRM

上記のコマンドで生成されたconfig/asset_sync.ymlを下記のように編集

defaults: &defaults
  fog_provider:               AzureRM
  azure_storage_account_name: ストレージアカウント名
  azure_storage_access_key:   ストレージアクセスキー
  fog_directory:              コンテナー名
  fog_region:                 リージョン # 西日本ならjapanwest
  existing_remote_files:      keep

development:
  <<: *defaults
  enabled: false

test:
  <<: *defaults
  enabled: false

production:
  <<: *defaults
  enabled: true

そしてアセットプリコンパイルの実行です。

$ RAILS_ENV=production bundle exec rake assets:precompile

あとは、アップロードしてある静的ファイルの方を参照させるようにconfig/environments/production.rbを設定します。

Rails.application.configure do
  ...
  if defined?(AssetSync) && AssetSync.enabled?
    config.action_controller.asset_host = "//#{AssetSync.config.azure_storage_account_name}.blob.core.windows.net/#{AssetSync.config.fog_directory}"
  end
  ...
end

解説

それぞれ設定内容などについて解説していきます。

Gemfile

gem 'asset_sync', '~> 2.4.0'
gem 'fog-azure-rm'
gem 'azure-core', '~> 0.1.14', require: false # ← なくてもいいです

AzureBlob対応したAssetSync gemは2.4.0になるので2.4.0以降を指定します。 fog-azure-rm gemはAssetSyncがAzureBlobを操作するために必要となります。

azure-core gemはfog-azure-rmをインストールすれば依存関係で自動的にインストールされるので明示的にGemfileに記述する必要はありませんが、0.1.14以降を利用することを明示したいために記述することをオススメします。

というのも、azure-core gemはアセットプリコンパイルされた静的ファイルたちをAzure Blobへアップロードするために利用されていますが、0.1.14より前のバージョンではAzure側から接断されてしまったときのリトライ処理がうまく動作せずに失敗してしまうバグがあるためです。 Azure Core PR#52 Azure Core PR#55

たぶん0.1.14をGemfileで明示しなくても bundle update azure-core するだけでもよいはず。

AzureSyncの設定

AzureSyncを設定する方法には以下の3種類があります。

  1. config/initializers/asset_sync.rb
  2. config/asset_sync.yml
  3. 環境変数

3つ目の環境変数は、1つ目2つ目の設定ファイルが存在しなかった場合に参照されます。

1つ目と2つ目の設定ファイルはいずれか一方でもいいですし、両方を併用することもできます。 併用した場合は、initializerの方が先に読み込まれ、yamlの値で上書きされる、という順番で評価されます。

yamlの方でもERB形式のロジックを記述できるので充分な表現力がありますが、一部の設定項目(アップロード対象ファイルを追加する設定やログ出力設定)はinitializerの方でしか設定できませんので、それを使いたい場合はinitializerを使うか併用するとよいと思います。

設定例

私が開発・メンテしているRailsアプリでは下記のような内容で併用するようにしました。

yamlの方には認証情報などのクレデンシャルなものを記述し、ansibleやcapistranoなどのデプロイツールを使ってステージング毎に違う設定ファイルを配置する想定です。

config/initializers/asset_sync.rb

if defined?(AssetSync)
  AssetSync.configure do |config|
    # Don't delete files from the store
    config.existing_remote_files = "keep"

    # Use the Rails generated 'manifest.yml' file to produce the list of files to
    # upload instead of searching the assets directory.
    config.manifest = true

    # Log silently. Default is `true`. But you can set it to false if more logging message are preferred.
    # Logging messages are sent to `STDOUT` when `log_silently` is falsy
    config.log_silently = false
  end
end

config/asset_sync.yml

defaults: &defaults
  fog_provider:               AzureRM
  azure_storage_account_name: ストレージアカウント名
  azure_storage_access_key:   ストレージアクセスキー
  fog_directory:              Blobコンテナー名
  fog_region:                 japanwest

development:
  <<: *defaults
  enabled: false

test:
  <<: *defaults
  enabled: false

production:
  <<: *defaults
  enabled: <%= $STAGE != "DEV" %> # ← DEV/STAGING/PRODUCTIONと開発工程毎に分かれている定数値でAssetSyncの利用を切り分け

アップロードした静的ファイルを参照させる設定

Railsのビューで生成されるlinkタグやimgタグのURLをアップロードしたAzure側のホストに向けるためにはconfig/environments/production.rbを下記のように設定します。

Rails.application.configure do
  ...
  if defined?(AssetSync) && AssetSync.enabled?
    config.action_controller.asset_host = "//#{AssetSync.config.azure_storage_account_name}.blob.core.windows.net/#{AssetSync.config.fog_directory}"
  end
  ...
end

上記はAzureBlobに対して匿名のHTTPアクセスを許可させている場合の設定です。AzureCDNを使う場合は.blob.core.windows.net の部分を .azureedge.net としてください。(もちろん、Azure側は適切に設定してくださいね。)

なお、linkタグやimgタグのURLを全部Azureに向ける場合は上記の設定でOKですが、一部のJSやCSS、画像ファイルをRailsコントローラーやnginxなどのフロントWebサーバーに処理させたい場合には下記のようにProcオブジェクトで条件分岐させることができます。Procを評価した結果nilが返った場合は外部ホストを参照しないURLが生成されます。

Rails.application.configure do
  ...
  if defined?(AssetSync) && AssetSync.enabled?
    config.action_controller.asset_host = Proc.new { |src, request|
        if src =~ /^\/assets/
          "//#{AssetSync.config.azure_storage_account_name}.blob.core.windows.net/#{AssetSync.config.fog_directory}"
        end
      }
  end
  ...
end

おまけ

上記までで基本的にはAssetSyncでAzureBlobに静的ファイルを保存させることができます。

以下はハマった箇所についての補足です。

CSSファイルから外部ホスト参照している場合のキャッシュ回避

dt.menu-tree-open {
    background-image: url(<%= asset_path("menu_tree/minus.png") %>);
}

上記のようにCSSファイル内で画像ファイルを指定している場合に前述の設定で外部ホストを参照するようにできますが、設定を変更して別のホストにしたり、一時的に外部ホストを使わないようにしたという場合にアセットプリコンパイルを実行してもキャッシュが効いてしまって再作成されずに古い設定のまま外部ホストを参照してしまいました。

キャッシュを削除してからプリコンパイルを実行すればもちろん解決するのですが、そのために全てのキャッシュを削除してプリコンパイルをやり直すとちょっと時間がかかりすぎてしまうのであまりやりたくないです。

そこで、画像ファイルを利用しているCSSファイルをxxx.cssからxxx.css.erbに名称変更して下記の内容を加えます。

/* 画像ファイルパスのキャッシュ回避 */
<% depend_on Rails.root.join("config/asset_sync.yml") %>
<% depend_on Rails.root.join("config/environments/production.rb") %>

<% asset_host = Rails.app_class.config.action_controller.asset_host %>
<% if asset_host.is_a?(Proc) %>
dummy_tag {
  background-image: url(<%= asset_host.call(Rails.app_class.config.assets.prefix) %>) ;
}
<% else %>
dummy_tag {
  background-image: url(<%= asset_host %>) ;
}
<% end %>

dt.menu-tree-open {
    background-image: url(<%= asset_path("menu_tree/minus.png") %>);
}

depend_onで指定したファイルが更新されているとプリコンパイルで再作成する対象となります。また、dummy_tagというところに外部参照ホストを書き出すようにすることでプリコンパイル結果が変わるので無事にAssetSync関係の変更が反映されるようになります。

おわりに

AssetSyncでAzureBlobへ静的ファイルを自動的にアップロードするやり方を紹介しました。

もともとAssetSyncはAzureに対応していなかったのですが、プルリクを送ってみたら採用してもらえました。はじめてのプルリクでドキドキしていましたがピカチュウっぽい人が丁寧に対応してくれて助かりました。自分の変更が取り込まれて世界中の人に使ってもらえるっていいですね〜。

Amazon LinuxでLet's Encrypt

Amazon LinuxLet's Encryptを使ってSSLサーバー証明書を取得してnginxに設定する方法を紹介します。

TL;DR(とりあえず結論)

コマンド一発でちょー簡単に証明書発行できてしまいます。ただし、Amazon Linux上ではLet's Encryptクライアントアプリがまだ実験段階のようです。とりあえず正常に発行できましたが、ご利用は自己責任でお願いいたします。

システム構成

せっかくなんでリリースされたばかりのAmazon Linux AMI 2016.03を利用してみます。

EC2のセットアップ

以下、Ansibleを使ってEC2インスタンスとnginxをセットアップしていきますが、そのへんはあまり本質ではないのでびゃーっと記事中段あたりまで読み飛ばして頂いても構いません。

事前作業

Ansibleが利用するAWSのアクセスキー&シークレットキーなどplaybook内には直書きしたくない情報を環境変数としてセットしておきます。 私の場合は普段direnvを利用しているので、ローカルの.envrcに登録して環境変数に反映されるようにしました。

$ cd REPO_ROOT
$ cat .envrc
export AWS_ACCESS_KEY_ID=XXXXXXXXXXXX
export AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXX
export AWS_REGION=ap-northeast-1

$ direnv allow

インベントリ

AWSリソースを用いますのでDynamic Inventoryを利用します。Dynamic Inventoryの解説は下記サイトなどを参照ください。

ec2.iniとec2.pyを取得します。

$ cd REPO_ROOT/provision/inventories
$ wget https://raw.githubusercontent.com/ansible/ansible/devel/contrib/inventory/ec2.ini
$ wget https://raw.githubusercontent.com/ansible/ansible/devel/contrib/inventory/ec2.py
$ chmod u+x ec2.py

site.yml

AWSリソースのセットアップタスクで、elasticacheロールとec2ロールを呼び出します。

# PLAYBOOK_ROOT/site.yml

---

- name: AWSリソースのセットアップ
  hosts: localhost
  connection: local
  roles:
    - ec2
  vars:
    service_name: nslides01
    group_name: application
    region: ap-northeast-1

- name: アプリケーションサーバーのセットアップ
  hosts: tag_Name_application
  remote_user: ec2-user
  become: yes
  roles:
    - nginx
  vars:
    private_key: ~/.ssh/nslides01.pem

ec2ロール

ec2ロールではセキュリティグループとEC2インスタンスを作成します。 EC2用セキュリティグループではSSH接続とHTTP/HTTPS接続のみを許可しておきます。

# PLAYBOOK_ROOT/roles/ec2/tasks/main.yml

---

- name: EC2用セキュリティグループを作成
  ec2_group:
    name: "{{ group_name }}"
    description: "{{ group_name }}"
    region: ap-northeast-1
    rules:
      - proto: tcp
        from_port: 22
        to_port:   22
        cidr_ip:   0.0.0.0/0
      - proto: tcp
        from_port: 80
        to_port:   80
        cidr_ip:   0.0.0.0/0
      - proto: tcp
        from_port: 443
        to_port:   443
        cidr_ip:   0.0.0.0/0
    rules_egress:
      - proto: all
        from_port: 0
        to_port:   65535
        cidr_ip:   0.0.0.0/0

- name: EC2インスタンスを作成
  ec2:
    image:         "{{ ami_image }}"
    instance_type: "{{ instance_type }}"
    region:        "{{ region }}"
    key_name:      "{{ service_name }}"
    group:         "{{ group_name }}"
    instance_tags:
      Name:        "{{ group_name }}"
    exact_count:   1
    count_tag:
      Name:        "{{ group_name }}"
    wait:          yes
    wait_timeout:  300
    volumes:
      - device_name: "{{ device_name }}"
        volume_type: "{{ volume_type }}"
        volume_size: "{{ volume_size }}"
        delete_on_termination: yes
    instance_profile_name: ap_servers
  register: ec2


- name: SSHで接続できるようになるまで待機
  wait_for: port=22 host="{{ item.public_ip }}" timeout=300 state=started
  with_items: ec2.instances

- name: 作成したEC2インスタンスをインベントリに追加
  add_host: hostname="{{ item.public_ip }}" groupname="tag_Name_{{ group_name }}"
  with_items: ec2.instances

上記で参照している変数にはAWSの無料枠で収まるように最低限のインスタンスサイズを下記のように指定しています。(節約♪節約♪)

# PLAYBOOK_ROOT/roles/ec2/defaults/main.yml

---

ami_image: ami-f80e0596  # ← Amazon Linux AMI 2016.03.0
instance_type: t2.micro
device_name: /dev/xvda
volume_type: gp2
volume_size: 30

nginxのセットアップ

# PLAYBOOK_ROOT/roles/nginx/tasks/main.yml

---

- yum: name={{item}} state=latest
  with_items:
    - nginx
    - git

- name: change log_dir permission
  become: yes
  file:
    path: /var/log/nginx
    state: directory
    owner: nginx
    group: nginx
    mode: 0755

- name: service登録
  service:
    name: nginx
    state: started
    enabled: yes

ここまでで、EC2インスタンスを立ち上げてnginxをHTTPのみで起動した状態となりました。

Let's Encrypt

ここからはLet's EncryptをセットアップしてSSLサーバー証明書を発行してみます。

クライアントアプリのインストール

Let's Encryptクライアントアプリをインストールします。今回は/opt以下に配置するようにしました。

Gitリポジトリをクローンします。

$ cd /opt
$ sudo git clone https://github.com/letsencrypt/letsencrypt
Cloning into 'letsencrypt'...
remote: Counting objects: 33135, done.
remote: Compressing objects: 100% (82/82), done.
remote: Total 33135 (delta 48), reused 0 (delta 0), pack-reused 33053
Receiving objects: 100% (33135/33135), 8.74 MiB | 4.66 MiB/s, done.
Resolving deltas: 100% (23496/23496), done.
Checking connectivity... done.

クライアントアプリを実行してみます。

$ ./letsencrypt-auto --help
WARNING: Amazon Linux support is very experimental at present...
if you would like to work on improving it, please ensure you have backups
and then run this script again with the --debug flag!

おや? なんかAmazon Linuxは実験的なサポートしかしてないよって警告でましたね...

--debugオプションをつけると先に進めるようなので、自己責任の元、すすめてみましょう。

$ ./letsencrypt-auto --help --debug
Bootstrapping dependencies via Amazon Linux...
yum は /usr/bin/yum です
読み込んだプラグイン:priorities, update-motd, upgrade-helper
パッケージ python-tools は利用できません。
依存性の解決をしています
--> トランザクションの確認を実行しています。
   (中略)

上記のメッセージがでたあと、ずらずらーっと依存パッケージのインストールが実行されます。インストールされるパッケージは下記とそれらが依存しているパッケージのようです。

  • python26
  • python26-devel
  • python26-pip
  • python26-virtualenv
  • augeas-libs
  • dialog
  • gcc
  • libffi-devel
  • openssl-devel
  • system-rpm-config

上記のインストールが完了したところで画面には下記のようなメッセージが出力されました。

  (中略)
Checking for new version...
Creating virtual environment...
Installing Python packages...
Installation succeeded.
Requesting root privileges to run letsencrypt...
   sudo /home/ec2-user/.local/share/letsencrypt/bin/letsencrypt --help --debug

  letsencrypt-auto [SUBCOMMAND] [options] [-d domain] [-d domain] ...

The Let's Encrypt agent can obtain and install HTTPS/TLS/SSL certificates.  By
default, it will attempt to use a webserver both for obtaining and installing
the cert. Major SUBCOMMANDS are:

  (default) run        Obtain & install a cert in your current webserver
  certonly             Obtain cert, but do not install it (aka "auth")
  install              Install a previously obtained cert in a server
  renew                Renew previously obtained certs that are near expiry
  revoke               Revoke a previously obtained certificate
  rollback             Rollback server configuration changes made during install
  config_changes       Show changes made to server config during installation
  plugins              Display information about installed plugins

Choice of server plugins for obtaining and installing cert:

  --apache          Use the Apache plugin for authentication & installation
  --standalone      Run a standalone webserver for authentication
  (nginx support is experimental, buggy, and not installed by default)
  --webroot         Place files in a server's webroot folder for authentication

OR use different plugins to obtain (authenticate) the cert and then install it:

  --authenticator standalone --installer apache

More detailed help:

  -h, --help [topic]    print this message, or detailed help on a topic;
                        the available topics are:

   all, automation, paths, security, testing, or any of the subcommands or
   plugins (certonly, install, nginx, apache, standalone, webroot, etc)

とりあえず実行準備は整ったようです。

SSLサーバー証明書の発行

下記のコマンドを実行します。

$ ./letsencrypt-auto certonly --webroot -w /usr/share/nginx/html -d nslides.devchick.link --agree-tos -m xxx@example.com

オプションの意味はそれぞれ下記のとおりです。

  • --webroot : HTTPサーバーが既に稼働している場合に指定します。今回はnginxが既に稼働している状態なのでこのオプションを利用します。HTTPサーバーが稼働していないマシンの場合は--standaloneオプションを指定すれば、Let's EncryptクライアントアプリがHTTPサーバーを一時的に立ち上げてくれるようです。
  • -w : HTTPサーバーのドキュメントルートのパスを指定します。このパスの下に.well-known/acme-challenge/xxxxxxxxxxxxxxxxxxxxというようなファイルを一時的に作成しLet's Encryptサーバー側から読み取ってもらうことでドメイン保有者であることを証明しているようです。認証完了後にはこのファイルは削除されます。
  • -d : 取得したいSSLサーバー証明書ドメインを指定します。
  • --agree-tos : Let's Encrypt の利用規約に同意します。事前に利用規約に目を通しておきましょう。https://letsencrypt.org/repository/
  • -m : 自分の連絡先メールアドレスを指定します。緊急の通知や鍵を紛失したときの復旧に使われます。
$ ./letsencrypt-auto certonly --webroot -w /usr/share/nginx/html -d nslides.devchick.link --agree-tos -m xxx@example.com
Checking for new version...
Requesting root privileges to run letsencrypt...
   sudo /home/ec2-user/.local/share/letsencrypt/bin/letsencrypt certonly --webroot -w /usr/share/nginx/html -d nslides.devchick.link --agree-tos -m xxx@example.com
Version: 1.1-20080819
Version: 1.1-20080819

IMPORTANT NOTES:
 - If you lose your account credentials, you can recover through
   e-mails sent to xxx@example.com.
 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/nslides.devchick.link/fullchain.pem. Your cert  # ← ここ!
   will expire on 2016-06-21. To obtain a new version of the
   certificate in the future, simply run Let's Encrypt again.
 - Your account credentials have been saved in your Let's Encrypt
   configuration directory at /etc/letsencrypt. You should make a
   secure backup of this folder now. This configuration directory will
   also contain certificates and private keys obtained by Let's
   Encrypt so making regular backups of this folder is ideal.
 - If you like Let's Encrypt, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

実行すると上記のようなメッセージが出力されます。「ここ!」と記載した箇所に発行されたSSLサーバー証明書が保存されました。 フォルダの中を確認してみると下記のようにシンボリックリンクが張られた状態になります。

$ sudo ls -al /etc/letsencrypt/live/nslides.devchick.link
合計 8
drwxr-xr-x 2 root root 4096  3月 23 13:35 .
drwx------ 3 root root 4096  3月 23 13:35 ..
lrwxrwxrwx 1 root root   41  3月 23 13:35 cert.pem -> ../../archive/nslides.devchick.link/cert1.pem
lrwxrwxrwx 1 root root   42  3月 23 13:35 chain.pem -> ../../archive/nslides.devchick.link/chain1.pem
lrwxrwxrwx 1 root root   46  3月 23 13:35 fullchain.pem -> ../../archive/nslides.devchick.link/fullchain1.pem
lrwxrwxrwx 1 root root   44  3月 23 13:35 privkey.pem -> ../../archive/nslides.devchick.link/privkey1.pem

シンボリックリンク先のcert1.pem1の部分は証明書を発行した回数が連番で記載されます。今後再発行をしていくとカウントアップされていきます。

ちなみに4つのファイルはそれぞれ下記を意味しています。

  • cert.pem : 今回発行したSSLサーバー証明書単体 (Subject: CN=nslides.devchick.link)
  • chain.pem : 上記のSSLサーバー証明書を発行した認証局の証明書 (Subject: C=US, O=Let's Encrypt, CN=Let's Encrypt Authority X1)
  • fullchain.pem : 上記の2つの証明書を1つにまとめたもの
  • privkey.pem : cert.pemに対応する秘密鍵

nginxのセットアップ その2

nginxの設定を修正してHTTPSで接続できるようにします。デフォルトの設定ファイルの中にサンプルがあるので、コメントアウトを解除します。

# /etc/nginx/nginx.conf

    server {
        listen       443 ssl;
        listen       [::]:443 ssl;
        server_name  localhost;
        root         /usr/share/nginx/html;

        ssl_certificate     "/etc/letsencrypt/live/nslides.devchick.link/fullchain.pem";  # ← ここ変更
        ssl_certificate_key "/etc/letsencrypt/live/nslides.devchick.link/privkey.pem";    # ← ここ変更
        # It is *strongly* recommended to generate unique DH parameters
        # Generate them with: openssl dhparam -out /etc/pki/nginx/dhparams.pem 2048
        #ssl_dhparam "/etc/pki/nginx/dhparams.pem";
        ssl_session_cache shared:SSL:1m;
        ssl_session_timeout  10m;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers HIGH:SEED:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!RSAPSK:!aDH:!aECDH:!EDH-DSS-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA:!SRP;
        ssl_prefer_server_ciphers on;

        # Load configuration files for the default server block.
        include /etc/nginx/default.d/*.conf;

        location / {
        }

        error_page 404 /404.html;
            location = /40x.html {
        }

        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }

「ここ変更」とマークした箇所を修正して先ほど発行されたSSLサーバー証明書のfullchainと秘密鍵を指定します。

動作確認

Webブラウザでサイトにアクセスします。

f:id:devchick:20160324000026p:plain

やった! 無事にSSLサーバー証明書が認識されました。

更新処理

Let's Encryptで発行される証明書は有効期限が90日間と非常に短いものとなっています。なので、定期的に更新してあげないといけません。

cronで1ヶ月に1回更新させるように、/etc/cron.monthlyディレクトリ以下にスクリプトをひとつセットしておきます。

$ sudo vi /etc/cron.monthly/letsencrypt
/opt/letsencrypt/letsencrypt-auto renew --webroot -w /usr/share/nginx/html --force-renew --debug
/sbin/service nginx reload

$ sudo chmod a+x /etc/cron.monthly/letsencrypt

--debugオプションを付けていないとまたAmazon Linuxは実験的なサポートしかしてないよって警告がでて処理失敗してしまうので付けています。

おわりに

非常に簡単に、かつ無料でSSLサーバー証明書を発行&設定できてしまいました。

HTTP/2の恩恵を受けるためにはHTTPS通信が必要ですが、Let's Encryptのおかげでいろいろなサイトで利用できるようになりそうです。

参考

ズンドコキヨシ with post-commit

gitでコミットする度にコミットハッシュ値でズンドコ判定します。

$ echo "zundoko" >> test.txt ; git add test.txt ; git commit -m "zundoko"
ズンドコズンドコズンドコドコドコズンドコズンドコズンドコズンドコズンズンドコズンズンドコドコドコドコドコズンドコドコズンドコズンズンズンドコズンズンズンズンドコズン
[master (root-commit) 0d6383d] zundoko
 1 file changed, 1 insertion(+)
 create mode 100644 test.txt

$ echo "zundoko" >> test.txt ; git add test.txt ; git commit -m "zundoko"
ドコズンズンドコドコドコドコドコドコズンドコドコドコドコズンズンズンズンズンドコキ・ヨ・シ!
[master 746bf11] zundoko
 1 file changed, 1 insertion(+)

実装

.git/hooks/post-commitに下記を登録

git rev-parse HEAD | ruby -ne 'puts $_.split(//).map { |c| ["ズン", "ドコ"][c.hex % 2] }.join.sub(/(.*(?:ズン){5}ドコ).*/) {"#{$1}キ・ヨ・シ!"}'

コミットハッシュ値を一文字ずつ数値化して偶奇をズンorドコにマッピングし、例のフレーズが出てきたら「キ・ヨ・シ!」を出力して終了するようにしています。

元ネタ

まとめ

qiita.com

ActionCable関係記事まとめ

Rails5の新機能であるActionCableを扱った記事・資料をまとめです。随時更新していきます。

f:id:devchick:20160317215605j:plain

photo by Nic McPhee | Flickr - Photo Sharing!

リリースノート

チュートリアル

これからActionCableを使ったアプリケーションを作りたい方はまずはチュートリアルを参照してみましょう。

アプリケーション事例

ActionCableを利用・応用したアプリケーション事例です。

ハック / 解説 / やってみた

インフラ構築 / デプロイ / 運用

ハマりどころ / 失敗談

だいたい皆さんallowed_request_originsにはハマるようですね。(もちろん私もハマりました...)

そもそもWebSocketとは

AnsibleでElastiCacheをセットアップしてRails5から利用する

下記の記事で紹介したスライド共有システム「nslides」ではAnsibleを用いてインフラ周りの設定をしています。今回はその中からElastiCacheでredisを立ち上げてRails5から利用する設定をしている部分を紹介します。

devchick.hatenablog.com

nslidesはこちらで公開中です。 公開終了しました (2017/02/27)

システム構成:

コードはGitHubにて公開しています。

https://github.com/devchick/nslides

事前作業

Ansibleが利用するAWSのアクセスキー&シークレットキーなどplaybook内には直書きしたくない情報を環境変数としてセットしておきます。 私の場合は普段direnvを利用しているので、ローカルの.envrcに登録して環境変数に反映されるようにしました。

$ cd REPO_ROOT
$ cat .envrc
export AWS_ACCESS_KEY_ID=XXXXXXXXXXXX
export AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXX
export AWS_REGION=ap-northeast-1
export DB_USERNAME=XXXXXX
export DB_PASSWORD=XXXXXX
export APP_SERVER_IP=XXXXXXXX
export SECRET_KEY_BASE=XXXXXXXXXXXXXXXXXXXXXXXX

$ direnv allow

playbook

インベントリ

AWSリソースを用いますのでDynamic Inventoryを利用します。Dynamic Inventoryの解説は下記サイトなどを参照ください。

ec2.iniとec2.pyを取得します。

$ cd REPO_ROOT/provision/inventories
$ wget https://raw.githubusercontent.com/ansible/ansible/devel/contrib/inventory/ec2.ini
$ wget https://raw.githubusercontent.com/ansible/ansible/devel/contrib/inventory/ec2.py
$ chmod u+x ec2.py

site.yml

AWSリソースのセットアップタスクで、elasticacheロールとec2ロールを呼び出します。

# PLAYBOOK_ROOT/site.yml

---

- name: AWSリソースのセットアップ
  hosts: localhost
  connection: local
  roles:
    - ec2
    - elasticache
  vars:
    service_name: nslides01
    group_name: application
    region: ap-northeast-1

- name: アプリケーションサーバーのセットアップ
  hosts: tag_Name_application
  remote_user: ec2-user
  become: yes
  roles:
    - sudo
    - nginx
    - mysql
    - ruby
    - panel_base
  vars:
    db_host: "{{ groups.rds[0] }}"
    db_username: "{{ lookup('env', 'DB_USERNAME') }}"
    db_password: "{{ lookup('env', 'DB_PASSWORD') }}"
    redis_host: "{{ groups.elasticache_redis[0] }}"
    private_key: ~/.ssh/nslides01.pem

今回はElastiCacheの設定の話なので直接は関係ないですが、DBへ接続するためのユーザー名やパスワードは先述の.envrcで設定した環境変数値を読み込むようにしています。

ec2ロール

ec2ロールではセキュリティグループとEC2インスタンスを作成します。 EC2用セキュリティグループではSSH接続とHTTP/HTTPS接続のみを許可しておきます。

# PLAYBOOK_ROOT/roles/ec2/tasks/main.yml

---

- name: EC2用セキュリティグループを作成
  ec2_group:
    name: "{{ group_name }}"
    description: "{{ group_name }}"
    region: ap-northeast-1
    rules:
      - proto: tcp
        from_port: 22
        to_port:   22
        cidr_ip:   0.0.0.0/0
      - proto: tcp
        from_port: 80
        to_port:   80
        cidr_ip:   0.0.0.0/0
      - proto: tcp
        from_port: 443
        to_port:   443
        cidr_ip:   0.0.0.0/0
    rules_egress:
      - proto: all
        from_port: 0
        to_port:   65535
        cidr_ip:   0.0.0.0/0

- name: EC2インスタンスを作成
  ec2:
    image:         "{{ ami_image }}"
    instance_type: "{{ instance_type }}"
    region:        "{{ region }}"
    key_name:      "{{ service_name }}"
    group:         "{{ group_name }}"
    instance_tags:
      Name:        "{{ group_name }}"
    exact_count:   1
    count_tag:
      Name:        "{{ group_name }}"
    wait:          yes
    wait_timeout:  300
    volumes:
      - device_name: "{{ device_name }}"
        volume_type: "{{ volume_type }}"
        volume_size: "{{ volume_size }}"
        delete_on_termination: yes
    instance_profile_name: ap_servers
  register: ec2


- name: SSHで接続できるようになるまで待機
  wait_for: port=22 host="{{ item.public_ip }}" timeout=300 state=started
  with_items: ec2.instances

- name: 作成したEC2インスタンスをインベントリに追加
  add_host: hostname="{{ item.public_ip }}" groupname="tag_Name_{{ group_name }}"
  with_items: ec2.instances

上記で参照している変数にはAWSの無料枠で収まるように最低限のインスタンスサイズを下記のように指定しています。(節約♪節約♪)

# PLAYBOOK_ROOT/roles/ec2/defaults/main.yml

---

ami_image: ami-383c1956
instance_type: t2.micro
device_name: /dev/xvda
volume_type: gp2
volume_size: 30

elasticacheロール

elasticacheロールでは、セキュリティグループとElastiCacheを作成します。 先ほど作成したEC2用のセキュリティグループを適用してあるインスタンスからのみRedisへの接続を許可するように設定します。

# PALYBOOK_ROOT/roles/elasticache/tasks/main.yml

---

- name: ElastiCache用セキュリティグループを作成
  ec2_group:
    name: redis
    description: redis
    region: "{{ region }}"
    rules:
      - proto: tcp
        from_port: 6379
        to_port:   6379
        group_name: "{{ group_name }}"  # ← EC2用セキュリティグループ名を指定
    rules_egress:
      - proto: all
        from_port: 0
        to_port:   65535
        cidr_ip:   0.0.0.0/0
  register: sg

- name: AWS ElastiCacheのセットアップ
  elasticache:
    name: "{{ service_name }}"
    state: present
    engine: redis
    cache_engine_version: 2.8.24
    node_type: cache.t2.micro
    num_nodes: 1
    cache_port: 6379
    cache_security_groups: []   # ☆
    security_group_ids:         # ☆
      - "{{ sg.group_id }}"     # ☆
    region: "{{ region }}"
    zone: ap-northeast-1a
  register: result

- name: 作成したElastiCacheのエンドポイント
  debug: msg="The new ElastiCache endpoint is {{ result.elasticache.data.CacheNodes[0].Endpoint }}"

ハマったのは上記の☆の部分。cache_security_groupsVPCがない頃?の古いインスタンスたちのときに利用するオプションのようですが、VPC内に作成するからといって無視してはいけなくて、空配列を明示的に指定しておかなければならないようです。

A list of cache security group names to associate with this cache cluster. Must be an empty list if inside a vpc

security_group_idsの方はVPC内のセキュリティグループを指定するのですが、なぜかここはセキュリティグループ名だと抽出に失敗してしまってエラーになりました。なのでセキュリティグループIDを指定するために先に作成したセキュリティグループの結果をregister: sgしておき、そのデータを利用しています。

Ansible 1.9を使ってるからかなー? 2.0で直ってたらいいな。

Railsアプリデプロイ先を構成

nslidesシステムではRailsアプリのデプロイにはCapistrano3を使っています。Capistrano3用にデプロイ先のフォルダやデプロイ先固有の設定を構築するようにAnsibleを設定します。

# PALYBOOK_ROOT/roles/panel_base/tasks/main.yml

---

- name: アプリケーションディレクトリ作成
  file:
    path: "{{ item }}"
    state: directory
    owner: "{{ remote_user }}"
    group: rbenv
    mode: 0775
  with_items:
    - /app/nslides/panel/shared/config
    - /app/nslides/panel/shared/config/initializers
    - /app/nslides/panel/shared/tmp/pids
    - /app/nslides/panel/shared/tmp/sockets
    - /app/nslides/panel/shared/tmp/cache
    - /app/nslides/panel/shared/tmp/sessions
    - /app/nslides/panel/shared/log
    - /app/nslides/panel/shared/vendor/bundle

- name: 環境設定
  become: yes
  template:
    src:  "{{ item.src }}"
    dest: "{{ item.dst }}"
    mode: 0644
  with_items:
    - { src: panel_envs.sh.j2, dst: /etc/profile.d/panel_envs.sh }

- name: 設定ファイル作成
  template:
    src: "{{ item.src }}"
    dest: "/app/nslides/panel/shared/{{ item.dst }}"
    owner: "{{ remote_user }}"
    group: rbenv
    mode: 0660
  with_items:
    - { src: database.yml.j2, dst: config/database.yml            }
    - { src: secrets.yml.j2,  dst: config/secrets.yml             }
    - { src: cable.yml.j2,    dst: config/cable.yml               }
    - { src: sidekiq.rb.j2,   dst: config/initializers/sidekiq.rb }

- name: Bitbucketアクセス用SSH鍵の配置
  copy:
    src: "{{ private_key }}"
    dest: "/home/ec2-user/.ssh/id_rsa"
    owner: "{{ remote_user }}"
    group: "{{ remote_user }}"
    mode: 0400

設定ファイルはAnsibleのテンプレート機能を使います。

例えばshared/config/cable.ymlの場合は下記のような内容になります。

# Action Cable uses Redis by default to administer connections, channels, and sending/receiving messages over the WebSocket.
production:
  adapter: redis
  url: "redis://{{ redis_host }}:6379"

development:
  adapter: async

test:
  adapter: async

shared/config/initializers/sidekiq.rbの場合は下記の内容になります。

Sidekiq.configure_server do |config|
  case Rails.env
    when 'production' then
      config.redis = { url: "redis://{{ redis_host }}:6379", namespace: 'sidekiq' }
    when 'staging' then
      config.redis = { url: "redis://{{ redis_host }}:6379", namespace: 'sidekiq' }
    else
      config.redis = { url: 'redis://127.0.0.1:6379', namespace: 'sidekiq' }
  end
end

Sidekiq.configure_client do |config|
  case Rails.env
    when 'production' then
      config.redis = { url: "redis://{{ redis_host }}:6379", namespace: 'sidekiq' }
    when 'staging' then
      config.redis = { url: "redis://{{ redis_host }}:6379", namespace: 'sidekiq' }
    else
      config.redis = { url: 'redis://127.0.0.1:6379', namespace: 'sidekiq' }
  end
end

Capistrano

RailsアプリをデプロイするCapistrano3の設定を下記のようにします。(関係ありそうな部分のみピックアップしてます)

# config valid only for current version of Capistrano
lock '3.4.0'

set :application, 'panel'
set :repo_url, 'git@bitbucket.org:tasaki-i3/nslides.git'

# Default deploy_to directory is /var/www/my_app_name
set :deploy_to, '/app/nslides/panel'

# Default value for :linked_files is []
set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/secrets.yml', 'config/cable.yml', 'config/initializers/sidekiq.rb')

# Default value for linked_dirs is []
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'tmp/sessions', 'vendor/bundle', 'public/system')

# capistrano/git/sub_directory_strategy
set :git_strategy, Capistrano::Git::SubDirectoryStrategy
set :project_root, 'panel'

Ansibleの方で/app/nslides/panel/sharedの下にファイルやフォルダをセットしてありますので、それらをリンクするようにしています。 なお、リポジトリ内のサブディレクトリにRAILS_ROOTがあるため、git_strategyを変更してます。

参考: capistrano 3 でサブディレクトリをデプロイするやつ · GitHub

実行

さて、準備ができたところでAnsibleを実行してインフラを整備します。

$ cd REPO_ROOT
$ ansible-playbook -i provision/inventories/ec2.py provision/site.yml --private-key=~/.ssh/nslides01.pem

PLAY [AWSリソースのセットアップ] *********************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [elasticache | ElastiCache用セキュリティグループを作成] ***
changed: [localhost] => {"changed": false, "group_id": "sg-bdcf08d9"}

TASK: [elasticache | AWS ElastiCacheのセットアップ] ********************
changed: [localhost] => {"changed": false, "elasticache": {"data": {"AutoMinorVersionUpgrade": true, "CacheClusterCreateTime": 1456835365.451, "CacheClusterId": "nslides01", "CacheClusterStatus": "available", "CacheNodeType": "cache.t2.micro", "CacheNodes": [{"CacheNodeCreateTime": 1456835365.451, "CacheNodeId": "0001", "CacheNodeStatus": "available", "Endpoint": {"Address": "nslides01.xxxxxx.0001.apne1.cache.amazonaws.com", "Port": 6379}, "ParameterGroupStatus": "in-sync", "SourceCacheNodeId": null}], "CacheParameterGroup": {"CacheNodeIdsToReboot": [], "CacheParameterGroupName": "default.redis2.8", "ParameterApplyStatus": "in-sync"}, "CacheSecurityGroups": [], "CacheSubnetGroupName": "default", "ClientDownloadLandingPage": "https://console.aws.amazon.com/elasticache/home#client-download:", "ConfigurationEndpoint": null, "Engine": "redis", "EngineVersion": "2.8.24", "NotificationConfiguration": null, "NumCacheNodes": 1, "PendingModifiedValues": {"CacheNodeIdsToRemove": null, "EngineVersion": null, "NumCacheNodes": null}, "PreferredAvailabilityZone": "ap-northeast-1a", "PreferredMaintenanceWindow": "thu:17:00-thu:18:00", "ReplicationGroupId": null, "SecurityGroups": [{"SecurityGroupId": "sg-bdcf08d9", "Status": "active"}]}, "name": "nslides01", "status": "available"}}

TASK: [elasticache | 作成したElastiCacheのエンドポイント] *********
changed: [localhost] => {
    "msg": "The new ElastiCache endpoint is {'Port': 6379, 'Address': 'nslides01.xxxxx.0001.apne1.cache.amazonaws.com'}"
}

...

上記のようなのがずらずらーっと出力されて実行が完了します。(一発でうまくはいきませんけどね (^^;

続いてRailsアプリのデプロイです。

$ cd REPO_ROOT/panel
$ bundle exec cap production deploy
INFO [91dc5ead] Running /usr/bin/env mkdir -p /tmp/panel/ as ec2-user@xx.xx.xx.xx
INFO [91dc5ead] Finished in 0.099 seconds with exit status 0 (successful).
INFO Uploading /tmp/panel/git-ssh.sh 100.0%
INFO [ba2628eb] Running /usr/bin/env chmod +x /tmp/panel/git-ssh.sh as ec2-user@xx.xx.xx.xx
INFO [ba2628eb] Finished in 0.100 seconds with exit status 0 (successful).
INFO [0a23f6b1] Running /usr/bin/env mkdir -p /tmp/panel/ as ec2-user@xx.xx.xx.xx
INFO [0a23f6b1] Finished in 0.099 seconds with exit status 0 (successful).
INFO Uploading /tmp/panel/git-ssh.sh 100.0%
INFO [ecc80eb7] Running /usr/bin/env chmod +x /tmp/panel/git-ssh.sh as ec2-user@xx.xx.xx.xx
INFO [ecc80eb7] Finished in 0.100 seconds with exit status 0 (successful).
Please enter branch (release):

...

上記のようなのがずらずらーっと出力されて実行完了です。

おわりに

AnsibleでElastiCacheをセットアップしてRails5のActionCableやSidekiqから利用するための設定を紹介しました。

Ansibleでばばーっとインフラが立ち上がっていくのは爽快感がありますね。誰かが「ピタゴラスイッチ感」って名づけてましたがそれを実感できました。

ただ、なかなか一発で全部が動くことはなく試行錯誤の上でやっとここまでたどり着いた感じです。なので、既に本番稼働しているシステムに対してplaybookを修正して再度実行するときはかなーりドキドキしますね。ステージング環境用意した方がいいかなぁ。あー、でもAWS無料枠超えてしまうな。うーむ。。。

初心者なのでAPI Gateway+LambdaなSlack Botで円の面積を求めてみた

整数で値をひとつ読み込み、それを半径とする円の面積を求めて表示するプログラムを作成しなさい。 円周率は3.14とし、計算結果は、小数第2位を四捨五入して小数第一位まで表示すること。

知恵袋方面に上記のプログラムをJavaScriptで書きたい人がいるという噂を聞きました。というわけで、AWSAPI GatewayとLambdaを利用して書いてみましょう。API GatewayもLambdaもNode.jsも初心者ですが、がんばります!

元ネタ:

qiita.com

Slack Bot「parvati」の作成

まずはSlack Bot本体を作成します。AWS Lambdaと連携したSlack Botを作るnpmモジュールが公開されていたのでこれを利用してみます。

qiita.com

GitHub - mumoshu/lambda_bot

まずはnpm初期化と必要なnpmモジュールのインストールをします。

$ npm init -y
Wrote to /Users/d-tasaki/dev/parvati/package.json:

$ npm install --save lambda_bot node-env-file bluebird
npm WARN package.json parvati@1.0.0 No description
npm WARN package.json parvati@1.0.0 No repository field.
npm WARN package.json parvati@1.0.0 No README data
lambda_bot@1.0.0 node_modules/lambda_bot

node-env-file@0.1.8 node_modules/node-env-file

bluebird@3.3.4 node_modules/bluebird

lambda_botのモジュールからサンプルをコピーしてきます。

$ cp node_modules/lambda_bot/example_bot.js ./index.js

コピーしてきたサンプルを元に今回の要件に合わせてロジックを修正します。

// NODE_ROOT/index.js

var LambdaBot = require('lambda_bot');
var env = require('node-env-file');

env(__dirname + '/.env');

var bot = new LambdaBot({
    iconEmoji: process.env['SLACK_ICON_EMOJI'],
    userName: process.env['SLACK_USER_NAME'],
    channelName: process.env['SLACK_CHANNEL_NAME'],
    slackIncomingWebhookURL: process.env['SLACK_INCOMING_WEBHOOK_URL']
});

var round = function(x) {
    return Math.round(x * 10) / 10.0;
};

bot.respond(/parvati:?\s*([\d\.]+)/, function(res) {
    const pi = 3.14;
    var r = parseFloat(res.match[1]);
    var area = round(r * r * pi);

    return res.reply(area);
});

exports.handler = bot.createHandler();

SlackのIncoming WebHookの登録

計算した円の面積をSlackのチャンネル上で回答を投稿するためにIncoming WebHookを登録します。

f:id:devchick:20160314233342p:plain

SlackのApp DirectoryからIncoming WebHookを選択します。

f:id:devchick:20160314233708p:plain

回答を投稿するチャンネルを指定します。

f:id:devchick:20160314233909p:plain

WebHook URLというモノが表示されますので、これをローカルコンソールの環境変数に登録します。 私の場合は普段direnvを利用しているので、ローカルの.envrcに登録して環境変数に反映されるようにしました。

# NODE_ROOT/.envrc

export SLACK_INCOMING_WEBHOOK_URL='https://hooks.slack.com/services/XXXXXX/XXXXXXXXXXXXXXXXXXXX'
export SLACK_CHANNEL_NAME='times_tasaki'
export SLACK_USER_NAME='parvati'
export SLACK_ICON_EMOJI=':parvati:'

AWSアカウントの開設

AWS LambdaとAPI Gatewayを利用するためにAWSアカウントを開設します。

f:id:devchick:20160314230344p:plain

メールアドレスとパスワードを入力します。

f:id:devchick:20160314230555p:plain

連絡先情報を入力します。

f:id:devchick:20160314230741p:plain

お支払い情報を入力します。

f:id:devchick:20160314230855p:plain

電話を使って本人確認します。

f:id:devchick:20160314231111p:plain

サポートプランを選択します。

これでアカウント開設は完了です。

Lambda関数の登録用のアクセスキーの作成

lambda_bot npmモジュールではLambda関数のデプロイ作業もやってくれる機能があります。中身を見るとどうやらaws cliを実行しているようなので、ローカルPCからaws cliでlambda create-functionなどが利用できるようにIAMユーザーを作成してアクセスキー&シークレットキーを取得します。

f:id:devchick:20160314231643p:plain

IAMの画面のUsersメニューを開き、Create New Userをクリックします。

f:id:devchick:20160314232013p:plain

ユーザー名は適当にuploaderさんとしてCreateをクリックします。

f:id:devchick:20160314232158p:plain

表示されたアクセスキーとシークレットキーをメモします。

アクセスキー&シークレットキーは環境変数に登録するか、~/.aws/credentialsにdefaultプロファイルとして登録します。 先述の通りdirenvを利用していますので、.envrcに追加登録します。

# NODE_ROOT/.envrc

export SLACK_INCOMING_WEBHOOK_URL='https://hooks.slack.com/services/XXXXXX/XXXXXXXXXXXXXXXXXXXX'
export SLACK_CHANNEL_NAME='times_tasaki'
export SLACK_USER_NAME='parvati'
export SLACK_ICON_EMOJI=':parvati:'

export AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXX
export AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
export AWS_REGION=ap-northeast-1

f:id:devchick:20160314232848p:plain

作成したIAMユーザーにはAWSLambdaFullAccessポリシーを適用しておきます。

Lambda実行用のIAMロールの作成

Lambda関数が実行できるように権限を付与したIAMロールを作成します。

f:id:devchick:20160314235503p:plain

lambda_botのデプロイコマンドの中身を見るとlambda_basic_executionという名前のロールを探しているようなので、この名前で作成します。

f:id:devchick:20160314235602p:plain

ロールタイプにはAWS Lambdaを選択します。

f:id:devchick:20160314235709p:plain

適用するポリシーにはAWSLambdaBasicExecutionRoleを選択します。

Lambda関数の登録

さていよいよLambdaにNodeモジュールを登録します。

登録するLambda関数名とハンドラーを環境変数に登録します。例によって.envrcに追加登録です。最終的に.envrcは下記の内容となりました。

# NODE_ROOT/.envrc

export SLACK_INCOMING_WEBHOOK_URL='https://hooks.slack.com/services/XXXXXX/XXXXXXXXXXXXXXXXXXXX'
export SLACK_CHANNEL_NAME='times_tasaki'
export SLACK_USER_NAME='parvati'
export SLACK_ICON_EMOJI=':parvati:'

export AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXX
export AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
export AWS_REGION=ap-northeast-1

export LAMBDA_FUNCTION_NAME='parvati_bot'
export LAMBDA_HANDLER='index.handler'

これらの環境変数を利用してlambda_botのデプロイコマンドを実行します。

$ ./node_modules/lambda_bot/bin/lambda_bot deploy
Archiving a lambda function.
Uploading the lambda function.
The lambda function named parvati_bot does not exist. Creating.
Testing the lambda function.

おぉー! なんかできたっぽい。念のためaws cliで確認してみます。

$ aws lambda list-functions --region ap-northeast-1
{
    "Functions": [
        {
            "Version": "$LATEST",
            "CodeSha256": "MpF1uD2maCj4bGsDv9GcggbjkqC7/1/YVtbWDNa7/60=",
            "FunctionName": "parvati_bot",
            "MemorySize": 128,
            "CodeSize": 177250,
            "FunctionArn": "arn:aws:lambda:ap-northeast-1:XXXXXXXXXXX:function:parvati_bot",
            "Handler": "index.handler",
            "Role": "arn:aws:iam::XXXXXXXXXXXXX:role/lambda_basic_execution",
            "Timeout": 3,
            "LastModified": "2016-03-14T12:35:37.110+0000",
            "Runtime": "nodejs",
            "Description": ""
        }
    ]
}

API Gateway

次はLambda関数を発火させるAPI Gatewayを設定します。

f:id:devchick:20160315000342p:plain

API名はなんか適当に。

f:id:devchick:20160315000535p:plain

なんかよくわからないけど/にPOSTメソッドAPIを作成したらいいらしい。

f:id:devchick:20160315000656p:plain

Lambda Functionを選択肢、先ほど作成したLambda関数名を指定します。

f:id:devchick:20160315000944p:plain

Lambda関数呼び出すための権限付与すんぜ!!? とかなんかきかれるので渋々OKします。

f:id:devchick:20160315001321p:plain

するとこんな画面が表示されます。なんかAPI GatewayとLambdaが連携できたっぽい感じになりました。

ただ、この状態だとAPI GatewayにはSlackからの通知はContent-typeがapplication/x-www-formurlencodedとして渡ってきます。一方、LambdaはJSON形式を想定しているので途中で変換する必要があるとかないとか。

それを上記画面のIntegration Requestというところで指定します。

f:id:devchick:20160315001903p:plain

画面下部のMapping TemplateというところでContent-typeのところにapplication/x-www-formurlencodedします。すると右側になんか入力するフォームが表示されるので、そこに以下のコードを入力します。

#set($httpPost = $input.path('$').split("&"))

{
#foreach( $keyValue in $httpPost )
 #set($data = $keyValue.split("="))
 "$util.urlDecode($data[0])" : "$util.urlDecode($data[1])"#if( $foreach.hasNext ),#end
#end
}

初めて見る書式ですが、VTL(Velocity Template Language)とかいう言語らしいです。

f:id:devchick:20160315003000p:plain

APIをデプロイします。ステージ名は適当に。

f:id:devchick:20160315003248p:plain

Invoke URLというものが表示されるのでこれをメモっておきます。

SlackのOutgoing WebHookの登録

最後はSlack上で入力された文字列をAPI Gatewayに通知するための設定です。

SlackのApp DirectoryからOutgoing WebHookを選択します。

f:id:devchick:20160315003426p:plain

監視するチャンネルを選択し、URLに先ほどメモっておいたAPI GatewayのInvoke URLを入力します。

ここでTrigger WordというのにBot名を指定しておきます。これを指定していないと全部の発言がLambdaに通知されます。今回は要件にないし、AWSの無料枠での運用なので少しでも稼働を少なくさせるために指定しました。

円の面積は?

SlackでBotに向かって発言してみます。

f:id:devchick:20160315004218p:plain

やった!初心者だけど出来ちゃった!

参考