Ponz Dev Log

ゆるくてマイペースな開発日記

Amazon EKSがGAになったので触った所感

AWSでKubernetesのマネージドサービス Amazon EKS がGAになりました。 今まではAWSでk8sをいじるときはkops経由でクラスターを作って、権限設定してと手間がかかっていましたが、EKSはどれくらい楽になるのかなという観点で触ってみました。

www.atmarkit.co.jp

全体的に

  • AWSのWebコンソール側はめっちゃシンプルで発展途上とさえ感じられるレベル。

    • クラスター情報の参照くらいしかできない最低限のものなので、これからGUIで各種操作できるようにできるのかな?
  • 今の所、使えるのは US West (Oregon) (us-west-2) or US East (N. Virginia) (us-east-1) のみ。

    • 東京リージョン早く来てほしい!
  • リソースの用意は基本はCLI(aws-cli/kubectl) & CloudFormationでやる。

クラスターの作成

  • ネットワーク関連は自分で用意しないといけない。

  • Get Startedの例だとVPC/SecurityGroupをCloudFormation経由でやらせていました。クラスターの作成も含めてCloudFormationでやったほうが早そう...

ノードの作成

  • ワーカーノードも自分で作成する。

    • クラスターとワーカーの紐付けを後から実施する形みたい。Configmapを編集して適用する。
  • Auto Scaling グループを設定してあげるので、最小・最大・希望ノード数の範囲で自動で増減できます。

    • KubernetesのPodのスケールは HolizontalPodAutoscalerで出来るけど、ノードのスケールまで出来るのは可用性を高めるのに良さげ。

Kubernetesにアプリケーションを乗せる

  • 通常のKubernetesの使い方同様に、ReplicationController, Service ect...を作成すればOK。

  • LoadBalancerはアクセスできるようになるまで10minくらい待つので気長にお茶でも飲んで待つ。

ハマりポイント

自分がハマったポイント集。

  • それとMFAでカッチリユーザー権限を設定している人は、事前に aws sts get-session-token --serial-number arn-of-the-mfa-device --token-code code-from-token でtokenを取得すること

    • An error occurred (InvalidClientTokenId) when calling the GetSessionToken operation: The security token included in the request is invalid.
  • もしInvalidClientTokenIdで弾かれる場合はkubeconfigから-r 行および <role-arn> 行を消すと通る。Roleをここで決めておきたいのに。。。

    • could not get token: InvalidClientTokenId: The security token included in the request is invalid.

雑感

  • 認証・認可を上手にIAMとKubernetesのRBACを統合しようとしているなと感じます。AWSでKubernetesを立てるメリットの1つではないでしょうか。

    • ただ、ドキュメントを見る限りEKSでのIAMの立ち位置は"認証"であって、認可はまだRBAC側で設定するみたいです。

    • Kubernetesのリソースアクセスの制御は、ConfigMapにIAMユーザー/ロール/アカウントを追加する必要がありますが、新しいユーザーやロールが追加されるたびに毎度毎度ConfigMapを手で書き換えるのは中々メンテナンスが大変そう。。。ここをGUIや自動化出来るとさらに使いやすいですね。

  • 他にも最初からリソース操作はかなり CloudFormationやkubectl に頼っています。

    • kopsと比較するとVPC/セキュリティグループ/Auto Scaling/Nodeの設定を細かく出来る一方で手作業となる部分が多いので、"とりあえず作って壊す" には少しハードルが高い印象を受けます。

認可の仕組みやVPC、CloudFormationあたりをもう少し探っていきたいと思います。ここら辺が明確になれば、ECSとの差別化要素も見えてきそうです。

npm v6が自動的に脆弱性を見つけるようになった

Node 10.0と合わせて自分のMacのnpmもv6にバージョンアップさせました。 色々いじってた時に脆弱性を教えてくれるようになったので、メモとして残します。

例えばnpm install でnodemonを入れると、以下のように脆弱性があるよと警告を親切に出してくれます。

