Fly.io のファーストインプレッション

Heroku の料金体系改定

Heroku’s Next Chapter | Heroku でアナウンスされている通り 11/28 から heroku の料金体系が改定されます。無料枠の FreeHobby プランは 11/28 で廃止されます。そのため、多くのエンジニアが無料枠を求めて fly.io, railway, render などの PaaS を候補に引っ越し準備をはじめているようです。

個人的には heroku の 新プランEcoBasic もかなり良心的だと思います。無料で使えなくなるのが残念な反面、価値のあるものにはきちんと対価を払いたいとも考えています。また、数ドルを惜しんで各 PaaS のコマンドや手順で自分のツールボックを書き換えるのも少し手間だな... といったようにベンダーロックイン脳になっているところもあります。(^_^;

今まで他の PaaS を使ったことがなかったため、Heroku への「刷り込み」も少なからずあります。このバイアスを是正するためには比較対象となる PaaS を使ってみるのがよいと考えました。今回は CDN とは別軸での Edge Computing の考え方と LiteFS に積極的な姿勢で、 Fly.io を使ってみます。

Fly.io の特徴

Fly.io を使って個人の Rails アプリをデプロイしてみました。まだ不明な部分も多いですが概要と気になった特徴を以下にまとめておきます。

  • デプロイコマンド(fly launch + fly deploy)すると Dockerfile の作成とイメージビルドが行われ、app 用と db 用(postgresql) のコンテナが 2 つ作成される。
  • db 用のコンテナの内部アドレスとパスワードは提供されるが外部アドレスは提供されない。ただし fly proxyで db 用コンテナまで ssh tunnel を開通することができる。
  • drop database や drop table ができない ため sequel での流し込みはできない? db はその都度作り直す流儀らしい。
  • 最初から grafana で各コンテナ用に詳細なメトリクスが用意されているのはよい。

作成されるファイル

how can I drop and re-create a production database attached to my app? - Questions / Help - Fly.io

$ git bn deploy/add-flyio
.dockerignore
Dockerfile
fly.toml
journal/20221104165758_add_flyio.md
journal/commands
lib/tasks/fly.rake

$ grep bn ~/.gitconfig
  bn = "!f() { top=$(git log --oneline --walk-reflogs ${1} | head -1 | awk '{print $1}'); bottom=$(git log --oneline --walk-reflogs ${1} | tail -1 | awk '{print $1}'); git diff ${bottom}^ ${top} --name-only; }; f"
  • grafana

開発環境の sqlite3@mac とステージング環境の postgresql@heroku をデータベースツールキット sequel とカスタム rake タスクで同期する

sqlite3 DB(on Mac) と postgresql DB(on Heroku) を同期する

私の場合 Rails の開発環境とステージング環境をそれぞれ以下のように構築しています。開発に伴う動作確認や実際の英単語の入力は開発環境(Mac mini)で行うことがほとんどなので、自然と両環境での DB に差分が発生します。

環境 ホスト DB
開発環境 Mac mini sqlite3
ステージング環境 Heroku postgresql

ここではデータベースツールキットの sequel とカスタム rake タスクを使用して両環境の DB を同期する方法を紹介します。同様の環境で開発している方の参考になれば幸いです。

実装内容

rails コマンド(rake タスク) に独自のネームスペースとして custom を用意し、その配下に db:sync タスクを定義します。タスク自体は heroku コマンドと sequel を組み合わせた単純なものです。

$ bundle exec rails -T | grep custom
rails custom:db:sync                     # Synchronize db between development and staging

lib/tasks/custom.rake

namespace :custom do
  namespace :db do
    desc "Synchronize db between development and staging"
    task :sync do
      app = `heroku apps | sed '/^$/d' | tail -1`.chomp
      postgresql_credentials_url = `heroku pg:credentials:url | tail -1`

      sh "heroku pg:reset -a #{app} --confirm #{app}"
      sh "bundle exec sequel -C sqlite://db/development.sqlite3 #{postgresql_credentials_url}"
      sh "heroku run rake db:migrate"
    end
  end
end

Gemfile

group :development do
(... snip ...)

  # Use the database toolkit for ruby [https://github.com/jeremyevans/sequel]
  gem "sequel"

(... snip ...)
end

group :staging do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "pg", platforms: %i[ mri mingw x64_mingw ]
end

カスタム rake タスクから heroku コマンドと sequel を呼び出した様子

LiteFS

SQLite 推しなので LiteFS の今後に注目しています (^_^)

参考情報

github.com

Connecting to Heroku Postgres | Heroku Dev Center

SQLite3からPostgreSQLへのデータ移行

fly.io

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