Rails + SQLite3 + React プロジェクトをGoogle Cloud Run にお安くデプロイする

GCPに興味はあるけれど、DB料金がお高くて個人開発には厳しいな。。。と思っていたところ、

というTweetに出会いました。
SQLite + Litestreamでお安くGCPにデプロイできるというのです。
自分のような初心者には難度が高いかと思いましたが、先人の資料が充実していたので、挑戦してみることにしました。

目次

環境

  • wsl2
  • Debian 11.5
  • Ruby 3.1.1
  • Rails 7.0.4
  • React 18.2.0
  • SQLite3 3.34.1
  • Docker 20.10.21
  • Google Cloud SDK 412.0.0 alpha 2022.12.09 beta 2022.12.09 bq 2.0.83 bundled-python3-unix 3.9.12 core 2022.12.09 gcloud-crc32c 1.0.0 gsutil 5.17

設定

Googleアカウント

  • アカウントを用意します
  • アカウントに二要素認証を設定します

Cloud Strageのバケットを作成

Google Cloudの設定

Dockerfileの作成

  • Dockerfileを作成します
FROM ruby:3.1.1

ENV RAILS_ENV="production"
ENV NODE_ENV="production"

# Cloud Run default port 8080
EXPOSE 8080

# nodeをインストール
RUN curl -fsS https://deb.nodesource.com/setup_16.x | bash - && apt-get install -y nodejs