f:id:accelerk:20180514234607p:plain

画像のインストラクション通り、npm auditで詳細が見られるようです。 素直にコマンドを叩くとこんな図のようにどのパッケージの何がマズイのか表示されるようになります。

f:id:accelerk:20180514234132p:plain

ガイドはNPM公式から出てます。 ガイドを見る限り、npm installを叩くと同時に npm auditも叩かれるみたいです。 もしパッケージインストール時に npm auditを叩かないようにしたい場合は、--no-auditフラグをつければOK。

今回のnodemonの例でやると、以下のようになる。

npm install nodemon --no-audit

また、npmのインストール全体を通して npm auditさせないようにするためには以下のように設定してあげればいいみたいです。

npm set audit false

docs.npmjs.com

Nodeだけじゃなくてnpmも進化してるって発見ですね。 セキュリティ周りをこうやって補強するのを助けてくれるのは嬉しい限りです。最近はGitHubもpackage.jsonから脆弱性アラートを出してくれるようになっているみたいなので、ちゃんとセキュリティ周りは固めておきたいね。

Goで簡易Webアプリ作成&Dockerコンテナにまとめる

ご無沙汰してます。約2ヶ月ぶりの投稿になります。

ここ2ヶ月くらいは社内のイベント向けのアプリを作ったり、Kubernetes勉強会に参加して初めてAWS触ったり、コンテナ周りで新人向けに講師としてスピーカーやったりと技術(特にDockerコンテナ)漬けの日々を過ごしておりました。(Kubernetes@AWSは今度記事にする予定です)

お陰様で今日の健康診断で2ヶ月で10kg体重落ちてました汗
技術だけじゃなく美味しいご飯どころも探さなきゃね。。。

さて、4月末から連休で時間ができたので新しいものに挑戦しようとGoに触り始めました。 折角なら馴染みのあるWebアプリを作成して、Dockerコンテナにまとめるくらいはやろうと全速力でやったので記事投下です。

ソースコード見たいよって方は以下のリンクからどうぞ。

github.com

Go出発点

今まで手を出そうと思いつつ何も手をつけていなかったので、Goについては知識0からスタート。 比較的最近出版された本ならばバージョンの違いでつまづくことは少ないはずなので、とりあえず『スターティングGo言語』買って3日ほどで手を動かしつつ読了。あとは作りたいものを完成させます。

www.shoeisha.co.jp

さっと書店で手に取った本にしてはアタリだった気がします。 全7章建てで、基本で小分けに5章割いて、残りはツールやパッケージの話で2章。1つ1つのGoの仕組みを分解してコード例も沢山載せてくれているので、写経しているだけでもかなり書けるようになります。

Go製のWebアプリを作る

サーバー側は最低限文字列とJSON返すだけ返せればよかったので、簡易なものとしてピッタリな Echo を選択。 公式のドキュメントは分かりやすいし、サイトも見やすいので取っ付きやすかったです。

"minimalist Go web framework" と謳うくらいだから、Node.jsでいうExpress.jsと同じような位置付けなのかな?

echo.labstack.com

以下、公式の例を使いつつルーティングとミドルウェアを設定した main.go 例です。
ルーティングのミドルウェアはデフォルトの設定を使うとJSON形式の見辛いログが出力されるので、ApacheCommon Log Format に似せて出力されるように設定してます。ログ出力は出力内容は柔軟に変えられるみたいですね。

package main

import (
    "net/http"
    "github.com/labstack/echo"
    "github.com/labstack/echo/middleware"
    "github.com/sasaken555/ponz_goecho_server/routes"
)

