production.log

株式会社リブセンスでエンジニアをやっている星直史のブログです。

Railsでpreflightリクエストを処理し、no route matches optionsを回避する方法

概要

前回の記事ではRailsを使って、APIの受け側を作りました。
ただ、このままだとJSでフロントエンドからAPIに対してリクエストした場合、preflightリクエストが飛ぶので、No route matches [OPTIONS]エラーが返ってしまい、正常終了できません。
今回は、Railsでpreflightリクエストを処理し、no route matches optionsを回避する方法を書きます。

処理の方針

fetch("http://localhost:3000/auth_user", {
  method: 'POST',
  body:  JSON.stringify({ emai: "emai@email.com", password: "********"})
}).then(function(response) {
  return response.json();
}).then(function(json) {
  ...
});

こんな感じでフロントからリクエストをすることを想定します。
fetchでリクエストする場合、クロスドメインアクセスが可能か確認するするため、OPTIONSリクエストを飛ばして、それが200になってから本来のリクエストを飛ばします。
また、preflightリクエストが成功にするには、2つ条件があります。

  • OPTIONSをルーティングできること
  • response.headersに適切なキーとバリューが設定されていること

今回は、上記2つの条件を満たすための処理を書くことを目標にします。

OPTIONSをルーティングできること

乱暴ではありますが、いかなるpathのリクエストに対してもOPTIONSリクエストであれば、response.headersの設定を行えるようにします。 config/routes.rbに下記を追加しましょう。

match '*path' => 'options_request#preflight', via: :options

今回はoptions_request_controller.rbを作成して、そこで処理をすることにします。

response.headersに適切なキーとバリューが設定されていること

ルーティングファイルに従って、options_request_controller.rbを作成します。 また、response.headersに適切なキーとバリューが設定しなければならないので、このように処理をします。

class OptionsRequestController < ApplicationController
  ACCESS_CONTROL_ALLOW_METHODS = %w[GET OPTIONS PUT DELETE POST].freeze
  ACCESS_CONTROL_ALLOW_HEADERS = %w[Accept Origin Content-Type Authorization].freeze
  ACCESS_CONTROL_MAX_AGE = 86_400

  protect_from_forgery except: :preflight

  before_action :set_preflight_headers!, only: [:preflight]

  def preflight
    response.headers['Access-Control-Max-Age'] = ACCESS_CONTROL_MAX_AGE
    response.headers['Access-Control-Allow-Headers'] = ACCESS_CONTROL_ALLOW_HEADERS.join(',')
    response.headers['Access-Control-Allow-Methods'] = ACCESS_CONTROL_ALLOW_METHODS.join(',')
    response.headers['Access-Control-Allow-Origin'] = '*'
    head :ok
  end
end

こちらの処理の通り、response.headersの中にいくつかキーとバリューを設定しています。
レスポンスヘッダーの詳細説明に関しては、こちらの記事に譲るとして、やっていることをざっくり説明するとこのようになります

キー バリュー 意味
Access-Control-Max-Age 86400 preflightリクエストの結果を24時間キャッシュ
Access-Control-Allow-Headers Accept,Origin,Content-Type,Authorization リクエスト時にどのヘッダーが使用可能か明示
Access-Control-Allow-Methods GET,OPTIONS,PUT,DELETE,POST リソースのアクセス時に許容するメソッドを明示
Access-Control-Allow-Origin * 許容できるアクセス元のURLを明示

まとめ

今回、とりあえずハマりを抜け出すために、簡易な処理を書きましたが、要点としては下記2点です。

  • OPTIONSリクエストを受け付けるようにする
  • 受け付けたOPTIONSリクエストのレスポンスにAccess-Control-Allow-ほげほげをつける

SPAでAPI連携をするときにハマりポイントではありますし、何回作っても毎度ハマるので参考にしていただければと思います

Rails 5.2でJWTとdeviseを使った認証の仕組みを作る

概要

最近フロントエンドからAPIを叩く実装における認証の仕組みをどうするか考えていました。
以前、AWS Cognitoを使った認証仕組みを作ったことはあったのですが、Railsの場合はどのようにJWTを扱うか知りたかったので、作ってみました。

今回は、タイトルの通りRails 5.2でJWTとdeviseを使った認証の仕組みについて解説します。*1

想定しているケース

想定しているケースはシンプルです。
ユーザーの動き的には、こんなことをすることを想定しています。

  1. ユーザーがブラウザからメール / パスワードを入力してユーザー登録
  2. ユーザーがAPIでデータのやりとりをするために、CUIでメール / パスワードを使ってトークンを取得
  3. 1で取得したトークンをヘッダーに入れて、リクエストをすると、APIが叩けるようになる。

この記事では、主に2, 3.の実装について、説明します。

調査したこと

想定しているケースの「ユーザーがブラウザからメール/パスワードを入力してユーザー登録」はdeviseに任せようと思います。 トークンの発行と検証は、よくわからなかったので、調査したこととしてて、2つありました

  • JWTを扱うための仕組みがdeviseにあるか?
  • なければ、それを補うためのgemがあるのか?

JWTを扱うための仕組みがdeviseにあるか?

まだ未実装でした\(^o^)/

なければ、それを補うためのgemがあるのか?

ありました!!

Starの数を見ると、ruby-jwtが圧倒的だったのと、READMEを見ても、使い方がシンプルでわかりやすかったので、採用しました。

ユーザーがブラウザからメール / パスワードを入力してユーザー登録

まずは、ブラウザからアクセスできるようにしたり、ユーザー登録をするための下準備をします。

# app/controllers/top_controller.rb
class TopController < ApplicationController
  def index
  end
end
# config/routes.rb
Rails.application.routes.draw do
  root 'top#index'
end

つづいて、Gemを追加

# Gemfile
...
gem 'devise'
gem 'jwt'
...

その後こちらのコマンドを実行

bundle install
rails g devise:install
rails g devise User
rake db:migrate

トークンの取得、検証処理の追加

ここから、一気にトークン発行、検証の実装をしていきます。

lib/json_web_token.rbを新規作成します。

# lib/json_web_token.rb
class JsonWebToken
  class << self
    def encode(payload)
      JWT.encode(payload, Rails.application.credentials.config[:secret_key_base])
    end

    def decode(token)
      HashWithIndifferentAccess.new(
        JWT.decode(
          token,
          Rails.application.credentials.config[:secret_key_base]
        )[0]
      )
    rescue
      nil
    end
  end
