Docker imageをbuildしてAWS ECRにpushする

最近ECSを勉強中です。
自作アプリをデプロイしたいのですが、とりあえずECRにDocker imageをpushする必要があるようなので、push方法について調べてみました。

AWS ECRにリポジトリの作成

ECRコンソールから、imageをpushするリポジトリを作成します。

f:id:yuya_tequila:20190303124344p:plain

作成すると、以下のようにリポジトリ一覧に表示されます。
ここで表示されているURIは、のちほどpushコマンドを実行する際に使用します。

f:id:yuya_tequila:20190303125022p:plain

Docker CLIの認証

imageのpushにはDocker CLIを使用します。
そのために、作成したレジストリに対して、Docker CLIを認証します。

aws ecr get-login --no-include-email --region ap-northeast-1


上記のコマンドを実行すると、以下のようなレスポンスが返ってきます。

docker login -u AWS -p password https://aws_account_id.dkr.ecr.ap-northeast-1.amazonaws.com


このレスポンスが、AWS ECRに対してDocker CLIを認証するためのコマンドです。
なのでそのまま実行しましょう。

pushしたいDocker imageの作成

ECRにpushしたいDocker imageを、以下のコマンドで作成します。
コマンドはベースとなるDockerfileのあるディレクトリで実行します。

docker build -t my_app_name


その後、pushするためにタグをつけます。

docker tag my_app_name:latest <YOUR ECR URI>/my_app_name:latest

ECRへのpush

以下のコマンドを実行することで、作成したリポジトリにimageをpushできます。

docker push <YOUR ECR URI>/my_app_name:latest


リポジトリの中を見てみると、pushされていることが確認できます。

f:id:yuya_tequila:20190303143424p:plain
これでDocker imageのpushは完了です。

ActiveRecordのorderメソッドに引数を複数渡す場合について

ActiveRecordのorderメソッドに引数を複数渡せることは有名だと思います。
ですが「複数の値を渡すとどのようにソートされるのか」について具体的に言及している記事が少ないように感じたので、簡単な例を挙げてまとめてみたいと思います。
(自分の検索能力が低いだけかも知れませんが)

検証準備

複数のカラムを持つUserテーブルを定義して、以下のレコードを格納します。

id name gender role
1 Alice 2 1
2 Bob 1 1
3 Carol 2 2
4 Dave 1 2


カラムはそれぞれ、名前、性別、ロールです。それぞれ単なる検証用の値なので、深い意味はありません。

そしてUserモデルの中に、以下のメソッドを定義します。

class User < ApplicationRecord
  def print
    puts "id:#{id} name:#{name} gender:#{gender} role:#{role}"
  end
end


これで準備は完了です。

引数が一つの場合

まずは簡単な例からやってみましょう。nameをorderの引数にする場合です。

irb(main):001:0> users = User.all.order(:name)
irb(main):002:0> users.each {|user| user.print}
id:1 name:Alice gender:2 role:1
id:2 name:Bob gender:1 role:1
id:3 name:Carol gender:2 role:2
id:4 name:Dave gender:1 role:2


ユーザーの名前はアルファベット順になっているので、このような結果になります。genderやroleを引数にした場合は以下のとおりです。

irb(main):001:0> users = User.all.order(:gender)
irb(main):002:0> users.each {|user| user.print}
id:2 name:Bob gender:1 role:1
id:4 name:Dave gender:1 role:2
id:1 name:Alice gender:2 role:1
id:3 name:Carol gender:2 role:2

irb(main):003:0> users = User.all.order(:role)
irb(main):004:0> users.each {|user| user.print}
id:1 name:Alice gender:2 role:1
id:2 name:Bob gender:1 role:1
id:3 name:Carol gender:2 role:2
id:4 name:Dave gender:1 role:2


gender, roleの場合、それぞれが1, 2という順で並びます。

引数が2つの場合

ここからが本題です。引数が複数渡された場合、どのような順序になるのでしょう。

