OPAでテストコードを書く

OPAでPolicyをRegoで書いていくという話は何度かしてきました。

では、そのPolicyを正しく書けたかどうかはどう判断するのでしょう? これまで扱ってきた記事ではInputを用意してOutputをチェックしてました。 それって大変ですし、Policyが鬼のようにたくさんあった場合、デグレチェックも大変です。

そこで登場するのがOPAのテストコードの仕組みです!公式ドキュメントでも詳しい説明があります。

www.openpolicyagent.org

本記事では、以前とりあげたIAM Policyの例を使ってテストコードを書いてみます!

kenfdev.hateblo.jp

上の記事でテストしたパターンは以下の4つです。

  • iam:ChangePasswordが 許可 されるパターン
  • s3:ListAllMyBucketsが 許可 されるパターン
  • s3:ListBucketが 許可 されるパターン
  • s3:CreateBucketが 拒否 されるパターン

対象となるRegoは以下のとおり。

package aws

allow {
    input.action == "iam:ChangePassword"
}

allow {
    input.action == "s3:ListAllMyBuckets"
}

allow {
    actions_match
    resources_match
}

actions_match {
    actions := ["s3:List.*","s3:Get.*"]
    action := actions[_]
    regex.globs_match(input.action, action)
}

resources_match {
    resources := ["arn:aws:s3:::confidential-data","arn:aws:s3:::confidential-data/.*"]
    resource := resources[_]
    regex.globs_match(input.resource, resource)
}

テストの書き方

Regoのテストは(当たり前かもしれませんが)Regoで書きます! ルールに test_ というprefixをつけることでOPAが暗黙的にテストだと認識します。 opa test を実行した場合もOPAは test_ で始まるルールを集めてテストを実行するようになってます。

では、さっそくテストを順に書いていってみましょう! テスト用のルールはどこに書いてもいいんですけど、 iam_test.rego ファイルを作ってそこに書きましょう。 パッケージは同じにしておきたいので、先頭に package aws を忘れずに書きます。

iam:ChangePasswordが許可されるパターン

まずは以下のルールをテストしてみましょう。

allow {
    input.action == "iam:ChangePassword"
}

これはすごくシンプルで、 input.actionがiam:ChangePasswordだったら許可する です。 そのままですね!テストも実はかなりそのまま書くことができます。

test_change_password_action_allowed {
  allow with input as { "action": "iam:ChangePassword" }
}

test_ 以降はわかりやすい、好きな名前をつけると良いです。 で、テストしたいルール(この場合 allow )を書いて、 input の中身を指定したいので withas を使ってテストデータを注入します。 as 以降にはJSONをそのまま書くことができます!

OPAのバイナリを持ってる場合は以下のコマンドでテストが実行できます。

注意:iam.regoとiam_test.regoがあること

opa test .

VSCodeで拡張を入れている方は OPA: Test Workspace というコマンドが用意されているので、 それを実行することで結果を見ることも可能です。

f:id:kenev:20190402223652p:plain

s3:ListAllMyBucketsが許可されるパターン

次にテストしたいパターンも上のパターン同様です(actionが違うだけ)。

allow {
    input.action = "s3:ListAllMyBuckets"
}

先程と同様な考えでテストを書くと、次のようになります。

test_s3_ListAllMyBuckets_allowed {
  allow with input as { "action": "s3:ListAllMyBuckets" }
}

s3:ListBucketが許可されるパターン

次にテストしたいのは以下のパターンです。

allow {
    actions_match
    resources_match
}

actions_match {
    actions = ["s3:List.*","s3:Get.*"]
    action = actions[_]
    regex.globs_match(input.action, action)
}

resources_match {
    resources = ["arn:aws:s3:::confidential-data","arn:aws:s3:::confidential-data/.*"]
    resource = resources[_]
    regex.globs_match(input.resource, resource)
}

言葉で表すと arn:aws:s3:::confidential-data配下のresourceであればs3:List系あるいはs3:Get系のactionを許可 になります。

それでは input を調整してテストしてみましょう!

test_s3_ListBucket_to_confidential_data_allowed {
  allow with input as { 
    "action": "s3:ListBucket",
    "resource": "arn:aws:s3:::confidential-data/12345678"
  }
}

これもわかりやすいですね!

s3:ListBucketarn:aws:s3:::confidential-data/12345678 に対して許可されることをテストしてます。

opa test . してテストが通ることを確認しましょう。

s3:CreateBucketが拒否されるパターン

最後に拒否されるパターンも見てみましょう!

test_s3_CreateBucket_to_confidential_data_not_allowed {
  not allow with input as { 
    "action": "s3:CreateBucket",
    "resource": "arn:aws:s3:::confidential-data/12345678"
  }
}

actions3:CreateBucket にしてみました。 ここのポイントは、 拒否されること をテストするということです!

テストをよく見ると not allow になっていることに気づきましたでしょうか?

そうです、 not を使うことで拒否されることのテストができます。

テストを実行してみましょう!

$ opa test .
data.aws.test_change_password_action_allowed: PASS (7.772µs)
data.aws.test_s3_ListAllMyBuckets_allowed: PASS (1.735µs)
data.aws.test_s3_ListBucket_to_confidential_data_allowed: PASS (1.239µs)
data.aws.test_s3_CreateBucket_to_confidential_data_not_allowed: PASS (978ns)
--------------------------------------------------------------------------------
PASS: 4/4

全部PASSしているはずです!

そして、VSCodeでやっている人は OPA: Toggle Workspace Coverage を実行するとカバレッジも見れますよ!

f:id:kenev:20190404201527p:plain

おわりに

  • OPAのRegoで書いたPolicyはテストできる!
  • input のようなデータを簡単に注入できる!
  • VSCode拡張機能と組み合わせるとテスト実行からカバレッジの表示までできる!

ということがわかりましたー。CIと組み合わせてPolicyのロバストネスを上げれそうですね!

2019-04-12追記:下の記事でCI連携についても書きました!

kenfdev.hateblo.jp

参考

kenfdev.hateblo.jp

OPAのVisual Studio Code拡張機能

OPAにはVSCode用の拡張機能があります!

marketplace.visualstudio.com

機能

できるようになることは次のとおり(2019/03/31時点)

  • 保存時に構文チェック(現時点でエディタ上で表示はされません)
  • OPAのpackageを評価
  • 選択中の箇所を評価
  • 選択中の箇所を部分評価(Partial Evaluation)
  • 選択中の箇所を評価して、トレースを表示
  • 選択中の箇所を評価してプロファイルを表示
  • ワークスペース内のテストを実行
  • ワークスペース内のカバレッジ表示の切り替え
  • 選択中の箇所のカバレッジ表示切り替え