end

JWTの秘密鍵は、Railsのsecret_key_baseを使いましょう。 *2

次に、上記のクラスをいちいちrequireをするのがめんどくさいので、config/initializers/jwt.rbを作って、Rails起動時に読み込まれるようにします。

# config/initializers/jwt.rb
require 'json_web_token'

続いて、ApplicationControllerでトークンのやりとりをするための処理を追加

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  attr_reader :current_user

  protected

  def authenticate_request!
    unless user_id_in_token?
      render json: { errors: ['Not Authenticated'] }, status: :unauthorized
      return
    end
    @current_user = User.find(auth_token[:user_id])
  rescue JWT::VerificationError, JWT::DecodeError
    render json: { errors: ['Not Authenticated'] }, status: :unauthorized
  end

  private

  def http_token
    @http_token ||= if request.headers['Authorization'].present?
                      request.headers['Authorization'].split(' ').last
                    end
  end

  def auth_token
    @auth_token ||= JsonWebToken.decode(http_token)
  end

  def user_id_in_token?
    http_token && auth_token && auth_token[:user_id].to_i
  end
end

最後に、トークンを作るためのAPIの受け口を作っていきます。

# app/controllers/authentication_controller.rb
class AuthenticationController < ApplicationController
  protect_from_forgery except: :authenticate_user
  def authenticate_user
    user = User.find_for_database_authentication(email: params[:email])
    if user.valid_password?(params[:password])
      render json: payload(user)
    else
      render json: {errors: ['Invalid Username/Password']}, status: :unauthorized
    end
  end

  private

  def payload(user)
    return nil unless user and user.id
    {
      auth_token: JsonWebToken.encode({user_id: user.id, exp: (Time.now + 2.week).to_i}),
      user: {id: user.id, email: user.email}
    }
  end

protect_from_forgery except: :authenticate_userを設定しないとAPIを叩いた時に、CORSエラーとなるため、設定しておきます。
※今回の実装だと、JWTは一度発行すると2週間は失効できなくなるので、payloadのexpをよしなに変更するか、別実装は考えておいた方が良いと思います。

config/routes.rbに下記を追加。

# config/routes.rb
post 'auth_user' => 'authentication#authenticate_user'

最後に、top_controllerにこれまで作ってきたactionをbefore_actionとして設定します。

# app/controllers/top_controller.rb
class TopController < ApplicationController
  before_action :authenticate_request!
  def index
    render json: {'logged_in' => true}
  end
end

これで実装完了です!

動作確認

実装が終わったので、意図した実装になっているか、確認します。

ブラウザからユーザー登録していくのはめんどくさいので、rails cでやっちゃいましょう。

$ rails c
User.create(email:'test@test.com', password:'changeme', password_confirmation:'changeme')

これで下準備は完了です。
rails sで起動して、curl http://localhost:3000/を実行してみましょう。

レスポンスとして{"errors":["Not Authenticated"]}が返ってくればOKです。

トークンの取得

curl -X POST -d email="test@test.com" -d password="changeme" http://localhost:3000/auth_user こちらを実行すると、レスポンスとしてトークンが返ってきます。

{"auth_token":"eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1MjYxOTA1MDB9.ABVCQGdzF3u2XcAp66vXZxeUy2dhsCuxsg88NsEdoFs","user":{"id":1,"email":"test@test.com"}}

トークンを使ってAPI実行

返ってきたトークンを使えば、認証が通るようになっているはずです。やってみましょう。

curl --header "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1MjYxOTA1MDB9.ABVCQGdzF3u2XcAp66vXZxeUy2dhsCuxsg88NsEdoFs" http://localhost:3000

{"logged_in":true}というjsonが返って来れば成功です!

まとめ

基本の形はこれで十分なのではないかと思います。

システムによって検討しなければならない点としては、先述の通り、トークンの失効するタイミングについてです。 この実装では2週間待たなければトークンは失効されず、ずっと有効なままです。そのため、仮に失効しなければならないことがあった場合に、ほぼ対応不能となるので、認証時の処理を少し変える必要があると思います。

とはいえ、RailsでJWTを簡単に扱えるgemがあったので、JWTを使った認証の仕組みをサクッと作ることができました。 deviseが入っているシステムに対しての連携もうまくいったので、良い学びになりました。

*1:Rails, JWT, deviseの説明は省きます

*2:Rails 5.2から秘密鍵周りが変わったので、5.1以下を使っている方は書き方が多少異なります。 おそらくRails.application.secrets.secret_key_baseとなるはず。

Redashのバージョンアップで消えたデータソースを復活させる方法

Redashのバージョン2系から3系にupgradeした際、DataSourceのプルダウンから色々なものが消えてしまいました(!)

  • BigQuery
  • Athena
  • Google Analytics
  • Google Spreadsheet

業務でも使っているデータソースだったので、これが使えなくなるとなかなか厳しいです。
今回は、Redashのバージョンアップで消えてしまったこれらのデータソースを復活させる方法を書きます。

続きを読む

AWS Database Migration Serviceを使ったデータベース統合を試してみました

概要

1月20, 21日と、会社の開発合宿に行ってきました。 取り組んだ内容はタイトルの通り、AWS Database Migration Service(以下DMS)を使ったデータベース統合です。

解決したかった問題

PIXTAではマイクロサービスアーキテクチャを採用しています。
また、サービスは大きく4つに分かれており、それぞれ独立したDBを持っています。

サービスの運用をしていると、しばしばデータ抽出依頼がくるのですが、その依頼内容の中には複数のDB間をまたいだテーブル結合が必要になってくる依頼もあります。
「Aサービスが持つDBのテーブルの条件 かつ Bサービスが持つDBのテーブルの条件を満たしたもののうち、CサービスのDBのテーブルの内容を抽出」といった感じです。

これまで、このようなデータ抽出依頼は下記のようなことをやっていました。

  1. CサービスのDBの値が取得できるように、外部キーをBサービスのDBから取得するクエリ
  2. そのBサービスの値を取得するための外部キーをAサービスのDBから取得するためのクエリ
  3. 最後にCサービスのDBに対しての本当に欲しかった値のクエリ