# 性別昇順、ロール昇順
irb(main):001:0> users = User.all.order(:gender, role: :asc)
irb(main):002:0> users.each {|user| user.print}
id:2 name:Bob gender:1 role:1
id:4 name:Dave gender:1 role:2
id:1 name:Alice gender:2 role:1
id:3 name:Carol gender:2 role:2

# 性別昇順、ロール降順
irb(main):003:0> users = User.all.order(:gender, role: :desc)
irb(main):004:0> users.each {|user| user.print}
id:4 name:Dave gender:1 role:2
id:2 name:Bob gender:1 role:1
id:3 name:Carol gender:2 role:2
id:1 name:Alice gender:2 role:1


最初に渡される引数は性別で、後に渡される引数はロールです。
その場合、性別がまず優先してソートされ、1が先、2が後という順序になります。
その後、第二引数として渡されたロールの順序によってソートされているのがわかるでしょうか。

つまりorderメソッドに引数を複数渡す場合、「先に渡した引数が優先条件としてソートされ、その後、後に渡した引数を条件としてソートされる」ということになります。

もうひとつの例として、ロール、性別の順で引数を渡した場合を見てみます。

# ロール降順、性別昇順
irb(main):001:0> users = User.all.order(role: :desc, gender: :asc)
irb(main):002:0> users.each {|user| user.print}
id:4 name:Dave gender:1 role:2
id:3 name:Carol gender:2 role:2
id:2 name:Bob gender:1 role:1
id:1 name:Alice gender:2 role:1

# ロール降順、性別降順
irb(main):001:0> users = User.all.order(role: :desc, gender: :desc)
irb(main):002:0> users.each {|user| user.print}
id:3 name:Carol gender:2 role:2
id:4 name:Dave gender:1 role:2
id:1 name:Alice gender:2 role:1
id:2 name:Bob gender:1 role:1


第一引数がロールの降順なので、2→1としてソートされた後、性別でソートされていますね。

以上、複数の引数でorderした場合の簡単な例でした。

Postgresqlでadd columnする際の順序制御

困ったこと

現在開発中の株式情報webアプリで、決算情報を持つテーブルにカラムを追加しようとしました。
ですがrails migrationのafterオプションでの順序指定ができず、困っていました。
以下がschema.rbに記載されたテーブルの定義です。

create_table "settlements", force: :cascade do |t|
  t.bigint "stock_id", null: false
  t.string "stock_code", null: false
  t.integer "quarter", null: false
  t.date "date", null: false
  t.float "eps", null: false
  t.float "expected_eps"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["stock_id", "date"], name: "index_settlements_on_stock_id_and_date", unique: true
end


上記のテーブルに、年度を示すyaerカラムを追加しようとし、以下のmigrationファイルを作成しました。

class AddColumnToStocks < ActiveRecord::Migration[5.2]
  def change
    add_column :stocks, :year, :integer, null: false, after: :stock_code
  end
end


stock_codeカラムの後ろに追加したかったので、afterオプションで順序指定をしています。
上記のmigrationを実行したあとのschema.rbが以下のとおりです。

create_table "settlements", force: :cascade do |t|
  t.bigint "stock_id", null: false
  t.string "stock_code", null: false
  t.integer "quarter", null: false
  t.date "date", null: false
  t.float "eps", null: false
  t.float "expected_eps"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.integer "year", null: false
  t.index ["stock_id", "date"], name: "index_settlements_on_stock_id_and_date", unique: true
end


位置を指定したはずなのに、カラムが末尾に追加されてしまっています。
調べてみたところ、Postgresqlでカラムを追加する場合、末尾にしか追加ができず、位置の指定ができないということでした。

今回は個人開発ということもあり、テーブルを作成するmigrationを作り直して無理やりカラムを追加したのですが、好きな位置にカラムを追加する方法があるとtwitterで教えていただいたので、その内容をまとめてみようと思います。

対策