func main() {
    /* Echoインスタンスの作成 */
    e := echo.New()

    /* Root Level Middleware */
    // ログ出力は Apache Common Log Format っぽく設定すると読みやすい
    e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
        Format: "${host} [${time_rfc3339_nano}] \"${method} ${uri}\" ${status} ${bytes_in} ${bytes_out}\n",
    }))
    e.Use(middleware.Recover())

    /* ルーティングの設定 */
    // 第2引数の値は別パッケージに外出しすると分かりやすい
    e.GET("/users/:id", routes.GetUser)
    e.GET("/users/json", routes.GetJSONUser)

    e.Logger.Fatal(e.Start(":1323")) // ポート1323で起動。
}

また、ルーティングの設定・関数は main に全部突っ込むと後で見づらくなるので、外出ししてあげます。 今回は routes/user.go としてルーティングしたときに返す関数をまとめてみました。

package routes

import (
    "net/http"
    "strconv"
    "github.com/labstack/echo"
    "github.com/sasaken555/ponz_goecho_server/util"
)

// GetUser ... Pathパラメータからユーザー(=ID)を取り出して返す
func GetUser(c echo.Context) error {
    // User ID from Path Parameter `users/:id`
    id := c.Param("id")
    return c.String(http.StatusOK, id)
}

// Customer ... 顧客情報の構造体
type Customer struct {
    ID        int64  `json:"id" xml:"id"`
    Name      string `json:"name" xml:"name"`
    OrderNum  int    `json:"ordernum" xml:"ordernum"`
    OrderProd string `json:"orderprod" xml:"orderprod"`
}

// GetJSONUser ... 顧客情報のJSONを返す
func GetJSONUser(c echo.Context) error {
    userID, err := strconv.ParseInt(c.QueryParam("userId"), 10, 0)  // strconvで文字列から整数に型変換
    userName := c.QueryParam("userName")
    orderNum := util.GetRand(100)  // 別のパッケージからインポートした指定桁数で乱数を返す関数を使う

    // userIDが整数でない(=型変換できない)ならば500エラーを返す
    if err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, "You Should Provide userId as Integer!")
    }

    // 構造体のポインタを作成
    u := &Customer{
        ID:        userID,
        Name:      userName,
        OrderNum:  orderNum,
        OrderProd: "Blend Coffee",
    }

    return c.JSON(http.StatusOK, u)
}

Dockerコンテナにまとめる

関心があったのと同時に割と引っかかったのが、ここ。特にビルドイメージ大きすぎなのが難点でした。

  • GoはOS依存させないために、OSが標準で備えているようなライブラリを使わず、ランタイムとアプリで使うパッケージを全て実行ファイルの中に取り込みます。この性質があるため、少しコード量の多いアプリでも成果物が数百MBになるのもザラ。

  • 実行ファイルを組み込んだDockerコンテナのイメージがデカイとなると、docker pull の度にディスクと時間が取られるのでポータビリティの点から使いづらい。

上記の問題があったため、ベースイメージを小さいものにするのに加えて、Dockerのマルチステージビルドで対応することしました。

docs.docker.com qiita.com

通常だとソースコードを全てDockerコンテナに含めますが、Goは良くも悪くもソースコードからシングルバイナリ(実行ファイル)を作成するので、実行ファイルだけ入れればOK。下のコード例の(1)が実行ファイル作成のビルド、(2)が(1)で作った実行ファイルをコピーして作った最終系のコンテナイメージになります。

ベースイメージも alpine イメージを使うことでさらに軽量化してます。

# Full SDK version ... (1)
FROM golang:1.10-alpine AS build

RUN apk update && apk upgrade \
    && apk add curl git

RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh

WORKDIR /go/src/github.com/sasaken555/ponz_goecho_server
COPY . .

RUN dep ensure
RUN go build -o ponz_goecho_server

# Final Output ... (2)
FROM golang:1.10-alpine
COPY --from=build /go/src/github.com/sasaken555/ponz_goecho_server/ponz_goecho_server /bin/ponz_goecho_server
CMD /bin/ponz_goecho_server

結果として、通常のベースイメージ+アプリソースを含めてビルドしたイメージ(tag: heavy)とalpineベースイメージ+マルチステージビルドのイメージでサイズは半分以下に抑えられています!やったね!

