selmertsxの素振り日記

ひたすら日々の素振り内容を書き続けるだけの日記

CloudFunctionsをServerless Framework & TypeScriptの環境で試してみる

やったこと

Serverless Frameworkを使って CloudFunctionを動かしてみました。 試した内容は下記の通りです。

  • TypeScriptで書けるようにしてみる
  • serverless.ymlの設定で環境変数を利用する
  • 日本のリージョンにDeployする

実際に利用したコード https://github.com/selmertsx/study/tree/master/serverless/cloudfunction-sls-sample

前提

https://serverless.com/framework/docs/providers/google/guide/quick-start/

上記チュートリアルを実行して、nodejsのコードをdeploy & invokeできるようにしてある前提で話を進めます。

TypeScriptで書けるようにする

https://github.com/prisma/serverless-plugin-typescript このpluginを利用します。

手順は簡単で、下記の手順を踏むだけで設定可能です

  • serverless.yml のpluginsに serverless-plugin-typescript を追加する
  • 指定されたフォーマットでtsconfig.jsonの設定をする
  • 既存のコードをTypeScriptに置き換える
  • (options) package.jsonのmain fieldを書き換える
plugins:
  - serverless-google-cloudfunctions
  - serverless-plugin-typescript
// Note: outDir と rootDir optionsを上書きしないこと
{
  "compilerOptions": {
    "target": "es5",
    "outDir": ".build", 
    "moduleResolution": "node",
    "lib": ["es2015"],
    "rootDir": "./"
  }
}

もし起点となるコードが index.ts でなければ、package.jsonのmain fieldを下記のように書き換える必要があります。

// handler.tsを起点にしたい場合
{
  // ...
  "main": "handler.js",
  // ..
}

serverless.ymlの設定で環境変数を利用する

https://serverless.com/framework/docs/providers/google/guide/variables/

公式ドキュメントを見ると、ymlやjsとして別のファイルに切り出したり、serverless.dev.ymlのように環境毎にymlを分けたりする方法が書かれています。今回はgitにcommitしたくないコードの管理がしたかったので、ちょっとその用途とは違うため別の方法を模索しました。

https://serverless.com/framework/docs/providers/aws/guide/variables/

awsの方のGuideを見てみると、下記のような機能がありました。

o reference environment variables, use the ${env:SOME_VAR} syntax in your serverless.yml configuration file. It is valid to use the empty string in place of SOME_VAR. This looks like "${env:}" and the result of declaring this in your serverless.yml is to embed the complete process.env object (i.e. all the variables defined in your environment).

これについてgoogleでも使えないかと試してみたところ、普通に使えたので今回はそれを利用しました。

service: cloudfunction-sls-sample
provider:
  name: google
  project: ${env:PROJECT}

日本のリージョンにDeployする

cloud functionsでServerless Frameworkを利用し、regionを何も指定せずにdeployをすると us-central1にdeployがされます。 deploy先のregionをserverless.ymlで設定していきましょう。 deployする先の指定は、serverless-google-cloudfunctionsのv2.0.0 から出来るようになっています。 けれども、sls コマンドでinstallされるversionはv1.2.0なので、pluginのバージョンを上げる必要があります。

https://github.com/serverless/serverless/blob/63b8dafbeb7bd463acf9fe9a4f5ec51c7e972928/lib/plugins/create/templates/google-nodejs/package.json#L12

"devDependencies": {
  "serverless-google-cloudfunctions": "^2.0.0",
  "serverless-plugin-typescript": "^1.1.5"
}

その後、serverless.ymlの値を書き換えて実行してみます。

service: cloudfunction-sls-sample
provider:
  name: google
  region: asia-northeast1
  runtime: nodejs8
  project: ${env:PROJECT}

実行結果がこちらになります。

$ sls deploy
service: cloudfunction-sls-sample
project: xxx
stage: dev
region: us-central1

Deployed functions
first
  https://us-central1-xxx.cloudfunctions.net/http

ぱっと見るとus-central1にdeployされていてギョっとしますがcliを使って実際にdeployされているregionを確認してみると、ちゃんとasia-northeast1になっていることが確認できます。

gcloud beta functions describe http --region asia-northeast1
availableMemoryMb: 256
entryPoint: http
httpsTrigger:
  url: https://asia-northeast1-xxx.cloudfunctions.net/http <==ここをチェック
labels:
  goog-dm: sls-cloudfunction-sls-sample-dev
name: projects/xxx/locations/asia-northeast1/functions/http
runtime: nodejs8
serviceAccountEmail: xxx@appspot.gserviceaccount.com
sourceArchiveUrl: gs://sls-cloudfunction-sls-sample-dev-xxx/serverless/cloudfunction-sls-sample/dev/xxx-2018-09-25T10:08:07.390Z/cloudfunction-sls-sample.zip
status: ACTIVE
timeout: 60s
updateTime: '2018

AWS Lambdaと比較した所感

  • localでの動作確認について、lambdaはpluginで提供されているが、cloud functionsはまだない。
  • LambdaではTypeScriptテンプレートがあるので、特に何の指定もしなくて良い
  • Lambdaは起動するためのeventsが豊富
    • cloud functionsはhttpとpub subだけ
    • これがcloud functionsのデメリットとなるかは、pub subを触ってみないとなんとも言えない

コミットチャンス

宿題

  • credentialsや環境変数をcode buildなどから扱えるようにしたい
  • cloud functionに渡す権限を、もっとちゃんと整理したい

Azure ADのGraphAPIを利用する

この資料を読んでできるようになること

手順

  • Appを登録する
  • AccessTokenを取得する
  • GraphAPIを叩く

Appを登録する

  • Azure Active Directoryを選択
  • App Registrationsを選択
  • nameとsign-on URLを設定
    • このとき sign-on URLは適当なものでも良い
  • createボタンを押す
  • APIが作成されて画面が変化する
  • API AccessカテゴリのRequired Permissionsを選択
  • Microsoft Graphの中から必要なものを選択
  • Grant permissionsを実行する
  • ふたたび API Access カテゴリの中からKeysを選択
  • 適当にpasswordのdescriptionsを記入し保存すると、secret_keyが表示されるのでメモっておく
  • Application IDもメモっておく

AccessTokenを取得する

https://developer.microsoft.com/ja-jp/graph/docs/concepts/auth_v2_service#4-get-an-access-token