対策手順を説明すると、以下のとおりです。

  1. 別のテーブル名で新カラムを加えたテーブルを定義する
  2. 旧テーブルのレコードを新テーブルにinsertする
  3. 旧テーブルをdrop
  4. 新テーブルを旧テーブルと同じ名前にrename


具体的に見ていきましょう。

1. 別のテーブル名で新カラムを加えたテーブルを定義する

文字通り、加えたいカラムを加えた上で、別のテーブル名でテーブルを定義します。

ただ、追加したカラムにnull制約はつけないほうがいいと思います。
手順2でデータをinsertするにあたって、コピー元には追加したカラムの値がないので、制約に引っかかってコピーしたレコードを作成することができません。

必要がある場合は、あとで別途付与しましょう。

create_table "temp_settlements", force: :cascade do |t|
  t.bigint "stock_id", null: false
  t.string "stock_code", null: false
  t.integer "year"
  t.integer "quarter", null: false
  t.date "date", null: false
  t.float "eps", null: false
  t.float "expected_eps"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.index ["stock_id", "date"], name: "index_settlements_on_stock_id_and_date", unique: true
end


2. select * from 旧テーブルを新テーブルにinsertする

以下のようなsqlを書いて、旧テーブルのレコードを新テーブルにinsertします。

insert into temp_settlements (stock_id, stock_code, quarter, date, eps, expected_eps, created_at, updated_at) (select stock_id, stock_code, quarter, date, eps, expected_eps, created_at, updated_at from settlements);


3. 旧テーブルをdrop

以下のようなmigrationファイルを作成して、旧テーブルをdropします。

rails generate migration DropOldTable


class DropOldTable < ActiveRecord::Migration[5.2]
  def change
    drop_table :settlements
  end
end


4. 新テーブルを旧テーブルと同じ名前にrename

以下のようなmigrationファイルを作成して、新テーブルを旧テーブルと同じ名前にrenameします。

rails generate migration RenameTempSettlementToSettlement


class RenameTempSettlementToSettlement < ActiveRecord::Migration[5.2]
  def change
    rename_table :temp_settlements, :settlements
  end
end


晴れてこれで、任意の位置にカラムを追加したテーブルを作成することができました。

まとめ

  • Postgresqlでは、カラムの追加は末尾にしかできない。
  • 任意の位置にカラムを追加したい場合は、以下の手順で作業を行う。
    1. 別のテーブル名で新カラムを加えたテーブルを定義する
    2. 旧テーブルのレコードを新テーブルにinsertする
    3. 旧テーブルをdrop
    4. 新テーブルを旧テーブルと同じ名前にrename

Action Cableにおけるチャンネルの動的な作り方

Action Cableで動的にチャンネルを作成したいときは、以下のように引数を渡しましょう。

App.cable.subscriptions.create { channel: "ChatChannel", user_id: user_id }


user_idが1の場合、上記のコードで以下のようなチャンネルが作成されます。

chat_channel_1


チャンネル作成はjavascript内で行われるので、user_idやroom_idなどのmodelのデータを使用するためには、rubyからjsに変数を渡す必要があります。

自分がReactのSPAでチャンネルを動的に作成したときには、Reactが呼んでいるAPIからuser_idを返すようにして、React内でチャンネルを作成しました。

短いですが今回はここまで。

Google検索結果をスクレイピングするときは、User-Agentの設定に気をつけよう

TL;DR

User-Agentの設定に気をつけないと、Google検索結果のスクレイピングがうまくいかないかもしれません、というお話です。
ついでに、Google検索結果のスクレイピングはグレーっぽいというお話です。

やってみて失敗したこと

Googleの検索結果をスクレイピングするなら、あなたはどのようなコードを書くでしょうか。
私はこんな感じで書きます。

search_url = "https://www.google.co.jp/search?hl=jp&gl=JP&"
query = URI.encode_www_form(q: "日本M&Aセンター")
search_url += query

charset = nil

html = open(search_url) do |f|
  charset = f.charset
  f.read
end

doc = Nokogiri::HTML.parse(html, nil, charset)