本当は、クエリ1つで済むはずなのに・・・。

そこで、今回の開発合宿はDMSを使ったデータベース統合に取り組むことにしました。

AWS Database Migration Serviceとは

簡単に言うとDB移行サービスです。
オンプレ / AWS内問わず、どちらかがAWS内にあれば、移行が可能です。
また、Oracle から AWS Auroraなど、異なるDBプラットフォーム間の移行もDMSがうまいことやってくれます。
さらに単純に移行だけではなく、MySQLとMariaDBとか、2DBを1つのDBに異種交配することも可能ですし、移行元のデータの差分更新を検知し、反映してくれます。ダウンタイムもありません。

f:id:watasihasitujidesu:20180122131648p:plain

AWS Database Migration Serviceでデータベースを統合するまでの手順

では具体的に処理の手順を説明していきます。 処理自体は3ステップなので簡単です。

  1. Replication Instanceを作成
  2. SourceとなるDBの設定と移行先のDBの設定
  3. データ移行タスクの設定

1. Replication Instanceを作成

DMSの本丸と言いますか、タスクを実行するために必要なインスタンスを作成します。

DMS > Replication Instances > Create replication instance

f:id:watasihasitujidesu:20180121121944p:plain

次に、インスタンスの情報を諸々設定していきます。 VPCに入れることが前提なので、ネットワーク関連でややハマるかもしれません。

f:id:watasihasitujidesu:20180121122141p:plain

今回、Production相当のRDSをターゲットに移行をしたのですが、インスタンスサイズに関してはあまりリソースは必要としていないようでした。 t2.smallでも十分でした。

2. SourceとなるDBの設定と移行先のDBの設定

続いて、Replication Instanceで処理する移行対象のDBの設定します。 データ移行元となるSourceと、データ移行先となるTargetの2つ設定が必要です。

Source Databaseの設定

DMS > Endpoints > Create endpointを押下します。

f:id:watasihasitujidesu:20180121122848p:plain

画像の赤枠でも囲ってあるServernameはホストを指定します。*1

次に、導通確認をします。

f:id:watasihasitujidesu:20180121123144p:plain

Run testが通れば最低限、Replication InstanceからDBに接続ができています。

Target Databaseの設定

Target Databaseも基本はSourceDBと同じです。
自分が勘違いしていた点は、TargetDBは、DMSの方で用意してくれるものかと思っていたのですが、DMSの挙動は、TargetDBに対して、SourceDBをコピーする動きになるので、TargetDBはあらかじめ用意する必要がありました。

3. データ移行タスクの設定

DMS > Tasks > Create taskを押下します。

f:id:watasihasitujidesu:20180121135011p:plain

基本的には、SourceDBとTargetDBを設定して、Replicationの設定を行うだけです。

Migration Typeは3種類あり、それぞれ、下記のような挙動となります。

Migration Type 用途
Migrate existing data 現在のSourceDBの情報のみをReplicationする
Migrate existing data and replicate ongoing changes 現在のSourceDBの情報と、SourceDBに変更が加わった場合に差分更新を行う
Replicate data changes only SourceDBに変更が加わった場合にその変わったデータのみを追加する

また、後続の設定項目である、Task Settings の Enable loggingにチェックを行うとDMSで行う処理のログがCloudWatchに吐かれます。
ログは、通常処理の状況ではなく、Errorになった場合の情報も吐かれるので、チェックをしておくのをオススメします。

エラー状況はStatusやTables errorsの状況でわかります。 下記の図はエラーが76件出ているので、ページ下部のLogsからCloudWatchのリンクを開き、確認するとログを見ることができるはずです。

f:id:watasihasitujidesu:20180122131829p:plain

タスクは実行されれば、テーブルごとに1万件ずつinsertが走っていきます。 また、最大8並列で実行されます*2

何事もなければ、TaskのStatusがLoad compliteとなり、レプリケーションは完了です。

まとめ

これまで、分割されたDBからデータ抽出をするのに、何回もクエリを実行したり、DBを統合するのにスクリプトなどを書く必要がありましたが、AWS DMSを使えば、マネジメントコンソール上からの設定で手軽にDB統合することができました。 また、データ統合だけではなく、その逆の統合されたDBを再度分割することも可能です。

DMSは当初、オンプレのDBを移行するだけのものだと思っていましたが、このような使い方ができるには驚きました。

ただ、200GB以上のDBをレプリケーションしようとした場合、移行にかかる時間が数時間かかっていたので、速度改善の余地は十分にありそうです。

*1:当初AWSのサービスだし、RDSのインスタンス名の入力かと思いきやそうではありませんでした

*2:TargetDBがRedshift以外の場合

macOS Sierraでssh時にパスフレーズを聞かれないようにする方法

概要

PCを変えてsshしたときにいつもパスフレーズを求められる => 設定する
って作業をしており、毎回どうやんだっけ?ってなるので、自分のためにメモ。

手順

1. 秘密鍵の登録

ssh-add -K ~/.ssh/id_rsa

2. macOS標準アプリのAutomatorの登録

2-1. Automatorを開く
f:id:watasihasitujidesu:20180105134357p:plain

2-2. アプリケーションを選択
f:id:watasihasitujidesu:20180105134428p:plain

2-3. シェルスクリプトを実行を選択し、コマンドを打ち込み、保存 f:id:watasihasitujidesu:20180105134555p:plain
コマンドは下記の通り。 ssh-add -A

3. ログイン時にAutomatorが実行されるように設定

3-1. システム環境設定 > ユーザーとグループを選択 f:id:watasihasitujidesu:20180105134710p:plain

3-2. + ボタンを押下し、2で作ったスクリプトを選択 f:id:watasihasitujidesu:20180105134846p:plain

まとめ

PC起動時にssh-key -Aが実行されるように、Automatorでスクリプトを作成し、そのスクリプトをログイン項目として設定。
ssh-key -Aをした場合に、ssh agentに秘密鍵が登録されていなければ、パスフレーズを聞かれてしまうので、事前にssh-add -K ~/.ssh/id_rsaで登録をした
という感じ。

crontabで使用するエディタを変更する方法

概要

新しくサーバーを立てた際、cron設定しようと思い、crontab -eをした時に、
あ、あれ、、何このエディタ....
って感じになり、エディタの設定を毎回変更するも、そのコマンド自体を覚えていないという由々しき状態になるので、メモとして残しておく

