Rails で Firebase Authentication を使う場合のユーザーモデルとテスト

Firebase Authentication を使う場合、ユーザーモデルとテストはどうする?

Rails で個人用の英単語帳アプリを開発しています。開発環境は Rails 7.0.4 + Ruby 3.1.2 です。

ユーザ認証に Firebase Authentication を導入すると、フォームに入力した email と password を Firebase Authentication API に渡して返り値を受け取るだけで認証が実装できます。認証のためにユーザーモデルを作成する必要はありません。

ただし、ユーザごとのデータを保存するテーブルの関連付けとしてユーザーモデルは必要になります。私の場合、英単語帳を flaschcards というテーブルに保存しているため、「誰が登録した英単語か」を識別するためにユーザーモデルを作成することにしました。ユーザーを作成するために外部の Firebase Authentication API を実行する必要があるため、(Devise 利用時など内部で完結する場合と違い)テストの方法も一工夫する必要が出てきました。

以下は Firebase Authentication を使った場合のユーザーモデルとテストの実装についてまとめたものです。同様の構成を考えている方の参考になれば幸いです。最期に Devise との個人的な比較と所感を載せています。

実装の流れ

大きな流れとしては以下になります。Firebase Authentication API を叩いた時に返ってくる localId (Firebase の管理画面上では User UID と表記される) をユーザーモデルに格納して利用します。

  1. FIrebase authentication を利用して認証機能を実装する(省略)
  2. ユーザーモデル(User)を作成する
  3. Firebase authentication の API で返される localId で User を作成する
  4. ユーザごとのデータを持つフォームを修正する(省略)
  5. テストを作成する(ユーザ作成のために外部の Firebase Authentication API の利用が必要ため、モデルやコントローラーのテストではなくシステムテストで行う)

ユーザーモデル(User)を作成する

$ bundle exec rails g model User firebase_local_id:string
$ bundle exec rails g migration AddUserIdToFlashcards user:references

app/models/user.rb

class User < ApplicationRecord
  has_many :flashcards
(... snip...)

app/models/flashcard.rb

class Flashcard < ApplicationRecord
  belongs_to :user
(... snip ...)

Firebase authentication の API で返される localId で User を作成する

app/controllers/home_controller.rb

class HomeController < ApplicationController
  before_action :set_user_data, only: %i[signup login]

  def signup
    uri = URI("https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=#{Rails.application.credentials.firebase_api_key}")
    response = Net::HTTP.post_form(uri, "email": @email, "password": @password)
    data = JSON.parse(response.body)
    session[:data] = data
    session[:firebase_local_id] = data["localId"]

    if response.is_a?(Net::HTTPSuccess)
      User.create(firebase_local_id: data["localId"])
      redirect_to flashcards_path, notice: "Signed up successfully"
    end
  end

  def login
(... snip ...)

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  helper_method %i[current_user authenticate_user]

  def authenticate_user
    redirect_to home_login_path, notice: 'You must be logged in to view your data.' unless current_user
  end

  def current_user
    @current_user ||= session[:firebase_local_id]
    @current_user_id ||= User.find_by(firebase_local_id: session[:firebase_local_id]).id
  end
end

テストを作成する(ユーザ作成のために外部の Firebase Authentication API の利用が必要ため、モデルやコントローラーのテストではなくシステムテストで行う)

test/system/flashcards_test.rb

require "application_system_test_case"

class FlashcardsTest < ApplicationSystemTestCase
  setup do
    @user = FactoryBot.create(:user)
    @flashcard = FactoryBot.create(:flashcard)

    visit home_signup_url

    fill_in "email", with: "foo@example.com"
    fill_in "password", with: "abc123"

    click_on "Submit"

  end

(... snip ...)

Devise と Firebase Authentication の比較

個人的な結論