人によって細かい違いはあるかもしれませんが、上記はごく普通のNokogiriでのスクレイピングだと思います。

ここから、最初の検索結果に現れたサイトのURLを取得したいとします。 f:id:yuya_tequila:20190203115933p:plain xpathでの指定が簡単そうでしたので、要素のxpathを指定し、そこからリンクテキストをスクレイピングするとします。

link = doc.xpath('//div[@class="r"]/a')
link[0].attribute('href').text


そうすると、変数linkには何が入っているでしょうか。

puts link
=> nil


何も入っていません。何故でしょう。

結論から言うと、xpathの指定が間違っているからです。
そしてなぜxpathが間違っているのかというと、User-Agentを指定していないからなのです。

User-Agentの検証

実際に検証してみましょう。
まずはUser-Agentの設定を何も変えずに検索してみます。
(User-Agent設定はChromeのdeveloper toolから見られます) f:id:yuya_tequila:20190203121426p:plain
この場合、自分の環境ではUser-Agentは以下のような設定になっています。

"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.81 Safari/537.36"


では次にUser-Agentに意味のない文字を設定して、同じ画面を表示してみます。 f:id:yuya_tequila:20190203123726p:plain

検索結果の画面が変わったことがわかるでしょうか。
表示されている内容が大きく変わったわけではありませんが、レイアウトやスタイルが変更されています。
これがUser-Agentを設定せずにGoogle検索をおこなった場合のレスポンス、というわけです。
この状態でもスクレイピングは可能なのですが、要素のxpathが変わってしまうため、Chromeを見ながらスクレイピングするということがやりにくく、非常に開発がしづらくなってしまいます。

対策

対策としては、User-Agentを明示的に設定してあげればよいです。 というわけで修正したソースコードがこちら。

user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.81 Safari/537.36"

search_url = "https://www.google.co.jp/search?hl=jp&gl=JP&"
query = URI.encode_www_form(q: "日本M&Aセンター")
search_url += query

charset = nil

html = open(search_url, 'User-Agent' => user_agent) do |f|
  charset = f.charset
  f.read
end

doc = Nokogiri::HTML.parse(html, nil, charset)

link = doc.xpath('//div[@class="r"]/a')
link[0].attribute('href').text


これで、自分の使用しているChrome環境と同じレスポンスを取得できるようになります。
スクレイピングのマナーとしてもUser-Agentは設定するべきですので、良いことづくめですね。

余談

ちなみにGoogleの検索結果をスクレイピングするにあたっていろいろ調べたのですが、Googleは検索結果のスクレイピングを認めていないようですね。
robots.txtにも、以下の記載がありますので。

User-agent: *
Disallow: /search


とはいっても、実際はある程度黙認されているようです。
何にせよ、スクレイピングは自己責任で行うべきですね。

rubyでURLエンコーディングする方法

最近、株価を分析するwebアプリを個人開発しています。
その中で、銘柄名でGoogle検索したときの結果をスクレイピングしたくなりました。

銘柄名と証券コードはStringでDBに登録済みなので、簡単に持ってこられます。
ですが持ってきた文字列をそのままクエリストリングに入れてしまうと、うまく検索できないことがありました。

search_url = "https://www.google.co.jp/search?hl=jp&gl=JP&q=日本M&Aセンター"


f:id:yuya_tequila:20190127222715p:plain
欲しい結果は取得できていますが、検索ワードが"日本M"で途切れています。
"&"が正しくエスケープできていないのかと思い、調べてみたところ、以下の方法でできました。

require 'uri'

query = URI.encode_www_form(q: '日本M&Aセンター')
=> "q=%E6%97%A5%E6%9C%ACM%26A%E3%82%BB%E3%83%B3%E3%82%BF%E3%83%BC"

search_url = "https://www.google.co.jp/search?hl=jp&gl=JP&"
=> "https://www.google.co.jp/search?hl=jp&gl=JP&"

search_url += query
=> "https://www.google.co.jp/search?hl=jp&gl=JP&q=%E6%97%A5%E6%9C%ACM%26A%E3%82%BB%E3%83%B3%E3%82%BF%E3%83%BC"


