PaparClipでCSVをS3にアップロードした時にContent-Typeが"text/plain"になってしまう問題

概要

PaperClipで、Railsで生成したCSVファイルをS3にアップロードするときに、Content-Typeが"text/plain"でアップロードされてしまうので、 S3にアップロードしたときに.txtファイルになってしまう問題がある。

とりあえずの解決方法

とりあえずS3にアップロードするタイミングでヘッダーにContent-Typeに"text/csv"を設定すればとりあえずOK。

今回利用したかったケースはシステム内部で生成したCSVをS3にアップロードする要件なので。 特にバリデーション周りが雑なんだけどまぁこんな感じで。

  class Document < ActiveRecord::Base

    has_attached_file :document,
      storage: :s3,
      s3_credentials: "#{Rails.root}/config/private_s3.yml",
      s3_permissions: :private,
      path: "example.com/:id/:filename"
      
    validates_attachment :document, less_than: 512.megabytes
    do_not_validate_attachment_file_type :document

  end
  class DocumentCsv < Document
    has_attached_file :document,
      storage: :s3,
      s3_credentials: "#{Rails.root}/config/private_s3.yml",
      s3_permissions: :private,
      path: "example.com/:id/:filename",
      s3_headers: {"Content-Type" => "text/csv"}
  end

弱小Railsアプリケーションで1日毎のお手軽な集計方法のご提案

こちらのアンサーブログだと思ってもらえれば。

webuilder240.hatenablog.com

結論

結論からいうと、集計をするためのModelを作っておいてそこから集計を行う方式にして、 色々なModelの集計に対して対応できるようにした。

  create_table "aggregaters", force: :cascade do |t|
    t.date    "aggregate_date",  null: false
    t.string  "aggregate_type",  limit: 255
    t.float "aggregate_value", null: false
  end

ちょっと筋悪な感も否めないが、なにもしないよりは良いだろうと思っている。 タスクにして実行しないといけないのは当然なんだけど、そもそも弱小アプリケーションなんだから問題ないだろうと思ってる。

簡単な利用方法

バッチで結果を格納する側 (RailsのRakeタスク)

require 'active_record'
require 'active_support'

namespace :aggregate_record do
  desc 'コメント数の集計を行って、集計レコードを作成します。'
  task create_daily_comment: :environment do
     Aggregater.create(aggregate_date: Date.now, aggregate_type: 'comment_count', aggregate_value:  Comment.size)
  end
end

呼び出し方

 # なんかここはうまいことやるのは解るけど、あくまで簡単なサンプルで。
 daily_comment_count_obj = Aggregater.find_by(aggregate_date: Date.yesterday, aggregate_type: 'comment_count')
 daily_comment_count = daily_comment_count_obj.aggregate_value || 0

今後 

弱小な間はしばらくはこの方式でなんとかなりそう。 今後もTresureDataやBigQuery等を利用した集計を行う為の外部サービスのクエリ結果をDBにキャッシュする(格納してRailsが取り出しやすい用に)という用途でも利用できそうなので、便利な感じはする。 ただ筋悪なのは否めないので、もっといい方法があれば教えていただきたい所存です。

Dockerで1つのイメージ複数のコマンドを切り替える方法

これをやりたい理由

作るDockeイメージは1つにしたいのだけど、デプロイする先によってコンテナの役割(例えばCronやJobワーカーを動かすコンテナとWebサーバーを実際に動かすコンテナ等)を分けたいときに利用する。

例えばAzureのWebApps for Container (Web Apps on Linux)で、 Webサーバーはスケールアウトしたいけど、Jobワーカーサーバーはスケールアウトしたくない(もしくはJobワーカーサーバーは任意のタイミングにしたい)とか 管理するコンテナを分けたいとかそういうときに利用すれば便利だと思っている。

やりかた

init.shという起動時の環境変数によって実行するコマンドを変更するスクリプトをCMDで実行してあげればOK。 これで実行時に環境変数を渡してあげればOK。

下記例はRubyアプリケーションを動かす際の自分のDockerfileの雛形のようなものである。

# Dockerfile
---
FROM ruby:2.5

ENV LANG C.UTF-8
ENV RACK_ENV production
EXPOSE 3000

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

COPY Gemfile /usr/src/app/
COPY Gemfile.lock /usr/src/app/
RUN bundle install -j4 --without development test

RUN mkdir tmp
COPY . /usr/src/app
RUN chmod u+x init.sh
CMD "./init.sh"
# init.sh
---
#!/bin/bash
set -e

if [[ "${RUN_WEBAPP}" != "" ]]
then
  echo "Starting uniron ..."
  bundle exec unicorn -c config/unicorn.rb
else [[ "${RUN_WEBJOB}" != "" ]]
  echo "Starting sidekiq ..."
  bundle exec sidekiq -r ./app.rb -C ./sidekiq.yml
fi

以上の内容でDockerイメージをBuildして、docker runするときに環境変数を渡せばOK。

サンプルでWebサーバーを実行する場合

 docker run -e RUN_WEBAPP=true xxx

サンプルでJobワーカーサーバーを実行する場合

 docker run -e RUN_WEBJOB=true xxx

おわりに

ちなみにAWSのElasticBeanstalkあたりだとマルチコンテナーをそもそもサポートしているので、そっちを使うほうがまぁ普通だと思っている。 もしくはCronを利用しないで、SidekiqやDelatedJobあたりでワーカーがいくらスケールアウトしても問題ないようにするとか、 WebサーバーとWorkerの同居を許容してしまうとか、そういう方法で解決したほうが至極真っ当な感じはする。

余談