Firebase Authentication のほうが扱いやすいように感じました。理由としては API を叩くだけで完結するのと、自分で情報を持たないからです。Devise を利用したユーザーモデル(User)は内部にメールアドレス、暗号化されたパスワード、その他接続時の情報を抱えるようです。接続時の情報など(例: current_sign_in_ip)は安易に流出しないようにデフォルトでは Trackable が無効になっています。こうしたガードレールがあるので、基本的には安全だと思いますが Devise の実装をきちんと理解していない限り、ブラックボックスの部分が残ってしまうように思います。内部に情報を持たず API だけで完結する Firebase のほうが扱いやすい、と感じたのはこうした理由からです。

個人的な比較

Devise Firebase Authentication
透明性 きちんと理解していないとブラックボックスな点が残る 基本的に API を使うだけなので単純明快に感じる
情報の見つけやすさ Deviseのドキュメントやネットの記事はあふれている 記事は多くないのでモデルやテストは自作する必要がある(簡単)
心理的安全性 内部に情報を抱えるので不安な点が残る(理解していれば大丈夫) 内部に情報を抱えないので不安にならない
ユーザーモデル(User)の大きさ 肥大化しがち 使い方にもよるが他テーブルへユーザIDを関連付けるだけなら Firebase authentication が返す locaIId だけでいい

比較時のスクリーンショット

Devise

Firebase Authentication

参考情報

Firebase authentication

Rails7 で Firebase authentication を使う方法

www.youtube.com

Devise と Firebase Authentication のどちらを使うべきか?

unbuffer と tee の組み合わせで標準出力とログのカラーを保持する

tee に渡すとカラー出力が打ち消されて色なしになってしまう

開発環境の構築などの定型処理は Rakefile でタスクを定義して、バッチ処理として実行しています。 この際にログも保持するようにしているのですが、単純に tee に渡すだけでは標準出力もログもカラー出力が打ち消されてしまうことに気がつきました。

$ rake init 2>&1 | tee -a init.log

これを解決するためには unbuffer を利用すればよいことがわかりました。本来の unbuffer の使い方とは少し違いますが... ログもエスケープシーケンスつきの色つきで保存されているので、less -R で色つきのまま確認することができます。

$ unbuffer rake init 2>&1 | tee -a init.log

参考情報

superuser.com

Real World Exception Handling

奇数(odd)がだめなら偶数(even)にすればいいじゃない

スプレッドシートからデータをインポート処理を書いています。指定地点 s から指定された h[:x], h[:y] の座標分、セルを切り取って配列(matrix) とハッシュ(kv_pair_matrix) のどちらからでもデータを利用可能にしてエクスポート処理につなげようと考えていました。

配列からハッシュへの変換に self[*key_and_value] -> Hash を利用したところ、配列の個数 (x - s) が奇数だとハッシュに変換するときに odd number of arguments for Hash (ArgumentError) の例外が発生しました。言われてみればその通りなのですが、とりあえず、対象範囲の読み込みを済ませるために、この場合は例外処理としてお尻に nil を足して偶数にしてリトライさせてしまいます。奇数(odd)がだめなら偶数(even)に(以下略)の、アントワネット的発想です。あとで nil が入った配列をチェックする処理を書けばいいですし。

(Hash.[] (Ruby 3.1 リファレンスマニュアル))

(... snip ...)
    @matrix = CSV.read(@in, headers: false).drop(1).map do |a| a[h[:s], h[:x]] end

    @matrix.each do |e| e.each do |x| if x.is_a? String then x.delete!("\n") end end end
    @kv_pair_matrix = @matrix.map do |e|
      begin
        Hash[*e]
      rescue ArgumentError
        # This is for the error: `odd number of arguments for Hash (ArgumentError)`
        e.push(nil) # make it even now!
        retry
      end
    end
(... snip ...)