準備するもの

OPAのバイナリを忘れずに!

opa コマンドが叩けることが前提になっているので、ちゃんとインストールしておきましょう。

github.com

opaPATH を通しておくか、VSCodeの設定で Opa: Path にバイナリへのパスを直接指定することもできます!

お試し

拡張機能ページでGIFの紹介アニメーションがあるんですけど、サラッとどんどん進んじゃうので、一個一個見ていきます!

まず下の内容で simple.rego を書きます。

package http.authz

default allow = false

allow {
  input.method = "GET"
  input.path = ["salary", user]
  input.user = user
}

文でこのポリシーを言い表すと

  • デフォルトは許可しない
  • 下が全て満たされていれば許可
    • 入力 method"GET"
    • 入力 path["salary", user] である
    • 入力 userpathuser と一致

となります。

ワークスペースのルートに input.json を置くと、その中身をOPAの入力としてお試し実行することができます。 まずは次の内容にしてみましょう!

{
  "user": "bob",
  "method": "GET",
  "path": ["salary", "bob"]
}

bobbob 自身の salary を見ようとしてるので、これは許可されるはずです!

f:id:kenev:20190331115415p:plain

Regoの allow の部分をvscodeで選択して Cmd + Shift + p して、OPA: Evaluate Selection すると、この部分だけ評価できます。 やってみましょう。

f:id:kenev:20190331115632p:plain

期待通り、この結果は true となります!

では、 pathbob から alice に変えてみるとどうなるでしょ。

{
  "user": "bob",
  "method": "GET",
  "path": ["salary", "alice"]
}

もう一度 allow を選択して実行すると次の通り。

f:id:kenev:20190331120019p:plain

自分の salary じゃなくなってるので false になってますね!

次はルールのBodyを選択して評価します!

f:id:kenev:20190331120658p:plain

input.methodGET なので結果は true ですね!もう一行増やして実行してみましょう。

f:id:kenev:20190331120950p:plain

この結果の output.json は、複数行関わった場合に何を基準にしているかちょっとわかっていないです。 予想としては ["salary", "alice"] = ["salary", user] になって、 useralice が代入されているのかなと思っています。そしてその結果が output.json に出ているのではないかと。(この評価方法が正しいかは別途確認したいです)

では最終行も含めて実行してみましょう!

f:id:kenev:20190331122143p:plain

これは最終的に "bob" = "alice" しようとして、 true にならないので、結果は空っぽになります。

input.jsonpathalice から bob に戻しましょう!

{
  "user": "bob",
  "method": "GET",
  "path": ["salary", "bob"]
}

そして実行してみると次のとおり!

f:id:kenev:20190331123356p:plain

何かしら戻り値が返ってくるのがわかります。 上でも返ってきたのと同様 user に関するJSONなのですが、なぜこの値になるのかは現段階でちょっとわからないです。 allow を選択して実行すると直感的な true が返ってくるのですが。。。

おわりに

本記事ではOPAのVSCode拡張機能を紹介しました! 個人的にかなりうれしいのは下の2項目です。

まだまだ使いこなせていないのでこの記事もわかったことが増え次第更新していきたいです!

IAM PolicyをOPAで書く

OPAシリーズです!今回は公式ドキュメントの「Comparison to Other Systems」について見てみます。 世の中に存在しているPolicyのパターンをOPAで実装した場合、どうなるかというのを教えてくれている項目です。

www.openpolicyagent.org

この中でも「Amazon Web Services IAM」をOPAで実装した場合について本記事では見ていきます。

IAM PolicyをOPAで書くとどうなるか

AWSのIAMではユーザーやロール、グループに加えてリソースに対してPolicyをアタッチできます。Policyは「 誰が何に何を して 良いか 、あるいは ダメか 」を定義することができます。

次のIAMのPolicyを想定してみます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "FirstStatement",
      "Effect": "Allow",
      "Action": ["iam:ChangePassword"],
      "Resource": "*"
    },
    {
      "Sid": "SecondStatement",
      "Effect": "Allow",
      "Action": "s3:ListAllMyBuckets",
      "Resource": "*"
    },
    {
      "Sid": "ThirdStatement",
      "Effect": "Allow",
      "Action": [
        "s3:List*",
        "s3:Get*"
      ],
      "Resource": [
        "arn:aws:s3:::confidential-data",
        "arn:aws:s3:::confidential-data/*"
      ]
    }
  ]
}

IAMに馴染みの無い人(僕もその一人)からするとわかりにくいと思うので、要点だけあげると

  • iam:ChangePassword というアクションだったら許可
  • s3:ListAllMyBuckets というアクションだったら許可
  • arn:aws:s3:::confidential-data というリソース、あるいはその配下のリソースに対して、 s3:List から始まるアクション、 s3:Get から始まるアクションを許可

という内容になります。これをOPAのRegoで書くと次のようになります!

注意:一部、公式のコメントを日本語に差し替えています

package aws

# FirstStatement
allow {
    input.action == "iam:ChangePassword"
}

# SecondStatement
allow {
    input.action == "s3:ListAllMyBuckets"
}

# ThirdStatement
# ここではヘルパールールを定義することで見やすくしています
# actions_matchとresources_matchがtrueの場合にallowがtrueになります
allow {
    actions_match
    resources_match
}

# actions_matchはinput.actionがactionsに含まれていればtrueになる
actions_match {
    actions := ["s3:List.*","s3:Get.*"]
    # actionはactionsを順番にイテレートする、という意味があります
    action := actions[_]
    # check if input.action matches an action
    regex.globs_match(input.action, action)
}

# resources_matchはinput.resourceがresourcesに含まれていればtrueになる
resources_match {
    resources := ["arn:aws:s3:::confidential-data","arn:aws:s3:::confidential-data/.*"]
    # resourceはresourcesを順番にイテレートする、という意味があります
    resource := resources[_]
    # check if input.resource matches a resource
    regex.globs_match(input.resource, resource)
}

Regoに対する慣れは必要ですが、上から読んでいってそれなりに意味を読み取れるのではないかと思います。 ただし、トリッキーな部分が少なくとも2個あります。

  • action := actions[_] のようにイテレーションを宣言してる部分
  • regex.globs_match(input.action, action) でビルトインな関数を使っている部分

actions_match のルールをもう少し噛み砕いてみます。 まず、このルールですけど action := actions[_] と代入してる場所を無くしてみましょう。 個人的には action が単数形なのがわかりにくくしている気がします。 関係のある箇所だけ見ると下のようになります。