AzureからAWSのDynamoDBって利用すると遅いのかな。 ぶっちゃけ用途があえばCosmosDBでもよかったんだけど、メッセージングアプリっぽいの作りたくて、 MongoベースのAPIでまともに要件を満たせるのか感があって、不安...

URLのOGP情報をスクレイピングするだけのWebAPIを作った

サイトのOGP情報をスクレイプするのが面倒なので、OGP情報のスクレイピングし、JSONに変換して情報を返すだけのマイクロサービスを作った。 マイクロサービスというか、用途的にはDockerが向いていると思ったので、Dockerイメージにもしてあります。

WebAPIから利用する

$ curl https://powerful-sierra-79664.herokuapp.com/?url=http://ogp.me | jq

{
  title: "Open Graph protocol",
  description: "The Open Graph protocol enables any web page to become a rich object in a social graph.",
  image: "http://ogp.me/logo.png",
  url: "http://ogp.me"
}

リポジトリにあるDockerComposeから利用する

同梱されているDockerComposeを利用して簡単に使い始めることも可能です。

$ docker-compose up

$ curl http://localhost:8080?url=http://ogp.me | jq

{
  title: "Open Graph protocol",
  description: "The Open Graph protocol enables any web page to become a rich object in a social graph.",
  image: "http://ogp.me/logo.png",
  url: "http://ogp.me"
}

Dockerイメージから利用する場合(Redisが必要です)

$ docker pull webuilder240/ogp_parse_api
$ docker run -p 8080:8080 webuilder240/ogp_parse_api

注意事項

  • Productionで利用する場合、サーバーサイドの前段でキャッシュを取ったりはしていないので、このMicroServiceの前にCDNを利用することを推奨しています。
  • バックエンドにRedisを使っているので、Redisを事前に用意してください。*1

その他の詳しい情報についてはGithubあたりに置いているREADMEをご参照頂ければと。

なんか特徴があるとすれば

  • Sinatra使ってる
  • 本体のコードは100行未満で、めっちゃ短い
    • ただ他のライブラリに依存していないとは言っていない。
  • バックエンドにRedisを利用している
    • OGPなんて消えてしまっても別に困らない。再度取得すればOK
    • キャッシュしたデータがもりもりになっても困るし、7日くらいで勝手に削除されるようにしてる。*2
  • 前段にNginxを噛ませてない
    • なるべくインストールするものを減らしたかったというのと、前段にCDNを置いて運用する前提で作っていたのでそもそも不要と思ったからでした。

作った背景

  • マイクロサービスの題材として最適だった。そのなかで色々な案件で使えそうな便利なものをと考えたらコレになった。
    • 機能単位でWebAPIとして切り出していくメリットはなんとなく掴めた。
  • サーバーサイドで利用するだけだったらGemだけでも良いけど、フロントエンドやRuby以外のサーバーサイドでも利用するのではないかと思うと、JSONでやり取りした方が良いのでは思ったから。
  • あとはリンクを解析して、色々な情報を解析して返してくれるSaasもあるけどなんかべらぼうに髙いように感じたのと、やけに高機能だったのでシンプルにして実装し直した。 というのが作ってみたモチベーション。

コレであれば使われなくても捨てれるサイズだし、何かしらアウトプットしとくか位の気持ちでやっている。*3

今後の展望

  • herokuボタン
  • Rubyではなく、JavaかGoにリプレースしてもいいのかと思っている。
  • これができるのがMicroServiceの強み

*1:開発用にDockerComponseも用意しているけど、それにはRedisのコンテナも定義してあるので。

*2:Dockerから利用する場合、環境変数から変更可能

*3:前に試しに作ってみたものをブログでアウトプットするネタのためにわざわざOSSにするという姑息な手を使ったというのは内緒...

self_updated_atがメソッド指定に対応しました

先日紹介した、拙作のself_updated_atなのですが、 この度v0.2.0へアップデートしたので、こちらの更新情報をお伝えします。

self_updated_atがどういうGemかよくわからんという人はこの記事を読んでみてください。 webuilder240.hatenablog.com

下記は更新内容について記載しました。

パラメータにmethodsを追加しました。

  • methodsはcolumnsよりも後にチェックされるもので、指定したもののどれかが真を返す場合、指定したカラムの日付を更新します。
  • methodsで指定されたものすべての結果が偽の場合は指定カラムの日付更新を行いません。

github.com

methodsのSyntaxについて

class Post

  self_updated_at, :service_updated_at, methods: [:true_method]

  private

  def true_method
    true
  end

end

上記コード例の場合、 「true_method」が真だった場合に「service_updated_at」カラムの更新日が更新されます。

もちろん、methodsとcolumnsを組み合わせても利用できます。

class Post

  self_updated_at, :service_updated_at, columns: [:title], methods: [:true_method]

  private

  def true_method
    true
  end
  
end

上記コード例の場合、 「title」カラムが更新された場合もしくは、「true_method」が真だった場合に「service_updated_at」カラムの更新日が更新されます。 式の評価は、columnsを優先して行います。

その他の細かい修正

  • self_updated_atの指定カラムに作成時に日時を設定した場合、その日時を優先して反映するようにしました。作成時に指定カラムがnilだった場合は現在日時を設定します。

ActiveRecordのVersionが5.1以上の場合に発生するDEPRECATION WARNINGへの対応

元々、xxxx_charged?メソッドを利用してModelの変更を検知していたのですが、ActiveRecordが5.1以降の場合、 近い将来廃止予定らしく、DEPRECATION WARNINGが発生したので移行予定のメソッドを利用するように修正を行いました。

github.com

今後について

機能面はもうこれで自分のケースは満たしていると思ってて、あんまりProcとかLambdaをサポートする意欲はないかも。 Specをある程度リファクタリングして、READMEを英語で書いた段階で、v1.0.0をリリースしちゃうかも。