Real world ではこういったインターフェイス部分の violation はよく発生しがちですね (´・ω・`)

ansible の command module を使用して複数ホストのログを確認する

複数ホストへコマンドを一括実行したい時

複数ホストに一括してコマンドを実行したい時がたまにあります。繰り返す作業ではなく、ざっとログにエラーメッセージが出ていないかなどをアドホックに確認したい時など。これは ansible の command module を利用することで実現できます。

$ pyenv exec ansible -i inventory webservers -u ec2-user --become -K --become-user root -a 'bash -c "grep slow_flush_log_threshold /var/log/td-agent/td-agent.log"'

$ cat inventory
[webservers]
web001
web002
web003

ansible.builtin.command の他にも ansible.builtin.shell モジュールもあります。パイプやリダイレクトはじめちょっと凝ったことをやりたい場合はこちらのほうがいいかもしれません。

So far all our examples have used the default ‘command’ module. To use a different module, 
pass -m for module name. For example, to use the ansible.builtin.shell module:

$ ansible raleigh -m ansible.builtin.shell -a 'echo $TERM'

https://docs.ansible.com/ansible/latest/user_guide/intro_adhoc.html

同様の作業(run commands on multiple hosts via ssh) を昔は pdshcapistranocap shell を使用して実行していました。対象ホストが3桁を超えるとさすがに煩雑になりますが、問題解決にあたってログの標本を抽出して確認したい場合なのには有用だと思います。

参考情報

ひとつの入力語に対して複数カラムを対象にして検索を行い結果をまとめる

「空気を読む」検索

Rails の検索には Ransack が重用されます。Ransack では基本的にモデルの単一カラムに対して cont(contain) や eq(equal) といった条件で検索が可能です。この場合、ユーザーがひとつの用語で複数のカラムを検索したい場合、別々の入力フォームで複数回の入力を求めることになります。ここで、ひとつの検索語で複数カラムを一気に実行できたらどうでしょうか。これはいわゆる「空気を読む」検索と言えるかもしれません。

ゲームでの例を挙げます。例えば「龍飛」という入力をして検索をするユーザーはアイテム名「龍飛剣」を探しているのかもしれませんし、スキル名「龍飛の心得」を探しているのかもしれません。もしくは両方かもしれません。

Ransack でこの「空気を読む」検索を実現するためには入力語に対してモデルの複数カラムで検索を行い、結果の集約(と重複排除やソート)を行う必要があります。ここで Ruby の Runnable Object(Proc) を使用すると便利です。この実装で The Well-Grounded Rubyist, Third Edition の Chapter 14 で紹介されている Callable and runnable objects をはじめて有効活用できた気がします。

class EndpointsController < ApplicationController
  def search
    @input = params[:input]

    if (@input.empty?) then
      @input = "nothing"
    end

    @items = Item.search(@input)
    @skills = Skill.search(@input)

    @outcome = empty?([@items, @skills])
  end

  def empty?(arg)
    (... snip ...)
  end
class Item < ApplicationRecord

  class << self

    def search(arg)
      result = {}
      result[:by_name] = search_by_name(arg)
      result[:by_description] = search_by_description(arg)
      result[:by_parameter] = search_by_parameter(arg)
      result
    end

    def search_by_name(arg)
      p = Proc.new do |x|
        { name: x.name, level: x.level, description: x.description,
          rarity: x.rarity.name, skills: x.skills.map(&:name) }
      end
      Item.ransack(name_cont: arg).result.map(&p) end
    end

    def search_by_description(arg)
      (... snip ...)
    end

(... snip ...)

参考情報

www.manning.com

AWS CDK で EC2 を構築する時の地雷処理

古い AWS アカウントの呪い(?)

私の AWS アカウントでは Tokyo リージョン(ap-northeast-1) の Availability Zone の a が使用できません。使用できるのは b と c だけです。これはアカウントを作成した時期(2011年)に関係しているようです。比較的新しい時期にアカウントを作成した人は Availability Zone の a が利用可能なようです。

この初期勢にまつわるアカウント呪い(?)によって、公開されている CFn や CDK のサンプルで「他の人のアカウント(新しめ)では動く」のに「自分のアカウント(古め)では動かない(古めのアカウント)」ことが稀によくあります。

解決方法として AWS CDK のドキュメント Control over availability zones を参照したところ、コンストラクタが使用可能な Availability Zone の getter を設定してこのアカウントの呪いを教えてあげる必要があることが判明しました。

$ basename $PWD
aws-cdk-examples

$ git diff
diff --git a/typescript/ec2-instance/lib/ec2-cdk-stack.ts b/typescript/ec2-instance/lib/ec2-cdk-stack.ts
index 4cfdec0..bc5b5f9 100644
--- a/typescript/ec2-instance/lib/ec2-cdk-stack.ts
+++ b/typescript/ec2-instance/lib/ec2-cdk-stack.ts
@@ -45,13 +45,13 @@ export class Ec2CdkStack extends cdk.Stack {
     // Use Latest Amazon Linux Image - CPU Type ARM64
     const ami = new ec2.AmazonLinuxImage({
       generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
-      cpuType: ec2.AmazonLinuxCpuType.ARM_64
+      cpuType: ec2.AmazonLinuxCpuType.X86_64
     });

     // Create the instance using the Security Group, AMI, and KeyPair defined in the VPC created
     const ec2Instance = new ec2.Instance(this, 'Instance', {
       vpc,
-      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.MICRO),
+      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.NANO),
       machineImage: ami,
       securityGroup: securityGroup,
       // keyName: key.keyPairName,
@@ -77,4 +77,9 @@ export class Ec2CdkStack extends cdk.Stack {
     new cdk.CfnOutput(this, 'Download Key Command', { value: 'aws secretsmanager get-secret-value --secret-id ec2-ssh-key/cdk-keypair/private --query SecretString --output text > cdk-key.pem && chmod 400 cdk-key.pem' })
     new cdk.CfnOutput(this, 'ssh command', { value: 'ssh -i cdk-key.pem -o IdentitiesOnly=yes ec2-user@' + ec2Instance.instancePublicIp })
   }
+
+  get availabilityZones(): string[] {
+    return ['ap-northeast-1b', 'ap-northeast-1c'];
+  }
+
 }

Getting started with Amazon EKS の地雷処理 もそうですが、Availability Zone 関係は地雷が多い印象です。

AWS CDK の印象

編集 > 実行のサイクルが自動化される watch 機能 や try&error のサイクルを早める --hotswap オプション があります。API の動作確認と実装作業を並行して進められ 各リソースのテスト も可能です。

Imperative approach な IaC の急先鋒として

個人的に Declarative approach(what) な CloudFormationImperative approach(how) な CDK を比べると後者は YAML から解放され、「書いていても鬱にならない」 IaC だなと思いました(高度に抽象化されていてやりたいことの書き方が見つからないなど短所もありますが...)

参考情報

Tenets of SRE を Github Issues/Pull Requests のラベルにする

Tenets of SRE を Github Issues/Pull Requests のラベルにする

Github にデフォルトで用意されているラベル(bug, duplicate, enhancement, ...) はアプリ開発向けです。インフラ開発向けの課題管理には SRE Book で定義されている Tenets of SRE(availability, latency, performance, efficiency, change management, monitoring, emergency response, and capacity planning) をラベルにすると管理しやすいと思います。どのラベルでの課題が最も多いのかも、意思決定に活用できます。

SRE(= Site Reliability Engineering) という言葉を生み出した Benjamin Treynor Sloss 自身が こちら で SRE Book を引用しつつ各 tenets を分解してわかりやすく説明しています。例えば、provisioning を change management と capacity planning と結びつけて説明するだけでなく、管理コスト/調達時間がかかる capacity (expensive と表現) と対比して「必要な時に素早く行えること」と定義しています。 その他 demand forecasting を organic growth と inorganic growth に分類して予測するなど各 tenet を実際にどうやって実装していくかについてヒントとなるものが多いのではないでしょうか。

参考情報

バッチ処理を cron から k8s cronjob に移行する

Shamshir のスタンドアローン版を k8s(minikube) の cronjob に移植した記録です。

cron でバッチを動かす時の問題点

node, ruby, python 他インタプリンタ系の言語でプログラムを作ると nodenv + npm/yarn, rbenv + bundler, pyenv + pip などのエコシステムを使って環境構築することになります。

これを cron で動かそうとすると ~/.nodenv/shims/node などエコシステム上の実行ファイルを crond に教えてあげる必要が出てきます。このため起動方法を bash -l cron.sh など(-l: Make bash act as if it had been invoked as a login shell (see INVOCATION below) 個人の動作環境をシミュレートするような特別な「忖度」が必要になってくる場合も。

このため 自分の環境では動くのに crontab に設定すると動かない! どうして!? となってしまうことが稀によくあります。(^_^; エコシステムを切り出したコンテナを作ることでこういった問題は解決でき、デプロイ環境が安定します。

もともとコンテナの定義として Content-agnostic, Infrastructure-agnostic という agnostic(= someone who believes that people cannot know whether God exists or not 神様がいるかどうかなんかわかりっこない = 神がいようといまいと関係ない: X に依存しない?) という概念があります。これが、コンテナが Content-agnostic (中身がスクリプトであろうがバイナリであろうが何のプログラミング言語で書かれていようが適切なイメージであれば動く)、Infrastructure-agnostic(物理でも仮想でもLinuxでもMacでも runc や gVisor といった OCI ランタイムがあればそこで差分を吸収して関係なく動く) と呼ばれる所以だと思います。

k8s cronjob はシンプルなバッチ実行環境ですが、プロダクション環境では依存関係などを設定可能な argo などのワークフローに載せることで処理の分割、依存関係の設定、部分実行/再実行がしやすくなるはずです。

k8s cronjob に移植する時の注意点

  • minikube でレジストリからイメージを DL せずローカルのイメージを使う場合( imagePullPolicy: Never )、eval $(minikube docker-env) で minikube の仮想 worker の docker context に切り替えて docker build する必要がある(下記 docker context の切り替えを参照)

docker context の切り替え

1. もともとの docker context は DOCKER ENDPOINT に unix socket 通信を使う

$ docker context ls
NAME        DESCRIPTION                               DOCKER ENDPOINT               KUBERNETES ENDPOINT                   ORCHESTRATOR
default *   Current DOCKER_HOST based configuration   unix:///var/run/docker.sock   https://192.168.49.2:8443 (default)   swarm

$ docker images
REPOSITORY                    TAG         IMAGE ID       CREATED          SIZE
thetitle                      latest      203fc3c16979   25 minutes ago   125MB
thetitle                      v1.0        203fc3c16979   25 minutes ago   125MB
kyagi/thetitle                1.0         22d1a9e10b4a   42 minutes ago   125MB
node                          16-alpine   0e1547c0f4a4   3 weeks ago      110MB
gcr.io/k8s-minikube/kicbase   v0.0.29     64d09634c60d   2 months ago     1.14GB

2. eval $(minikube docker-env) で切り替えると

$ eval $(minikube docker-env)

3. minikube の仮想 worker の docker context になり DOCKER ENDPOINT が tcp://192.168.x.x になる。minikube にイメージを渡すためにはこちらのコンテキストで docker build する必要がある

$ docker context ls
NAME        DESCRIPTION                               DOCKER ENDPOINT           KUBERNETES ENDPOINT                   ORCHESTRATOR
default *   Current DOCKER_HOST based configuration   tcp://192.168.49.2:2376   https://192.168.49.2:8443 (default)   swarm
Warning: DOCKER_HOST environment variable overrides the active context. To use a context, either set the global --context flag, or unset DOCKER_HOST environment variable.

$ docker images
REPOSITORY                                TAG         IMAGE ID       CREATED             SIZE
shamshir                                  v1.0        145688e57875   About an hour ago   165MB
thetitle                                  v1.0        39a4338f4a4f   About an hour ago   125MB
<none>                                    <none>      da981f50f1eb   2 hours ago         125MB
<none>                                    <none>      f062c5aecb69   2 hours ago         125MB
kyagi/kubia                               latest      d5e0f5c0c6f0   13 days ago         906MB
node                                      16-alpine   0e1547c0f4a4   3 weeks ago         110MB
busybox                                   latest      ec3f0931a6e6   3 weeks ago         1.24MB
node                                      14-alpine   755b96824e40   4 weeks ago         119MB
k8s.gcr.io/kube-apiserver                 v1.23.1     b6d7abedde39   2 months ago        135MB
k8s.gcr.io/kube-proxy                     v1.23.1     b46c42588d51   2 months ago        112MB
k8s.gcr.io/kube-scheduler                 v1.23.1     71d575efe628   2 months ago        53.5MB
k8s.gcr.io/kube-controller-manager        v1.23.1     f51846a4fd28   2 months ago        125MB
k8s.gcr.io/etcd                           3.5.1-0     25f8c7f3da61   4 months ago        293MB
k8s.gcr.io/coredns/coredns                v1.8.6      a4ca41631cc7   4 months ago        46.8MB
k8s.gcr.io/pause                          3.6         6270bb605e12   6 months ago        683kB
kubernetesui/dashboard                    v2.3.1      e1482a24335a   8 months ago        220MB
kubernetesui/metrics-scraper              v1.0.7      7801cfc6d5c0   8 months ago        34.4MB
gcr.io/k8s-minikube/storage-provisioner   v5          6e38f40d628d   11 months ago       31.5MB

4. ログアウトするともとの context にもどる

いままでとこれから

いままで)

$ cat cron.sh
#!/usr/bin/bash

set -m
shamshir_pat=<HERE_IS_TOKEN> node shamshir-stand-alone.js --owner kyagi --repo awesome-project --label "releasable" --quorum 2

$ crontab -l
*/15 10-19 * * 1-5 cd /home/ec2-user/git/shamshir/src;  bash -l -c /home/ec2-user/git/shamshir/src/cron.sh

これから)

$ kubectl config current-context
minikube

$ kubectl get cronjobs
NAME       SCHEDULE           SUSPEND   ACTIVE   LAST SCHEDULE   AGE
shamshir   */15 10-19 * * *   False     0        16s             9m49s

$ cat cronjob5.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: shamshir
spec:
  schedule: "*/15 10-19 * * *"
  jobTemplate:
    spec:
      backoffLimit: 5
      ttlSecondsAfterFinished: 100
      template:
        spec:
          containers:
          - name: shamshir
            image: shamshir:v1.0
            env:
              - name: TZ
                value: Asia/Tokyo
              - name: shamshir_pat
                valueFrom:
                  secretKeyRef:
                    name: shamshir
                    key: pat
            imagePullPolicy: Never
          restartPolicy: OnFailure
      parallelism: 1
      completions: 1
  concurrencyPolicy: "Forbid"
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 5

$ cat secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: shamshir
type: Opaque
stringData:
    pat: HERE_IS_TOKEN

$ cat Dockerfile
FROM node:16-alpine

# Create app directory
WORKDIR /app

COPY package*.json ./

RUN npm install

# Bundle app source
COPY . .

WORKDIR /app/src

CMD [ "node", "shamshir-stand-alone.js", "--owner", "kyagi", "--repo", "awesome-project", "--label", "releasable", "--quorum", "2" ]

$ kubectl logs shamshir-27439718-hl7jf
{"level":"info","message":"Shamshir started.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:22"}
{"level":"info","message":"Shamshir got pulls: 2602,2598,2596,2575,2573,2557,2553,2551,2540,2539,2481,2478,2295,2281,1981,1951,1685","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:22"}
{"level":"info","message":"Shamshir added releasable label to pull/2598.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:24"}
{"level":"info","message":"Shamshir added releasable label to pull/2596.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:24"}
{"level":"info","message":"Shamshir added releasable label to pull/2575.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:25"}
{"level":"info","message":"Shamshir removed releasable label from pull/2573.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:26"}
{"level":"info","message":"Shamshir added releasable label to pull/2551.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:28"}
{"level":"info","message":"Shamshir added releasable label to pull/2540.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:29"}
{"level":"info","message":"Shamshir added releasable label to pull/2539.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:30"}
{"level":"info","message":"Shamshir added releasable label to pull/2478.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:31"}
{"level":"info","message":"Shamshir added releasable label to pull/2295.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:32"}
{"level":"info","message":"Shamshir added releasable label to pull/1951.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:34"}
{"level":"info","message":"Shamshir finished.","mode":"live","owner":"kyagi","repo":"awesome-project","service":"shamshir","timestamp":"2022-01-31 00:54:34"}

参考情報

minikube.sigs.k8s.io

Chrome で検索対象の言語を英語のみにして検索する

設定方法

  1. URL バーを右クリックして「検索エンジンを管理」のメニューを表示します。 f:id:kyagi:20220308054556p:plain

  2. メニューを以下のように修正します。検索エンジン名とキーワード(トリガーとなるキー)をお好みで指定します。

https://www.google.com/search?gl=us&hl=en&gws_rd=cr&pws=0&q=%s

f:id:kyagi:20220308054649p:plain

使用方法

URL バーで「e」+「スペース」をタイプすると検索エンジンの設定が上記で設定した「Google(e)」に切り替わり英語のみ検索対象になります。

f:id:kyagi:20220308054612p:plain

上記の設定で検索した場合(左)と普通に検索した場合(右) の比較

f:id:kyagi:20220308054630p:plain

aws-minikube: ELB とも連携できてフットワークが軽い AWS k8s 環境

k8s 環境として minikube, eks, aws-minikube を比較してみました(k3s は未経験)。個人的には aws-minikube がコスト面/機能面でよさそうに思えたので紹介です。

aws-minikube とは?

EC2 1 台で k8s 環境が整うツールです。Red Hat 社の Jakub Scholz 氏が 2017 年から開発しています。最新の k8s への対応も早いです。 terraform ベースの aws-minikube と kubeadm ベースの aws-kubernetes の 2 段揃えの構成です。

github.com

(日本ではあまり知られていないようです... 私は minikube で ELB も使えるような拡張がないかな... とぐぐっていた時にたまたま見つけました)

使用する AWS リソース

terrafrom apply すると my-minikube という EC2 と f:id:kyagi:20220221201110p:plain

my-minikube.<your.domain.here> という Route53 レコードが追加されます。 f:id:kyagi:20220221201142p:plain

Security Group も作成されます。以下では自宅 IP に変更していますがデフォルトは 0.0.0.0/0。6443 は k8s の API をたたくポートで専用の .kube/config (認証情報を含む設定ファイル) が構築時に発行されます。 f:id:kyagi:20220221202550p:plain

何が嬉しいのか

  • 1 台で k8s 環境がそろう
  • 使う ec2 は t2.medium でコストが安い。1 ヶ月 4377 円(= 0.0608 USD * 100 * 24 * 30)。これはデフォルトで t3.medium 2 台を使って EKS 代も徴収する EKS の 1/3。環境構築のスピードも早いのでそのたびに作って壊してもストレスがない。
  • 環境構築のスピードがはやい。EKS だと cluster 作成に 20 分くらいかかるけどこちらは 5 分ほど。
  • minikube の制約だったサービスの外部公開が簡単に可能。service を type:LoadBalancer で create/delete すると応じて ELB が作られる/削除される
  • いじる aws リソースが最小限(EC2, Route53, SecurityGroup)
  • terraform apply/destroy でまるごと消せる

想定する使い方

開発者ごとに t2.medium の EC2 1 台を支給し、手軽に k8s を試してもらって組織の k8s 力をあげる、といった使い方がよさそうです。

minkube, aws-minikube, eks の比較

候補 お手軽さ 時間 できること コスト 後始末
minikube
aws-minikube
eks

コスト内訳

候補 コスト構成 コスト/日 コスト/月
minikube ローカルの Mac で動かせば無料、EC2 の場合は EC2 料金(*1) 146円 4,377 円
aws-minikube EC2 料金(*1) 146円 4,377円
EKS EC2 料金(*2) + EKS 料金 440円 13,200円
  • *1 t2.medium で計算(vCPU が 2 つある EC2)
  • *2 t3.medium * 2 で計算(EKS がデフォルトで選択する)

使い方

*ほぼ README 通りですが AWS MarketPlace の centos AMI を subscribe する必要があります。無料です)

1. centos の AMI を subscribe する(これをしないと terraform apply 時に止まります)

https://aws.amazon.com/marketplace/pp/prodview-qkzypm3vjr45g

f:id:kyagi:20220221201412p:plain

2. 設定ファイルを書き換える

$ git diff
diff --git a/example.tfvars b/example.tfvars
index 177a1fe..a16914c 100644
--- a/example.tfvars
+++ b/example.tfvars
@@ -1,5 +1,5 @@
 # AWS region where should the Minikube be deployed
-aws_region = "eu-central-1"
+aws_region = "ap-northeast-1"

 # Name for role, policy and cloud formation stack (without DBG-DEV- prefix)
 cluster_name = "my-minikube"
@@ -8,13 +8,13 @@ cluster_name = "my-minikube"
 aws_instance_type = "t2.medium"

 # SSH key for the machine
-ssh_public_key = "~/.ssh/id_rsa.pub"
+ssh_public_key = "~/.ssh/id_rsa.ubuntu.com.amazonaws.info.myservice.dev.all.pub"

 # Subnet ID where the minikube should run
-aws_subnet_id = "subnet-8a3517f8"
+aws_subnet_id = "subnet-0a102dc87cd20932e"

 # DNS zone where the domain is placed
-hosted_zone = "my-domain.com"
+hosted_zone = "my.service.com"
 hosted_zone_private = false

 # AMI image to use (if empty or not defined, latest CentOS 7 will be used)

3. terraform apply する

f:id:kyagi:20220221201437p:plain

(... snip ...)

f:id:kyagi:20220221201452p:plain

4. EC2 がたちあがるので SSH ログインする

5. config ファイル作成して完了

[centos@ip-10-0-0-87 ~]$ mkdir .kube
[centos@ip-10-0-0-87 ~]$ cp kubeconfig ~/.kube/config
[centos@ip-10-0-0-87 ~]$ kubectl get svc --all-namespaces
NAMESPACE     NAME                        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                  AGE
default       kubernetes                  ClusterIP   10.96.0.1        <none>        443/TCP                  11h
kube-system   dashboard-metrics-scraper   ClusterIP   10.103.238.218   <none>        8000/TCP                 11h
kube-system   kube-dns                    ClusterIP   10.96.0.10       <none>        53/UDP,53/TCP,9153/TCP   11h
kube-system   kubernetes-dashboard        ClusterIP   10.96.254.133    <none>        443/TCP                  11h
kube-system   metrics-server              ClusterIP   10.103.193.240   <none>        443/TCP                  11h

6. サービスを作って ELB で外部公開

[centos@ip-10-0-0-87 ~]$ kubectl create deployment kubia --image=kyagi/kubia --replicas=3 --port=8080
deployment.apps/kubia created

[centos@ip-10-0-0-87 ~]$ kubectl expose deployment kubia --port=8080 --target-port=8080 --type=LoadBalancer --name=kubia
service/kubia exposed

[centos@ip-10-0-0-87 ~]$ kubectl get svc
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP                                                                    PORT(S)          AGE
kubernetes   ClusterIP      10.96.0.1       <none>                                                                         443/TCP          11h
kubia        LoadBalancer   10.97.188.164   a81e8fb4f947f444f9bae599cf38ca3a-2070947448.ap-northeast-1.elb.amazonaws.com   8080:31678/TCP   10s

[centos@ip-10-0-0-87 ~]$ kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
kubia-7f585c778c-k4t5h   1/1     Running   0          10m
kubia-7f585c778c-nkzbr   1/1     Running   0          10m
kubia-7f585c778c-p77hr   1/1     Running   0          10m

(外部公開を確認) f:id:kyagi:20220221201516p:plain

(ELB が作られている) f:id:kyagi:20220221201537p:plain