Heroku SSL Endpoint から Automated Certificate Managementへ移行してみた
Herokuの公式ドキュメントを読むとSSLを導入する方法は
の3種類がありますが、Automated Certificate Management (ACM)にすると
というメリットから、アドオンで提供されていたHeroku SSL Endpoint から移行してみました。
移行の際、一旦Heroku SSLに移行してからでないとダウンタイムが発生してしまうということで
のように2段階に分けて移行を行いました。
具体的な手順は公式の以下のページを参照しました。
Heroku SSL | Heroku Dev Center
- ①Heroku SSL Endpoint => Heroku SSL 移行
- ②DNSのCANME変更
- ③Heroku SSL => Automated Certificate Management(ACM) 移行
①Heroku SSL Endpoint => Heroku SSL 移行
既存のSSL証明書の用意
まずは、SSL Endpointに登録したものと同じSSL証明書、秘密鍵を用意します。
ここでは
とします。
Heroku SSLに証明書と秘密鍵を追加
以下のようにHerokuコマンドを実行して、Heroku SSLに証明書と秘密鍵を追加します。
$ heroku certs:add example.crt example.key --type sni -a 【project-name】
SNIは、Heroku SSLが使っているTLS protcolを拡張したもののようです。
Behind the scenes Heroku SSL uses Server Name Indication (SNI), an extension of the TLS protocol, which is widely supported in modern browsers.
SSL証明書の追加確認
Heroku SSLに証明書が追加されたことを確認します。2つ目のようにTypeがSNI
で追加されていればOKです。
$ heroku certs -a 【project-name】 Name Endpoint Common Name(s) Expires Trusted Type ────────────────── ──────────────────────── ──────────────────────── ────────── ********** **********.herokussl.com ********** 2020-03-26 01:20 UTC True Endpoint ********** (Not applicable for SNI) ********** 2020-03-26 01:20 UTC True SNI
DNS targetの確認
次にDNSに設定するtargetを確認します。***.herokudns.com
のようなエンドポイントが表示されます。
$ heroku domains -a 【project-name】 === pplog Heroku Domain ***.herokuapp.com === pplog Custom Domains Domain Name DNS Record Type DNS Target ───────────── ─────────────── ─────────────────────────── ********** CNAME ***.herokudns.com
②DNSのCANME変更
次にDNSの設定画面を開いてCNAMEを変更します。
現状*****.herokussl.comのようなエンドポイントになっているところを新しいエンドポイントに変更します。
digコマンドで、エンドポイントが****.herokudns.comのように切り替わっていることを確認します。
$ dig CNAME 【domain】 ;; ANSWER SECTION: *********.com. 3408 IN CNAME ***.herokudns.com.
DNS設定変更後、24時間後に③のオペレーションを行います。
③Heroku SSL => Automated Certificate Management(ACM) 移行
SSL Endpointアドオンを削除する
$ heroku addons:destroy ssl -a 【project-name】
Automated Certificate Management有効化
$ heroku certs:auto:enable -a 【project-name】
ACMの設定確認
ACMが有効になることを確認します。
heroku certs:auto -a 【project-name】 === Automatic Certificate Management is enabled on 【project-name】
再度、証明書も確認し、TypeがACM
になっていればOKです。
$heroku certs -a 【project-name】 Name Common Name(s) Expires Trusted Type ────────────────── ──────────────────────── ──────────────────────── ────────── ********** ********** 2020-06-29 05:30 UTC True ACM
Rails権限管理ライブラリ cancancan gem バージョンアップ(2.x => 3.x)の際の対応と注意点
Ruby on rails の権限管理ライブラリの中で一番ダウンロード数が多いGemであるcancancanのv3.0が2019年4月にリリースされました。
仕事のプロジェクトで使っているcancancanのバージョンは随分前にv3.0に上げていたのですが、結構大きな変更が入っていることを認識できていなかったことに最近気が付き、これからアップグレードをする方や新規にCancancanを導入する方に向けて、どんなところに注意したら良いのかをまとめてみることにしました。
注意点を認識していないと、気がつかない間に
- N+1(パフォーマンス問題)が発生する
- SQLクエリ実行時にエラーが発生する
といった問題が発生する可能性があります。
Cancancan公式のドキュメントのURLも載せておきます。
GitHub - CanCanCommunity/cancancan: The authorization Gem for Ruby on Rails.
4つの注意点(Breaking changes)
注意点(Breaking changes)は4点ありますが、特に2番目、3番目を意識すると問題の発生を避けられそうです。
①権限を定義するアビリティにSubjectを定義しなくてはいけなくなった
例えば、ダッシュボードに対する権限を指定する場合
can :dashboard
のように書くことができなくなりました。具体的に
can :read, :dashboard
のように、ダッシュボードのリード権限があるという書き方をする必要があります。
- 以前よりDefining AbilitiesのWikiページ で、後者の書き方が載っていたので、あまり前者の書き方をしているプロジェクトはない
- 前者の書き方をしているとエラーが発生するようになった
ため、この注意点に関してはあまり意識しておかなくても問題ないと思います。
しかし
- 権限のテストをあまり書いていないプロジェクト
- アップグレード後に手動でも動作確認をしていない
場合だと、気がつかずにプロダクションに反映する恐れがあるので注意が必要になりそうです。
②Eager loadingを自動的に行わなくなった。
具体的には、このPRの差分になるのですが、今まではモデルに :has_many
などで紐付いているモデルは includes
して読み込んでいたのですが、left_joins
に変わったのでN+1が発生する可能性がでてきました。
具体的には、Book(本)モデルの :has_many
にAuthor(著者)モデルがあるような場合で、書籍一覧(Booksのindexページ)で、著者名(author.name)を表示するようなケースが該当します。
なので
- N+1を検知するbullet gemを導入 (公式でも推奨されている)
- 一覧画面で、N+1が発生し得る箇所で関連モデルを
includes
する
といった対応を行った方が良さそうです。
③Distinctを使うようになった
②と同じ差分で、クエリでdistinctを行うようになったことがわかります。
例えば、load_resource
を読み込んだ結果(ActiveRecord::Relation)に対してorder
やgroup by
を付けて以下のようなクエリになっていた場合は、PostgreSQLの場合クエリ実行時にエラーになります。
SELECT DISTINCT users.id FROM users order by users.email => ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list
このような場合、selectの値を見直し、users.emailを含めるようにするといった対応が必要になります。
④アビリティファイルをマージした際、エイリアスもマージされるようになった
権限が複雑になってくるとアビリティファイルを分けたり、それらのアビリティをマージしたくなってきます。
cancancanのアビリティには、mergeというメソッドが定義されていて以前からマージを行うことができるようになっていましたが、エイリアスはマージされないといった問題があったようです。
- https://github.com/CanCanCommunity/cancancan/issues/468
- https://github.com/CanCanCommunity/cancancan/issues/280
cancancan 3.0でエイリアスもマージできるようになったようですが、Railsプロジェクトはそこまで複雑になる案件には向かないと思うので
- アビリティのマージをしている
- エイリアスも使っている
というのはあまり多くはなさそう。ということでこの4点目は比較的重要度は低そうです。
Rails bulk insertのパフォーマンス比較 (Railsのinsert_allとactiverecord-import)
Railsでbulk insertする方法は主に
の2つの方法があります。パフォーマンス観点では、どちらが良いのか知りたくて検証してみました。
activerecord-importに関しては、READMEを読む限り、いくつかimportの方法がありますが、Rails6のinsert_allと同様にバリデーションを行わない以下2つの方法で比較しました。
- Works with raw columns and arrays of values (fastest)
- => カラム名と値の配列を使う方法
- Works with model objects (faster)
- => モデルオブジェクトを使う方法
結論
結論としては、activerecord-importのカラム名と値の配列を使う方法が一番早かったです。
以下は10万件のレコードをbulk insertした結果なのですが、件数を変えたり、何回か実行してみても、大体同じような結果になりました。レコード件数を変えて、複数回実行して平均を取った結果を以下の比較図に記載しています。
[1] pry(main)> Book.bulk_insert user system total real Rails6 insert_all 13.024230 0.051869 13.076099 ( 14.523158) import (カラム名と値の配列) 11.924948 0.048174 11.973122 ( 13.471458) import (モデルオブジェクト) 20.419098 0.155907 20.575005 ( 22.313445)
環境
検証環境のスペックは以下の通りです。
- OS: Ubuntu 18.04.2 LTS
- PC: メモリ 15.4 G、CPU Itel Core i7-8650U(1.90GHz [コア/スレッド数 4/8])
- MySQL: 5.7.29 (InnoDB)
検証方法
以下のコードを実行しました。
require 'benchmark' class Book < ActiveRecord::Base class << self def bulk_insert # Rails6で導入された insert_all で bulk insert するデータ insert_data = 100_000.times.each_with_object([]) do |_, array| array << { name: 'book name', author: 'book auther', created_at: Time.current, updated_at: Time.current } end # activerecord-import の import(カラム名と値の配列) で bulk insert するデータ import_data = [['book name', 'book auther', Time.current, Time.current]] * 100_000 # activerecord-import の import(モデルオブジェクト) で bulk insert するデータ import_model_data = 100_000.times.each_with_object([]) do |_, array| array << new(name: 'book name', author: 'book auther', created_at: Time.current, updated_at: Time.current) end Benchmark.bm 30 do |r| # Rails6で導入された insert_all で bulk insert transaction do r.report 'insert_all' do insert_all insert_data end raise ActiveRecord::Rollback end # activerecord-import の import(カラム名と値の配列で bulk insert transaction do r.report 'import (カラム名と値の配列)' do columns = [:name, :author, :created_at, :updated_at] import columns, import_data, validate: false end raise ActiveRecord::Rollback end # activerecord-import の import(モデルオブジェクト) で bulk insert transaction do r.report 'import (モデルオブジェクト)' do import import_model_data, validate: false end raise ActiveRecord::Rollback end end end end end
比較図
レコード10万件から1桁ずつ減らして、パフォーマンスを計測して図(値はBenchmarkのReal)にしてみました。Rails6 insert_allは、activerecord-import の import (カラム名と値の配列) よりちょっと遅いくらいだということがわかります。
感想
2020年3月時点では、Rails 6のinsert_allはパフォーマンス面ではactiverecord-importほどではなく、バッチサイズを指定できなかったり、モデルのバリデーションやコールバックが実行できないので、もう少し機能が増えてきたら用途が増えそうという感じがしています。
関わっている案件ではactiverecord-import使っているけど、置き換えるのはまだちょっと先かなと思ってます.
Rails6アプリのデプロイ時にヘルスチェックに失敗した時にやったこと
先日、稼働しているRailsのWebアプリケーションをRails5.2から6.0に上げて本番デプロイしようとした際に、ヘルスチェックに失敗していてデプロイできない状態になっていたのですが、そのときに対応した内容を書いてみます。
何が起こったか
まず、AWS EC2インスタンスにデプロイしようとした際にタイムアウトが発生して、デプロイに失敗していました。 このタイムアウトの原因を探していたら、AWSのターゲットグループのヘルスチェックに失敗していることが分かりました。
// デプロイ失敗時のログ Deploying new task definition................................................................................................................................................................................................................................................................................................. Deployment failed due to timeout. Please see: https://github.com/fabfuel/ecs-deploy#timeout
なぜ、ヘルスチェックに失敗したか
Rails6で導入されたDNSリバインディング対策の機能で、ActionDispatch::HostAuthorization
というミドルウェアがブラウザからアクセスされた時のホストをチェックしてくれるようになりました。
github.com
外部からアクセスされるドメインは設定しておいたのですが、それ以外は設定していませんでした。
WebアプリはAWS ECS(EC2インスタンス)で運用、ターゲットグループのヘルスチェックを有効にしていていたのですが、このヘルスチェックはどうやらプライベートIPアドレスを使っているようで、ホストとして登録しておかないとアクセスできない状態になっていました。
ActionDispatch::HostAuthorization
でホストを許可するための指定の仕方
デフォルトでは、development環境で以下の3つが登録されていて、staging, production環境では設定がないのでホストを指定しておく必要があります。
Rails.application.config.hosts = [ IPAddr.new("0.0.0.0/0"), # All IPv4 addresses. IPAddr.new("::/0"), # All IPv6 addresses. "localhost" # The localhost reserved domain. ]
config/application.rb
や config/environments
の中で
config.hosts << 'my-web-app.com'
のように記載するとホストを指定するとアクセスが許可されるようになります。
具体的に設定した値
EC2などの仮想マシンはVPC内に構築しているので、VPCのIPV4 CIDRの設定を入れることでデプロイできるようになりました。
config.hosts <<IPAddr.new('10.0.0.0/16')
VPCにIPV4 CIDRを追加した時忘れそうなので、そのときまたデプロイできなくなりそう。。という懸念は残っています。 あと、IPアドレスをもう少し絞れたら嬉しいのですがそこまでは分からなかった。。。サブネットのCIDR使えば、もうちょっと範囲を絞れるかな。どなたか知っている方は教えてもらえると嬉しいです。
Rails と Firebase Authentication でJWT認証実装してみた
最近スマホアプリの開発は大体firebaseを使うようになっていて、認証もfirebase authenticationを使っておけば、実装がすごく楽になっていると感じます。
既にRuby on railsでwebアプリケーションを開発していて後からJWT認証を付けようとするとちょっと面倒かと思いますが、 firebase-auth-railsというGemを使って比較的簡単にJWT認証を行えるようになったので設定方法を書いてみます。
基本的にREADME通りで動いたのですが、どんな設定が必要なのかチョット補足します。 スマホアプリのバックエンドにこのGemを使ったので、Rails APIモードで実装したコードを記載しています。
- 環境
- 概要図
- ディレクトリ/ファイル構成
- セットアップ
- 実装
- ApplicationControllerへ追記
- ユーザ登録用のコントローラを作成
- ログインが必要な画面のコントローラを作成
- Routes追加
- デプロイ後のオペレーション
- 補足
環境
概要図
ユーザ登録時、以下のような仕組みでユーザを作ります。
ユーザ登録後、例えばマイ書籍一覧画面のような特定のユーザにしか見れない画面を開く時は以下のような仕組みで特定のユーザを判別します。
ディレクトリ/ファイル構成
├── app │ └── controllers │ └── api │ ├── v1 │ │ └── auth │ │ └── registrations_controller.rb │ ├ application_controller.rb │ └ books_controller.rb └── config └── initializers └── firebase_id_token.rb
セットアップ
Redis
Redisをサーバにインストールしておきます。
$ sudo apt install redis-server
内部でfirebase_id_tokenというgemを使っていて、このgemはRedisが必須のためインストールしておきます。 google x509 証明書とその期限を管理するためにRedisを使っているようです。
Gem追加
Gemfileに以下を追記し、
gem 'firebase-auth-rails'
Gem
をインストールしておきます。
$ bundle
実装
イニシャライザーの追加
config/initializers/firebase_id_token.rb
にRedisとfirebabaseプロジェクトの設定を記載します。
FirebaseIdToken.configure do |config| config.redis = Redis.new config.project_ids = ['firebase_project_id'] end
上記概要図④、⑨でRailsサーバ側でfirebaseにJWT認証するので設定しておきます。
ユーザのuidカラムを追加
firebaseのUIDを保存しておくカラムを追加します。(上記、概要図⑤、⑩で使うカラム)
$ rails g migration AddUidToUsers uid:string
できたマイグレーションを実行しておきます。
class AddUidToUsers < ActiveRecord::Migration[5.1] def change add_column :users, :uid, :string end end
$ rails db:migrate
ApplicationControllerへ追記
以下のようにApplicationControllerにinclude Firebase::Auth::Authenticable
, before_action :authenticate_user
の2行を追加しておきます。
# app/controllers/api/v1/application_controller.rb module Api class V1::ApplicationController < ActionController::API include Firebase::Auth::Authenticable before_action :authenticate_user end end
こうすることでV1::ApplicationController
を継承したコントローラのメソッドにアクセス時、HTTPリクエストヘッダに、「Authorization: “Bearer *****************(トークン)”」を入れるだけで、勝手にJWT認証してくれるようになります。(上記、概要図⑨、⑩の箇所)
ユーザ登録用のコントローラを作成
ユーザ登録用のコントローラ(上記概要図④のRailsサーバ側の処理)はこんな感じで作ってみました。
# app/controllers/api/v1/auth/registrations_controller.rb require_dependency 'api/v1/application_controller' module Api module V1 module Auth class RegistrationsController < V1::ApplicationController skip_before_action :authenticate_user def create FirebaseIdToken::Certificates.request raise ArgumentError, 'BadRequest Parameter' if payload.blank? @user = User.find_or_initialize_by(uid: payload['sub']) if @user.save render json: @user, status: :ok else render json: @user.errors, status: :unprocessable_entity end end private def token_from_request_headers request.headers['Authorization']&.split&.last end def token params[:token] || token_from_request_headers end def payload @payload ||= FirebaseIdToken::Signature.verify token end end end end end
ログインが必要な画面のコントローラを作成
Api::V1::ApplicationController
を継承しているので、indexアクセス時勝手にJWT認証(上記、概要図⑨、⑩の箇所)してくれます。
# app/controllers/api/v1/books_controller.rb module Api module V1 class BookssController < Api::V1::ApplicationController def index render json: current_user.books, status: 200 end end end end
Routes追加
routes.rbには以下のように記載しました。
Rails.application.routes.draw do 〜〜〜 namespace 'api' do namespace 'v1' do resources :books, only: %i(index) namespace 'auth' do post 'registrations' => 'registrations#create' end end end end
デプロイ後のオペレーション
サーバにコードデプロイ後は、firebase_id_tokenのREADME に記載してある以下をrails console
で実行し、Googleのx509証明書をダウンロード・確認することで、JTW認証が動くようになりました。
FirebaseIdToken::Certificates.request FirebaseIdToken::Certificates.present? => true
補足
スマホアプリはReact Nativeで実装したのですが、以下のようなコードになりました。 firebaseパッケージを使っています。
[サインアップ時の処理]
// SignupScreen.js サインアップ画面のコード export default class SignupScreen extends React.Component { 〜〜〜 onSignupPress = () => { firebase.auth().createUserWithEmailAndPassword(this.state.email, this.state.password) .then(apiClient.authenticate, authError); // => サインアップ後、apiClient.authenticate 認証処理を実行 } 〜〜〜 }
[ログイン時の処理]
// LoginScreen.js ログイン画面のコード export default class LoginScreen extends React.Component { 〜〜〜 onLoginPress = () => { firebase.auth().signInWithEmailAndPassword(this.state.email, this.state.password) .then( apiClient.authenticate , authError); // => ログイン後、apiClient.authenticate 認証処理を実行 } 〜〜〜 }
[ログイン処理(ログイン時と、サインアップ時で共通)のコード]
class ApiClient { async authenticate() { const token = await firebase.auth().currentUser.getIdToken(true) const data = { token } postRequest('/api/v1/auth/registrations', data,); } }
Golangとginのリクエストハンドリングについて
最近仕事で使い始めた Golang
( net/http
標準ライブラリ)とWebフレームワーク Gin
のリクエストハンドリングについてどのような仕組みで行っているのかをまとめてみます。
前提
動作確認環境は以下の通りです。
- Ubuntu 18.04.3 LTS
- go 1.13.6
- gin v1.5.0
Golang
マルチプレクサとは
GolangではユーザがWebページにアクセスされた際にマルチプレクサというものを使って、どのページを呼び出すかを決めます。 以下のように、内部構造にURLとページ(ハンドラ)を対応づける構造体を持っていてもので、アクセスされた時のURLから呼び出すページ(ハンドラ)が決まります。
goデフォルトのマルチプレクサ
実際に以下のようにコードを書いて、ローカルで http://localhost:8080/cart
のようにアクセスすると Cart page
と表示されるかと思います。
package main import ( "fmt" "net/http" ) func defaultHandler(writer http.ResponseWriter, request *http.Request) { fmt.Fprintf(writer, "Hello World! (default Page)") } func cartHandler(writer http.ResponseWriter, request *http.Request) { fmt.Fprintf(writer, "Cart page") } func main() { http.HandleFunc("/", defaultHandler) http.HandleFunc("/cart", cartHandler) http.ListenAndServe(":8080", nil) }
このように、 http.ListenAndServe()
メソッドなどでマルチプレクサが指定されていない(nil
の)場合は、マルチプレクサの構造体ServerMux
のインスタンスであるDefaultServerMux
がデフォルトで使われ、手前の行のhttp.HandleFunc()
で登録されたルーティングが処理されます。
[参照コード] go/server.go at bbbc6589dfbc05be2bfa59f51c20f9eaa8d0c531 · golang/go · GitHub
goカスタムのマルチプレクサ
マルチプレクサは、標準ライブラリであるnet/http
以外のものに変更可能です。
標準ライブラリでは、例えば http://localhost:8080/books/100 のように一部がIDになっているような可変のURLの場合に対応できないですが、HttpRouterのような別のマルチプレクサに置き換えることが実現できます。
package main import ( "fmt" "github.com/julienschmidt/httprouter" "net/http" ) func defaultHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) { fmt.Fprintf(writer, "Hello World! (default Page)") } func cartHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) { fmt.Fprintf(writer, "Cart page") } func main() { mux := httprouter.New() mux.GET("/", defaultHandler) mux.GET("/cart", cartHandler) http.ListenAndServe(":8080", mux) }
ListenAndServe()
には、HttpRouter
のインスタンスが引数に入っています。
Gin
Ginのマルチプレクサ
最後にGolangのWebフレームワークの一つであるGinのマルチプレクサに関してですが、前述のHttpRouterを独自に改良したものが使われているようです。changelogにcustom hand optimized HttpRouter for Ginのような記載があったり、コードの各所のHttpRouterのライセンスの記載がありました。
ginのコードを読んで見ると独自にカスタマイズした engine
(マルチプレクサ) を ListenAndServe()
の引数に渡しているの読み取れます。
[参照コード] gin/gin.go at master · gin-gonic/gin · GitHub
同様に、こちらも実際にコードを書いてみると
package main import ( "github.com/gin-gonic/gin" ) func defaultHandler(ginctx *gin.Context) { ginctx.String(200, "Hello World! (default Page)") } func cartHandler(ginctx *gin.Context) { ginctx.String(200, "Cart page") } func main() { mux := gin.Default() mux.GET("/", defaultHandler) mux.GET("/cart", cartHandler) mux.Run(":8080") }
のようになります。
Ginのマルチプレクサの特徴
HttpRouterとは異なり、ルーティングをグループ化できるという特徴があるようです。(こちらは未検証のため公式ドキュメントのコードを引用します)
func main() { router := gin.Default() // v1 のグループ v1 := router.Group("/v1") { v1.POST("/login", loginEndpoint) v1.POST("/submit", submitEndpoint) v1.POST("/read", readEndpoint) } // v2 のグループ v2 := router.Group("/v2") { v2.POST("/login", loginEndpoint) v2.POST("/submit", submitEndpoint) v2.POST("/read", readEndpoint) } router.Run(":8080") }
細かいルーティングの指定ができて良さそうです。
Kubernetesクラスタを作ってみた with ラズベリーパイ〜上品なファブリックケーブルを添えて〜
一家に一台Kubernetesクラスタの時代が到来したとちらほらと聞こえ始めてきたので、いまさらながらRaspberry PIでKubernetesクラスタを作ってみました。
Raspberry PIのKubernetesクラスタであれば、EKSのようなクラウドベンダーが提供しているサービスではないので
- マスターノードに制約がなく自由に設定ができる
- サーバ費用をかけずに、複数ノード環境でaddonの検証ができる
というのが良いところか思います。
用意したもの
ラズパイとmicroSDカード、microUSBは千石電商、LANケーブルとケースはAmazon、その他はヨドバシAkibaで入手しました
- Raspberry PI 3 model B+ × 3
- 2.4A microUSB ケーブル 20cm × 5
- 6ポート 50W USB 充電機 (Elecom MPA-ACD03BK) × 1
- 50cm LANケーブル × 4
- コンパクト無線親機 (BUFFALO WMR-433W2-BK) × 1
- microUSB 給電 スイッチングハブ (LAN-SW05PSBE) × 3
- 積層式ケース × 1
- 六角スペーサー予備(M3×5mm) × 1
揃えた中でちょっと微妙だったかもと思ったは以下の2つです。
- LANケーブルは見た目にこだわった結果、長いものしかなく上記のものを選択しましたが短い方が配線は綺麗になるかと思います。
- 充電機に使ったElecom MPA-ACD03BKは思ったより大きく、横にすると入らなくなったので、もう少し小さいものを購入した方が良さそうです。
環境の構成
SSHで接続するPC環境はSSHを使える環境、Raspbianイメージを作成するPC環境はイメージをSDカードにwriteできる環境であればどんな環境でも大丈夫です。
- Raspberry PI k8s環境 (マスター, ワーカノード共通)
- Raspbian Buster Lite (Version:July 2019, Release date:2019-07-10, Kernel version:4.19)
ラズパイクラスタをRaspbian stretch lite
で作ったという記事を良くみかけますが、Raspbian Buster Lite
に関してもそれらの記事の設定方法とまったく同じで手順で大丈夫でした。
完成したところ
ケーブルはよく見かける感じのものだと味気ないので、白黒のケーブルにしてみました。ケースもグレーにして大人っぽい上品な感じに仕上げてみました。
ビールかなんかと一緒に置くとより一層大人っぽい雰囲気を楽しめるかもしれません。
作り方
組み立て
組み立て方はつまるところがないと思うので省略します。
イメージの準備
以下のSDカードの準備イメージ内のファイル編集は、ラズパイ3台分 (SDカード3枚)行います。
SDカードの準備
Raspbianイメージを作成するPC環境(Windows 10環境を使いました)で、Raspbian Buster Lite (旧raspbian stretch lite)の最新版のイメージをダウンロードします。
イメージのSDカードへの書き込みは、Win32 Disk Imagerというツールを使いました。
イメージ内のファイルを編集
SDカード作成後、Windows OS上で以下のファイルを変更しました。
/boot
ディレクトリ直下にあるcmdline.txt
を編集し、cgroup_enable=cpuset cgroup_enable=memory
を追加- cgroupsのcpusetとmemoryを有効化
Raspbian起動後の下準備
microSDカードをラズパイに挿入、充電器からmicroUSBケーブルを指して給電してラズパイを起動します。ラズパイ各環境でIPアドレス、ホスト名設定するまではHDMIケーブルで外部モニタ、USBキーボードを接続して操作しました。
下準備の設定内容は、マスターノード、ワーカーノードで共通です。
パッケージ更新、スワップオフ
デフォルトの
- ユーザ: pi
- パスワード: raspberry
でログイン。
aptパッケージを最新化します。
$ sudo apt-get -y update $ sudo apt-get -y upgrade
Kubernetes 1.8以降スワップが有効のままだとkubeletが起動しなくなったので、スワップをオフにします。
$ sudo dphys-swapfile swapoff $ sudo dphys-swapfile uninstall $ sudo update-rc.d dphys-swapfile remove
ホスト名・Pアドレスを固定
次に /etc/hostname
, /etc/hosts
を編集しホスト名を変更、/etc/dhcpcd.conf
を編集しIPアドレスを固定します。
ホスト名、IPアドレスは3台それぞれ
をホスト名 (IPアドレス)にしてみました。
以下のようにマスターノードのホスト名の設定しました。
$ sudo vi /etc/hostname - raspberrypi + k8s-master-01
$ sudo vi /etc/hosts - 127.0.1.1 raspberrypi + 127.0.1.1 k8s-master-01
同様にマスターノードのIPアドレス設定を設定しました
$ sudo vi /etc/dhcpcd.conf interface eth0 static ip_address=192.168.13.101/24 static routers=192.168.13.1 static domain_name_servers=192.168.13.1 8.8.8.8
PC環境からSSH接続できるように
SSHの秘密鍵と公開鍵を作っておきます。鍵は同じものを使うようにしたため、実行は1回のみです。
$ ssh-keygen Generating public/private rsa key pair. Enter file in which to save the key (/home/username/.ssh/id_rsa): Enter passphrase (empty for no passphrase): <パスフレーズ> Enter same passphrase again: <パスフレーズ> Your identification has been saved in 【秘密鍵のファイル名】 Your public key has been saved in 【秘密鍵のファイル名】.pub.
作った公開鍵をラズパイ側の~/.ssh/authorized_keys
にコピーします。以下はマスターノードの設定で実行したコマンドです。
$ ssh-copy-id -i 【秘密鍵のパス】 pi@192.168.13.101 /usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "秘密鍵のファイル名.pub" The authenticity of host '192.168.13.101 (192.168.13.101)' can't be established. ECDSA key fingerprint is SHA256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX. Are you sure you want to continue connecting (yes/no)? yes /usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed /usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys pi@192.168.13.101's password: Number of key(s) added: 1 Now try logging into the machine, with: "ssh 'pi@192.168.13.1021" and check to make sure that only the key(s) you wanted were added.
SSH接続しやすいようにPC設定
.ssh/configに以下のように追加します。
Host k8s-master-01 HostName 192.168.13.101 User pi Port 22 IdentityFile 【秘密鍵のパス】
ここまででSSH接続の設定は終わりです。以後の作業は、PC環境からSSHで各ラズパイ環境接続して作業を行っていきます。
Dockerをインストール
各ラズパイ環境にDockerをインストールしていきます。
$ curl -sSL https://get.docker.com/ | sh
kubelet kubeadm kubectl kubernetes-cni のインストール
https経由でaptを実行するために必要なパッケージをインストールします。
$ sudo apt-get install -y apt-transport-https
Google cloudの公式キー登録します。
$ curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
ソースリストにkubernetesを追加します。
$ echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
apt更新
$ sudo apt-get update
kubelet kubeadm kubectl kubernetes-cniをインストールします。
$ sudo apt-get install kubelet kubeadm kubectl kubernetes-cni
各ノード間でバージョンが異なると予期せぬ動きに繋がるので、バージョンが変わらないようにしておきます。
$ sudo apt-mark hold kubelet kubeadm kubectl kubernetes-cni
マスターノードのセットアップ
以下のコマンドを実行し、kubernetesクラスタのコントロールプレーンを初期化します。
$ sudo kubeadm init --pod-network-cidr=10.244.0.0/16
後にネットワークaddonとしてインストールするflannelのドキュメントを読むとkubeadmの場合、pod-network-cidrに制約があるようなのでドキュメント通りにしています。
他の有名どころのネットワークaddonとして
- Calicoは、AMD64プロセッサが動作要件にある。Raspbianは32bitなので候補から除外
- Weaveに関しては、ARMをサポートしているが32bitに対応しているか明示されていないので候補から除外
消去法で残ったflannelを使うことにしています。
kubeadm init
すると実行結果の一番最後の他のワーカーノードがジョインするためのコマンドが表示されるので控えておきます。
Your Kubernetes control-plane has initialized successfully! To start using your cluster, you need to run the following as a regular user: mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config You should now deploy a pod network to the cluster. Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at: https://kubernetes.io/docs/concepts/cluster-administration/addons/ Then you can join any number of worker nodes by running the following on each as root: kubeadm join 192.168.13.101:6443 --token XXXXXXXXXXXXXXXXX \ --discovery-token-ca-cert-hash sha256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
続いて、kubeadm initの実行結果に記載されているコマンドを実行します。
$ mkdir -p $HOME/.kube $ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config $ sudo chown $(id -u):$(id -g) $HOME/.kube/config
現時点でどのPodが動いているか確認してみます。
$ kubectl get pods --all-namespaces NAMESPACE NAME READY STATUS RESTARTS AGE kube-system coredns-5c98db65d4-4bhgq 0/1 Pending 0 14m kube-system coredns-5c98db65d4-p5s4t 0/1 Pending 0 14m kube-system etcd-k8s-master-01 1/1 Running 0 14m kube-system kube-apiserver-k8s-master-01 1/1 Running 0 13m kube-system kube-controller-manager-k8s-master-01 1/1 Running 0 14m kube-system kube-proxy-dz99s 1/1 Running 0 14m kube-system kube-scheduler-k8s-master-01 1/1 Running 0
Pod network addonがなく、corednsのpodがREADYの状態になっています。 Network addonとしてflannelを入れてみます。
# コマンドは https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/#pod-network から取得 $ kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/62e44c867a2846fefb68bd5f178daf4da3095ccb/Documentation/kube-flannel.yml
数分待ってから再度Podの状態を確認してみると
$ kubectl get pods --all-namespaces NAMESPACE NAME READY STATUS RESTARTS AGE kube-system coredns-5c98db65d4-4bhgq 1/1 Running 0 37m kube-system coredns-5c98db65d4-p5s4t 1/1 Running 0 37m kube-system etcd-k8s-master-01 1/1 Running 0 37m kube-system kube-apiserver-k8s-master-01 1/1 Running 0 36m kube-system kube-controller-manager-k8s-master-01 1/1 Running 1 37m kube-system kube-flannel-ds-arm-szj4t 1/1 Running 0 4m50s kube-system kube-proxy-dz99s 1/1 Running 0 37m kube-system kube-scheduler-k8s-master-01 1/1 Running 1
全Pod Runningになっていることを確認できました。
ワーカーノードをKubernetesクラスに追加
残りのワーカーノードにSSHログインして、先程控えておいたクラスタ参加のためのコマンド実行するだけです。
$ sudo kubeadm join 192.168.13.101:6443 --token XXXXXXXXXXXXXXXXX \ --discovery-token-ca-cert-hash sha256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
再度マスターノードにSSHログインして、以下のコマンドを実行すると
$ kubectl get nodes NAME STATUS ROLES AGE VERSION k8s-master-01 Ready master 58m v1.15.3 k8s-worker-01 Ready <none> 17m v1.15.3 k8s-worker-02 Ready <none> 19s v1.15.3
無事3台のノードでkubernetesクラスタができていることが確認できました。