actions_match {
    actions := ["s3:List.*","s3:Get.*"]
    regex.globs_match(input.action, actions[_])
}

だいぶすっきりしましたね!このルールを言葉で言うと

インプットで渡ってきた actionactions 内の正規表現s3:List.*, s3:Get.* ) とマッチすれば許可する

という意味になります。 regex.globs_match は公式ドキュメントにも記載がありますが、最初から使えるビルトインな関数です。

www.openpolicyagent.org

true if the intersection of regex-style globs glob1 and glob2 matches a non-empty set of non-empty strings.

ドキュメントにも書いてあるように、 第1引数と第2引数を総当たりして、正規表現がマッチするものがあれば true になるという関数です。 試してみたところ、引数は string でも stringの配列 でも正常に動作する様です。

Playgroundで検証

OPAにはPlaygroundがあるので、下のPlaygroundを使って検証してみましょう!

play.openpolicyagent.org

次のようなInputを渡してみましょう。

{
  "action": "s3:ListBucket",
  "resource": "arn:aws:s3:::confidential-data/12345678"
}

arn:aws:s3:::confidential-data 配下のリソースに対して s3:List から始まるアクションを行っているので許可されるはずです。 Outputをみてみましょう。

{
  "result": {
    "actions_match": true,
    "allow": true,
    "resources_match": true
  }
}

true の評価結果が全部返ってくるので actions_matchresources_match も返ってきますが、本命の allowtrue になっているのがわかりますね!

では s3:ListBucket を下のように s3:CreateBucket にしたらどうなるでしょう?

{
  "action": "s3:CreateBucket",
  "resource": "arn:aws:s3:::confidential-data/12345678"
}

結果は下の通り。

{
  "result": {
    "resources_match": true
  }
}

リソースのルールは許可されているけど、アクションが拒否されたので allow は返ってきませんでした! 想定した動きになってそうですね。 では、ちょっと戻りますけど、最初に登場する2つの allow ルールについても検証してみましょう。

パスワード変更を想定して次のようなInputを渡します。

{
  "action": "iam:ChangePassword",
  "resource": "arn:aws:iam::123456789012:user/JohnDoe"
}

結果は下の通り。

{
  "result": {
    "allow": true
  }
}

iam:ChangePassword であればなんでも許可なので allowtrue になってます。

最後に s3:ListAllMyBuckets についても見てみましょう。

{
  "action": "s3:ListAllMyBuckets",
  "resource": "arn:aws:s3:::*"
}

結果は下の通り。

{
  "result": {
    "actions_match": true,
    "allow": true
  }
}

allowtrue で返ってきますね!また、 s3:List から始まるアクションなので actions_matchtrue で返ってきているのがわかります。

おわりに

OPAではこのようにAWSのIAMに似たPolicy管理もしていくことができそうですね。ただし、シンプルなパターンを見ているだけなので、複雑なパターンでも検証してみたいところです。 今後のネタとしてその点についてはストックしておきます!

おまけ

regex.globs_match(glob1, glob2) は第1引数( glob1 )と第2引数( glob2 )を総当たりして true があれば true になるみたいなので、Inputを下のようにすると

{
  "action": ".*",
  "resource": ".*"
}

正規表現としては なんでもOK という意味になるので結果は下の通り。

{
  "result": {
    "actions_match": true,
    "allow": true,
    "resources_match": true
  }
}

この動きはどうなんだろう、と思いながらも使う側の問題なので制御自体はできそうです。 あるいは、ヘルパー関数は自分でも作れるので、例えば 第1引数の文字列が、第2引数の正規表現のいずれかにマッチする かチェックする関数を作れば良さそうです。 この点についても今後深掘りしていけたらと思います!

参考

kenfdev.hateblo.jp

kenfdev.hateblo.jp

OPAでSSHとsudoの認可をする

前回 に引き続きOPAの勉強をしています。今回の記事では公式チュートリアルの「SSH and sudo Authorization」を見ます。

www.openpolicyagent.org

ゴール

このチュートリアルではLinux-PAM(Pluggable Authentication Modules)とOPAを連動させています。

en.wikipedia.org

Linux-PAM(以下PAM)について僕は今回初めて知ったのですが、Linuxに認証の仕組みをプラグインとして追加できるものという理解です。ここでOPAと連動できるPAMを使うことで、認証に加えてPolicyに沿った 認可 も行えるようにします。OPA公式のリポジトリにこのチュートリアル用のOPAのPAMがあります。

github.com

これを使って次のことを実現します!

  • Adminであればどのホストにもsshできるし、そこでsudoもできる
  • 通常ユーザーであれば、ホスト内のコードにコントリビュートしている場合にsshができる

さらに、次の登場人物がいると仮定します。

  • frontend-devfrontend ホストで動いているコードにコントリビュートしている開発者
  • backend-devbackend ホストで動いているコードにコントリビュートしている開発者
  • ops は組織内のAdmin

図示すると次のようになります。

f:id:kenev:20190323134623p:plain

準備

まずは作業場所を用意します。

$ mkdir ssh_sudo_auth
$ cd ssh_sudo_auth

上の図の状況を作り上げるために frontend, backend ホストとOPAを docker-compose で立ち上げます。

# 好きなエディタで作ります
$ vim tutorial-docker-compose.yaml

yaml の中身は次の通り。

version: '2'
services:
  opa:
    image: openpolicyagent/opa:0.10.5
    ports:
      - 8181:8181
    # WARNING: OPA is NOT running with an authorization policy configured. This
    # means that clients can read and write policies in OPA. If you are
    # deploying OPA in an insecure environment, be sure to configure
    # authentication and authorization on the daemon. See the Security page for
    # details: https://www.openpolicyagent.org/docs/security.html.
    command:
      - "run"
      - "--server"
      - "--log-level=debug"
  frontend:
    image: openpolicyagent/demo-pam
    ports:
      - "2222:22"
    volumes:
      - ./frontend_host_id.json:/etc/host_identity.json
  backend:
    image: openpolicyagent/demo-pam
    ports:
      - "2223:22"
    volumes:
      - ./backend_host_id.json:/etc/host_identity.json

WARNING の部分が何を言っているかというと、 OPA自身をPolicyで守っていないのでセキュアじゃないということです。 例えばREST APIは誰でも叩けてしまうので、OPAに対してリクエストを投げられる人がいれば、誰でもPolicyに変更を加えられちゃいます。 本来どうOPA自身を守るべきかという点については以下の公式ドキュメントにまとめられています。