$ docker image ls ponz_goecho_server
REPOSITORY           TAG                 IMAGE ID            CREATED              SIZE
ponz_goecho_server   light               24b61e6c4f83        7 seconds ago        386MB
ponz_goecho_server   heavy               20bac15a0acf        About a minute ago   908MB

新しい技術(今回は言語でしたが)として今回はGoを選択しましたが、謳い文句の通りシンプルかつ効率的に開発&ビルドできたのはすごい...!!
バイナリ単位でアプリはまとめられるので、マイクロサービスと相性が良さそうな印象です。しかもブロックチェーンのHyperledger fabricもGoで開発できるようなので、次はGoを使ってブロックチェーンに挑もうかなと夢が膨らんだところで締めます。

自分の勤怠状況を Kubernetes, Slack, Metabaseで可視化

システム構成はタイトル通りで、Slackから飛ばした出勤・退勤をKubernetesにデプロイしたアプリ経由でDBに登録して、 Metabaseから可視化したよって話。

タイトルだけ見ると、いや勤怠管理のアプリ使えよとなるかもしれない

やったことと

狙いとしては、Docker / Kubernetesがどれくらい使えそうか、メリットは何かをざっくり感じれればいいなと。 ついでに、勤怠報告に使えればなおよし。

全体的なフロー

  1. Kubernetesクラスターを作成して、Dockerコンテナ化したアプリをデプロイ
  2. SlackのSlashコマンドと(1)のアプリのHTTPエンドポイントを紐付け
  3. SlackにSlashコマンドで勤怠を入れる
  4. Kubernetes上のアプリ経由でMongoDBにレコードを登録
  5. Embulkで MongoDB → MySQL にデータ転送
  6. Metabaseから勤怠状況をグラフに落とし込む!

完成図

[Slackで勤怠をちょこちょこ入れて] f:id:accelerk:20180203192457p:plain

[Metabaseで結果をダッシュボードに出す] f:id:accelerk:20180203191728p:plain

うーん、激しい勤務実態が見えてしまいましたね。可視化した時点で休み時間入れるの忘れてたことに気がつきました。。。

大変だったとこ

フローごとに難所があったので、覚え書きとして。

Kubernetes

  • Docker コンテナのイメージさえできていれば、Deploymentは問題なくいける。

  • ServiceはLoad Balancerを選択しましたが、クラウドプロバイダーによってはデフォルトがHTTPになっています。HTTPSにしないといけないサービスの場合は注意が必要です。例として、SlackのMessage ButtonはHTTPSでないとボタンアクションの質問は出るものの、ボタンを押した後のRequestURLが叩かれません。

  • ただ、突然Podの再生成がスケジュールできなくなる事象に遭遇。これはKubernetesクラスターを動かしていたGoogle Cloud Platformの問題っぽい。

    • 解決策は リソースの自動拡張をONにすること。でないと突発的にリソースを食い始めた時に全て落ちる。
    • 調べていたら、もう一つ解決策はあったようですね。Servicesに SessonAffinity プロパティをを加える...ことらしい。

GCE services type LoadBalancer · Issue #18347 · kubernetes/kubernetes · GitHub

  • MongoDB も立てるはずだったけど、上記理由から不安があったので断念。本当は一緒のクラスター上に乗っている方がいいよ。
    • 結局DBaaSのmLab使って解決。サービス使えると楽でいいわ

mlab.com

Embulk

  • 言わずとしれたデータ転送ツール

github.com

  • MongoDBからデータを抽出した時に、カラムが分割されない問題がありましたがFilterで対応。
    • ここは別記事で紹介したい。。。

Metabase

  • セットアップから可視化までは相当楽でした。ここは昔の記事を参照いただければと。