Graph APIを実行する際に必要なAccess Tokenの取得方法について、上記のドキュメントに記載されています。それによると、Access Tokenを取得するために必要なパラメータはclient_id、scope、client_secret, grant_typeの4つです。

// Line breaks are for legibility only.

POST /{tenant_id}/oauth2/v2.0/token HTTP/1.1
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded

client_id=535fb089-9ff3-47b6-9bfb-4f1264799865
&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
&client_secret=qWgdYAmab0YSkuL1qKv5bPX
&grant_type=client_credentials

上記リクエストで必要なパラメータは、それぞれ下記のように設定します。

実際にAccessTokenを取得するためのコードは下記のようになります。

import * as request from "request";

const OauthTokenEndpoint = process.env.OAUTH_TOKEN_ENDPOINT;
const AppID = process.env.APP_ID;
const AppKEY = process.env.APP_KEY;

export function getAccessToken(): Promise<string> {
  const requestParams = {
    grant_type: "client_credentials",
    client_id: AppID,
    client_secret: AppKEY,
    scope: "https://graph.microsoft.com/.default"
  };

  return new Promise((resolve, reject) => {
    request.post({ url: OauthTokenEndpoint, form: requestParams }, function(err, _response, body) {
      var parsedBody = JSON.parse(body);
      if (err) reject(err);
      if (parsedBody.error_description) reject(parsedBody.error_description);
      resolve(parsedBody.access_token);
    });
  });
}

このとき、OAUTH_TOKEN_ENDPOINTはhttps://login.microsoftonline.com/${tenant_id}/oauth2/v2.0/tokenのような値になります。

GraphAPIを叩く

Azure ADにおいてディレクトリ内のデータに対して操作をするには Graph APIを利用する必要があります。今回はAzure AD上にGroupを作ってみることにしました。 https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/group_post_groups

このAPIを実行するために、microsoftがpackageを提供しているので利用してみても良いでしょう。 https://github.com/microsoftgraph/msgraph-typescript-typings

実装

import * as auth from "./auth";
import { AzureClient } from "./AzureClient";

async function handler(): Promise<void> {
  const token: string = await auth.getAccessToken();
  const client: AzureClient = new AzureClient(token);
  const res = await client.createGroup();
  console.log(res);
}

handler();
import { Client } from "@microsoft/microsoft-graph-client";

export class AzureClient {
  client: Client;

  constructor(token: string) {
    this.client = Client.init({
      authProvider: done => {
        done(null, token);
      }
    });
  }
  public createGroup() {
    const requestParams = {
      displayName: "sample Display Name",
      mailEnabled: false,
      mailNickname: "SAMPLE",
      securityEnabled: true
    };
    return this.client.api("/groups").post(requestParams);
  }
}

レスポンス

{ 
  '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups/$entity',
  id: 'xxxx',
  deletedDateTime: null,
  classification: null,
  createdDateTime: '2018-09-21T04:21:05Z',
  creationOptions: [],
  description: null,
  displayName: 'sample Display Name',
  groupTypes: [],
  mail: null,
  mailEnabled: false,
  mailNickname: 'idaas',
  onPremisesLastSyncDateTime: null,
  onPremisesSecurityIdentifier: null,
  onPremisesSyncEnabled: null,
  preferredDataLocation: null,
  proxyAddresses: [],
  renewedDateTime: '2018-09-21T04:21:05Z',
  resourceBehaviorOptions: [],
  resourceProvisioningOptions: [],
  securityEnabled: true,
  visibility: null,
  onPremisesProvisioningErrors: [] 
}

ということで無事groupを作成することができました。

OpenID Foundationのガイドラインに沿ったRailsでのOIDC Implicit Flow実装

IDaaSを社内サービスに適用したかったので、その準備としてOpenID Foundation Japanのガイドラインが伝える OpenID Connect Implicit Flowの実装方法を試してみました。

前提

  • 本ドキュメントではOpenID Connectの仕様については説明しない
  • OpenID Foundation Japanが提供する実装ガイドに従って認証機能の実装を行う
  • 認証のフローはImplicit Flowを採用している
    • Authorization Code Flowは、OPとRPが直接やりとりをする必要がある
      • RP側でOPからアクセスできる口を開けなければならない
    • 上記理由から、ガイドラインではImplicit Flowの実装方法のみ記載されている (p.40 3.4.2 認証フロー参照)
  • 今回の実装では、強制ログアウト機能は未実装
  • SCIMも未対応

目次

  • Implicit Flowの概要
  • 実際の作業とRails実装
  • 懸念点

Implicit Flowの概要

f:id:selmertsx:20180822104348p:plain