コマンド

$ select-editor

こんだけ!!!!!

そしたら多分、

Select an editor. To change later, run 'select-editor'.
1. /bin/ed
2. /bin/nano <---- easiest
3. /usr/bin/vim.basic
4. /usr/bin/vim.tiny

Choose 1-4 [2]:

こうなるから、いつも使ってるvimを選択*1

*1:easiestってなんやねん!!!

「伝説の外資トップが説く リーダーの教科書」を読みました。

概要

会社の輪読会で「伝説の外資トップが説く リーダーの教科書」が題材となったので、読んだ感想をまとめます。

内容

この本は、下記の通り4章に分かれています。

  • 第1章 これからリーダーになる人へ 上司の心得
  • 第2章 リーダーとして歩き始めた人へ 上司としての認識
  • 第3章 リーダーシップをさらに磨きたい人へ 上司のスキル
  • 第4章 選ばれたリーダーをめざす人へ 上司の役割

見てわかるとおり、これからリーダーになる人(目指している人)〜現在リーダーの人を対象に、 心構え、リーダーのあるべき姿、スキル、役割について、一章につき14項目で述べられています。

冒頭で会社や組織は人で決まると書かれており、書いてある内容のほとんどは、 個人としての振る舞いや心構え、部下や組織の考え方など、人にフォーカスした内容でした。

感想

この本は、ビジネスパーソンとしての普遍的なスキル(マインド、考え方)を浅く広く紹介する内容でした。 そのため、1つ1つの項目についてそこまで深堀はせず、網羅的に重要であることが書かれています。

最初に読んだ感想は、だいたいが「それくらいのことはわかっている」「多くの内容は知っていた」という内容でした。 しかし、実践し、結果に残しているとは言えないと感じたのと、まだまだできていない部分はまだまだ多くあると痛感しました。
本の内容を何も考えずに単純に読んでいると「ほぇ〜〜〜、これが重要なんや〜(ハナホジー)」で終わると思います。

しかし、現在リーダーとして動いている人が、自分と組織の状態について

  • これまでしてきたこと
  • 現在行なっていること
  • これから行うべきこと

これら過去〜現在〜未来を頭に浮かべて、過去の経験や今巻き起こっている課題感と具体的なメンバーの顔、これから実行すべき課題を深く考えながら読むと、懐かしい気持ちになる一方、まだまだ自己研鑽が必要であることを再認識させられると思います。

そのため、学生や入社1年目といった、組織で働いた経験がない方にとっては、

  • 経験値の低さ
  • 視野の低さ
  • 視座の低さ
  • 自己認識能力の未発達
  • 客観的評価をする力の未発達

といった様々な理由から、深い洞察は得られないと思います。

逆に、自分の場合は、 ポテンシャル => メインプレイヤー => リーダー => マネージャー と、各役割を経験してきたので読み応えがありました。
本に書かれているスキルやマインドをそのまま学ぶというよりは、自分の経験や価値観などから洞察と学びを得るといった感じです。
数年後に読み返した時には、違った学びを得られるのではないかと思います。

その他個人的なことについて

人は誰しも褒められ、承認されることに喜びを感じる。
褒めと叱りの比重は8褒め2叱り

という記述があります。 ただ、自分は超絶ストイック人間なので、自分に対しては1褒め9叱りです。
また、ついつい「自分ができるんだから他の人もできるのでは?」という錯覚に陥ってしまうことが度々あり、 相手の力量やどのような人かを考えずに、過度な要求や、異常に高い期待値で結果を求めてしまうことが多々あり、クリティカルな問題だと痛感しました。

また、第1章はこれからリーダーになる人に向けた内容なのですが、要所要所に、自分(やチーム)がこれまで実行してきたことが書かれていて、懐かしく感じると共に、自分の行動は間違いではなかったと感じました。
とはいえ、自分のストイックさから来るスキル習得に対しての再現性の低さは頭が痛いだと感じました。
先述の通り、この本をポテンシャルに渡すだけだと何の学びも得られないまま読み終えてしまう可能性が高いので、

  • 本の内容
  • 自分の過去の経験や考え方
  • 行動を起こして実践できるようになるまでの道筋

これらを丁寧に共に理解しながら接していく必要があると感じました。

リーダー以上のポジションにいる方にとっては、深い洞察を得られると思うので、ぜひ一読を!

【AWS CLI】OpsWorksAgentのインストーラがAgent installation failed.になる場合の対処方法

概要

AWS 既存のEC2インスタンスをOpsWorksに登録する際、インストーラーを実行するのですが、Agent installation failed.になってしまいハマったので、対処方法を書きます。

環境

$ cat /etc/os-release
NAME="Ubuntu"
VERSION="14.04.5 LTS, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04.5 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"

実行したコマンド

$ aws opsworks register --infrastructure-class ec2 --region ap-northeast-1 --stack-id {{stackid}} --local
...
...
[Wed, 04 Oct 2017 07:35:14 +0000] opsworks-init: Starting the installer
/tmp/opsworks-agent-installer.41mxNiW1JxzOdN0t/opsworks-agent-installer/opsworks-agent/lib/bootstrap/installer.rb:74:in `install_instance_agent': /opt/aws/opsworks/releases/20170402215230_4023-20170402215230 already exists and is current - aborting. (RuntimeError)
    from /tmp/opsworks-agent-installer.41mxNiW1JxzOdN0t/opsworks-agent-installer/opsworks-agent/lib/bootstrap/instance_agent_registration_installer.rb:27:in `block in run'
    from /tmp/opsworks-agent-installer.41mxNiW1JxzOdN0t/opsworks-agent-installer/opsworks-agent/lib/bootstrap/log.rb:96:in `measure'
    from /tmp/opsworks-agent-installer.41mxNiW1JxzOdN0t/opsworks-agent-installer/opsworks-agent/lib/bootstrap/instance_agent_registration_installer.rb:27:in `run'
    from /tmp/opsworks-agent-installer.41mxNiW1JxzOdN0t/opsworks-agent-installer/opsworks-agent/lib/bootstrap/instance_agent_registration_installer.rb:11:in `run'
    from /tmp/opsworks-agent-installer.41mxNiW1JxzOdN0t/opsworks-agent-installer/opsworks-agent/bin/opsworks-agent-registration-installer.rb:8:in `<main>'