www.openpolicyagent.org

それでは、このチュートリアル用のPAMがホストを識別するための host_id 情報を含んだ json を作ります。 PAMはこのJSONを必要に応じて読み込んで、自分が frontend なのか backend なのか判別しています。

$ echo '{"host_id": "frontend"}' > frontend_host_id.json
$ echo '{"host_id": "backend"}' > backend_host_id.json

準備が整ったので docker-compose で立ち上げます!

$ docker-compose -f tutorial-docker-compose.yaml up

OPA連携するPAMの概要

このチュートリアルで用意されているPAMとOPAの関係は下図のようだと理解しました。

f:id:kenev:20190323153551p:plain
PAMとOPA

  1. PAMがOPAの pull エンドポイントに、システムから取得するファイル一覧を聞く
  2. PAMがOPAの sshd/authz エンドポイントに、SSHしていいか聞く
  3. PAMがOPAの sudo/authz エンドポイントに、sudoしていいか聞く

PAMの設定を実際に見てみましょう。 frontend コンテナに入ります。

$ docker-compose -f tutorial-docker-compose.yaml exec frontend bash

sudo のPAM設定を覗いてみます。

$ cat /etc/pam.d/sudo
# Enable the Authz PAM module
# At image build time, the value of HOST_UUID is replaced with the role of the image (for example, webapp)
auth required /lib/security/pam_authz.so url=http://opa:8181 authz_endpoint=/v1/data/sudo/authz display_endpoint=/v1/data/display pull_endpoint=/v1/data/pull log_level=debug
  • auth required/lib/security/pam_authz.so を指定している
  • OPAのエンドポイントをurlhttp://opa:8181 と指定している
  • pull_endpoint/v1/data/pull を指定している
  • authz_endpoint/v1/data/sudo/authz を指定している

のがわかります。 sudo するときOPAに認可を問い合わせしている雰囲気がなんとなくですけど伝わってきますね!

注意: display_endpoint は本記事では扱わないので説明は割愛します。

Policyの作成

pull Policy作成

それでは、まずは pull エンドポイント用のPolicyを作ります。PolicyはRegoで書きます。

$ cat >pull.rego <<EOF
package pull

# Which files should be loaded into the context?
files = ["/etc/host_identity.json"]

# Which environment variables should be loaded into the context?
env_vars = []

EOF

上記を用意しておくことで、PAMが pull エンドポイントに問い合わせをしたときに、どんなファイルを読み込む必要があるかを知ることができます。(この場合は /etc/host_identity.json を読み込みます)

では、このPolicyをOPAにREST API経由で入れます。

$ curl -X PUT --data-binary @pull.rego \
  localhost:8181/v1/policies/pull

sshdの認可Policy作成

では、SSHするときに適用する認可Policyを作りましょう。

$ cat >sshd_authz.rego <<EOF
package sshd.authz

import input.pull_responses
import input.sysinfo

import data.hosts

# By default, users are not authorized.
default allow = false

# Allow access to any user that has the "admin" role.
allow {
    data.roles["admin"][_] = input.sysinfo.pam_username
}

# Allow access to any user who contributed to the code running on the host.
#
# This rule gets the `host_id` value from the file `/etc/host_identity.json`.
# It is available in the input under `pull_responses` because we
# asked for it in our pull policy above.
#
# It then compares all the contributors for that host against the username
# that is asking for authorization.
allow {
    hosts[pull_responses.files["/etc/host_identity.json"].host_id].contributors[_] = sysinfo.pam_username
}


# If the user is not authorized, then include an error message in the response.
errors["Request denied by administrative policy"] {
    not allow
}
EOF

ポイントとしては以下のとおり。

  • 問い合わせしてきたユーザー( sysinfo.pam_username )が rolesadmin に含まれていれば許可
  • 問い合わせしてきたユーザー( sysinfo.pam_username )が pull で指定されたファイル /etc/host_identity.json のホストにコントリビュートした人( contributors )であれば許可
  • 上に該当しなければエラーとする

input に関連する情報はPAMがOPAにリクエストするときに渡ってくるデータです。次のようなJSONがリクエストに含まれてくるとイメージしてください。

{
    "input": {
        "display_responses": {
            "last_name": "<ユーザーが入力した情報。本記事では扱いません。>",
            "secret":    "<ユーザーが入力した情報。本記事では扱いません。>"
        },
        "pull_responses": {
            "files": {
                "/etc/host_identity.json": {
                    "host_id": "<JSON内に指定されたhost_id>"
                }
            },
            "env_vars": {}
        },
        "sysinfo": {
            "pam_username":     "<PAMセッション内の値>",
            "pam_service":      "<PAMセッション内の値>",
            "pam_req_username": "<PAMセッション内の値>",
            "pam_req_hostname": "<PAMセッション内の値>"
        }
    }
}

また、Policy内に登場する rolescontributors のデータはこの後OPAに投入します!

まずは上のPolicyをOPAに投入しましょう。

$ curl -X PUT --data-binary @sshd_authz.rego \
  localhost:8181/v1/policies/sshd/authz

sudoの認可Policy作成

sudoするときのPolicyも同じ要領で作ります。

$ cat >sudo_authz.rego <<EOF
package sudo.authz

# By default, users are not authorized.
default allow = false

# Allow access to any user that has the "admin" role.
allow {
    data.roles["admin"][_] = input.sysinfo.pam_username
}

# If the user is not authorized, then include an error message in the response.
errors["Request denied by administrative policy"] {
    not allow
}
EOF

これはシンプルで、

  • 問い合わせしてきたユーザー( sysinfo.pam_username )が rolesadmin に含まれていれば許可
  • 上に該当しなければエラーとする

というPolicyになります。 これもOPAに投入します。

$ curl -X PUT --data-binary @sudo_authz.rego \
  localhost:8181/v1/policies/sudo/authz

その他必要なデータの投入

roles

roles のデータを投入します。これで ops ユーザーだけが admin となります。

$ curl -X PUT localhost:8181/v1/data/roles -d \
'{
    "admin": ["ops"]
}'

hosts

ホストの情報と、そのホストに対するコントリビューターの情報を投入します。

frontend のコントリビューターとして frontend-dev を入れて、 backend のコントリビューターとして backend-dev を定義します。

$ curl -X PUT localhost:8181/v1/data/hosts -d \
'{
  "frontend": {
    "contributors": [
      "frontend-dev"
    ]
  },
  "backend": {
    "contributors": [
      "backend-dev"
    ]
  }
}'