# APTライブラリを更新してインストール
# aptキャッシュをクリーンする。yオプションですべての問い合わせにyes
# /var/lib/apt/lists/*を削除してイメージを減らす
# https://docs.docker.jp/engine/articles/dockerfile_best-practice.html
RUN apt-get update \
    && apt-get install -y --no-install-recommends sqlite3 \
    && apt-get -y clean \
    && rm -rf /var/lib/apt/lists/*

# Download the static build of Litestream directly into the path & make it executable.
# This is done in the builder and copied as the chmod doubles the size.
# ref: https://github.com/steren/litestream-cloud-run-example/blob/main/Dockerfile
ADD https://github.com/benbjohnson/litestream/releases/download/v0.3.9/litestream-v0.3.9-linux-amd64-static.tar.gz /tmp/litestream.tar.gz
RUN tar -C /usr/local/bin -xzf /tmp/litestream.tar.gz

# working directoryの設定
WORKDIR /app

# リポジトリのファイルをコピー
COPY . /app

# gemのインストール
RUN bundle install

# アセットをプリコンパイルする
# コンパイルが終わったら不要なキャッシュと node_modules を消してイメージの容量を減らす
RUN SECRET_KEY_BASE=placeholder bundle exec rails assets:precompile \
 && rm -rf node_modules tmp/cache

# Copy Litestream configuration file & startup script.
COPY ./litestream.yml /etc/litestream.yml
COPY entrypoint.sh /app/entrypoint.sh

RUN chmod +x /app/entrypoint.sh

# public配下のファイルを公開する
ENV RAILS_SERVE_STATIC_FILES="true"
ENTRYPOINT ["/app/entrypoint.sh"]

entrypoint.shファイルの作成

  • entrypoint.shファイルを作成します
#!/bin/bash
set -e

# コンテナ起動時に持っているSQLiteのデータベースファイルは、
# 後続処理でリストアに成功したら削除したいので、リネームしておく
if [ -f /app/db/production.sqlite3 ]; then
  mv /app/db/production.sqlite3 /app/db/production.sqlite3.bk
fi

# Restore the database
- litestream restore -v -if-replica-exists -config /etc/litestream.yml /app/db/production.sqlite3

# リストアに成功したら、リネームしていたファイルを削除
if [ -f /app/db/production.sqlite3]; then
  echo "---- Restored from Cloud Storage ----"
  rm /app/db/production.sqlite3.bk
else
  # 初回起動時にはレプリカが未作成であり、リストアに失敗するので、
  # その場合には、冒頭でリネームしたdbファイルを元の名前に戻す
  echo "---- Failed to restore from Cloud Storage ----"
  mv /app/db/production.sqlite3.bk /app/db/production.sqlite3
fi

# server.pidが残ってしまって起動できなくなることを防ぐために削除する
# server.pid は、削除してもrails serverを実行した時に自動で作成される
rm -f /app/tmp/pids/server.pid

# Run litestream with your app as the subprocess.
exec litestream replicate -exec "rails server -p 8080 -b 0.0.0.0"

Litestreamの設定ファイルの作成

dbs:
  - path: /app/db/production.sqlite3
    replicas:
      - url: gcs://kankendb

.gcloudignore ファイルの作成

  • Cloud Runではコピー対象外のファイルは.gcloudignoreファイルで指定します
    .gcloudignoreは、.gitignoreに指定されているファイルも無視するので注意が必要ですが、

This .gcloudignore (similar to the one generated when Git files are present) would prevent the upload of the .gcloudignore file, the .git directory, and any files in ignored in the .gitignore file:
gcloud topic gcloudignore  |  Google Cloud CLI Documentation

.gcloudignore で一旦全部 ignore して、必要な特定のファイルやディレクトリだけを指定すれば、実行に必要ないファイルをアップロードせずに済みます
.gcloudignore で全部無視して必要なものだけ指定する - ぽ靴な缶

# ignore all
*

# directories
!app
!bin
!config
!db
!frontend
!lib
!node_modules
!public
!storage
!vendor

# ignore directories
frontend/src/__tests__

# files
!Dockerfile
!entrypoint.sh
!Gemfile
!Gemfile.lock
!package.json
!package-lock.json
!config.ru
!identifier.sqlite
!litestream.yml
!postcss.config.cjs
!Procfile.dev
!Rakefile
!tailwind.config.cjs
!tsconfig.json
!tsconfig.node.json
!vite.config.ts

デプロイ

  • 以下のコマンドでデプロイします
gcloud beta run deploy kanken-practice   \
--source .    \
--set-env-vars REPLICA_URL=gcs://kankendb/database   \
--max-instances 1   \
--execution-environment gen2   \
--no-cpu-throttling   \
--allow-unauthenticated   \
--region asia-northeast1   \
--project keikami

引数は

  • Litestreamは複数サーバとの互換性が無いため、最大1インスタンスにする
  • Cloud Run の第2世代を使用する
  • CPUが常に割り当てられるようにする
  • 認証されていない呼び出しを許可することで、サービスにアクセスできるようにする

の設定です
gcloud run deploy  |  Google Cloud CLI Documentation
GitHub - steren/litestream-cloud-run-example: An example of using Litestream within Cloud Run

デプロイ開始

⠛ Building and deploying... Uploading sources.                                                                                                         
  ⠛ Uploading sources...                                                                                                                               
  . Building Container...
  . Creating Revision...
  . Routing traffic...
  . Setting IAM Policy...

成功したようです

Done.                                                                                                                                                  
Service [kanken-practice] revision [kanken-practice-00159-jis] has been deployed and is serving 100 percent of traffic.
Service URL: https://kanken-practice-b5ve7zdnrq-an.a.run.app

動作確認

参考サイト

ソースコード

https://github.com/kei-kmj/kanken_practice_note

Rubyで<ArgumentError: invalid byte sequence in US-ASCII>エラーを解決した時のメモ

<ArgumentError: invalid byte sequence in US-ASCII>エラーを解決した時のメモです。

環境

問題

コメントに全角文字があるだけで、debuggerのところでエラーで落ちてしまう。

# frozen_string_literal: true
require 'debug'

arr = ["a", "b", "c"] #コメント

arr.each do |a|
  debugger
  puts a.capitalize
end

エラーログ

#<ArgumentError: invalid byte sequence in US-ASCII>
["/home/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/reline-0.3.2/lib/reline/unicode.rb:71:in `ord'",
 "/home/rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/reline-0.3.2/lib/reline/unicode.rb:71:in `block in escape_for_print'",
 "/home/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/reline-0.3.2/lib/reline/unicode.rb:70:in `map!'",
 "/home/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/reline-0.3.2/lib/reline/unicode.rb:70:in `escape_for_print'",
 "/home/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/irb-1.6.2/lib/irb/color.rb:137:in `block in colorize_code'",
 "/home/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/irb-1.6.2/lib/irb/color.rb:190:in `block (2 levels) in scan'",
 "/home/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/irb-1.6.2/lib/irb/color.rb:203:in `block (2 levels) in scan'",
 "/home/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/irb-1.6.2/lib/irb/color.rb:201:in `each'",
 "/home/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/irb-1.6.2/lib/irb/color.rb:201:in `block in scan'",
 "/home/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/irb-1.6.2/lib/irb/ruby-lex.rb:45:in `rescue in compile_with_errors_suppressed'",
 "/home/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/irb-1.6.2/lib/irb/ruby-lex.rb:34:in `compile_with_errors_suppressed'",
 "/home/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/irb-1.6.2/lib/irb/color.rb:177:in `scan'",
 "/home/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/debug-1.7.1/lib/debug/breakpoint.rb:69:in `suspend'",
 "/home/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/debug-1.7.1/lib/debug/breakpoint.rb:178:in `block in setup'",
 "/home/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/debug-1.7.1/lib/debug/session.rb:2586:in `debugger'",
  "sample.rb:7:in `block in <main>'",
 "sample.rb:6:in `each'",
 "sample.rb:6:in `<main>'"]

解決策①

ファイルの先頭にマジックコメント# encoding: UTF-8を付ける

解決策②

Rubyエンコーディングを変更する

$ ruby -e 'puts Encoding.default_external'
US-ASCII
$ export RUBYOPT=-EUTF-8
$ ruby -e 'puts Encoding.default_external'
UTF-8

調べたこと

p "aiueo".encoding                        # #<Encoding:UTF-8>
p "aiueo".encode('US-ASCII').encoding   # #<Encoding:US-ASCII>

参考にしたサイト

RubyのデフォルトはUTF-8だと勘違いしてたので、勉強になりました。

Vite.jsの本番環境用のbuildでError: 'fileURLToPath' is not exported by __vite-browser-external, imported by node_modules/local-pkg/dist/shared.mjsを解決したメモ

Vite.jsで本番環境のbuildエラーを解決したときのメモです。

エラーログ

$ npm run build

> frontend@0.0.0 build
> tsc && vite build

vite v3.2.5 building for production...
transforming (155) ../node_modules/axios/lib/platform/browser/index.js

✓ 235 modules transformed.
'fileURLToPath' is not exported by __vite-browser-external, imported by node_modules/local-pkg/dist/shared.mjs
file: /home/dev/projects/node_modules/local-pkg/dist/shared.mjs:41:9
39: import path from "path";
40: import fs, { promises as fsPromises } from "fs";
41: import { fileURLToPath } from "url";
             ^
42:
43: // node_modules/.pnpm/yocto-queue@1.0.0/node_modules/yocto-queue/index.js
error during build:
Error: 'fileURLToPath' is not exported by __vite-browser-external, imported by node_modules/local-pkg/dist/shared.mjs

原因

Vite.jsは、developmentではEsbuildを使用しますが、productionではRolleupを用いてbuildするようで、 Rolleup用のPolyfillを自分で追加しないといけないようです。

解決方法

vite-plugin-node-stdlib-browserをインストールする

https://github.com/sodatea/vite-plugin-node-stdlib-browser

  • npm add node-stdlib-browser
  • npm add -D vite-plugin-node-stdlib-browser
// vite.config.ts
import nodePolyfills from 'vite-plugin-node-stdlib-browser'

export default defineConfig({
  plugins: [nodePolyfills()]
})

参考URL

RailsのSQLite3テーブルをCSVでインポート/エクスポートする

RailsのSQLite3テーブルをCSVでインポート/エクスポートしてみました。

目次

環境

  • Rails7.0.4
  • wsl2(Debian11)

Railsの準備

アプリケーションフレームワークを作成します

$ rails new Library

SQLite3を操作するためのテストデータがあれば良いので、今回はscaffoldでリソースを作成します

$ rails g scaffold book title:string description:text  

scaffold`で作成されたマイグレーションファイルから、データベースを作成します

$ rails db:migrate  

== 20221018080750 CreateBooks: migrating ======================================
-- create_table(:books)
   -> 0.0106s
== 20221018080750 CreateBooks: migrated (0.0135s) =============================

Railsを起動します

$ rails server  

データをいくつか登録してコンソールで確認します

$ rails console
irb(main):001:0> Book.all
  Book Load (0.1ms)  SELECT "books".* FROM "books"
=>
[#<Book:0x00007faeb62dcde8
  id: 1,
  title: "こんにちは",
  description: "Hello",
  created_at: Tue, 18 Oct 2022 08:09:38.977918000 UTC +00:00,
  updated_at: Tue, 18 Oct 2022 08:09:38.977918000 UTC +00:00>,
 #<Book:0x00007faeb6297310
  id: 2,
  title: "朝",
  description: "morning",
  created_at: Tue, 18 Oct 2022 08:10:09.200306000 UTC +00:00,
  updated_at: Tue, 18 Oct 2022 08:10:09.200306000 UTC +00:00>,
 #<Book:0x00007faeb6297248
  id: 3,
  title: "雨",
  description: "rain",
  created_at: Tue, 18 Oct 2022 08:10:34.348508000 UTC +00:00,
  updated_at: Tue, 18 Oct 2022 08:10:34.348508000 UTC +00:00>]

データが登録されていることが確認できました

エクスポート

登録したデータをエクスポートしてみます

.headers コマンドを使うと SELECT 文などでデータを取得し表示する時にヘッダーとしてカラム名を表示するかどうかを設定できます。
今回は.headers onにしてみます

$ rails db
sqlite> .headers on

CSVモードを指示します

sqlite> .mode csv

出力するCSV ファイルを指定します

sqlite> .output output.csv

データを選択し、data.csvファイルにエクスポートします

sqlite> SELECT * FROM books;
sqlite> .quit

エクスポートできました output.csv

インポート

$ rails db sqlite> .mode csv

インポートしたいデータを作成します

CSVモードを指示します

$ rails db
sqlite> .mode csv

ファイルを指定してBooksテーブルにインポートします。

sqlite> .import output.csv books

インポートできました

参考にした書籍/サイト

Rails7系 + React18系 + vite.js でReactコンポーネントを動かす

目次

Vite.js とは

  • Vite.jsは、高速で動作するビルドツール
  • Vue.js作者Evan Youさんが開発し、2020年に発表された
  • 従来のビルドツールと違い、開発時に全てのファイルをコンパイルしバンドルしない
  • ES modulesのimportにより、必要なファイルだけをコンパイルしてブラウザに送るような仕組みで高速化している (プロダクションではバンドルする)
  • Go 言語によって開発されたesbuild を使用して依存関係の事前バンドルを高速化
  • 類似ツールに比べて設定が簡単
  • .ts ファイルのインポートもサポートしているが、トランスパイルするだけで、型チェックはしない

公式ドキュメント

Vite_ruby とは

  • Vite Ruby は、Ruby アプリケーションに Vite.js を統合するためのライブラリ
  • ウェブパッカーと同じように、アプリケーションのエントリーポイントを推測する
  • エントリーポイントを参照するスクリプトやスタイルタグをレンダリングするタグヘルパーを提供し、Vite.jsによって処理されるようにする

公式ドキュメント

環境

セットアップ

Rails

javascriptは不要なのでskipしてrails newします

$ rails new vite_rails --skip-javascript

vite_rails

vite_railsをGemfile に追加してbundle installします。

$ gem 'vite_rails'

vite

$ bundle exec vite install

Javascript + React という組み合わせで、frontendというディレクトリに作成します

$ yarn create vite
yarn create v1.22.17
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Installed "create-vite@3.1.0" with binaries:
      - create-vite
      - cva
✔ Project name: … frontend
? Select a framework: › - Use arrow-keys. Return to submit.
❯   Vanilla
    Vue
    React
? Select a framework: › - Use arrow-keys. Return to submit.
? Select a framework: › - Use arrow-keys. Return to submit.
✔ Select a framework: › React
✔ Select a variant: › JavaScript

Scaffolding project in /home/kei-kmj/dev/projects/rails/vite_rails/frontend...

Done. Now run:

  cd frontend
  yarn
  yarn dev

Done in 18.19s.

config/vite.json

sourceCodeDirをapp/frontend から、Vite.jsで生成した frontend に書き換えます。

{
  "all": {
    "sourceCodeDir": "frontend",
    "watchAdditionalPaths": []
  },

設定の調整

frontend/package.json

package.json

package.jsonファイル2つ出来ているので、frontend/package.jsonpackage.jsonにマージしてfrontend/package.jsonは削除します。

{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.0.17",
    "@types/react-dom": "^18.0.6",
    "@vitejs/plugin-react": "^2.1.0",
    "vite": "^3.1.4",
    "vite-plugin-ruby": "^3.1.2"
  }
}

frontend/vite.config.js

vite.config.ts

こちらも2つ出来ているので、vite.config.tsを書き換えて、frontend/vite.config.jsを削除します。

import { defineConfig } from 'vite'
import RubyPlugin from 'vite-plugin-ruby'
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [
    react(),
    RubyPlugin(),
  ],
})

コンポーネント作成

Controller と View の作成

$ in/rails g controller hello index

app/views/layouts/application.html.erb

vite_railsが用意しているタグを追加します。

  • <%= vite_client_tag %> Hot Module Reload 用
  • <%= vite_react_refresh_tag %> React 向け Hot Module Reload 用
  • <%= vite_javascript_tag 'main.jsx' %> frontend/src/main.jsxを読み込みこむタグ
<!DOCTYPE html>
<html>
<head>
  <title>ViteRails</title>
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <%= csrf_meta_tags %> 
  <%= csp_meta_tag %>

  <%= vite_client_tag %>
  <%= vite_react_refresh_tag %>
  <%= vite_javascript_tag 'main.jsx' %>
   
</head>

<body>
<%= yield %>
</body>
</html>

app/views/hello/index.html.erb

書き換えます

<div id="root"></div>

routesの修正

hello#indexrootとして設定します。

config/routes.rb

Rails.application.routes.draw do
  root 'hello#index'
end

Helloコンポーネントの作成

"Hello React!"と表示するコンポーネントを作成します。

frontend/src/components/Hello.jsx

const Hello = ({name}) => <h1>Hello,{name}!</h1>

export default Hello

frontend/src/main.jsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import Hello from './components/Hello'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Hello name="React" />
  </React.StrictMode>
)

config/vite.json

エントリポイントを変更するためconfig/vite.json"entrypointsDir": "src"を追加します

{
  "all": {
    "sourceCodeDir": "frontend",
    "entrypointsDir": "src", 
    "watchAdditionalPaths": []
  },

動作確認

動作確認してみます

$ bin/vite dev
$ bin/rails s

"Hello,React!"が表示されました

ソースコード

https://github.com/kei-kmj/vite_rails_hello

参考書籍&サイト

Ruby とseleniumでアマゾンの価格チェックアプリを作ったメモ

仕様

特定の商品の価格が一定以下に下がったら、LINEに通知するアプリを作りました。

環境

Amazon.co.jpの商品データを取得する

selenium-webdriverのインストール

$ gem install selenium-webdriver

chrome driverのインストール

$ sudo apt-get install google-chrome-stable

# ダウンロード
$ wget https://chromedriver.storage.googleapis.com/94.0.4606.41/chromedriver_linux64.zip

# 解凍
$ unzip chromedriver_linux64.zip

# パスを通す
echo 'export DISPLAY="${DISPLAY:-:0}"' >> ~/.zshrc

fetchする

require 'selenium-webdriver'

# Selenium::WebDriver::Chrome::Optionsクラスをインスタンス化する
session = Selenium::WebDriver::Chrome::Options.new

# GUIでは無くCUIで動作させるためheadlessモードにする
session.add_argument('--headless')

# ブラウザとオプションを指定する
page = Selenium::WebDriver.for :chrome, options: session

# ページ遷移する
page.navigate.to VISIT_URL

https://www.selenium.dev/selenium/docs/api/rb/Selenium/WebDriver/Chrome/Options.html

データを整形する

商品名、出版社、金額、リンクをLINEに通知します。

titleを取得する

p page.title 
# プログラミングTypeScript ―スケールするJavaScriptアプリケーション開発 | Boris Cherny, 今村 謙士, 原 隆文 |本 | 通販 | Amazon

https://www.seleniumqref.com/api/ruby/window_get/Ruby_title.html

class属性から要素を取得する

page.find_element(:class, 'a-text-price').text

https://www.seleniumqref.com/api/ruby/element_get/Ruby_find_element_class_name.html

find_elementとfind_elementsの違い

find_elementが最初に一致する要素を返すのに対しfind_elementsは一致するすべての要素を配列で返します。

string型の金額データをint型にする

string型のままでは金額を比べられないので、int型にします

price.delete('^0-9').to_i

delete('^0-9')で数値以外の¥,を取り除いて、to_iでint型にしています。

LINEに通知する

https://blog.hatena.ne.jp/kei_kmj/kei-kmj.hatenablog.com/edit?entry=4207112889921799540

ソースコード

https://github.com/kei-kmj/price_notifier

Nokogiriというgemを使う方法もある

require 'open-uri'
require 'nokogiri'
require 'net/http'

html = URI.parse(FETCH_URL).open
page = Nokogiri::HTML.parse(html)

でも同じことができます。

https://nokogiri.org/

Amazon.co.jpを題材に、クローリングとスクレイピングの利用規約と法律を確認する

Amazonの商品ページをスクレイピングしたかったので調べてみました。

クローリング

クローリングとは

インターネット上の膨大なコンテンツを、HTMLのリンクをたどって巡回し、機械的に情報収集することを、クローリング又はクロールと言います。

クローリングの制限

robots.txtを見れば、サイトのクローリングの可否が分かります。 robots.txtは必ずドキュメントルートに配置してあるので、https://www.amazon.co.jp/robots.txt を見てみます。


User-agent: * はすべてのクローラーが対象となる、といいう意味
Disallow :対象のクローラーにアクセスして欲しくないパス
Allow :対象のクローラーがアクセスして良いパス

ほとんどDisallowですが、一部Allowもあります。

スクレイピング

スクレイピングとは

ウェブスクレイピングとは、ウェブサイトにある情報を抽出するコンピュータソフトウェアの技術のこと
内閣府 「ウェブスクレイピングを用いた価格指数の推計に関する調査研究報告書

Amazon利用規約

Amazon.co.jp利用規約 によると、 とあり、この規約により、Amazonスクレイピング禁止だと言われています。

総務省と法律事務所の見解

総務省 消費者物価指数 (CPI) へ のウェブスクレイピングの活用について より

ということは、

  • 利用者の制限のない、だれでもアクセスできるページの
  • 著作物でない情報を
  • 手動と同じ程度のアクセス速度

で取得すれば、スクレイピングしても問題なさそうです。

参考にした書籍とサイト