ponzmild.hatenablog.com

  • このグラフや他の結果(週次の勤務時間合計とか)をSlackやメールで飛ばせるにはのMetabase自体にもSlackとメールでスケジュール連携する機能があるのでこっちをありがたく使う。ここは今後やってみたいですね。
  • FaaSで色んなものと連携させている人は、ZapierIFTTTでスケジュールorトリガーさせるのもいいかもしれません。

MongoDBでのmongoとmongod以外のクライアントプログラム

DBって基本のクライアントプログラム以外にも、クライアントプログラムが入ってるって最近気づきました。 NoSQLのMongoDBも同じようなクライアントプログラムがないか確認しました。MySQLも同様にたくさんあるようですね。

今回はMongoDBのクライアントプログラムの備忘録です。 MongoDBのDBaaSである、mlabの"Tools"タブに用例が書いてありました。

mlab.com

MongoDB Package Components — MongoDB Manual 3.6

使いそうなクライアントプログラムには、以下のようなものがありました。

  • はじめによく使うやつら
    • mongo, mongos, mongod
  • CSV, TSV, JSONを扱うとき
    • mongoimport
    • mongoexport
  • バックアップ・リストア(バイナリ)をするとき
    • mongodump
    • mongorestore

単純にCSVをインポートするなら、オプションをつけて実行すればOK。

# 基本形
mongoimport -h <hostname> -d <dbname> -c <collection> \
                       -u <user> -p <password> --file <input .csv file> \
                       --type csv --headerline 

# カラムに属性を明示的に付ける場合
mongoimport -h <hostname> -d <dbname> -c <collection> \
                       -u <user> -p <password> --file <input .csv file> \
                       --type csv --columnsHaveTypes \
                       --fields "field1.string(),field2.int32()"

ES6からのトランスパイルするときのBabel 7 対応

タイトル通り、Babelの話。 JavaScript書いていると、避けて通れないのがES6からのトランスパイル。 このとき使うデファクト・スタンダードになったBabelで最近つまづいたのでメモです。

ことの発端

MochaでES6のコードを実行させるために babel-core, babel-register, babel-preset-env 入れたけど、deprecatedとか言われたのが事のはじまり。 ターミナルで言われてるバージョンをただ入れるだけでは済まないとは知らず。。。

Babel 7 ??

要はバージョンのメジャーアップデートで単にモジュールが変わっただけじゃない。そもそもの名称から変わっていたのです。 今までの要領でNPM覗いても、バージョン6系しか載ってないので注意しないといけないですね。ちなみに、新しいバージョンは7系(beta版)です。

また、v7系にする場合は、使用するbabelのモジュールのバージョンはすべてv7系にしないと動きません。

[babel-core 今まで] www.npmjs.com

[babel-core v7以降] www.npmjs.com

基本的には、以下のように名称が改定されている模様です。(2018/01/09時点)

  • 〜v6 : babel-xxyyzz
  • v7〜 : @babel/xxyyzz

名前がわからない場合は以下の、packageから見たいbabelのモジュールを探せばOK。

github.com

Githubのissuesを辿る限りまだまだ議論されているようですが、そろそろ新しいものになると考えた方が良さそうです。JS界隈は流れ早いので、ついていくに越したことないですしね。

設定例

名前が変わったとはいえ、基本的には設定方法は以下2つで変わらないですね。

  • コマンドラインからオプションでbabelのモジュールを指定する。
  • .babelrcにモジュール名を入れる

以下、設定例です。 ほぼ公式の例から分かりやすいところを取り出しました。

{
  "presets": [
    ["@babel/preset-env", {
    "targets": {
      "browsers": ["last 2 versions"]
      }
    }]
  ],
  "plugins": ["@babel/plugin-transform-runtime"]
}

あとは、package.json で以下のようにbabel-cliを叩くようにすればOK。 babel ./src/ --out-dir dist/ --ignore ./node_modules --copy-files

やることは変わらないとはいえ、やっぱり名前から変わっちゃうのは気づかないとハマって抜けない罠ですわ。。。