実践!

準備ができました!では、Policyがちゃんと動作するか確認していきましょう。

opsユーザーとしてSSHとsudo

まずは ops ユーザーでSSHとsudoをしてみましょう。 opsadmin なので成功するはずです。 ローカルのポート2222が frontend のホストにつながるはずなので試してみます。

$ ssh -p 2222 ops@localhost \
   -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null

ops@f55968aabd47:~$

SSHはできました!では、sudoはどうでしょう?

$ sudo ls /
OPA-PAM[49]: Session log level is set to debug
OPA-PAM[49]: Commencing display cycle.
OPA-PAM[49]: Initializing HTTP request GET /v1/data/display
OPA-PAM[49]: HTTP request body: (null)
OPA-PAM[49]: HTTP request complete, libcURL returned with 0.
OPA-PAM[49]: HTTP response body: {}
OPA-PAM[49]: Value of field 'result' does not have type object in JSON response. Please ensure that your endpoint flag '/v1/data/display' matches your package path.
OPA-PAM[49]: Commencing pull cycle.
OPA-PAM[49]: Initializing HTTP request GET /v1/data/pull
OPA-PAM[49]: HTTP request body: (null)
OPA-PAM[49]: HTTP request complete, libcURL returned with 0.
OPA-PAM[49]: HTTP response body: {"result":{"env_vars":[],"files":["/etc/host_identity.json"]}}
OPA-PAM[49]: Loaded JSON from file :
{
  "host_id": "frontend"
}
OPA-PAM[49]: Collecting system information.
OPA-PAM[49]: Loaded sysinfo pam_username: ops
OPA-PAM[49]: Loaded sysinfo pam_service: sudo
OPA-PAM[49]: Loaded sysinfo pam_req_username: ops
OPA-PAM[49]: Loaded sysinfo pam_req_hostname:
OPA-PAM[49]: Commencing authz cycle.
OPA-PAM[49]: Initializing HTTP request POST /v1/data/sudo/authz
OPA-PAM[49]: HTTP request body: {"input":{"display_responses":{},"pull_responses":{"files":{"/etc/host_identity.json":{"host_id":"frontend"}},"env_vars":{}},"sysinfo":{"pam_username":"ops","pam_service":"sudo","pam_req_username":"ops","pam_req_hostname":""}}}
OPA-PAM[49]: HTTP request complete, libcURL returned with 0.
OPA-PAM[49]: HTTP response body: {"result":{"allow":true,"errors":[]}}
OPA-PAM[49]: Freeing allocated data.
OPA-PAM[49]: Application invoked pam_sm_setcred.
bd_build  bin  boot  create_user.sh  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
OPA-PAM[49]: Application invoked pam_sm_setcred.

sudo ls / してみると、ドカッとログがいっぱい出てきました!これはPAMが debug モードで走ってるのでログがたくさん出るようになっているからです。

1行1行見ていくとなんとなく何をしているかというのがわかります。ポイントとしては次の行です。

  • Initializing HTTP request GET /v1/data/pull
  • Loaded sysinfo pam_username: ops
  • Initializing HTTP request POST /v1/data/sudo/authz
  • HTTP response body: {"result":{"allow":true,"errors":[]}}

上で用意したPolicyが動作しているのがわかりますね!最終的に allow: true になっているので、 ls の内容も出力されています。

bd_build  bin  boot  create_user.sh  dev  etc  home  lib  lib64  media mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

SSHから抜けておきましょう。

$ exit

adminじゃないユーザーでsudo

それでは、adminじゃないユーザーでsudoをしてみましょう。

frontend-dev ユーザーとして frontend ホストにSSHします。

$ ssh -p 2222 frontend-dev@localhost \
  -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null

frontend-dev@f55968aabd47:~$

SSHは成功します。ではsudoしたらどうなるでしょう?

$ sudo ls /
OPA-PAM[81]: Session log level is set to debug
OPA-PAM[81]: Commencing display cycle.
OPA-PAM[81]: Initializing HTTP request GET /v1/data/display
OPA-PAM[81]: HTTP request body: (null)
OPA-PAM[81]: HTTP request complete, libcURL returned with 0.
OPA-PAM[81]: HTTP response body: {}
OPA-PAM[81]: Value of field 'result' does not have type object in JSON response. Please ensure that your endpoint flag '/v1/data/display' matches your package path.
OPA-PAM[81]: Commencing pull cycle.
OPA-PAM[81]: Initializing HTTP request GET /v1/data/pull
OPA-PAM[81]: HTTP request body: (null)
OPA-PAM[81]: HTTP request complete, libcURL returned with 0.
OPA-PAM[81]: HTTP response body: {"result":{"env_vars":[],"files":["/etc/host_identity.json"]}}
OPA-PAM[81]: Loaded JSON from file /etc/host_identity.json:
{
  "host_id": "frontend"
}
OPA-PAM[81]: Collecting system information.
OPA-PAM[81]: Loaded sysinfo pam_username: frontend-dev
OPA-PAM[81]: Loaded sysinfo pam_service: sudo
OPA-PAM[81]: Loaded sysinfo pam_req_username: frontend-dev
OPA-PAM[81]: Loaded sysinfo pam_req_hostname:
OPA-PAM[81]: Commencing authz cycle.
OPA-PAM[81]: Initializing HTTP request POST /v1/data/sudo/authz
OPA-PAM[81]: HTTP request body: {"input":{"display_responses":{},"pull_responses":{"files":{"/etc/host_identity.json":{"host_id":"frontend"}},"env_vars":{}},"sysinfo":{"pam_username":"frontend-dev","pam_service":"sudo","pam_req_username":"frontend-dev","pam_req_hostname":""}}}
OPA-PAM[81]: HTTP request complete, libcURL returned with 0.
OPA-PAM[81]: HTTP response body: {"result":{"allow":false,"errors":["Request denied by administrative policy"]}}
OPA-PAM[81]: Received authz error log from OPA: Request denied by administrative policy
OPA-PAM[81]: Freeing allocated data.
Sorry, try again.
OPA-PAM[81]: Application invoked pam_sm_authenticate.
OPA-PAM[81]: Session log level is set to debug
...
OPA-PAM[81]: HTTP response body: {"result":{"allow":false,"errors":["Request denied by administrative policy"]}}
OPA-PAM[81]: Received authz error log from OPA: Request denied by administrative policy
...
Sorry, try again.
...
OPA-PAM[81]: Session log level is set to debug
...
OPA-PAM[81]: HTTP response body: {"result":{"allow":false,"errors":["Request denied by administrative policy"]}}
OPA-PAM[81]: Received authz error log from OPA: Request denied by administrative policy
...
sudo: 3 incorrect password attempts