[Wed, 04 Oct 2017 07:35:19 +0000] opsworks-init: Agent installation failed.
[Wed, 04 Oct 2017 07:35:19 +0000] opsworks-init: Please verify the log files found under /var/log/aws/opsworks and submit findings to AWS Support.

解決方法

調べてみると、インスタンスに既にOpsWorksAgentが入っていた場合、それを削除しなければならないようでした。

docs.aws.amazon.com

ドキュメントを確認し、下記コマンドを実行し、既存のOpsWorksエージェントを削除。

sudo /etc/init.d/monit stop
sudo /etc/init.d/opsworks-agent stop
sudo rm -rf /etc/aws/opsworks/ /opt/aws/opsworks/ /var/log/aws/opsworks/ /var/lib/aws/opsworks/ /etc/monit.d/opsworks-agent.monitrc /etc/monit/conf.d/opsworks-agent.monitrc /var/lib/cloud/ /var/chef /opt/chef /etc/chef
sudo apt-get -y remove chef
sudo dpkg -r opsworks-agent-ruby

再度インストールコマンドを実行し、無事にsuccessになりました。

$ aws opsworks register --infrastructure-class ec2 --region ap-northeast-1 --stack-id {{stackid}} --local

技術誌に寄稿しました!テーマ選定、執筆の流れと使ったツールの紹介

概要

技術評論社から発刊されているSoftware Designの2017年10月号に「サーバーレスで実現!素材集サービスを効率化した自動画像管理システムに学ぶ」というタイトルで寄稿しました!
この記事では、記事執筆にあたり、テーマ選定、記事執筆の全体的な流れ、使ったツールなどをまとめます。 gihyo.jp

テーマ選定: 想定読者に何を伝えるのか?

最初に意識したことは、想定読者を意識して、何をどのように伝えるべきかを強烈に考えさせられるということです。

  • 想定読者は初心者~上級者のどの層か
  • 読者は記事を読んだあとに何を得るのか
  • 記事を書く上で前提は何か
  • なぜこの記事を書き、何を伝えるのか

などなどです。上記の中でも個人的に、読者は記事を読んだあとに何を得るのかが、最も重要なのだと思います。
ここがしっかり固まっていると、記事執筆中に内容がブレることなくなると思います。記事の大枠などを考える前に、まずはここを抑えると良いと思います。

また、内容については、今回は依頼されたページ数は10ページでした。文字数にすると15,000文字にもなるので、少ししか触ったことがない技術について書くと中身が薄くなってしまう可能性があります。 そのため、寄稿する際は、実際に自分が経験し、ハマって、深く理解した内容でなければ、何も伝わらない記事になってしまうと思いました。

執筆の手順

読者に何を伝えるかが決まった後の執筆の手順は下記の通りです。

  1. 編集の方にコンタクト/アポを取る*1
  2. 自分に何が書けるかを伝える。ここで想定読者と題材を伝えるとGood
  3. 編集の方との記事の方向性の検討
  4. 原稿執筆
  5. 脱稿*2
  6. ゲラ*3受け取りと修正依頼
  7. 二校、三校....*4
  8. 校了*5
  9. 見本受け取り
  10. 出版

先述の想定読者と伝えることが決まって入れば、1~2はスムーズに進みます。
この手順の中で最も力がいる作業は3~5です。原稿執筆と最初の修正依頼です。

ブログと同じ感覚やろ(ハナホジー)って感じでやってたら夏休みの宿題状態になりました。
もともとブログで文章を書く方だったのと、記事の大枠(h1, h2レベル)は決まっていたので気楽に構えていたのですが、細かな文言や言葉の意味を調べたりしなければならないのは辛い作業でした。
なかでも、記事の導入部分の枕詞に何日も費やしました。記事の"はじめに"の部分は、読み手がこの先の記事を読みたくなる内容にしなければならないと感じたのと、これまでそのような文を書いたことがなかったので、そもそも書き方がわかりませんでした。
色々な本を参考にしてなんとか書けましたが、個人的には一番大変でした。

また、最初の修正は、原稿の文が悪ければ悪いほど、指摘が多くなります。
30項目くらい指摘があって、死にそうになりました。さらに、修正依頼を受けてから返信するまでの期間が結構短いです。数日というレベルだったりします。
本業の傍ら、記事を書いていると体力的しんどくなってきます。

ただ、ここを乗り切ると修正も軽微なものになってきますし、PDFの見た目もだんだん雑誌の記事らしくなってくるので、達成感も出てきます。

執筆に使ったツール類

Google Document

エンジニアの方はgithubで書かれているような印象(?)ですが、普通にGoogle Documentを使いました。 理由は下記の通りです。

  • 編集者とデザイナーの方はgithubが使えない可能性が高い
  • 修正が容易
  • コメントでやりとりできる
  • なんなら提案モードで直接修正してもらう

