Railsプルリクのぞき見 - 2018/10/10
RubyやRailsにちょっとでも貢献したいんですが、まだまだ若輩者なのでRubyやRailsのプルリクなどを半年ROMっていようかと。
継続できるように、雑に眺めて内容がわかったものをピックアップして紹介してみる。
nilClassのtryを高速化
メソッドの引数定義を def try(*args)
から def try(method_name = nil, *args)
ってするだけで処理速度が上がるみたい。
nilClassだけなのはなんでかな?と思ったら、他のクラスは元々第一引数を method_name
としてあったんですねー。nilClassはどうせnil返すだけで引数使わないからって*args
だけにしてたらRubyがメソッド呼び出すのに時間がかかってしまうようですね。
Rubyがメソッドの可変長引数を解釈しているところはどこだろう。そのコードまで追えるかな...
Rubyでの引数の扱い
↑の記事によると、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種類があります。
- config/initializers/asset_sync.rb
- config/asset_sync.yml
- 環境変数
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 LinuxでLet'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の解説は下記サイトなどを参照ください。
- 【Ansible】EC2 External Inventory Scriptを使った動的ホスト一覧生成 | Developers.IO
- Ansible Dynamic Inventoryを使ってAWSのEC2インスタンスに付与されたタグでプロビジョニング対象を決める - Qiita
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.pem
の1
の部分は証明書を発行した回数が連番で記載されます。今後再発行をしていくとカウントアップされていきます。
ちなみに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ブラウザでサイトにアクセスします。
やった! 無事に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ドコにマッピングし、例のフレーズが出てきたら「キ・ヨ・シ!」を出力して終了するようにしています。
元ネタ
Javaの講義、試験が「自作関数を作り記述しなさい」って問題だったから
— てくも (@kumiromilk) 2016年3月9日
「ズン」「ドコ」のいずれかをランダムで出力し続けて「ズン」「ズン」「ズン」「ズン」「ドコ」の配列が出たら「キ・ヨ・シ!」って出力した後終了って関数作ったら満点で単位貰ってた
まとめ
ActionCable関係記事まとめ
Rails5の新機能であるActionCableを扱った記事・資料をまとめです。随時更新していきます。
photo by Nic McPhee | Flickr - Photo Sharing!
リリースノート
- Riding Rails: Rails 5.0.0.beta1: Action Cable, API mode, Rails command
- Rails公式リリース情報
- Rails 5.0で追加される主な新機能(Ruby on Rails公式ブログより) - Qiita
- @jnchito氏による上記の日本語解説記事
チュートリアル
これからActionCableを使ったアプリケーションを作りたい方はまずはチュートリアルを参照してみましょう。
- Rails 5: Action Cable demo - YouTube
- DHH氏によるActionCableを使ったシンプルなチャットアプリの作り方チュートリアル動画
- Rails 5 + ActionCableで作る!シンプルなチャットアプリ(DHH氏のデモ動画より) - Qiita
- @jnchito氏による上記動画を元にしたシンプルなチャットアプリの作り方チュートリアル記事
- コード: https://github.com/JunichiIto/campfire
アプリケーション事例
ActionCableを利用・応用したアプリケーション事例です。
- Rails5の新機能ActionCableを使ったスライド共有システムを作ってみた - devひよこのあしあと
- 手前ミソですがスライド共有サイト構築事例
- コメント追加・配信とページ遷移追従のためにActionCableを利用
- 実例: https://nslides.devchick.link/
- コード: GitHub - devchick/nslides: スライド共有システムnslides
- ActionCable と react-rails でサンプル作ってみた — Commerce Hack
- テスト環境用のDNSサーバー設定切り替えシステム構築事例
- DNSサーバーの起動・終了の通知、ログ配信にActionCableを利用
- コード: GitHub - essa/ore_ore_dns
- Action Cable + Flux + React でリアルタイムチャットデモを作ってみた - Qiita
- ActionCableによるデータのやりとりをFluxに当てはめた事例
- 実例: http://fluxchat.takeyu-web.com/
- コード: https://github.com/takeyuweb/rails-fluxchat-example
- Building a chess server in Rails 5 with Action Cable-powered WebSockets — Jargon
- チェスアプリ
- ケーブル接続時に生成したUUIDでブロードキャスト先のケーブルを識別
- コード: https://github.com/joeyschoblaska/rails5chess
- 天下Wiki武道会のスライド - WikiNote
- Wikiページのリアルタイム同時編集
- React.jsのstateの変更をケーブルで繋がった先にブロードキャスト
- 実例: WikiNote - いますぐ書き始められるWikiノート
ハック / 解説 / やってみた
- Action CableのREADMEを読んでみた! - 酒と泪とRubyとRailsと
- ActionCable公式のREADMEの日本語解説
- ActionCableを読み解く(cable server 編) - Qiita
- ActionCableの実装を覗いて仕組みを解説
- ActionCableコードリーディングその1 - Qiita
- [Rails5]Action Cableのサンプルを読み解いてみる - Qiita
- Rails公式のActionCableサンプルを題材に実装方法を解説
- Rails4系でのActionCableの使い方 - 珈琲駆動開発
- Rails4系でActionCableを利用した事例
- 4系でも5系でも大きな違いなく実装できるっぽいですね
- コード: https://github.com/totzyuta/chat-actioncable
- ActionCableをwebsocket APIとして使ってUnityと通信する - Qiita
- C#からActionCableを利用する方法を紹介
インフラ構築 / デプロイ / 運用
- Rails5のActionCableをCapistrano経由でデプロイ - Qiita
- nginx⇔puma⇔ActionCableの構成を設定する方法を紹介
- AnsibleでElastiCacheをセットアップしてRails5から利用する - devひよこのあしあと
- ActionCableが利用するRedisの構築と設定を紹介
ハマりどころ / 失敗談
だいたい皆さんallowed_request_originsにはハマるようですね。(もちろん私もハマりました...)
- Vagrantでサーバー立ててActionCableを試してる時にRequest origin not allowedがでて辛い - Qiita
- allowed_request_originsの罠
- ruby on rails - ActionCable.server.broadcast from the console - Stack Overflow
- Rails consoleからActionCableのブロードキャストを実行する方法
- developmentモードでデフォルト設定になっているasyncアダプターは同じプロセス上でしか通信できないもよう。redisアダプターを使ったらconsoleからでも利用できるみたい。
- websocket - Rails 5 actioncable undefined method `fetch' for nil:NilClass - Stack Overflow
- Rails5のbeta2からcable.ymlの配置場所が変わっているために設定が読めなかったためっぽい
- ruby on rails - How can I test ActionCable channels using RSpec? - Stack Overflow
- ActionCableのテストのやり方
- Unable to use wscat to connect to an ActionCable WebSocket on Ruby on Rails 5 Beta - Stack Overflow
- wscatで接続できない
- ruby on rails - ActionCable - how to display number of connected users? - Stack Overflow
- 接続数を取得するには
ActionCable.server.connections.length
としたらよいみたい
- 接続数を取得するには
そもそもWebSocketとは
AnsibleでElastiCacheをセットアップしてRails5から利用する
下記の記事で紹介したスライド共有システム「nslides」ではAnsibleを用いてインフラ周りの設定をしています。今回はその中からElastiCacheでredisを立ち上げてRails5から利用する設定をしている部分を紹介します。
nslidesはこちらで公開中です。 公開終了しました (2017/02/27)
システム構成:
- アプリケーション
- インフラ
- Ops
- Ansible 1.9.4
- Capistrano 3.4.0
コードは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の解説は下記サイトなどを参照ください。
- 【Ansible】EC2 External Inventory Scriptを使った動的ホスト一覧生成 | Developers.IO
- Ansible Dynamic Inventoryを使ってAWSのEC2インスタンスに付与されたタグでプロビジョニング対象を決める - Qiita
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_groups
はVPCがない頃?の古いインスタンスたちのときに利用するオプションのようですが、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で書きたい人がいるという噂を聞きました。というわけで、AWSのAPI GatewayとLambdaを利用して書いてみましょう。API GatewayもLambdaもNode.jsも初心者ですが、がんばります!
元ネタ:
Slack Bot「parvati」の作成
まずはSlack Bot本体を作成します。AWS Lambdaと連携したSlack Botを作るnpmモジュールが公開されていたのでこれを利用してみます。
まずは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を登録します。
SlackのApp DirectoryからIncoming WebHookを選択します。
回答を投稿するチャンネルを指定します。
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アカウントを開設します。
メールアドレスとパスワードを入力します。
連絡先情報を入力します。
お支払い情報を入力します。
電話を使って本人確認します。
サポートプランを選択します。
これでアカウント開設は完了です。
Lambda関数の登録用のアクセスキーの作成
lambda_bot npmモジュールではLambda関数のデプロイ作業もやってくれる機能があります。中身を見るとどうやらaws cliを実行しているようなので、ローカルPCからaws cliでlambda create-functionなどが利用できるようにIAMユーザーを作成してアクセスキー&シークレットキーを取得します。
IAMの画面のUsersメニューを開き、Create New User
をクリックします。
ユーザー名は適当にuploaderさんとしてCreate
をクリックします。
表示されたアクセスキーとシークレットキーをメモします。
アクセスキー&シークレットキーは環境変数に登録するか、~/.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
作成したIAMユーザーにはAWSLambdaFullAccessポリシーを適用しておきます。
Lambda実行用のIAMロールの作成
Lambda関数が実行できるように権限を付与したIAMロールを作成します。
lambda_botのデプロイコマンドの中身を見るとlambda_basic_execution
という名前のロールを探しているようなので、この名前で作成します。
ロールタイプにはAWS Lambda
を選択します。
適用するポリシーには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を設定します。
API名はなんか適当に。
なんかよくわからないけど/
にPOSTメソッドのAPIを作成したらいいらしい。
Lambda Functionを選択肢、先ほど作成したLambda関数名を指定します。
Lambda関数呼び出すための権限付与すんぜ!!? とかなんかきかれるので渋々OKします。
するとこんな画面が表示されます。なんかAPI GatewayとLambdaが連携できたっぽい感じになりました。
ただ、この状態だとAPI GatewayにはSlackからの通知はContent-typeがapplication/x-www-formurlencoded
として渡ってきます。一方、LambdaはJSON形式を想定しているので途中で変換する必要があるとかないとか。
それを上記画面のIntegration Request
というところで指定します。
画面下部の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)とかいう言語らしいです。
APIをデプロイします。ステージ名は適当に。
Invoke URLというものが表示されるのでこれをメモっておきます。
SlackのOutgoing WebHookの登録
最後はSlack上で入力された文字列をAPI Gatewayに通知するための設定です。
SlackのApp DirectoryからOutgoing WebHookを選択します。
監視するチャンネルを選択し、URLに先ほどメモっておいたAPI GatewayのInvoke URLを入力します。
ここでTrigger WordというのにBot名を指定しておきます。これを指定していないと全部の発言がLambdaに通知されます。今回は要件にないし、AWSの無料枠での運用なので少しでも稼働を少なくさせるために指定しました。
円の面積は?
SlackでBotに向かって発言してみます。
やった!初心者だけど出来ちゃった!