失敗しているのがわかります。エラー文言もPolicyで設定していた Request denied by administrative policy になっています。 3回挑戦した結果、 sudo: 3 incorrect password attempts と言ってあきらめています。

SSHから抜けておきます。

$ exit

コントリビューターじゃないホストへSSH

では、最後にコントリビュートしていないホストへのSSHがちゃんと拒否されるか確認しましょう。 frontend-dev ユーザーとして backend ホストにSSHしてみます。( backend はローカルのポート2223で待ち受けています)

$ ssh -p 2223 frontend-dev@localhost \
   -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null

frontend-dev@localhost: Permission denied (keyboard-interactive).

ちゃんと拒否されます! docker-compose のログをみると下のようにエラーを返しているのも確認できます。

opa_1       | time="2019-03-23T07:45:53Z" level=info msg="Sent response." client_addr="172.18.0.3:48434" req_id=39 req_method=POST req_path=/v1/data/sshd/authz resp_body="{\"result\":{\"allow\":false,\"errors\":[\"Request denied by administrative policy\"]}}" resp_bytes=79 resp_duration=2.7604 resp_status=200

準備したPolicyに対して、想定した動きになっているのが確認できました!

おわりに

本記事ではOPAとLinux-PAMを連携することで、SSHとsudoにOPAの認可Policyを適用する方法について見ました! 公式チュートリアルでは最後に 動的にユーザーの権限を昇格させる方法 について述べています。この記事では割愛していますが、ぜひそちらもチャレンジしてみてください。

「どんな人」が「何を」していいかというのをPolicyで制御できる

OPAの可能性の1つを学ぶことができました。

参考

kenfdev.hateblo.jp

Open Policy Agentを始めてみよう

www.openpolicyagent.org

Open Policy Agent(以下OPA)を最近ちょっとずつ耳にすることが多くなってきた気がします。KubeCon + CloudNativeCon 2017 - Austinで「How Netflix Is Solving Authorization Across Their Cloud」を見たときに「こんな感じに認可を外に出せたらなー」と思った記憶があります。 この記事ではOPAの簡単な紹介と、公式TutorialのGetting Startedを実践した記録を共有します!

OPAとは

OPAは汎用的なPolicy Engineと言われています。Policy Engineとは、定義されたルールに従って 判断を下すことができる専門家 です。Policyの中でも「やっていいかどうか(見ていいかどうか)」に関するものが「Access Policy」と呼ばれ、馴染みがあるかと思います。

例えばWebサービスでユーザーAの情報をログイン中のユーザーが見ていいかどうか、というのをDBに見に行くときに次のようなコード(ここではJavaScript)に見覚えがあるはずです。

// ログイン中のユーザーがAさんのマネージャーだったら参照してOK
const canView = login.id === userA.manager_id;

if (canView) {
    // DBからAさんの情報を取得する
}

これはこれですぐに読めるコードですが、OPAを使った場合なら次のように書くことができます。

// Aさんの情報を参照していいかどうかチェックする
const canView = opa.checkCanView(userA);

if (canView) {
    // DBから取得する処理を記述する
}

見ていいかどうかの 判断 がOPAにおまかせ(委譲)になった、というのが大きな違いです。こうすることでメインのアプリケーションでは「やっていいかどうか」を判断する必要が無くなり、OPAに 聞けばOK となります。

  • 【注意その1】OPAは別のサービスとして立ち上がっているので、 opa.checkCanView の中では、OPAへのHTTPリクエストが実行されていると思ってください。
  • 【注意その2】上の例はOPAを使ってREST APIリクエストの認可を行う例です。OPAが利用できる場面はこれだけではなく、Docker, SSH, sudo, Kafka, Kubernetes Admission Controlなど幅広いです。詳しくは 公式のTutorial を参照ください。

また、 login.id == userA.manager_id というコードはとても繊細で、 id だったり manager_id が固定(ハードコード)されてしまっています。この条件が変わると、メインのアプリケーションのソースコードに影響が出てしまいます。ちょっと考えただけでも下の理由で壊れてしまいそうです。

  • manager_idmanager.id のように構造が変わった
  • 「ユーザーAのマネージャー」という観点ではなく、「ログイン中のユーザーの部下」という観点に変わった
    • 例: login.subordinates.include(userA.id)
  • 「特定の時期に入社していること」が条件に加わった
    • login.id == userA.manager_id && userA.joined_at.year >= 2019

OPAにおまかせしていれば、ユーザーAというコンテキストが変わらない限りは、メインのアプリケーションへの影響はありません。変更はOPA側で吸収できます。

「やっていいかどうかは専門家におまかせ」で、メインのアプリケーションは本来届けたいと思っている価値の部分に集中できます。

それでは、OPAの仕組みがどうなっているのかというのを見てみましょう!

OPAのPolicy言語Rego

「ログイン中のユーザーが、あるユーザーのマネージャーだったら○○を許可する」というのはPolicyの一例です。OPAは定義されたPolicyに従って、リクエストに応じた判断を返してくれます。そして、Policyを書く際にRegoと呼ばれるOPA用の言語を使うというのが大きな特徴です。

Regoは、プログラミングを経験したことがある人ならなんとなーく読めるのではないでしょうか?長くなってしまうので詳しい説明についてはこの記事では割愛しますが、公式ドキュメントを一度は読んで見ることをおすすめします。

www.openpolicyagent.org

今のところRegoを書く際に最大限気をつけないといけないと思っていることは = の挙動です。

x := 7 declares that x is a variable local to the rule. Furthermore, the compiler will stop you from writing two such assignments in the same rule, e.g. x := 7; x := 8 creates a compile-time error.

Neither of those are true with x = 7. The rule x = 7 will assign x to 7 if x is unbound, and it will compare x to 7 if x is already bound. Also, if there is a global variable named x, x = 7 will compare that global value of x to 7.

In short, use := if you want assignment inside a rule.

何を言っているのかと言うと、要は変数に値が入っているかどうかで = は挙動が変わるということです。変数に何も入っていなければ 代入 になり、入っていれば 比較 になります。

OPAはRego用のREPLを用意しているので実際にやってみましょう。

$ docker run -it --rm openpolicyagent/opa run

> x = 7
Rule 'x' defined in package repl. Type 'show' to see rules.
> x = 7
true
> x = 8
undefined