( ※ https://www.openid.or.jp/news/eiwg_implementation_guideline_1.0.pdf のp.13より引用 )

Implicit FlowではOPとRPが直接やりとりをせずに、ブラウザ経由でIDトークンの受け渡しを行う。そのためRPでIDトークが改ざんされていないことを検証する必要がある。IDトークンとはエンドユーザーのOPに対する認証結果であり、emailやprofileなどのユーザー情報、OpenID Providerの情報、リプレイ攻撃対策のための文字列などを含んでいる。

Railsの実装とAzure AD側の設定

参考にした資料

参考実装1は、Dynamic Client Registrationを用いた OPの設定をしているものの、gem作成者が下記のような意見をしている。

https://github.com/nov/openid_connect/wiki/Client-Discovery

Some OPs are supporting Discovery without supporting Dynamic Registration, but I don't think RPs "need" to discover OP config for such OPs. Almost all RPs won't need this feature currently.

参考実装2は、シンプルな実装なので読みやすいものの、最新のopenid_connect gemでは動かない。このコミット で動作しなくなったものと思われる。

OPにRPの情報を登録する (AzureADの設定)

f:id:selmertsx:20180822104434p:plain f:id:selmertsx:20180822104444p:plain

Azure ADに対してRP側の情報を登録する(登録方法)。登録するべき情報はリダイレクト先のURLと、ログアウト要求を受け付けるエンドポイントのURLの2点である( ガイドライン P.31 ) 。リダイレクト先のURLは完全一致で指定しなければならない(p.30)。ここで登録することによって、OPからRPにClientIDが割り振られる。

RPにOPの情報を登録する

上記手順により得られたClientIDをRP側に設定する。ガイドラインから公開鍵をJWK Set形式で、外部からアクセスできるURIに配置した場合は、下記4点のパラメータをRPに登録することとなる。( P.32~33 )

  • OPから割り当てられたClient ID
  • Issuerの識別子
  • 認証エンドポイントURI
  • JWKs URI

Azure ADにてこれらのパラメータを取得する方法については、このドキュメントを参照。コレをもとに実際に実装したRailsのコードは下記のようになる。

# app/models/authorization.rb
class Authorization
  include ActiveModel::Model

  AUTHORIZATION_ENDPOINT = Rails.application.credentials.oidc[:authorization_endpoint].freeze
  ISSUER = Rails.application.credentials.oidc[:issuer].freeze
  IDENTIFIER = Rails.application.credentials.oidc[:identifier].freeze
  JWKS_URI = Rails.application.credentials.oidc[:jwks_uri].freeze

  attr_reader :client

  def initialize
    @client = OpenIDConnect::Client.new(
      issuer: ISSUER,
      identifier: IDENTIFIER,
      authorization_endpoint: AUTHORIZATION_ENDPOINT
    )
  end

  private

  // JWKは鍵を適宜DLして利用する
  def jwk_json
    @jwks ||= JSON.parse(
      OpenIDConnect.http_client.get_content(JWKS_URI)
    ).with_indifferent_access

    JSON::JWK::Set.new @jwks[:keys]
  end
end

RPからブラウザリダイレクトをしてOPに認証要求をする

ガイドラインのP.49 4.1.3 利用企業の認証サーバーへの認証要求 に、OPへの認証要求の方法が記載されている。そこではqueryパラメータに値を設定し、OPにアクセスをすることで認証の要求をすることになっている。

// OPに認証リクエストをする際のURL
https://login.microsoftonline.com/xxx/oauth2/v2.0/authorize?
  client_id=xxx
  &nonce=33cf756a5fc3c69c05f40ae6baa17dac
  &redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback
  &response_type=id_token
  &scope=openid+email+profile
  &state=8fd4930f285a4e9e

今回はOPとしてAzure ADを利用しているため、各種パラメータの値はAzureADの公式ドキュメントを参照する。このサンプルではユーザーの基本情報しか取得しないため、response_typeはid_tokenとした。

# app/controllers/authorizations_controller.rb 一部抜粋
class AuthorizationsController < ApplicationController

  def new
    redirect_to authz.authorization_uri(new_state, new_nonce)
  end

  private

  def authz
    @authz ||= Authorization.new
  end

  def new_state
    session[:state] = SecureRandom.hex(8)
  end

  def new_nonce
    session[:nonce] = SecureRandom.hex(16)
  end
end
# app/models/authorization.rb 一部抜粋
class Authorization
  include ActiveModel::Model
  ...
  def authorization_uri(state, nonce)
    client.authorization_uri(
      response_type: 'id_token',
      state: state,
      nonce: nonce,
      scope: %w[openid email profile]
    )
  end
end

OPから認証結果を受け取る

OPに対して認証要求をした結果、redirect_uri で設定したURIに対して、認証結果がフラグメントとして付与される(ガイドライン p.50)。

// 受け取るレスポンス
http://localhost:3000/authorization
#token_type=Bearer
&expires_in=3599
&scope=profile+openid+email+xxxx%2fUser.Read
&id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6ImlBdzUifQ.eyJpc3MiOiJodHRwczovL29wLmNvbS5
leGFtcGxlLmNvLmpwIiwic3ViIjoiZTEyMzQ1NjciLCJhdWQiOiJwV0JvUmFtOXNHIiwib
m9uY2UiOiJxOGstdXBCWDRaX0EiLCJleHAiOjE0Mfafsafadfdafavfabfdaa
xNDM1NzA5NzYyfQ.paxubCBAPwITlNgg-Mi_RkX5EfaRVU8YGT
Z2e9UwUX1EIwBDU3i_uCqUj-yUMY6Li1rjbpHurYEw7N3ZQPdBlVy6GiMQtUFg6Ju-0MY4
rw0uiZ7HFoGenAMzoR8fNd4iIuyM4oOEF6WuV5JLfZmJ5fvgjTbAD5H-2SGeM38P8UibRy
v2lB-YldI8a7nEUGXz5m5iw-WpBgk4SboMWXsyg-jr-_fbRMn9_RR76-691P45-1p8BU8w
BMEwgHVARTbRg53W6dlILAl_FDWWIBxUboI9_uTKu7HCYuZecU7Uub6MXG5EMWJKUPjVuu
HuvkzBsl7MdKCRZuwI1fEAU35lYQ
&state=d612642b27161676
&session_state=e9ed8a15-9337-4cb1-bb8b-823dfef7a15d

今回は GET authorization/callback をredirect先に指定したため、下記のような実装になる。なおJavaScriptのコードについてはOpenID Connect Core 15.5.3 のコードそのままとしている。

<%# app/views/authorizations/callback.html.erb %>
<h1>CallBack!!</h1>
<%= javascript_pack_tag "openid_connect" %>
// app/packs/openid_connect.js
// authorization/callbackで実行されるJavaScript
let params = {}
const postBody = location.hash.substring(1);
const regex = /([^&=]+)=([^&]*)/g;
let m;
while (m = regex.exec(postBody)) {
  params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
}
var req = new XMLHttpRequest();
req.open('POST', '//' + window.location.host + '/authorization', true);
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
var token = document.querySelector("meta[name='csrf-token']").attributes['content'].value;
req.setRequestHeader('X-CSRF-Token', token);
req.onreadystatechange = function (e) {
  if (req.readyState == 4) {
    if (req.status == 200) {
      console.log("success!!");
    }
    else if (req.status == 400) {
      alert('There was an error processing the token')
    } else {
      alert('Something other than 200 was returned')
    }
  }
};
req.send(postBody);

ID トークンの検証を行う

認証に成功した後に、IDトークンの検証を行う。IDトークンの検証においては、state値が認証要求時に指定した値と同一であるかチェックした後(p.52)、jwkを用いてIDトークンをdecodeして検証を行う。検証項目についてはガイドラインを参照とする(p.54)。

# app/controllers/authorizations_controller.rb 一部抜粋
class AuthorizationsController < ApplicationController
  def create
    if stored_state != fragment_params[:state]
      return render status: :unauthorized
    end

    id_token = authz.verify!(fragment_params[:id_token], stored_nonce)
    session[:identifier] = id_token.subject
    render json: id_token.raw_attributes, status: :ok
  end
end
# app/models/authorization.rb 一部抜粋
class Authorization
  include ActiveModel::Model

  def verify!(jwt_string, nonce)
    id_token = decode_jwt_string(jwt_string)
    id_token.verify!(
      issuer: ISSUER,
      client_id: IDENTIFIER,
      nonce: nonce
    )
    id_token
  end

  private

  def decode_jwt_string(jwt_string)
    OpenIDConnect::ResponseObject::IdToken.decode(jwt_string, jwk_json)
  end

  def jwk_json
    @jwks ||= JSON.parse(
      OpenIDConnect.http_client.get_content(JWKS_URI)
    ).with_indifferent_access

    JSON::JWK::Set.new @jwks[:keys]
  end
end
// ID TokenをDecodeした結果
{
 "aud"=>"xxxx", // required: IDトークンを利用しようとするクライアント
 "iss"=>"https://login.microsoftonline.com/xxxx/v2.0", //required: ID Tokenの発行者(Issuer)を表す識別子
 "iat"=>1534834161,  // require: IDトークンが発行された時刻。UTC
 "nbf"=>1534834161,
 "exp"=>1534838061, // required: IDトークンの有効期限
 "aio"=> "xxx",
 "name"=>"selmertsx",
 "nonce"=>"fcecee85d465e7df8e6d225c8e00bf15", // option: リプレイ攻撃の対策に利用される文字列
 "oid"=>"xxx",
 "preferred_username"=>"shuhei.morioka@hogehoge.com",
 "sub"=>"xxx", //required: 認証されたユーザーを表す識別子. Issuer内で一意
 "tid"=>"xxx",
 "uti"=>"xxx",
 "ver"=>"2.0"
}

懸念点

  • openid_connectのgemではlogout_termの検証をしていない

openid_connectのgemではlogout_termの検証をしていない

ガイドラインにおいて、IDトークンの検証として、最低限下記の項目について検証することとしている。(p.54)

  1. iss クレームが、認証要求を送った利用企業の認証サーバーの URL と一致することを検証する。
  2. aud クレームが、自クラウドサービスに割り当てられた client_id であることを検証する。 もし aud クレームが複数の値を持つ場合は、OpenID Connect Core 1.0 [OpenID.Core] の Section 3.1.3.7 の定義に従った追加の検証が必要である。
  3. ID トークンの署名を、RS256 アルゴリズムを用いて検証する。 ID トークンの base64url エンコーディングされた 1 番目、2 番目のパートを '.' で連結し た文字列を、ハッシュ関数に SHA-256 を使うように構成された RSASSA-PKCS1-v1_5 署名検証器にかけることで検証を行なう。 具体的な手順は JWS [RFC7515] の Section 5.2, Section A.2 などに示されている。署名 検証に用いる公開鍵の取得方法は [4.1.9 節] に示す。 もし、ID トークンに RS256 以外の署名アルゴリズムが指定されており、その署名をク ラウドサービスが検証可能な場合は、その検証で代替しても良い。
  4. 現在時刻が exp クレームで指定された時刻よりも前であることを検証する。
  5. nonce クレームが、クライアント(ブラウザ)セッションと紐付いた nonce 値と一致す ることを検証する。
  6. logout_only クレームが含まれていないことを検証する。

ここで3と6の項目について、現在のopenid_connectのライブラリでは検証がされていないように見受けられた。現在、これが検証されていない場合、どのようなリスクがあるのか調査中。

# openid_connect/lib/openid_connect/response_object/id_token.rb
# https://github.com/nov/openid_connect/blob/007d35f249b028a43391550153c22e9cf006e661/lib/openid_connect/response_object/id_token.rb#L24-L35

      def verify!(expected = {})
        raise ExpiredToken.new('Invalid ID token: Expired token') unless exp.to_i > Time.now.to_i
        raise InvalidIssuer.new('Invalid ID token: Issuer does not match') unless iss == expected[:issuer]
        raise InvalidNonce.new('Invalid ID Token: Nonce does not match') unless nonce == expected[:nonce]

        # aud(ience) can be a string or an array of strings
        unless Array(aud).include?(expected[:audience] || expected[:client_id])
          raise InvalidAudience.new('Invalid ID token: Audience does not match')
        end

        true
      end

その他

ユーザー情報の受け取り方

ユーザー属性情報の受け取り方について、scopeを用いるものとUserInfoエンドポイントを提供するものがある。Azure ADにおいては、今回の実装ではUserInfoエンドポイントを使わずに、scopeに値を追加する形にした。

https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow#send-the-sign-in-request

RailsでOpenID Connect Implicit Flowを試す

モチベーション

  • RailsでIDaaSを使った認可認証を一通り自分で作りたい
  • それによってOIDC Implicit Flowの実現において、必要な処理に関する理解を深めたい
  • OIDC Implicit Flowを使ったのは、既存のSPAサービスに組み込むのであれば、これが一番楽そうだったので
    • 他にもSAML認証などがある。おいおい試していきたい

自分で用意したサンプル実装はこちら

GitHub - selmertsx/pomodoro

今回参考にした資料

前準備

https://docs.microsoft.com/ja-jp/azure/active-directory/develop/v1-protocols-oauth-code#register-your-application-with-your-ad-tenant

上記URLの「AD テナントへのアプリケーションの登録」をそのままなぞれば大丈夫。1点だけ気をつけるべきポイントは、Reply URLsにはフルパスを指定する必要があること。

f:id:selmertsx:20180813172651p:plain

今回の処理の概要

今回は実装にあたって、openid-foundation-japanの提供するサンプルを参考にした。サンプル実装を取り入れるにあたって、あまりフローの整理などはしていない。もう少し整理ができるはずなので、ちょいちょいコードを整えていきたい。

  1. Client(JS Frontend)からServerに、IdP(Azure AD)への認証URLを問い合せる
  2. Rails Serverで、Clientを認証用 URLにリダイレクトさせる
  3. IdPでユーザーの認証を行う
  4. IdPでユーザーの認証ができたら、Rails Serverの任意のURLにリダイレクトをさせる
  5. Rails Serverで、IdPから受け取ったID Tokenをdecodeする
  6. 問題なければLogin後画面に遷移させる

実装説明

IdPの認証URL問い合わせ

# app/controllers/authorizations_controller.rb
class AuthorizationsController < ApplicationController

  def new
    redirect_to authz.authorization_uri(new_state, new_nonce)
  end

  private

  def authz
    @authz ||= Authorization.new
  end

  def new_state
    session[:state] = SecureRandom.hex(8)
  end

  def new_nonce
    session[:nonce] = SecureRandom.hex(16)
  end
end
# app/models/authorization.rb 一部抜粋
class Authorization
  include ActiveModel::Model

  def initialize
    @issuer = Rails.application.credentials.oidc[:issuer]
    @identifier = Rails.application.credentials.oidc[:identifier]
    @redirect_uri = Rails.application.credentials.oidc[:redirect_uri]
  end

  def authorization_uri(state, nonce)
    client.redirect_uri ||= redirect_uri
    response_type = %i[id_token token]
    client.authorization_uri(
      response_type: response_type.collect(&:to_s),
      state: state,
      nonce: nonce,
      scope: %i[openid email profile].collect(&:to_s)
    )
  end
  
  private

  def client
    @client ||= OpenIDConnect::Client.new(
      issuer: issuer,
      identifier: identifier,
      authorization_endpoint: config.authorization_endpoint
    )
  end

  def config
    @config ||= OpenIDConnect::Discovery::Provider::Config.discover! issuer
  end
end

authorization_uri methodで生成される文字列は下記のようになる。

https://login.microsoftonline.com/xxx/oauth2/v2.0/authorize?
client_id=xxx&
nonce=33cf756a5fc3c69c05f40ae6baa17dac&
redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&
response_type=id_token+token&
scope=openid+email+profile&
state=8fd4930f285a4e9e

ID TokenのDecode

authorizations/callback

受け取るレスポンス

http://localhost:3000/authorization
#access_token=xxx
&token_type=Bearer
&expires_in=3599
&scope=profile+openid+email+00000003-0000-0000-c000-000000000000%2fUser.Read
&id_token=xxxx
&state=d612642b27161676
&session_state=e9ed8a15-9337-4cb1-bb8b-823dfef7a15d
# app/models/authorizations_controller.rb 一部抜粋
class AuthorizationsController < ApplicationController
  def callback
  end
end
# app/views/authorizations/callback.html.erb
<h1>CallBack!!</h1>
<%= javascript_pack_tag "openid_connect" %>
// app/packs/openid_connect.js 一部抜粋
let params = {}
const postBody = location.hash.substring(1);
const regex = /([^&=]+)=([^&]*)/g;
let m;
while (m = regex.exec(postBody)) {
  params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
}
var req = new XMLHttpRequest();
req.open('POST', '//' + window.location.host + '/authorization', true);
req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
var token = document.querySelector("meta[name='csrf-token']").attributes['content'].value;
req.setRequestHeader('X-CSRF-Token', token);
req.onreadystatechange = function (e) {
  if (req.readyState == 4) {
    if (req.status == 200) {
      console.log("success!!");
    }
    else if (req.status == 400) {
      alert('There was an error processing the token')
    } else {
      alert('Something other than 200 was returned')
    }
  }
};
req.send(postBody);

authorization/create

# app/controllers/authorizations_controller.rb 一部抜粋
class AuthorizationsController < ApplicationController
  def create
    # TODO: strong parameter使う
    id_token = authz.decode_id_token(params) 
    id_token.verify!(issuer: authz.issuer, client_id: authz.identifier, nonce: stored_nonce)
    session[:identifier] = id_token.subject
    render json: id_token.raw_attributes, status: :success
  end
end

細かい説明については、OIOI載せていく

Query and Visualize AWS Cost and Usage Data Using Amazon Athena and Amazon QuickSightのメモ

モチベーション

  • 弊社ではセキュリティ対策の一環からAWS Organizationとそれに関連するセキュリティ対策を導入した
  • セキュリティ対策で利用しているAWS Config Rule, GuardDuty, CloudTrailのサービスについては按分計算が必要
  • 按分をいい感じに計算したいので諸々調査してみる

この記事に書かれていること

aws.amazon.com

上記ページを読んで簡単にサマリーしてみる。

  • CloudFormationを使って、AWS Cost and Usage Reportsを Athenaで分析できるようにする
  • LambdaはAthenaが読み込めるように、適切なS3のパスでデータを格納させる
  • AWS Cost and Usage Reportsは、RedShiftで見れるように設定しとかなければならない
    • これはマニフェストが変更されるだけなので、実際にRedShiftにデータが入る訳ではない。
    • この設定をしても、実際に反映されるのは翌日

このCloudFormationがやってくれること

  • athenaで読み込める形のデータがS3に突っ込まれる
    • パス: year=${current_year}/month=${current_month}
    • ちゃんとmonth単位で分割することで、余計なデータを読み込みに行かない
  • aws_billing_report databaseがathenaに作られる
  • my_cur_reportというテーブルがathenaに作られる

CloudFormationの設定

  • CostnUsageRport: AWS Cost and Usage Reportの名前
  • S3BucketName: Athenaで分析するためのデータが格納される S3 bucketの名前
  • S3 CURBucket: AWS Cost and Usage Reportが現在格納されているS3 bucketの名前
  • CloudFormationを実行したら、書きのbucketがS3に生成される
    • year=current_year
    • aws-athena-query-results

Adding a Lambda trigger

  • CloudFormation Stackが実行完了したら、Lambda Triggerを設定する。
  • LambdaのTriggerは、AWS Cost and Usage Reportを出力しているS3 bucketが良い。
  • lambda console => aws-cost-n-usage-main-lambda-fn-A => Trigger S3
  • S3 bucket Nameは、Cost and Usage Reportsを出しているやつ。
  • Event TypeはObject Created.

Resources built, trigger created… now what?

これらのlambda functionが作成されていることを確認しといてね

  • aws-cost-n-usage-S3-lambda-fn-B
  • aws-cost-n-usage-main-lambda-fn-A
  • aws-cost-n-usage-S3-lambda-fn-B-2
  • aws-cost-n-usage-Athena-lambda-fn-C
  • aws-cost-n-usage-Athena-lambda-fn-C-2
  • Athena DatabaseやTableが作成されたあとQueryを実行できる
SELECT from_iso8601_timestamp(lineitem_usagestartdate) AS lineitem_usagestartdate,
        from_iso8601_timestamp(lineitem_usageenddate) AS lineitem_usageenddate,
        product_instancetype,
        count(*) AS count
FROM aws_billing_report.my_cur_report
WHERE lineitem_productcode='AmazonEC2'and (lineitem_operation LIKE '%RunInstances%'
        OR lineitem_usagetype LIKE '%BoxUsage%')
        AND lineitem_usagetype NOT LIKE 'SpotUsage%'
        AND lineitem_usagetype NOT LIKE '%Out-Bytes%'
        AND lineitem_usagetype NOT LIKE '%In-Bytes%'
        AND lineitem_usagetype NOT LIKE '%DataTransfer%'
        AND pricing_term='OnDemand'
GROUP BY  lineitem_usagestartdate,lineitem_usageenddate,product_instancetype,lineitem_usagetype
ORDER BY  lineitem_usagestartdate, product_instancetype;

LambdaのTest

感想

  • コードをGitHubで確認させて欲しい
  • 現在の要件なら、S3 Selectでも十分に対処できそう
  • AWS Lambda + S3 Selectで作ることにした

おまけ CloudFormation.yml

AWSTemplateFormatVersion: 2010-09-09
Description: >-
  This CloudFormation Template builds a Serverless Solution for Querying Amazon
  Cost & Usage Report!. It creates an S3 bucket that will be used to store your
  Cost & Usage Reports after they have been extracted and transformed. The
  extraction and transformation is done by an Amazon Lambda function that is
  also created by this template. This Lambda functions job also includes, the
  creation of an Amazon Athena database and table. After table creation, it adds
  the partition to the Athena (or Hive) meta-data store. Something to note, on
  the Athena table is crreated from the column title/header field of the Cost &
  Usage report. This means that the table schema will keep up with any changes
  made by Amazon billing to the Cost & Usage Report. A few other things created
  by this CloudFormation template are an IAM LambdaFullAccess Role and
  Environment Variables for Lambda.
Parameters:
  CostnUsageReport:
    Description: Please enter the the name of your Cost & Usage Report
    Type: String
    ConstraintDescription: This field should only hold the name of your Cost & Usage Report.
  s3CURBucket:
    Description: >-
      Please enter the name of the S3 Bucket in which the Amazon Billing System
      currently writes your Cost & Usage Reports. You must enter the name only,
      for eg. MYBUCKETNAME
    Type: String
  S3BucketName:
    Type: String
    Description: >-
      This solution creates one S3 bucket to store your transformed CUR. Please
      enter the name you would like this bucket to have. The bucket names must
      be a series of one or more labels. It can contain lowercase letters,
      numbers, and hyphens but bucket names must not be formatted as an IP
      address (e.g., 192.168.5.4). Each label must start and end with a
      lowercase letter or a number.
    MinLength: '3'
    MaxLength: '63'
    Default: S3 Bucket Name being created
Mappings:
  RegionMap:
    us-east-1:
      bucketname: a-athena-nvirginia
    us-east-2:
      bucketname: a-athena-ohio
    us-west-1:
      bucketname: a-athena-ncalifornia
    us-west-2:
      bucketname: a-athena-oregon
    ca-central-1:
      bucketname: a-athena-central-canada
    eu-west-1:
      bucketname: a-athena-ireland
    eu-west-2:
      bucketname: a-athena-london
    ap-southeast-1:
      bucketname: a-athena-singapore
    ap-northeast-1:
      bucketname: a-athena-tokyo
    ap-south-1:
      bucketname: a-athena-mumbai
    ap-northeast-2:
      bucketname: a-athena-seoul
    ap-southeast-2:
      bucketname: a-athena-sydney
    eu-central-1:
      bucketname: a-athena-frankfurt
    sa-east-1:
      bucketname: a-athena-sao-paulo
Resources:
  myS3Bucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Ref S3BucketName
  LambdaExecutionRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      Policies:
        - PolicyName: lambdaroot
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 'cloudwatch:*'
                  - 'athena:*'
                  - 'glue:*'
                  - 'dynamodb:*'
                  - 'events:*'
                  - 'iam:ListAttachedRolePolicies'
                  - 'iam:ListRolePolicies'
                  - 'iam:ListRoles'
                  - 'iam:PassRole'
                  - 'lambda:*'
                  - 'logs:*'
                  - 's3:*'
                  - 'sns:ListSubscriptions'
                  - 'sns:ListSubscriptionsByTopic'
                  - 'sns:ListTopics'
                  - 'sns:Subscribe'
                  - 'sns:Unsubscribe'
                  - 'sns:Publish'
                  - 'sqs:ListQueues'
                  - 'sqs:SendMessage'
                  - 'tag:GetResources'
                  - 'kms:ListAliases'
                  - 'ec2:DescribeVpcs'
                  - 'ec2:DescribeSubnets'
                  - 'ec2:DescribeSecurityGroups'
                  - 'xray:PutTraceSegments'
                  - 'xray:PutTelemetryRecords'
                Resource: '*'
  CURLambdaFn:
    Type: 'AWS::Lambda::Function'
    Properties:
      FunctionName: aws-cost-n-usage-main-lambda-fn-A
      Handler: File-Reader-Main-Lmbda-Fn-A.handler
      Role: !GetAtt
        - LambdaExecutionRole
        - Arn
      Timeout: '300'
      Runtime: nodejs6.10
      MemorySize: 1536
      Code:
        S3Bucket: !FindInMap
          - RegionMap
          - !Ref 'AWS::Region'
          - bucketname
        S3Key: File-Reader-Main-Lmbda-Fn-A.zip
      Environment:
        Variables:
          S3_BUCKET: !Ref S3BucketName
          iCUR_NAME: !Ref CostnUsageReport
          CUR_S3_BUCKET: !Ref s3CURBucket
          REGIONX: !Ref 'AWS::Region'
  S3CURLambdaFn:
    Type: 'AWS::Lambda::Function'
    Properties:
      FunctionName: aws-cost-n-usage-S3-lambda-fn-B
      Handler: File-Reader-S3-CUR-Parser-Lmbda-B.handler
      Role: !GetAtt
        - LambdaExecutionRole
        - Arn
      Timeout: '300'
      Runtime: nodejs6.10
      MemorySize: 1536
      Code:
        S3Bucket: !FindInMap
          - RegionMap
          - !Ref 'AWS::Region'
          - bucketname
        S3Key: File-Reader-S3-CUR-Parser-Lmbda-B.zip
      Environment:
        Variables:
          S3_BUCKET: !Ref S3BucketName
          iCUR_NAME: !Ref CostnUsageReport
          CUR_S3_BUCKET: !Ref s3CURBucket
          REGIONX: !Ref 'AWS::Region'
  S3CURLambdaFnB:
    Type: 'AWS::Lambda::Function'
    Properties:
      FunctionName: aws-cost-n-usage-S3-lambda-fn-B-2
      Handler: File-Reader-S3-CUR-Parser-Lmbda-B-2.handler
      Role: !GetAtt
        - LambdaExecutionRole
        - Arn
      Timeout: '300'
      Runtime: nodejs6.10
      MemorySize: 1536
      Code:
        S3Bucket: !FindInMap
          - RegionMap
          - !Ref 'AWS::Region'
          - bucketname
        S3Key: File-Reader-S3-CUR-Parser-Lmbda-B-2.zip
      Environment:
        Variables:
          S3_BUCKET: !Ref S3BucketName
          iCUR_NAME: !Ref CostnUsageReport
          CUR_S3_BUCKET: !Ref s3CURBucket
          REGIONX: !Ref 'AWS::Region'
  AthenaCURLambdaFn:
    Type: 'AWS::Lambda::Function'
    Properties:
      FunctionName: aws-cost-n-usage-Athena-lambda-fn-C
      Handler: File-Reader-Athena-Lambda-Fn-C.handler
      Role: !GetAtt
        - LambdaExecutionRole
        - Arn
      Timeout: '300'
      Runtime: nodejs6.10
      MemorySize: 1536
      Code:
        S3Bucket: !FindInMap
          - RegionMap
          - !Ref 'AWS::Region'
          - bucketname
        S3Key: File-Reader-Athena-Lambda-Fn-C.zip
      Environment:
        Variables:
          S3_BUCKET: !Ref S3BucketName
          CUR_S3_BUCKET: !Ref s3CURBucket
          REGIONX: !Ref 'AWS::Region'
  AthenaCURLambdaFnC:
    Type: 'AWS::Lambda::Function'
    Properties:
      FunctionName: aws-cost-n-usage-Athena-lambda-fn-C-2
      Handler: File-Reader-Athena-Lambda-Fn-C-2.handler
      Role: !GetAtt
        - LambdaExecutionRole
        - Arn
      Timeout: '300'
      Runtime: nodejs6.10
      MemorySize: 1536
      Code:
        S3Bucket: !FindInMap
          - RegionMap
          - !Ref 'AWS::Region'
          - bucketname
        S3Key: File-Reader-Athena-Lambda-Fn-C-2.zip
      Environment:
        Variables:
          S3_BUCKET: !Ref S3BucketName
          CUR_S3_BUCKET: !Ref s3CURBucket
          REGIONX: !Ref 'AWS::Region'
  InitFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        ZipFile: >
          const AWS = require("aws-sdk"); const response =
          require("cfn-response"); const docClient = new
          AWS.DynamoDB.DocumentClient(); exports.handler = function(event,
          context) {    var darray = [];
              darray.push("new-table-response");
              console.log(JSON.stringify(event,null,2));
              var params = {
                TableName: event.ResourceProperties.DynamoTableName,
                Item:{
                    "iKey": 10007770157,
                    "Flag": 0,
                    "TableDescription": darray
                }
            };
          docClient.put(params, function(err, data) { if (err) {
          response.send(event, context, "FAILED", {});

          } else {  response.send(event, context, "SUCCESS", {});

          } }); };
      Handler: index.handler
      Role: !GetAtt
        - LambdaExecutionRole
        - Arn
      Runtime: nodejs6.10
      Timeout: 60
  AthenaTable:
    Type: 'AWS::DynamoDB::Table'
    Properties:
      TableName: AthenaTable
      KeySchema:
        HashKeyElement:
          AttributeName: iKey
          AttributeType: 'N'
      ProvisionedThroughput:
        ReadCapacityUnits: '50'
        WriteCapacityUnits: '50'
  InitializeDynamoDB:
    Type: 'Custom::InitFunction'
    DependsOn: AthenaTable
    Properties:
      ServiceToken: !GetAtt
        - InitFunction
        - Arn
      DynamoTableName: !Ref AthenaTable

Minikube Tutorial をまるっとやる

最初に

このドキュメントは、このチュートリアルをまるっとやったことの記録。 kubernetes.io

まとめ

  • minikubeはlocalのimageを簡単に参照可能である
  • minikubeが具体的にどこからどこまで担保してくれてるのかよく分かってない。
  • kubectl はcontextを切り替えることで、gkeかminikubeかを選択して操作する
    • kubectl config use-context コマンド
  • kubernetesを使うために、わざわざyamlを使う必要は無かった

minikube

このチュートリアルのゴールは簡単なnode.jsのアプリケーションがkubenetesで動くところまで。 このチュートリアルは、machine上で開発したコードを、Docker imageに落とし込み、Minikube上で動かしていくよ。

Objecitves

  • hello world する簡単なnode.jsアプリを動かす
  • そのアプリケーションを minikubeに乗っける
  • applicationのlogを見る
  • applicationのimageをupdateする

Create a Minikube cluster

このチュートリアルでは、minikubeをlocal clusterで動かしてく。ここでは Docker for Macを使ってると仮定する。

brew cask install minikube
brew install kubernetes-cli

もしkubectlコマンドを、すでにgcloudからinstallしてたら、下記の手順を踏むこと。

gcloud components install kubectl
curl -LO https://storage.googleapis.com/minikube/releases/latest/docker-machine-driver-hyperkit \
&& chmod +x docker-machine-driver-hyperkit \
&& sudo mv docker-machine-driver-hyperkit /usr/local/bin/ \
&& sudo chown root:wheel /usr/local/bin/docker-machine-driver-hyperkit \
&& sudo chmod u+s /usr/local/bin/docker-machine-driver-hyperkit

下記URLを実行して、プロセスなしでアクセス可能なのか確認する

curl --proxy "" https://cloud.google.com/container-registry/

もし proxyが必要なければ、minikube start --vm-driver=hyperkit を実行すること。 --vm-driver=hyperkitはDocker for Macを使っているというフラグ。デフォルトはVirtualBox

https://github.com/kubernetes/minikube/blob/master/docs/drivers.md#hyperkit-driver

➜ minikube start --vm-driver=hyperkit
Starting local Kubernetes v1.10.0 cluster...
Starting VM...
Getting VM IP address...
Moving files into cluster...
Downloading kubelet v1.10.0
Downloading kubeadm v1.10.0
Finished Downloading kubeadm v1.10.0
Finished Downloading kubelet v1.10.0

minikube switch

kubectl config use-context minikube

➜ kubectl config get-contexts
CURRENT   NAME                                             CLUSTER                                          AUTHINFO                                         NAMESPACE
          xxx_pomodoro   xxx_pomodoro   xxx_pomodoro pomodoro
*         minikube                                         minikube                                         minikube

minikube dashboard

minikube dashboard

で色々見れる。便利。

f:id:selmertsx:20180720185617p:plain

Create your Node.js application

var http = require('http');
var handleRequest = function(request, response) {
  console.log('Received request for URL: ' + request.url);
  response.writeHead(200);
  response.end('Hello World!');
};
var www = http.createServer(handleRequest);
www.listen(8080);

こんなコードを用意した。

➜  k8s git:(master) node server.js
➜  k8s git:(master) curl localhost:8080
Hello World!%

で動くことを確認。

Create a Docker container image

FROM node:8.11.3
EXPOSE 8080
COPY server.js .
CMD node server.js

node imageはLTSの8.11.3を選択。 このチュートリアルでは、docker image をregistryにpushする代わりにminikubeを利用するので、 Docker hostと同じ場所でimageをbuildする必要がある。

そのため、下記のコマンドを実行する

eval $(minikube docker-env)

その後、docker imageをbuild.

docker build -t hello-node:v1 .

これでminikubeはimageをbuild出来るようになる。

Create a Deployment

kubenets podは、何か1つの目的に合わせてまとめられた、1つ、または複数のコンテナのグループである。 このチュートリアルにおいてPodは一つのコンテナでのみ出来ている。 KubeのDeploymentはPodの健康状態をcheckし、Pod Containerが死んだらrestartする。 Deploymentは、podを作ったりスケールする上で推奨される方法である。

kubectl run commandを使って、podを管理するためのDeploymentを作る。 Podは hello-node:v1 Docker imageを元にしたContainerを実行する。 local imageの利用を優先するために、--image-pull-policyフラグをNeverにセットする。

➜  k8s git:(master) ✗ kubectl run hello-node --image=hello-node:v1 --port=8080 --image-pull-policy=Never
deployment.apps/hello-node created
➜  k8s git:(master) ✗ kubectl get deployments
NAME         DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
hello-node   1         1         1            1           18s
➜  k8s git:(master) ✗ kubectl get pods
NAME                          READY     STATUS    RESTARTS   AGE
hello-node-57c6b66f9c-7d26q   1/1       Running   0          31s

何か気になることがあったらkubectlコマンドを実行するとよいよ。 https://kubernetes.io/docs/reference/kubectl/overview/

Create a Service

デフォルトでは、PodはKubenetesクラスタ内の内部IP addressからしかアクセスできません。 K8s Virtual networkの外から、hello-node Containerにアクセス可能にするためには、PodsをServiceとして公開しなければならない。 開発環境においてはkubectl exposeコマンドでそれができる。

➜  k8s git:(master) ✗ kubectl expose deployment hello-node --type=LoadBalancer
service/hello-node exposed
➜  k8s git:(master) ✗ kubectl get services
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
hello-node   LoadBalancer   10.107.74.133   <pending>     8080:32311/TCP   6s
kubernetes   ClusterIP      10.96.0.1       <none>        443/TCP          45m

--type=LoadBalancerflagはサービスをclusterの外に公開することを示す。 LoadBalancerをサポートしているcloud providersであれば、Serviceにアクセスするためのexternal IP addressが用意される。

Minikubeでは、LoadBalancerタイプのサービスには、minikube service ${service_name} コマンドでアクセスすることが出来る。 このコマンドでブラウザが開かれて、サービスがアクセス可能になる。

podsの情報やlogの内容に関しては下記のコマンドで獲得可能である。

➜  k8s git:(master) ✗ kubectl describe pods
Name:           hello-node-57c6b66f9c-7d26q
Namespace:      default
Node:           minikube/192.168.64.2
Start Time:     Fri, 20 Jul 2018 16:54:31 +0900
Labels:         pod-template-hash=1372622957
                run=hello-node
Annotations:    <none>
Status:         Running
IP:             172.17.0.4
Controlled By:  ReplicaSet/hello-node-57c6b66f9c
Containers:
  hello-node:
    Container ID:   docker://5333602c10e2b6ef2677125996cf7ccd51db23946a7b085b85354fc0c30516f8
    Image:          hello-node:v1
    Image ID:       docker://sha256:80bf931ffb07668a2ef31dd3e5c08350ed5d64477ba424a847bb29b5b1eef0a7
    Port:           8080/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Fri, 20 Jul 2018 16:54:32 +0900
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-shf7x (ro)
Conditions:
  Type           Status
  Initialized    True
  Ready          True
  PodScheduled   True
Volumes:
  default-token-shf7x:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-shf7x
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason                 Age   From               Message
  ----    ------                 ----  ----               -------
  Normal  Scheduled              14m   default-scheduler  Successfully assigned hello-node-57c6b66f9c-7d26q to minikube
  Normal  SuccessfulMountVolume  14m   kubelet, minikube  MountVolume.SetUp succeeded for volume "default-token-shf7x"
  Normal  Pulled                 14m   kubelet, minikube  Container image "hello-node:v1" already present on machine
  Normal  Created                14m   kubelet, minikube  Created container
  Normal  Started                14m   kubelet, minikube  Started container
➜  k8s git:(master) ✗ kubectl logs hello-node-57c6b66f9c-7d26q
Received request for URL: /
Received request for URL: /favicon.ico

Clean up

➜ kubectl delete service hello-node
service "hello-node" deleted

➜ kubectl delete deployment hello-node
deployment.extensions "hello-node" deleted

➜ docker rmi hello-node:v1 hello-node:v2 -f
Untagged: hello-node:v1
Deleted: sha256:80bf931ffb07668a2ef31dd3e5c08350ed5d64477ba424a847bb29b5b1eef0a7
Deleted: sha256:05f6355afc692766f99575564dd9d8e32917779c91285e8ddbadd615eb528bab
Deleted: sha256:137c95a151503aaa351d3430fa2ca089e28487516a7cbe61af318b2717cce6af
Deleted: sha256:1725ca28b86411d20d508a8275129fe68e68e4a6d32fd5a0c7d453a0de544f07
Error: No such image: hello-node:v2
➜ minikube stop
eval $(minikube docker-env -u)
Stopping local Kubernetes cluster...
Machine stopped.