f:id:yuya_tequila:20190127222728p:plain
上記のURLを打ち込むと、"&"が正しくエスケープされているのがわかります。他の文字はエスケープ前の文字に戻っていますが。

ちなみに調べている中で最初に出てきた方法はURI.encodeを使う方法だったのですが、そのやり方では正しくエスケープされませんでした。

require 'uri'

search_url = "https://www.google.co.jp/search?hl=jp&gl=JP&q="
=> "https://www.google.co.jp/search?hl=jp&gl=JP&q="

search_url += "日本M&Aセンター"
=> "https://www.google.co.jp/search?hl=jp&gl=JP&q=日本M&Aセンター"

URI.encode search_url
=> "https://www.google.co.jp/search?hl=jp&gl=JP&q=%E6%97%A5%E6%9C%ACM&A%E3%82%BB%E3%83%B3%E3%82%BF%E3%83%BC"


最終的に生成された文字列を見比べてみると、"&"がエスケープされていないことがわかります。

# "M&A"が"M%26A"になっている。
success_url = "https://www.google.co.jp/search?hl=jp&gl=JP&q=%E6%97%A5%E6%9C%ACM%26A%E3%82%BB%E3%83%B3%E3%82%BF%E3%83%BC"

# "M&A"が"M&A"のまま。
failure_url = "https://www.google.co.jp/search?hl=jp&gl=JP&q=%E6%97%A5%E6%9C%ACM&A%E3%82%BB%E3%83%B3%E3%82%BF%E3%83%BC"


Ruby 2.6.0 リファレンスマニュアルによると、encodeメソッドはobsoleteとのことなので、今後は使用しないほうがよいかもしれません。

2月の目標

ものすごくお久しぶりです。

ほぼtwitterでしか発信しない日々でしたが、ブログでのアウトプットもやっぱり大事なので、頑張って更新していきます。

 

というわけで今回は「2月の目標について」です!

 

2月の目標

  • Webアプリの完成
  • 転職活動の開始

 

この2つの目標は、段階を踏んだ目標です。

 

いろいろ考えた結果、やはり自分はWebアプリ業界での仕事がしてみたいと思っています。

ですが自分には、Web業界での業務経験がありません。

未経験者の募集があるところもありますが、「自発的にアプリの1つも開発できない人間が求められているのだろうか?」と考えると。答えはNoだと思います。

 

自分が行きたい場所は「事業目標のために、エンジニアみんなが技術力を磨くことを推奨される場所」です。 

人数がいれば工数商売でお金を稼げると考えているような会社ではありません。

 

そういう場所に行きたいからには、そういう場所にふさわしい自分にならなければなりません。

そうなるために、簡単なものでも「自分で作りました!」といえるアプリケーションを作り、それを引っさげた上で転職活動がしたいと思っています。

 

どんなWebアプリを作っているの?

物語のプロットのネタ出しをお助けするツールです。

とりあえずREADMEを書いてあるので、興味のある方は以下のリポジトリを見てみてください!

 

github.com

 

ちなみにこれ、READMEにも書いてある通り、以下のやる夫スレで紹介されているテクニックをアプリ化したものです。

やる夫が小説家になるようです | やる夫 Wiki | FANDOM powered by Wikia

「タロットプロット」という名前は「韻を踏んでていい感じかなー」と思って付けたものなのですが……

 

タロットプロット - 小説・漫画・脚本のプロット作成ツール

同名で、同じコンセプトのサイトが既に存在していました……。

しかもかなりクオリティが高いです……。

 

目的はあくまでも学習と技術力アピールなので、内容が被っていることは転職活動の妨げにはならないと考えていますが、さすがに名前は変えると思います。

 

まとめ

そんなわけで、2月の目標は「Webアプリの完成」と「転職活動の開始」です!

当然2月中に開始するからには、もっと早くWebアプリを完成させなければいけません。

達成に向けて頑張ります!