1回目は x に何も値が入っていないので 代入 になります。2回目は x に既に値が入っているので 比較 になって、結果が true になっているのがわかります。3回目は直感的には false になってほしいところですが、 undefined になります。これも= の特徴の様ですが、 true になるかどうかを判定しにいくため、 true と判定できないものに関しては undefined になるというのが仕様みたいです。詳しくは公式ドキュメントの The Basics に記載してます。

この挙動は 直感的じゃない ので、別途代入には := という構文が増えて、比較には見慣れた == が使えるようになりました。

$ docker run -it --rm openpolicyagent/opa run

> x := 7
Rule 'x' defined in package repl. Type 'show' to see rules.
> x == 7
true
> x == 8
false

こちらのほうがだいぶわかりやすいですね!

Get Startedチュートリアル

さて、Regoの構文の話をしてもPolicyが見えづらいと思うので、実際にPolicyを書きながら雰囲気をつかんでいきましょう。公式のチュートリアル「Get Started」をやってみます。

www.openpolicyagent.org

まずは下図のようにServer, Network, Port情報があったとします。

f:id:kenev:20190317232317p:plain

自分が保有しているサーバーが上の通りだとして、

パブリックネットワークでは暗号化された通信しか許可しない(HTTPはNG)

というPolicyを持ちたいとします。これをRegoで書いてみましょう。

まずは作業用のディレクトリを作ってその中に入ります。

mkdir opa
cd opa

上図の情報をJSONにしたものを data.json として用意します。

$ cat >data.json <<EOF
{
    "servers": [
        {"id": "s1", "name": "app", "protocols": ["https", "ssh"], "ports": ["p1", "p2", "p3"]},
        {"id": "s2", "name": "db", "protocols": ["mysql"], "ports": ["p3"]},
        {"id": "s3", "name": "cache", "protocols": ["memcache"], "ports": ["p3"]},
        {"id": "s4", "name": "dev", "protocols": ["http"], "ports": ["p1", "p2"]}
    ],
    "networks": [
        {"id": "n1", "public": false},
        {"id": "n2", "public": false},
        {"id": "n3", "public": true}
    ],
    "ports": [
        {"id": "p1", "networks": ["n1"]},
        {"id": "p2", "networks": ["n3"]},
        {"id": "p3", "networks": ["n2"]}
    ]
}
EOF

上のデータに対してRegoファイルを以下のようにして作ります。

cat >example.rego <<EOF
package opa.example

import data.servers
import data.networks
import data.ports

public_servers[s] {
    s := servers[_]
    s.ports[_] == ports[i].id
    ports[i].networks[_] == networks[j].id
    networks[j].public == true
}

violations[s] {
  s = servers[_]
  s.protocols[_] = "http"
  public_servers[s]
}
EOF

example.rego というRegoをこれで書いたことになります。意味としてはざっくり以下のとおり。

  • opa.example というパッケージを宣言
  • data として予め読み込んである data.servers, data.networks, data.portsimport してルール( public_servers, violations )で使えるようにする
  • public_servers[s] ルールを宣言
    • servers の中で ports で宣言している portnetworks で宣言している network を使っていて、 networkpublic のものを public_servers とする
  • violations[s] を宣言
    • servers の中で、 protocolhttp かつ public_servers に含まれる server であれば violations とする

これを実際 opa に読み込ませてREPLで使ってみましょう。

docker run -it --rm -v $(pwd):/tmp/workspace -w /tmp/workspace openpolicyagent/opa run data.json example.rego

まず、データがあることを確認してみます。

> data.servers[_]
+-------------------------------------------------------------------------------+
|                                data.servers[_]                                |
+-------------------------------------------------------------------------------+
| {"id":"s1","name":"app","ports":["p1","p2","p3"],"protocols":["https","ssh"]} |
| {"id":"s2","name":"db","ports":["p3"],"protocols":["mysql"]}                  |
| {"id":"s3","name":"cache","ports":["p3"],"protocols":["memcache"]}            |
| {"id":"s4","name":"dev","ports":["p1","p2"],"protocols":["http"]}             |
+-------------------------------------------------------------------------------+

先程用意したJSONの情報が読み込まれているのがわかりますね。では public_servers はどうでしょう?

> data.opa.example.public_servers[_]
+-------------------------------------------------------------------------------+
|                      data.opa.example.public_servers[_]                       |
+-------------------------------------------------------------------------------+
| {"id":"s1","name":"app","ports":["p1","p2","p3"],"protocols":["https","ssh"]} |
| {"id":"s4","name":"dev","ports":["p1","p2"],"protocols":["http"]}             |
+-------------------------------------------------------------------------------+

public == true なネットワーク(n3)の中のポート(p2)を使用している appdev サーバーが返ってきているのがわかります。ちなみに [_] の部分は _ に値が無いので 全部 という意味になります。(なので該当するサーバーが全部出てます)

では、 violations も同様に見てみましょう。

> data.opa.example.violations[x]
+-------------------------------------------------------------------+
|                  data.opa.example.violations[x]                   |
+-------------------------------------------------------------------+
| {"id":"s4","name":"dev","ports":["p1","p2"],"protocols":["http"]} |
+-------------------------------------------------------------------+

(結果はトリムしてますが、)先程の public_servers の結果の中で、プロトコルhttp を使用している dev サーバーが violation として返ってきているのがわかります。

f:id:kenev:20190318003839p:plain

このように、宣言的にPolicyを書いていくことができるのがOPAの強みです!

余談: data.opa.example.violations[_] と書いた場合になぜかREPLではJSONが返ってきました。 x にすると表だったのですが、この違いがなぜ起きるのかはまだ原因がつかめていません。時間をみつけてOwnerに聞いてみます。

おわりに

今回はOPAの概要とPolicy言語Rego、そしてREPLについて紹介しました。まだ具体的にどう使うのかというのが見えづらい部分があると思いますがこの記事はいったんここまでとします!

  • やっていいかどうかを判断してくれる
  • 特定の条件を満たしているかどうかを教えてくれる
  • PolicyをRegoという独自の言語で書くことができる

点についてはなんとなくイメージできたのではないでしょうか?次回はREPLではなく、実際に「やっていいかどうか」を判断してもらう使い方について紹介します!

参考

コンテナの疲れをk3sとRemoで癒やした話

f:id:kenev:20190316002715p:plain

Cloud Native Kansai #02 に参加してきました!

cnjp.connpass.com

そしてこの記事の題名にもあるように「コンテナの疲れをk3sとRemoで癒やした話」という題名でLTもやらせていただきました。

発表資料

ストーリー

