FiNCのエンジニアの方々と一緒に書いた本が、この度 インプレス R&D様から出版していただくことになりました。
僕は最初の章を書かせて貰っています。宜しければ見てやってください〜。
Serverless Frameworkを使って CloudFunctionを動かしてみました。 試した内容は下記の通りです。
実際に利用したコード https://github.com/selmertsx/study/tree/master/serverless/cloudfunction-sls-sample
https://serverless.com/framework/docs/providers/google/guide/quick-start/
上記チュートリアルを実行して、nodejsのコードをdeploy & invokeできるようにしてある前提で話を進めます。
https://github.com/prisma/serverless-plugin-typescript このpluginを利用します。
手順は簡単で、下記の手順を踏むだけで設定可能です
serverless-plugin-typescript
を追加する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", // .. }
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}
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のバージョンを上げる必要があります。
"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
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
のような値になります。
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を作成することができました。
IDaaSを社内サービスに適用したかったので、その準備としてOpenID Foundation Japanのガイドラインが伝える OpenID Connect Implicit Flowの実装方法を試してみました。
( ※ 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の情報、リプレイ攻撃対策のための文字列などを含んでいる。
参考実装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では動かない。このコミット で動作しなくなったものと思われる。
Azure ADに対してRP側の情報を登録する(登録方法)。登録するべき情報はリダイレクト先のURLと、ログアウト要求を受け付けるエンドポイントのURLの2点である( ガイドライン P.31 ) 。リダイレクト先のURLは完全一致で指定しなければならない(p.30)。ここで登録することによって、OPからRPにClientIDが割り振られる。
上記手順により得られたClientIDをRP側に設定する。ガイドラインから公開鍵をJWK Set形式で、外部からアクセスできるURIに配置した場合は、下記4点のパラメータをRPに登録することとなる。( P.32~33 )
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
ガイドラインの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に対して認証要求をした結果、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トークンの検証においては、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" }
ガイドラインにおいて、IDトークンの検証として、最低限下記の項目について検証することとしている。(p.54)
ここで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に値を追加する形にした。
自分で用意したサンプル実装はこちら
上記URLの「AD テナントへのアプリケーションの登録」をそのままなぞれば大丈夫。1点だけ気をつけるべきポイントは、Reply URLsにはフルパスを指定する必要があること。
今回は実装にあたって、openid-foundation-japanの提供するサンプルを参考にした。サンプル実装を取り入れるにあたって、あまりフローの整理などはしていない。もう少し整理ができるはずなので、ちょいちょいコードを整えていきたい。
# 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
受け取るレスポンス
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);
# 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載せていく
上記ページを読んで簡単にサマリーしてみる。
これらのlambda functionが作成されていることを確認しといてね
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;
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
このドキュメントは、このチュートリアルをまるっとやったことの記録。 kubernetes.io
kubectl config use-context
コマンドこのチュートリアルのゴールは簡単なnode.jsのアプリケーションがkubenetesで動くところまで。 このチュートリアルは、machine上で開発したコードを、Docker imageに落とし込み、Minikube上で動かしていくよ。
このチュートリアルでは、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
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
で色々見れる。便利。
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!%
で動くことを確認。
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出来るようになる。
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/
デフォルトでは、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=LoadBalancer
flagはサービスを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
➜ 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.