Mongooseの処理が非同期であるのを忘れてはいけない(戒め)

Node.jsのMongoDBのORMといえば、Mongoose。APIドキュメントをよく読んで使用されている方ならお分かりかと思いますが、よく使う.save(ドキュメント保存)や.find (ドキュメント検索)といった処理は非同期で実行されます。
これ、意識してないと処理はあってるのに値がundifinedになってハマる(実際ハマった)ので、非同期で処理されるという前提を頭に置こう(戒め)というメモです。

一応MongooseのPromiseのページにしれっと以下のように書いてあります。

Mongoose Promises v5.0.0-rc1

Built-in Promises
Mongoose async operations, like .save() and queries, return ES6 promises. This means that you can do things like MyModel.findOne({}).then() and await MyModel.findOne({}).exec() (if you're using async/await.
Queries are not promises
Mongoose queries are not promises. However, they do have a .then() function for yield and async/await. If you need a fully-fledged promise, use the .exec() function.

日本語で要約すると、以下のようになるかと。

  • .save()や query(find() / findOne() / etc...) といった 非同期の処理 は、ES6のPromiseオブジェクトを返します。

    • これらのメソッドは .then()を続けられる。
    • async / await 句をつけて実行できる。
  • ただし、queryは厳密にはPromiseではない。

    • .then(), async / await 句を使える。
    • 厳密にPromiseオブジェクトを使いたい場合は .exec()を使おう。

非同期であることはもちろん、返却されるのがPromiseオブジェクト(的なもの)であることもポイントです。 素直に配列が返却されると思いきや、オブジェクトが返ってくるので軽くビビります。

以下のようなコードが非同期で処理されるOK / NGパターンかな。

[NGの例]

 /**
  * findRcd(filterKeys)
  * @description 引数に渡された値・キーでレコードを検索し、検索結果を返却。
  * @param {Object} filterKeys - 更新するレコードの検索キー
  * @return {Array} resultSet - 検索結果
  */
findRcd(filterKeys) {
    knmKanr.find(filterKeys, (err, awesomeRecords) => {
 
    log.debug(`FilterKeys: ${JSON.stringify(filterKeys)}`);

    // レコード検索失敗
    if (err) {
        throw new Error("Find Docs Error");  // エラーを投げて処理を中断
    }

    // レコード検索成功
    log.debug(`AwesomeRecords: ${awesomeRecords}, Size: ${awesomeRecords.length}`);

    return awesomeRecord;    // ここで結果の配列が返却される
});

const searchResultSet = findRcd({ flg : '1' });    // 非同期で処理完了前に、変数に値が格納される
console.log(JSON.stringify(searchResultSet));    // 処理が未解決のため、undefined になる

[OKの例]

  /**
   * findRcd(filterKeys)
   * @description 引数に渡された値・キーでレコードを検索し、検索結果を返却。
   * @param {Object} filterKeys - 更新するレコードの検索キー
   * @return {Promise} resultSet - 検索結果
   */
  async findRcd(filterKeys) {
    const resultSet = await knmKanr.find(filterKeys, (err, awesomeRecords) => {
 
     console.log(`FilterKeys: ${JSON.stringify(filterKeys)}`);

        // レコード検索失敗
        if (err) {
            throw new Error("Find Docs Error");  // エラーを投げて処理を中断
        }

        // レコード検索成功
        console.log(`AwesomeRecords: ${awesomeRecords}, Size: ${awesomeRecords.length}`);

    });

    console.log(`resultSet: ${resultSet}`);    
    return resultSet;    // ここでPromiseオブジェクトが返却される
  }
}

findRcd(query)
    .then((searchResultSet) => {
        console.log(`searchResultSet: ${JSON.stringify(searchResultSet)}`);    // {...} 非同期処理が解決されてから値がセットされる
    }

こういう時こそ、async / await が効果的なんだってわかりますね。 改めて、Mongooseの処理が非同期であるのを忘れてはいけないです(戒め)。