コンテナに限らずですが技術の進化が早すぎ&自分の守備範囲が比較的広いため、様々な分野にキャッチアップしていくのが年々大変になっていってる気がします。「手を出しすぎなだけでしょ?」と言われてしまえばそうなのかもしれないですが、無知であることが怖いと思うところもあってついつい手を出してしまうのです。

いろいろあってなんだか最近「追われながら学習している」ことが多い気がするので、純粋に技術を楽しもうと思って家にある「ラズパイ」、「Nature Remo」を使ってお家ハックしてみました!「開発の疲れは開発で癒やす」と昔先輩に教えてもらったので、「コンテナの疲れはコンテナで癒す」を実践しました(笑)

出来上がるとラズパイ1台で、 k3s で作ったk8s上のPrometheus, Grafanaを使って次の画面が作れるようになります!

注意:Grafanaのグラフは自分で設定する必要があります

f:id:kenev:20190313220955p:plain
GrafanaでRemoのデータを可視化

家のラズパイで試してみたい人は以下リポジトリの手順に従ってぜひ構築してみてください!

github.com

まとめ

  • コンテナの疲れはコンテナで癒やす
  • 純粋にテクノロジーを楽しむのも大事
  • アプリエンジニアと言えど守備範囲は広めよう
  • 「パパすごい」は最高の癒やし(褒め)言葉

余談

Clean Architectureに気づいてくれる人がいた

Cloud Native Meetupだけど、スライドに入っている図から以下を共感してもらえたことが実はすごくうれしかった。

armとamdで同じイメージパスを指定

おまけ情報ですが、今回のお家ハックを通してarmとamdアーキテクチャが違っていても、Docker HubのURLを同じものとして使える方法について知ることができました。参考になるのは以下のGrafanaのPR。これで、ラズパイ(arm)だろうがmacbookamd)だろうが、同じ grafana/grafana:latest イメージを指定することができます! kenfdev/remo-exporter も真似をして、同一のイメージパスを指定できます!

github.com

参考

  • 基盤には前回の記事でも紹介したk3sを使っています

kenfdev.hateblo.jp

  • ダッシュボードの雰囲気は以下記事を参考にしています

qiita.com

k3sをラズパイで起動するまでにやったこと

LightweightなKubernetesとして最近話題に上がっているk3s。僕もミーハーなのでさっそく家のラズパイに入れてみました。

k3s.io

k3sがなんなのか、という点についてはQiitaの以下の記事が簡潔によくまとまっています。

qiita.com

我が家には半年ほど前にkubeadmで構築したラズパイが放置されていたので、初期化してk3sを入れてみました。なんだかんだでいつも同じようなページをググりながらセットアップするので以下に手順を残しておきます。

【注意】この記事ではPodを動かしたりしません。あくまでラズパイをセットアップしてk3sでノード情報を取得(kubectl get nodes)するところまでをゴールとしています

MicroSDの準備

Raspbian Stretch Liteを使うので公式サイトからダウンロードします。

www.raspberrypi.org

これをMicroSDに焼きたいのでbalenaEtcherというソフトを使います。以下からダウンロードしておきます。

balenaEtcher - Home

これを使ってMicroSDにRaspbian Stretch Liteが入れられます。

ラズパイ用のディスプレイは持っていないのでsshできるようにしておきます。これはMicroSDのルートに ssh というファイルを置いておけばOK。おそらく boot というボリュームでマウントされているのでほとんどの場合次のコマンドでOKです。

$ sudo touch /Volumes/boot/ssh

あとはMicroSDをラズパイに差し込んで完了!

ラズパイの下準備

ラズパイを起動したらIPアドレスルーターなりで調べて ssh で繋ぎます。デフォルトユーザーは pi でパスワードは raspberry です。どのみち家でしか使わないのと、またすぐ破壊する環境なのでここらへんは特に変更しません。

$ ssh pi@192.168.11.4

ホスト名を変更

この手順は任意ですが、わかりやすいようにホスト名を変更しておきます。これには raspi-config を使用。

$ sudo raspi-config

次の画面が出てくるので 2 Network Options を選択。

f:id:kenev:20190311090918p:plain
raspi-configトップメニュー

続いて N1 Hostname を選択します。

f:id:kenev:20190311091018p:plain
raspi-config ホスト名変更

k3s-master とでも設定しておきます。

f:id:kenev:20190311091108p:plain

決定すると再起動するか聞かれるので再起動します。

cgroupの有効化

再起動後に再度 ssh して、次はRaspbianで cgroup を有効にしておきます。 この手順を忘れるとk3sの起動時にエラーとなる ので要注意です。

# 好きなエディタで開きます
$ sudo vi /boot/cmdline.txt

次の値を 行の末尾に追加します

 cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory

新しい行に追加しちゃダメです。 必ず 末尾 に追加してください。 追加したあとは↓のようになります。(わかりにくいですが1行です)

dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=PARTUUID=9cdfde31-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory

保存したら再起動しましょう。ラズパイの下準備はこれでOKです。

$ sudo reboot

k3sのインストールと起動

ラズパイに再度 ssh して次のコマンドを叩くだけでk3sの

  • 最新版のダウンロード
  • systemdへのサービス登録
  • 起動

まですべて完了します!

$ curl -sfL https://get.k3s.io | sh -

シェルの中身が気になる人は以下にアクセスすれば中身チェックできます。

https://get.k3s.io

関数の名前からも何をやっているかはなんとなくわかります。

# --- run the install process --
{
    verify_systemd
    setup_env ${INSTALL_K3S_EXEC} $@
    download_and_verify
    create_symlinks
    create_uninstall
    systemd_disable
    create_env_file
    create_service_file
    systemd_enable_and_start
}

kubectl get nodes をk3sで実行してみましょう。通常の kubectl の先頭に k3s をつければOKです。

$ k3s kubectl get nodes
NAME         STATUS   ROLES    AGE   VERSION
k3s-master   Ready    <none>   8d    v1.13.4-k3s.1

正常に実行できているのが確認できます。

k3s kubectl 毎回入力するので kalias 貼っておきます。

$ vi ~/.bashrc

末尾に以下を追加しておきました。

alias k='k3s kubectl'

これで k get nodes のようにもっと省略してコマンド打てるようになるのですっきり!

おわりに

以上でラズパイ上での k3s のセットアップは完了です!ほとんど同じような手順でクラスタにノードを追加していけるのですが、その話はまたの機会にします。

k3sで「お家ハック」もしていけると、モチベーション維持しながらkubernetesの経験も積んでいけるので引き続き共有していきたいと思います!