commitしてpushする手間もないし、個人的にはGoogle Documentで必要十分かと思います。
文自体はMarkdownで書きました。h1, h2, h3(#とか)とコード記述の方法(```で囲う)を最初に「こう書きます〜」っと定義しておけば、それを見て良い感じに(!)やっていただけます。
また、記事中に差し込む画像や図も、自分で凝った図を書くというよりは、ざっくり書いた図を基に「こうしてほしいです」とコメントをすれば、それを見て対応していただけたりします。餅は餅屋にですね!

Microsoft Word

Google Document使ってるのになんでワード!?ってなるかもしれませんが、Wordは文章の校閲に使います。
Wordの校閲機能は結構優秀で、ですます/である調の修正、助詞の欠落、常用漢字の使用などを注意してくれます。

これらは、編集者の方と打ち合わせした際に、結構細かく指定されます
また、自分はこういう作業が得意ではないので、機械的にやりました。*6

文章校閲の機能は書きの通り設定します。

メニューバー > Word > 環境設定 > 文章校正 > 文章校正 設定... f:id:watasihasitujidesu:20170926161401p:plain

設定ボタンを押すと、文法とスタイルの規則の設定 + 必要条件で、機械的にチェックすることが可能です。 プログラムと同様にtextlintを行うって感じですね。

まとめ

今回の記事では、原稿を書くまでのテーマ作りの話、校了までの大きな流れ、使ったツールの紹介でした。
校正などは、技術評論社の方に対応していただいたので、ツールに関しては最低限のもので済みました。*7
参考になれば幸いです。

自分の人生の目標の一つとして、単著で書籍を出版することがあります。
今回は雑誌でしたが、自分の寄稿した記事が掲載されるのは貴重な体験でした。
また、15,000文字書く大変さと、そこから推論できる書籍の原稿を書く場合の大変さもわかり、今後、一層精進する必要があると思いました。 また機会があれば積極的に書いていきたいと思います!

f:id:watasihasitujidesu:20170922131525j:plain f:id:watasihasitujidesu:20170922131616j:plain

*1:ここが一番難易度が高いか...

*2:原稿を編集の方に渡すこと

*3:分量やレイアウト調整のため、原稿をとりあえずPDFにしたもの

*4:文言などの調整の繰り返し

*5:校正が完了し,印刷しても差し支えない状態になること

*6:仕事でも使える技だと思います

*7:調べたらもっと便利そうなものがありそう。

【第三回】成田空港で一人開発合宿をしてきました

概要

雑誌の記事の執筆がいよいよ締め切りに迫ってきたので、一気に書き上げるべく、合宿を行いました。 開発合宿というより執筆合宿ですね。

合宿候補地はどんな感じで決めたの?

第一回目の合宿地が羽田空港で、だいぶ快適だったので、成田空港もきっとええとこやろ!!という軽い気持ちで決めました。 いつものように判断項目をあげると

  • 一人で活動できるか
  • すぐに充電が確保できるか
  • すぐにご飯を調達できるか
  • ネットワークが安定していて、かつ十分に早いか
  • 一泊二日を想定した場合、宿泊料は十分安いか(10,000円以下)
  • 作業時間を確保できるか(遠すぎないか?)
  • 楽しそうか

基本的には羽田と同じノリで楽しそうか?ってところを主眼に決めました。ぜんぜんPDCA回せてねぇ!!!
ただ、快速電車もあるし、課金アイテムの成田エクスプレス使えば、それなりに近い(作業時間を確保できる)だろうということと、 成田空港に併設されているカプセルホテルも早割を使えば5,000円だったので、リーズナブルにキメられるだろうということで、成田にしました。 電源もご飯もネットワークも、まぁ空港だから大丈夫でしょっ!って感じです。

良かった点

非日常感

f:id:watasihasitujidesu:20170722183645j:plainf:id:watasihasitujidesu:20170722184005j:plain
ばーん

f:id:watasihasitujidesu:20170722184425j:plainf:id:watasihasitujidesu:20170722185557j:plain
なんか綺麗だったから撮った。

f:id:watasihasitujidesu:20170722185634j:plain
第二ターミナルから第三ターミナルの連絡通路。
地面がクッションみたいになってて、膝に水がたまらなくてすむ。

f:id:watasihasitujidesu:20170723090129j:plain

空港!!!
って感じですね!めっちゃ海外旅行にいきたくなりました!

ご飯がうまい

さすが成田空港、食べることに困らない。

f:id:watasihasitujidesu:20170722190731j:plain
赤酢を使ったシャリがめちゃくちゃうまかった。いい仕事してますね〜〜
全部で10貫くらい食べましたが、最も美味しかったのは、カツオの上に乗っていた、玉ねぎの醤油漬けでした。

f:id:watasihasitujidesu:20170722232413j:plain
まさかの吉野家で吉呑みするという。 が、安くて、久しぶりにジャンクな肉を食べて普通に満足!

f:id:watasihasitujidesu:20170723091305j:plain
朝食にお茶漬け。
鯛茶漬けと冷汁というお茶漬けをおかずにお茶漬けを食べるお茶漬け定食にしてしまった! 出汁が本体だった。

f:id:watasihasitujidesu:20170723143743j:plain
とみ田にいったことなかったから中華そばとみ田にきた。 タレご飯美味しかった!タレでご飯食べたい!

f:id:watasihasitujidesu:20170722215115j:plain
空水という、空港限定の水! 500mlで180円!!たけぇ!!

交通

f:id:watasihasitujidesu:20170723150116j:plainf:id:watasihasitujidesu:20170723150806j:plainf:id:watasihasitujidesu:20170723151557j:plain
行きは成田スカイラインで行きました。若干遠かったけど、1.5時間で着いたからさほど気にならないレベル。 帰りは成田エクスプレス!! 座席に電源あるし、全席指定席だし、割とゆったりしていました! もしかしたら帰りの1時間の作業が最も捗ったかも?

悪かった点

作業環境

作業環境はぶっちゃけ最悪でした。 歩けど歩けど、電源は無い。椅子があまりない。なんか歩いただけで終わった感じでした。 第一ターミナルは空港感はあるけど、電源がない。第二ターミナルは電源はあるが、空港感がない。第三ターミナルは壊滅的。という状況でした。どのターミナルも良いところがなく、今一歩でした。楽しさはありませんでした。

f:id:watasihasitujidesu:20170723112045j:plainf:id:watasihasitujidesu:20170723092526j:plainf:id:watasihasitujidesu:20170722202316j:plainf:id:watasihasitujidesu:20170723134024j:plain
これが第一ターミナルと展望デッキからの写真。 電源は見つけられたのは唯一ここだけ…。死ぬほど探して死ぬほど歩いた…。 (あれ、写真でみると良さげに思える…)

f:id:watasihasitujidesu:20170722230832j:plain
第二ターミナルの普通の椅子。

第三ターミナルは何もなかったから写真なし!!

とはいえ、第二ターミナル1Fにある、北ウェイティングエリアは穴場っぽかったです。 人は少ないし、電源もあるし、畳あるし、椅子もあるし。 ただ、なんというか、収容所のような感じがして、個人的には好きになれませんでした。

f:id:watasihasitujidesu:20170722203603j:plainf:id:watasihasitujidesu:20170722203548j:plain
北ウェイティングエリア。綺麗っちゃ綺麗。

宿泊施設

f:id:watasihasitujidesu:20170722194724j:plainf:id:watasihasitujidesu:20170723195742j:plain
9h ninehours(ナインアワーズ)というカプセルホテルに泊まりました。
人生初のカプセルホテルだったんですが、カーテン一枚で仕切られているのは若干怖かったんですが、数十分居れば慣れました。 ベッドはエアウィーブでした。高反発ベッドが合わなかったのか、背中が痛くなりました。 あと、近くの宿泊客のいびきが猛烈でした。耳栓あってよかった。。。 コンパクトな宿泊施設だったので閉塞感がありました。 唯一良かったのは、シャワーがグローエだったことです。それ以外はちょっと微妙でした。たぶん、2回目は行きません。

まとめ

成田空港、もう二度と行きません。 電源ないわ、作業スペースもないわ、宿泊施設微妙だし、食事はまぁ、よかったけど、選択肢が少ない。 空港だから当たり前なんですが、羽田空港と比べると歴然とした差がありました。 第三回目にして、ついに微妙な合宿になってしまいました…。
振り返ると、羽田空港って最高なんじゃないかって思えてきました。次はどこの空港に行こうかな!!!!

AWS Rekognitionで画像内の物体名を取得する方法

概要

AWS re:Invent 2016で発表された新機能の中にAWS Rekognitionという画像内の物体、シーン、および顔を検出するサービスが発表されました。
今更感ありますが、Rekognitionで画像内の物体名を取得する方法を書きます。

Lambdaでの処理

'use strict';
const AWS = require("aws-sdk");
AWS.config.region = 'us-west-2';
AWS.config.apiVersions = { 
  rekognition: '2016-06-27',
};
const rekognition = new AWS.Rekognition();

module.exports.handle = (event, context, callback) => {

  var params = { 
    Image: {
      S3Object: {
        Bucket: "BUCKET_NAME",
        Name: "OBJECT_KEY"
      }   
    }   
  };  
  rekognition.detectLabels(params, function(err, data) {
    if (err) console.log(err, err.stack); // an error occurred
    else     console.log(data);           // successful response
  });
};

こんだけ!!
基本的にはS3のバケットとオブジェクトのキーをparamsに指定するだけです。
処理速度は2秒かかることもあるので、バッチ処理以外ではあまり使えなさそうです。
また、当たり前ですが、固有名詞は出てきませんし、返却されるラベルは全て英語です。
そのため、例えば、東京タワーの画像を解析した場合BuildingとかTowerというラベルが返却されます。

注意事項

Rekognitionは使用できるリージョンがとても少ないです。記事投稿時点では下記の3リージョンでしか使用できません。

  • us-east-1
  • us-west-2
  • eu-west-2

Tokyoリージョンに来ることを期待!

まとめ

たった数十行で画像内の物体を検出できるとは、未来に生きている気がしました。

Rekognitionは他にも、人物の顔を認識して傾きとかを検出したり、人物画像同士の比較をし、同一人物かを検出したり、アダルト画像か検出したりできます。価格も100万枚まで1,000枚あたり1.00USDだったり、低価格なのも魅力です。

一方GCPのVision APIはAWSと比較して高機能である代わりに1,000ユニットあたり$1.50 と多少割高になります。

Rekognitionに存在しない機能を使いたい時はGCPを使う感じですかね。

【ServerlessFramework】lambdaでgmを使って画像加工をする方法

概要

ServerlessFrameworkにおいて、Lambdaで画像加工をする場合、Imagemagickかgm(GraphicsMagick)を使用することになります。
AWS公式ドキュメントではgmを使用しているので、今回はgmを使用してサムネイル作成処理について、書こうと思います。

ハマりどころ

gmがrequireできなくてハマった

lambdaのコード中に

const async = require('async');
const gm = require('gm').subClass({ imageMagick: true });

といった感じでrequireしなければならないのですが、単純にこのように記述してデプロイしても、 module not found的な感じでデプロイしてすぐにエラーとなってしまいました。

lambdaをデプロイするとAWSの中で既にmoduleが用意されているものだと勘違いしていました。

改めてAWS公式ドキュメントを読むと.zipを作成しなければならないので、手順に従って回避します。

デプロイパッケージの作成

次にハマったのが、ServerlessFrameworkにおいて、デプロイパッケージの作成です。 AWS公式ドキュメントでは下記の通りにファイルを配置し、.zipを作成せよ。とあります。

CreateThumbnail.js
/node_modules/gm
/node_modules/async

上記の通り.zipを作るのは良いんですが、ServerlessFrameworkの枠組みからいきなり外れてしまうのが問題でした。*1

ただ、sls deploy後の.zipはS3の所定のバケットに配置されることに気づきました。 つまり、ServerlessFrameworkも実は単純を.zipを作ってlambdaにデプロイしているのだとわかりました。

そのため、単純にlambdaの処理が記述されているフォルダの直下で下記を実行すれば/node_modules/が出来上がります。

  • package.jsonを用意
  • yarn add async gm

その後にいつものようにsls deployをすれば完了です。

あとは
aws lambda gm とかで検索するとgmでの処理についての記事が出て来るのでそれを参考に!

まとめ

当たり前ですが、まずは、AWSのドキュメントをしっかり読むこと!!これが重要だと感じました。
他のブログではサムネイル作成処理については書かれているのですが、
ServerlessFrameworkと組み合わせた場合の方法は書かれていない印象だったので、この記事が参考になればと思います。

*1:単純にsls deploy だけで完結しなくなってしまう

RedashでAWS DynamoDBに対してDQLを発行しデータ取得する方法

概要

表題の通りです。 RedashでDynamoDBからデータ取得する場合DQLという、SQLライクな書き方でデータを取得することになります。
今回はDynamoのIndexを指定して絞り込みを行う際に、ちょいハマりしたので、メモとして記録します。

AWS DynamoDBの設定

テーブル構造

  • Table name: test_tables
  • Primary partition key: partition_id (Number)
  • Primary sort key: sort_id (Number)

インデックス

  • Name: partition_id-index
  • PartitionKey: partition_id (Number)

DQLの書き方

test_tablespartition_idで絞り込んでデータ抽出したい場合、DQLでは下記の書き方となります。

SELECT *
FROM test_tables
WHERE partition_id = 60515 USING partition_id-index; # USINGでindex名を指定することが重要

WHERE句で属性名だけで条件指定したいところですが、DynamoDBの特性上、インデックスを指定しなければなりません。
そのため、上記のように、partition_idはpartition_id-indexというインデックスやで〜
というように明示させる必要があります。

もし、USINGを書かないと下記エラーが発生します。
No index specified with USING <index>, but multiple possibilities for query: TABLE, partition_id-index

自前で立てたRedash(0.12.0+b2449)を最新1.0.3にupgradeする方法

概要

Redashが0.12.0の時に、AWS AMIから立ち上げたのですが、
この度、メジャーバージョンアップしたので、手動でupgradeした時のメモです。

手順もなにも、ドキュメントがあるのでその通りにやるだけですが、 コマンド実行する際に設定を変更する必要があったので、その説明をします。

手順

ドキュメントの通りwgetし、実行権限を付与。

wget https://raw.githubusercontent.com/getredash/redash/master/bin/upgrade
chmod +x upgrade

その後、処理を走らせます。

sudo ./upgrade

すると、こんなメッセージが出ます。

Before upgrading to this version, please make sure to do the following changes to your /opt/redash/.env file:
    
1. If you have local PostreSQL database, you will need to update the URL from `postgresql://redash` to `postgresql:///redash`.
2. Remove the `REDASH_STATIC_ASSETS_PATH` definition.

Make sure to complete these changes before doing the actual upgrade.

この変更をしなければ、DBが見当たらない旨のエラーが吐かれてアップグレードできないので、 /opt/redash/.envを開き、下記の通り修正します。

  1. postgresql://redashpostgresql:///redashに修正。*1
  2. REDASH_STATIC_ASSETS_PATHを消す。

まとめ

ドキュメントには書かれておらず、実行時に注意してくる感じなので、
英語読めない><
とりあえずy ><
ってやってると見落としまうので、上記修正は忘れず対応しましょう!

*1:/が一つ多くなる

【ServerlessFramework】DynamoDB Streamsでデータの更新をトリガーにLambdaを動かす方法。

概要

表題の通り、DynamoDB Streamsでデータの更新をトリガーにLambdaを動かす方法について説明します。

AWSマネジメントコンソール上でのDynamoDB Streamsの有効化

まず、AWSマネジメントコンソールでStreamsを有効化する必要があります。 DynamoDB > Tables > テーブル選択 > Overview > Stream details > ManageStreamを選択

これを選択すると、ストリームを経由して渡されるデータ構造を選択できるラジオボタンが表示されます。 それぞれの意味は下記の通りです。

  • [Keys only] — 変更された項目のキー属性のみ取得。
  • [New image] — 変更後のデータのみ取得。
  • [Old image] — 変更前のデータのみ取得。
  • [New and old images] — 変更後、変更前のデータの両方取得。

あとは作成すれば、ARNが表示されます。

serverless.ymlの設定

次にserverless.ymlに設定を書き、Lambdaと紐づける必要があります。

provider:
  name: aws
  runtime: nodejs6.10
  stage: dev
  region: us-west-2
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - dynamodb:DescribeStream
        - dynamodb:GetRecords
        - dynamodb:GetShardIterator
        - dynamodb:ListStreams
      Resource:
        - "${AWSマネジメントコンソール上に表示されているARN}"
functions:
  dynamo_stream:
    handler: handler.dynamo_stream
    events:
      - stream:
          arn: "${AWSマネジメントコンソール上に表示されているARN}"
          batchSize: 1
          startingPosition: TRIM_HORIZON
          enabled: true

functions.dynamo_stream.events.stream内の各項目は

batchSize: 一度に処理するストリームの数を表します。1~10000まで指定できます。1以上の値を指定した場合、Lambdaの処理の中でループで処理をする必要があります。 startingPosition: 処理開始するストリームの位置を指定します。最新から取得か最古から取得かのどちらかを指定できます。LATEST | TRIM_HORIZON enabled true: 有効 / 無効を指定 true | false

また、eventの設定以外には、IAMの設定も必要となります。 上記の設定項目は最低限の設定です。これを設定しないとsls deploy -vをした時に下記エラーに見舞われます。

  Serverless Error ---------------------------------------
 
     An error occurred while provisioning your stack: DynamoUnderscorestreamUnderscoreproxyEventSourceMappingDynamodbDevitems
     - Cannot access stream AWSマネジメントコンソール上に表示されているARN
     Please ensure the role can perform the GetRecords, GetShardIterator,
     DescribeStream, and ListStreams Actions on your stream
     in IAM..
 

取得できるJSON

{
  "Records": [
    {
      "eventID": "78h9r97gawegaj7ddnga6e6w",
      "eventName": "MODIFY",
      "eventVersion": "1.1",
      "eventSource": "aws:dynamodb",
      "awsRegion": "us-west-2",
      "dynamodb": {
        "ApproximateCreationDateTime": 1492654321,
        "Keys": {
          "id": {
            "N": "123456789"
          }
        },
        "NewImage": {
          "column_a": {
            "N": "2"
          }
        },
        "OldImage": {
          "column_a": {
            "N": "1"
          }
        },
        "SequenceNumber": "405760000000000123456789",
        "SizeBytes": 123,
        "StreamViewType": "NEW_AND_OLD_IMAGES"
      },
      "eventSourceARN": "AWSマネジメントコンソール上に表示されているARN"
    }
  ]
}

しっかり新旧両方のデータが取得できていますね。

解決できなかった点

  1. ServerlessFrameworkだけで完結できない。 説明の通り、AWS マネジメントコンソールでストリームを追加し、それを元にserverless.ymlを設定しなければなりません。 deploy時に失敗するから気づけるとは思うのですが、漏れが発生する気しかしないです。

  2. 設定できるストリームが1テーブルにつき1つ 当初、レコード追加時のストリーム、Aカラムを更新したときのストリーム….といったように、細かく設定できると思っていたのですが、 試して見た結果、「何かしらの変更が加えられたらJSONに新旧両方のデータを詰めて渡すからよしなにやってね」って感じでした。 DynamoDBにカラムが新規作成されたらLambdaAを、更新の時はLambdaBを実行しようとした場合、テンプレートメソッドパターンや、プロキシパターンなり使わないと収集つかなくなりそうな印象を受けました。

まとめ

  1. AWSマネジメントコンソールでStreamsを有効化
  2. serverless.ymlに設定

この2ステップだけでDynamoDBストリームを受け取りLambdaに渡せるのはやはり便利です。 ただ、マネコンとserverless.ymlの両方に設定を加えなければならないのは漏れが発生しそうなので、もう一声といったところでしょうか。