1クール続けるブログ

とりあえず1クール続けるソフトウェアエンジニアの備忘録

AWS Summit 2019 Day2に参加してきました

幕張で開催されたAWS Summit2019にDay2から参加してきました!各セッションの感想を書いていきたいと思います。
セッションの他にもブース等回ろうと思っていましたが時間がなく、認定者ラウンジにちょっと立ち寄ったのみでした。ラウンジでは自撮りブースというものがあり、そこで自身の経歴もしくは資格を取って活きたこと、受験者に向けてのアドバイス等5つの設問から選んで、ビデオカメラに向けて話すとTシャツがもらえるとのことだったので、参加してもらってきました。
(背面のキャラクターかわいいけど誰なんだ…?)

f:id:jrywm121:20190614193500p:plain

英語力強くしたかったので、英語のセッションに関しては翻訳ではなくそのまま聞いています。
もしかしたら解釈違いなどあるかもしれません。

コンテナ化されたアプリケーションのAWSでの構築・運用指針

全体的に真新しい話はありませんでしたが、コンテナワークロードのメリットや近年の機能追加を再確認できるセッションだったかと思います。

Fargateの強み

インスタンスをManageする必要がなくなるため、Operationも非常にシンプルなり、ビジネスロジックに注力できる。
またSecurityに関してもAWS側で随時パッチを当てるので、その部分の対応コストもいらないし安全である。
Securityにも関わることですが、実行基盤のFirecrackerも同時に紹介されていました。

下記の点からAWSはFargateをもっと使ってもらいたいのかなと感じた

  • Fargate利用者が伸びていることをスライドで紹介
  • コンテナをテーマにしたセッションの中でServerlessを強く推していたこと
  • 年末年始にFargateの価格が大幅に下がったことにも触れていた

CDKを是非使ってほしい

※ Developer Preview
十数行〜何百行のyamlを書かずともJS×CDKで書くことで、yamlよりかなり簡潔な記述でAWSのInfrastructureを構成することが可能。
対応言語としては「C#/ .NETJavaJavaScriptPython、TypeScript」 裏ではCloudFormationが動いていてスタックとしてProvisioningされるようです。 下記の記事にはAWS FargateをTypescriptでProvisioningするサンプルコードがありますが、かなり簡潔に書けるようです。このサンプルだと600行超のコードが21行で記述できてます。
暗黙でセットしている値や作成しているリソースが多そうなのでそこは気をつけたいですね。

docs.aws.amazon.com

Elastic Container Services for Kubernetes

KubernetesAWSの既存のサービスの統合にはかなり力を入れているとのことでした。 特にCNIプラグイン周り。 また、Maste NodeをAZ3箇所で冗長化を図っているのというのはびっくりしました。GCPはデフォルトだと1Zoneのみなので、AWSがマルチゾーンに展開していると思っていませんでした。
セッションの中でUpstreamであることを強調されていた印象です。Amazon AuroraのようにForkしてAWS独自の改修を加えているわけではないとのことで、ベンダーロックインの無い状態でKubernetesを使える+オープンソースコミュニティに寄り添っているという主張だったのでしょうか。嬉しいです。

ここ1年間でのLaunch

事例紹介

UberEatsなどの配信プロバイダと連携するためのプラットフォーム
ECSやSQSを組み合わせて100msecのレイテンシで20000order/secまで拡張が可能

aws.amazon.com

サーバーレスのエンタープライズへの拡大とベストプラクティス

サーバーレスのメリットなどは既に分かっている前提で、では現在ある問題点に対してAWSはどうアプローチしているかという観点でのお話が多かったと思います。Lambdaを使う上で自分も問題だなと思う点はだいたい網羅されていたように感じます。
Lambda Layer × AWS Configの話はもうちょっと掘って理解したいと思います。

サーバーレスへの懸念

  • 新しい開発者がLambdaに適合できるか
  • SecurityやGovernanceはどのように担保するのか
  • MonitoringやLoggingはどのように行えばいいのか
  • Performanceはどうチューニングすればいいのか

AWS SAM

FaaS開発の速度を爆速にしてくれる。
CloudFormationをServerless用に拡張したものテンプレートが構成要素の一つ。Serverless用のResourceTypeが用意されている。例えば、コードに紐付けるAPI Gatewayも同時に定義することができるApiリソースやDynamoDBのテーブルを定義できるSimpleTableなどが存在する。
非ServerlessリソースもSAMテンプレートには含めることができる。例えば併せて使うことが多いS3などは通常のCloudFormationの定義と同じように書ける。

SAMの構成要素の一つでコアな部分を担っているのがaws sam-cliです。
ログの追跡やローカルでの関数の呼び出しAPI Gatewayの実行が可能になる。
ローカルで実行できるDynamoDBと併せて使うとなおよし。

Security & Governance

2つのPolicyを使い分けて権限を操作する - 同期・非同期呼び出しのときはFunction Policy - ストリーム呼び出しのときはExecution Role SAMテンプレートにPolicyも記述できる。権限に関してもコードで管理できる。

Lambdaを外から見たときのGovernanceという観点では下記の2つ - CloudTrail - API呼び出しをCaptureする - AWS Config - AWS Config が AWS Lambda 関数のサポートを追加

ではコード自体のガバナンスをどうするか - PipelineでCodeを自動チェックする - レイヤーの作成を追跡可能にする - レイヤーはイミュータブルで更新はバージョニングで行われる - SAM テンプレートでレイヤを定義することができる

Monitoring/Logging/Trouble Shooting

ビルトインのMetricsも充実しているが、lambdaのsdkを使えば、putMetricDataでカスタムメトリクスも収集が可能。
"debug: in "という記述とともにログ出力するとinsightで見つけやすい。

Performance

Memory割当てを少なくすればするほど安くなるとは限らない。
Menory割当によってCPUの割当が変わるので、実行しているアプリケーションがCPUバインドなのかそうでないのか見極めてMemoryを割り当てるべき。

構成されているメモリの量に比例して CPU パワーが直線的に割り当てられます。1,792 MB では、関数は 1 つのフル vCPU (1 秒あたりのクレジットの 1 vCPU 秒) に相当

DeNA の QCT マネジメント IaaS 利用のベストプラクティス

オンプレからそのままクラウドに移行すると単純にコストが増加するという話は非常にうなずけるお話でした。クラウドに適合する形にしつつ、オンプレのナレッジが潤沢にある企業の場合はどのような戦略を取っていくのかというのが見られて勉強になりました。

オンプレから全面クラウド移行

オンプレでのナレッジがものすごく溜まっていてそれを活かそうとする構成にするために最適化させた構成にしているという印象でした。
基本は全てApp on EC2の構成。一番前段にALBを置いていて、それより後ろのレイヤではMyDNSを使ってServiceDiscoveryを行っているそうです。Route53のAutoNaming APIとかに乗っかってもいいのではと個人的には思いましたが、おそらくそれでは要件を満たせなかったのだと思います。
Scalingに関しても、ASGやCloudWatchを使わず自前で実装を行っていましたが、これも柔軟性を求めた結果なのだと思います。スクラッチで組み上げてしまうなんて、ものすごい技術力のある会社さんだなと思いました。

スポットインスタンスの終了処理

スポットインスタンスが強制Terminateされたときのハンドリングについてですが、リンクローカルアドレスを数秒間間隔で叩くDaemonを用意する。terminateのエンドポイントが200で帰ってきたら終了処理。
ログの扱いについてですが、終了直前のログが転送できないという課題を持っていたそうです。おそらくfluetndを使った高頻度での転送などは行っていなかったのだと思います。そのためEBSをインスタンスがTerminateされるタイミングで削除せず、そのまま残してバッチ処理で残っているログを転送するという形にしたそうです。

スポットインスタンスの枯渇対策

スポットインスタンスの大量Terminateが起こることがある。例えば、AmazonのCyber MondayやBlack Fridayでは顕著にその傾向が見られた。そのため、20プールの確保をするようにした。 プール=AZ × インスタンスタイプ × 世代。ファミリーを統一してタイプをバラバラにすることでアプリケーションの挙動差異を少なくしている。コア数が多いものが起動したときはIP数を増やして(ENIをアタッチして)その分多くリクエストをさばくようにする。

Serverless/AppSync によるモバイル開発の今

アプリ開発のバックエンド周りは疎い自分でも多くのメリットを享受できることが分かるセッションでした。

AppSync概要

AppSyncはManaged GraphQL Service
GraphQLはクライアントからQuery / Mutation / Subscription出来るようにするためのデータ言語。GraphQLのクエリではクライアント側がレスポンスの形式を指定。SubscriptionはMutationをトリガーとしてリアルタイムでのデータ更新を行う(Websocket通信)。Mutationはオフラインの状態で更新したら、復帰した段階でサーバ側と同期を取ってくれる。 AppSyncのデータソースはLambdaやDynamoDB、ElasticSearchなど…。

Gunosy事例

グノシースポーツでの事例。
各スポーツによって画面の仕様や欲しい情報などが異なることがあるため、クライアント側で定義できるGraphQLを採用。

導入手順 - スキーマ定義 - Resolver Mapping Templateの作成 - デフォルト値の定義やValidation, Format, データの変換などを行う - 厳密には下記の2つ - リクエスマッピングテンプレート - レスポンスマッピングテンプレート

試合のスコアなどをリアルタイムに更新 - DynamoDBにUpdateが行われる - DynamoDB streamでLambdaをキック - LambdaからAppSyncにpublish - AppSyncがアプリと同期する

新規開発時の悩みが解消 - 仕様書書くのだるい - 必要なデータを選ぶのはアプリエンジニア - UIの細かい改善がフロントだけで出来るようになる

Demo

amplifyを使ってSPA+AppMeshの構成でアプリケーションを作成していました。amplifyを知りませんでしたが、あそこまでシームレスにアプリが構築できてしまうのを見ると用途によってはゴリゴリ使えるような気がしますね。
amplify add apiAWSリソースがProvisionされました。本当に簡単でした。びっくり。
DemoのPartに入られる前にこのPartが本当に怖いよとおっしゃっていたのがキュートで印象的でしたね。

Amazon Managed Blockchainの使いどころとソニー・ミュージック様における使用事例について

AWSが提供しているブロックチェーンのマネージドサービスの一つ。サポートしているアルゴリズムHyperledger Fabricで、ethereumも近日サポート予定になっている。ブロックチェーンネットワークの作成・管理を行うことができる。

ユースケース

  • ヘルスケア
    • カルテの電子化管理
  • 製造業
  • デジタルコンテンツ
    • 画像や写真の所有権の管理

AWSブロックチェーン関連サービス

re:Inventで発表されていたものの、あまり具体的なユースケースが自分の中で分かっていなくてモヤモヤしていた部分のブロックチェーン関連サービス。
国内事例やQLDBとの使い分けを聞くことで、何かに使えないかと意識するきっかけになりました。

  • QLDB
    • 所有権が一元化された台帳の機能を持つ
    • Amazon.comで既に使用されているらしい
  • Block chain partner
    • Blockchainのsolutionを提供するAWS Partner Network
  • AWS Blockchainテンプレート
    • EtheriumとHyperledger Fabricに対応
    • CloudFormationのテンプレート

よくいただく相談

  • どうやってスケールさせるか
    • ノードのスケーラビリティをお客様自身で設定
    • スケール設定によってはリーズナブルにPoCが可能

構築フロー

鍵管理のベストプラクティス

コンプライアンス要件を厳しいケースにはKMSではなくCloudHSMを使用
他のネットワークから分離するためにVPCも併せて利用することで厳しい要件をクリアできる またCloudHSMがサポートsecp256k1をサポート(ブロックチェーンの鍵は楕円曲線暗号で暗号化される必要がある?ため)

ブロックチェーンの主な2つのユースケース

  • 信頼された中央機関による台帳
  • 分散機関によるトランザクション実行(Managed Blockchain)
    • peer-to-peerの金融取引

ここが詳しい

ソニー・ミュージックの事例

音楽業界の課題という背景から、実際に使用されてみたTips、デモなどをして頂いたのですが、自分に音楽業界とブロックチェーンのどちらの知識も無くちゃんと内容追えなくなってしまいました。スライド公開されたらゆっくり理解したいと思います。

事業責任者も必見!AWS Well-Architected Frameworkのビジネスへの有効活用

既にスライドが公開されています。
CAという大きな会社で高品質のインフラを維持するためには、社内向けのWell-Architected Frameworkを作るというのが非常にプラスにつながっていくんだなと感じました。マルチクラウドで共通のチェックシートを作成する+項目を絞って75項目にするという作業は非常に難しかったのではないかと思います。特にプライベートクラウドもあるわけですし…。
また定期的なチェックというのは凄く良いよなと思いました。新機能のリリースなどで初期の頃に仕方なくイケてない形で構築したものなどを改善できるケースは多くありそうです。

事業責任者も必見! AWS Well-Architected Frameworkの ビジネスへの有効活用 / AWS Well-Architected Framework - Speaker Deck

AWSのマネージドサービスを活かした Kubernetes 運用とAmazon EKS によるクラスタのシングルテナント戦略について

こちらも既にスライドが公開されていました。 どうしてシングルテナントを選んだのか?namespaceで分けるのではなくクラスターごと分けるという意味です。自分の勤めている会社ではSREというポジションが無いのですが、SREが陥ってしまった状況とその問題点からそれを解決するための権限と業務の委譲、シングルテナントを選択するに至った動機が丁寧にストーリーに落とし込まれていて、とても知見でした。
移譲する上で品質の低下や移譲先の負荷を高めないと言う点での取り組みも勉強になりました。例えばクラスター作成用のeksclstという社内ツール作成している点などです。

AWSのマネージドサービスを活かした Kubernetes 運用とAmazon EKS によるクラスタのシングルテナント戦略について - Speaker Deck

まとめ

コンテナなど慣れ親しんでいるサービスからAppsyncやManaged Blockchainなどのあまり関わったことのないサービスまで多くの知見が得られました!
来年も行きたい!

AWSソリューションアーキテクト アソシエイトに合格したので試験の雰囲気や勉強方法について書きます

何番煎じかも分からない記事にはなってしまいますが、こういう人もいるぞという材料にしていただければ幸いです。

当日・結果

試験の登録を行った際に、確認のメールが届くのですがそこに下記のような記載があります。 なので、パスポートと免許証を持ってく必要がありました。

You are required to present two forms of original (no photo copies), valid (unexpired) IDs; one needs to be government-issued with registered name, photo, and signature and the second with registered name and signature or recent recognizable photo.

テストセンターで試験を受けるのはSPIや玉手箱以来でした。その時には無かったと記憶しているのですが、この試験ではデジタル署名と写真撮影を行いました。

試験時間は最初の説明と最後のアンケート含め140分と時間が取られていますが、自分は4〜50分ほど残して試験を終えました。 本試験前に模試を受けていたのですが、その模試は25問/30分だったのに対し、本番は65問/120分だったので、模試のペースより少し遅めに解いてもだいぶ余裕がありました。

結果は以下になります。100点〜1000点の間で評価されて、720点以上で合格するそうです。

f:id:jrywm121:20190519013304p:plain

準備

自分のプロフィール

  • 業務でAWSを使い始めたのちょうど1年前
  • インフラ関連の業務に関わり始めたのも同じタイミング
  • 持っている資格はIPAのやってるFE/AP/DBで、LPICなどのインフラ系の資格は無し
  • 主に業務で扱ってるのはECS、ECR、EC2(ASG)、CW、EKSとコンテナ関連がほとんど

勉強方法

基本的には学んだすべてのことをdynalistにまとめていって、試験前におさらいしていました。

黒本を読む

徹底攻略 AWS認定ソリューションアーキテクト アソシエイト教科書 を読みました。1章で軽く主要なサービスをおさらいして、その後の章でWell-Archetected Frameworkに沿った観点での記述が見れたのが、個人的には頭を整理しやすくてよかったです。 パフォーマンスに関する記述はそのサービスを使っていないと意識できていない点なので、しっかり叩き込みました。例えば、EBSでRAID 0を使うとI/0の効率が良くなるなどです。

book.impress.co.jp

わからない箇所をBlackBeltやDevelopers.ioで確認する

サービスが全くつかめないという場合には、 AWS クラウドサービス活用資料集 | サービス別資料 からBlackbeltを読むようにしました。試験によく出やすいEC2やEBS、ELBは絶対読んでおいたほうが良いと思います。本だけではつかみにくいKinesisやOpsworksなども読んでおくと点数取りやすいのではないかと思います。 ネットワーク関係に関しては、いろんなサイトで勉強しました。Internet GatewayがStatic NATで、NAT GatewayはDynamic NATであるだとか、ENIにより、DHCPサーバーから毎回同じMACアドレスIPアドレスが付与されるなど、内部の動きに言及しているものを見つけると理解が深まりました。

模試を受けてみる

AWS training and certification から模試を受けました。全問題スクショを取りました。どの問題で間違ったとかはわかりませんが、各分野で何%とれているかは出るので、それをもとにこの問題が間違っているのではなど推測して復習しました。模試よりも本試験のほうが難しいとはお聞きしていましたが、正答率92%だったので受けても大丈夫と判断し、模試を受けた5日後に本試験をセッティングしました。

復習がてら他の方が試験のためにまとめたノートを読む

英語ですが、下記のサイトが非常に良くまとまっていてよい復習になりました。S3のストレージクラスのあたりが最高で、可用性や耐久性がどう違うのか非常に覚えやすかったです。

loige.co

まとめ

今後AWSのサービスを使う上で基盤となる知識を体系的に学ぶことが出来て非常に良かったです。 業務では、GCPも使ってるので、次はGoogle Cloud Proffesional Archetect取ってみようかなと考えています。 最近GoやってないのでGoがっつりやるのも良いかもしれない。

ローカル開発環境(apache/tomcat)をコンテナ化+SSL化

ローカルのapache+tomcatで動いているアプリケーションをdockerに乗っけてみました。 SSL込みで設定していきました。Docker for Macを使用しています。

成果物をこちらのリポジトリにあげておきます。

github.com

Apache

ApacheでVirtualHostで3つのFQDNの通信を扱うようにしていきます。SSLの設定も行っていきますが、その際にはmkcertを使用してローカル認証局と鍵の作成を行います。

httpd.confの作成

SSL通信を有効にするため、conf/httpd.confをベースのdockerイメージからコピーしてきて変更を加えていきます。以下の項目のコメントアウトを外します。

#LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
...
#LoadModule ssl_module modules/mod_ssl.so
...
#Include conf/extra/httpd-ssl.conf

今回はVirtualHosts+Proxy Moduleを使っていきたいと思うので下記のコメントアウトも外します。tomcatへの通信をhttpで行うために必要なモジュールもついでに外します。

#Include conf/extra/httpd-vhosts.conf
...
#LoadModule proxy_module modules/mod_proxy.so
...
#LoadModule proxy_http_module modules/mod_proxy_http.so

各VirtualHostsで共有の鍵を使いたいので、httpd-ssl.confに鍵の設定をして、httpd-vhosts.confの設定がhttpd-ssl.confより後にくるように記述の位置を変えます。下記のようになるはずです。

...
# Secure (SSL/TLS) connections
Include conf/extra/httpd-ssl.conf

# Virtual hosts
Include conf/extra/httpd-vhosts.conf
...

鍵の作成

mkcertを使って認証局+鍵の作成を行っていきます。Goで作られているらしいので興味ある方はぜひ。

github.com

brew install mkcert
brew install nss  # Firefoxでアクセスする際には必要のようです
mkcert -install # ローカル認証局作成 Macの方はキーチェーンアクセスを見ると出来ているのが分かる
mkcert localhost "*.example.com " "*.cool.com"
# generateされた下記のファイル2つをdockerコンテキストに配置

httpd-ssl.confの設定

httpd-ssl.confの下記をコメントアウトします。今回は先程生成した2つのpemファイルを利用するためです。

SSLCertificateFile "/usr/local/apache2/conf/server.crt"
...
SSLCertificateKeyFile "/usr/local/apache2/conf/server.key"

そして下記のように記述を加えます。

SSLCertificateFile "/usr/local/apache2/conf/localhost+2.pem"  # 後ほどDockerfileでCOPYするように宣言します
SSLCertificateKeyFile "/usr/local/apache2/conf/localhost+2-key.pem"

httpd-vhosts.confの設定

tomcatのコンテナへajpで通信するように設定を加えます。 通常は静的ファイルはDocumentRoot配下を参照させて、それ以外はtomcatと通信という形になるかと思いますが、今回はとりあえず全部tomcatコンテナに流します。 下記のような形としました。VirtualHostのセクション内です。

   <Directory "/usr/local/apache2/docs/one.cool.com">
        Options Indexes 
        Require all granted
    </Directory>

   <Location /todolist>
        ProxyPass http://app:8080/todolist/
    </Location>

/private/etc/hostsに今回設定したFQDNを追加

本題とは関係ないですが自分のPCからアクセスできるように、/private/etc/hostsに記述しておきます。

127.0.0.1       www.example.com
127.0.0.1       one.cool.com
127.0.0.1       two.cool.com

Dockerfileの作成

設定ファイルと鍵、httpのドキュメントをコンテナにCOPYしポート80番と443番をListenします。イメージはなるべく軽くしたいのでDebian系OSがベースのものでなく、alpineベースのものを選んでいます。

FROM httpd:2.4-alpine

# copy configuration files
COPY --chown=root:www-data conf/httpd.conf /usr/local/apache2/conf
COPY --chown=root:www-data conf/extra /usr/local/apache2/conf/extra

# copy keys for ssl/tls
COPY --chown=root:www-data keys /usr/local/apache2/conf

# copy document
WORKDIR /usr/local/apache2/docs
RUN chown root:www-data /usr/local/apache2/docs
COPY --chown=root:www-data docs .

EXPOSE 80 443

httpdがうまく動いているかテスト

docker-compose.yamlを作成し、下記のようにhttpdの設定を宣言します。

version: '3'
services:
  web:
    container_name: httpd
    build: ./httpd
    ports:
    - "9080:80"
    - "9443:443"

以下のURLにSSLでアクセスできることが確認できました。

Tomcat

kotlin + SpringBootで作成した簡単なアプリケーションをコンテナに乗っけてみます。黒べこ本のサンプルに載っているTodoアプリです。

kotlinのアプリケーションの設定を一部変更

最初にkotlinのアプリケーションのbuild.gradleに変更を加えていきます。今回はwarを作成してそれをデプロイするため、下記のように記述を加えていきます。

apply plugin: 'war'
dependencies {
    providedRuntime("org.springframework.boot:spring-boot-starter-tomcat")  // SpringBoot組み込み以外のTomcatを利用するときに必要みたいです
}

次に、warファイルをTomcatに展開するにあたって必要らしいServletInitializerというclassをエントリポイントのあるclassと同パッケージに配置します。下記のような内容のclassです。

package com.example.todolist

import org.springframework.boot.builder.SpringApplicationBuilder
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer

class ServletInitializer : SpringBootServletInitializer() {

    override fun configure(application: SpringApplicationBuilder): SpringApplicationBuilder {
        return application.sources(TodolistApplication::class.java)
    }
}

Dockerfileの作成

Warに固める作業自体はローカルのIntelliJで行います。Dockerコンテキスト配下にソースを置いてそこに成果物を固めて良いのですが、今回は単純にcopyしてコンテキスト配下に盛ってきたいと思います。いちいちその作業をやるのが面倒くさいのでMakefileを作ります。
※ AutoDeployしてDockerで都度都度検証したい場合には、Volumeマウントして動かしたほうが良いとは思います。

build:
  cp ~/ideaProjects/todolist/build/libs/todolist.war tomcat/build
  docker-compose up --build

run: 
  docker-compose up 

makefileに記述したように、warはtomcat/build配下に格納するので、そのwarをdockerの方にcopyするだけです。なのでDockerfileは下記のようになります。tomcat側もイメージは軽くしたいのでalpineを使います。 またSpringBootは環境変数からコンテキストパスを設定できるのでしておきます。SpringBoot1.x系は別の変数名になるらしいので注意です。

FROM tomcat:9.0.16-jre8-alpine

ENV SERVER_SERVLET_CONTEXT_PATH /todolist
COPY build/todolist.war /usr/local/tomcat/webapps
EXPOSE 8080

動作を検証する

docker-comopseに下記を追記し、make buildしてみます。[https://one.cool.com:9443/todo]にアクセスしてみるとアプリケーションの画面が表示されました。

  app:
    container_name: tomcat
    build: ./tomcat
    ports:
    - "8080:8080"

f:id:jrywm121:20190218021406p:plain
表示された画面

まとめ

アプリケーションをBuildするところからDockerでできるように今度する。マルチステージビルドが使えそう。

SpinnakerのPipeline Expressionsで他stageの情報を取ってくる

SpinnakerのPipeline Expressions

KubernetesAmazon ECSを絡めたCI/CDでは既にデファクト感のあるSpinnakerですが、日本語の記事が少ないのと公式ドキュメント自体も不足はしてないのですが若干物足りない部分があります。KubernetesのBlue/Green Deployを構築した際にPipeline Expressionsで他stageの情報を取ってくるということをしたんですが、ちょっと分かりづらかったので記事に残しておきます。

とはいえ下記のページ見ていただければ十分とも思います。

Pipeline Expressions Guide - Spinnaker

やりたいこと

以下のような流れでパイプラインを構築したいと考えました。項目はStageの単位と対応しています。 providerはKubernetes V2です。

  • 新しいReplicaSetをDeploy(ただしPod数は1)。
  • Enableし新しいReplicaSetにもTrafficが流れるようにする。
  • manual judgement。このままデプロイを続けるかRollbackするか選択させる。
  • Continue
    • 新しいReplicaSetを本番相当までスケールアウト
    • 古いReplicaSetをDisableしTrafficが流れないようにする
    • 古いReplicaSetのサイズを0にする
  • Rollback
    • 新しいReplicaSetをDisableにする
    • 新しいReplicaSetをdeleteする <- ここが問題になった

ScaleやEnableは、対象をstaticとdynamicに分かれます。staticは固定値かつ既に存在するReplicaSetしか選べないので今回の用途に合いません。Dynamicでは、NewestやSecond Newest等の選択肢があります。逆にそれらの選択肢からしか選べません。常に現在動いているReplicaSetが最新のものである前提でないとPipelineの設定がうまくいきません。そのためRollbackではReplicaSetを削除する必要があります。
このDeleteのstageなんですが、ScaleやEnableと違って削除するリソースをNewestやSecond Newestから選べません。namespace/リソースタイプに加え、リソースの名前もしくはラベルを直接指定する必要があります。リソースの名前を指定するのが手っ取り早そうなのですが、なぜかPipeline Expressionsを記述してSaveしても消えてしまったので、タグの方で実装しました。

Pipeline ExpressionsでDeploy時のラベル情報をとってくる

Spinnakerはデプロイ時にこちらが指定したマニフェストに更に情報を付加するので、最終成果物からラベルを取ってくる必要性があります。 結論から言うと以下です。

${ #stage("Deploy new ReplicaSet").context["outputs.manifests"][0].metadata.labels["moniker.spinnaker.io/sequence"].toString() }

Key名moniker.spinnaker.io/sequenceだけ取ってきていますが、他のラベルに関しては固定値のため取ってくる必要性がありません。 この情報は、http://localhost:8084/pipelines/${PIPELINE_ID}で取ってこれるJSONをベースにアクセスしていきます。PIPELINE_IDは、パイプライン実行結果のDetailを選択し展開されたウィンドウの右下のSourceをクリックして飛ぶURLに含まれています…というか先程のURLそのものになっています。 #stage("ステージ名")でstages配列の中の指定したstageのJSONをルートにしてアクセスできます。outputs.manifestsのようにKeyに.(ドット)が含まれる場合にはmap形式でアクセスしないとだめです。

Pipeline Expressionsをテストする

以下のようにcurlを叩くことでテストできます。

curl  http://localhost:8084/pipelines/${PIPELINE_ID}/evaluateExpression \
-H "Content-Type: text/plain" \
--data '${ #stage("Deploy new ReplicaSet").context["outputs.manifests"][0].metadata.labels["moniker.spinnaker.io/sequence"].toString() }'

まとめ

お家でSpinnkerを試せるような環境が作れれば良いのだけれど…。 お家でKubernetes触りたいときには、Katacodaで今まで動かしていたので、minikube+Spinnakerに自分のPCのスペックが足りるかどうか。

試して理解 Linuxのしくみ 読みました 1章~3章

gihyo.jp

上記を読んだので学んだことをアウトプットしたいと思います。大学の授業で低レイヤのことを学んできませんでした。そのため、本書でLinuxに関して体系的に学べたの非常に良かったです。
本書の中で手を動かすパートに関しては、C言語Pythonの代わりにGo言語を使って試してみてます。また、PCはMacを使っているのですが、Linuxの環境で試したいという気持ちが有ったので、Dockerで仮想的にDebian系のOS上で動かすようにしています。リポジトリは以下になります。

github.com

第1章概要

ハードウェアの動作の流れ

1.外部デバイスから処理を依頼される

2.メモリに存在する命令を読み出してCPUにおいて実行
3.出力する

  • HDDやSSDに書き込む
  • ネットワークを介して別のコンピュータに転送する
  • 出力デバイスを介して人間に見せる

4.1に戻る

コンピュータの起動

  1. BIOSUEFIと呼ばれるハードウェア組込ソフトウェアによるハードウェア初期化処理
  2. 起動させるOSを選択するブートローダが動作
  3. ストレージデバイスからOSを読み出す コンピュータにとってストレージデバイスは欠かせない存在

2章ユーザモードで実現する機能

ユーザモードとカーネルモード

バイスの操作やプロセス管理、メモリ管理、プロセススケジューラなど、通常のプロセスから実行できると困る、OSの核となる処理をまとめたプログラムをカーネルという。
これらは特権モード、即ちカーネルモードで動作する。
OS=カーネルではなく、カーネル以外にもユーザモードで動作する様々なプログラムから構成される。
ユーザモードのプロセス処理から、システムコールを通じてカーネルの処理を呼び出す。それを呼び出すのは、プロセス固有のコードやそれが使用するライブラリ(OS提供のもの・そうでないもの)

CPUのモード遷移

システムコールを発行すると、CPU割り込みが発生。これによって、CPUでは、ユーザモードからカーネルモードに遷移して、カーネルの処理を行う。終わったらユーザモードに戻る。

システムコールの呼び出し

sh main.sh -d 02-syscall-and-non-kernel-os/hello
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。

-------------------------PROGRAM START-------------------------
hello world
--------------------------PROGRAM END--------------------------
-------------------------SYSCALL START-------------------------
execve("/go/bin/app", ["/go/bin/app"], [/* 7 vars */]) = 0 <0.000401>
arch_prctl(ARCH_SET_FS, 0x54c6b0)       = 0 <0.000087>

***(省略)*******************************************************

fcntl(2, F_GETFL)                       = 0x1 (flags O_WRONLY) <0.000139>
write(1, "hello world\n", 12)           = 12 <0.000149>  
exit_group(0)                           = ?
+++ exited with 0 +++
--------------------------SYSCALL END--------------------------

下から3行目のwriteがデータを画面やファイルに出力する「write()」システムコール。省略している部分もシステムコールだが、それはプログラムの開始処理が発行するもの。

ユーザモードとカーネルモードの割合

自前のアプリケーションを走らせない場合

以下のようにUbuntuの環境でsarコマンドを発行してみた

docker run --rm -it ubuntu:xenial /bin/bash
# apt-get update
# apt-get install -y sysstat
# sar -P ALL 1 1

結果は以下のように得られた

12:36:45        CPU     %user     %nice   %system   %iowait    %steal     %idle
12:36:46        all      0.00      0.00      0.50      0.00      0.00     99.50
12:36:46          0      0.00      0.00      0.00      0.00      0.00    100.00
12:36:46          1      0.00      0.00      0.99      0.00      0.00     99.01

Average:        CPU     %user     %nice   %system   %iowait    %steal     %idle
Average:        all      0.00      0.00      0.50      0.00      0.00     99.50
Average:          0      0.00      0.00      0.00      0.00      0.00    100.00
Average:          1      0.00      0.00      0.99      0.00      0.00     99.01

各行が1つのコアに対応する。今回は殆どがidleモードとなっている。

  • %user -> ユーザモード
  • %nice -> ユーザモード
  • %system -> カーネルモード
  • %idle -> 何もしていない時間

自前の無限ループするだけのアプリケーションを走らせた場合

sh main.sh -d 02-syscall-and-non-kernel-os/loop
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。

Linux 4.9.93-linuxkit-aufs (aad086a81679)    01/14/19    _x86_64_    (2 CPU)

13:18:18        CPU     %user     %nice   %system   %iowait    %steal     %idle
13:18:19        all     50.00      0.00      0.50      0.00      0.00     49.50
13:18:19          0      0.00      0.00      1.00      0.00      0.00     99.00
13:18:19          1    100.00      0.00      0.00      0.00      0.00      0.00

Average:        CPU     %user     %nice   %system   %iowait    %steal     %idle
Average:        all     50.00      0.00      0.50      0.00      0.00     49.50
Average:          0      0.00      0.00      1.00      0.00      0.00     99.00
Average:          1    100.00      0.00      0.00      0.00      0.00      0.00

CPUコア1上でユーザプロセス、すなわちloopプログラムが常に動作していることがわかる。

自前の親プロセス取得を無限ループするアプリケーションを走らせた場合

sh main.sh -d 02-syscall-and-non-kernel-os/ppidloop
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。

Linux 4.9.93-linuxkit-aufs (4bbb02f02797)    01/14/19    _x86_64_    (2 CPU)

13:26:00        CPU     %user     %nice   %system   %iowait    %steal     %idle
13:26:01        all     20.71      0.00     30.30      0.00      0.00     48.99
13:26:01          0     41.00      0.00     59.00      0.00      0.00      0.00
13:26:01          1      0.00      0.00      1.02      0.00      0.00     98.98

Average:        CPU     %user     %nice   %system   %iowait    %steal     %idle
Average:        all     20.71      0.00     30.30      0.00      0.00     48.99
Average:          0     41.00      0.00     59.00      0.00      0.00      0.00
Average:          1      0.00      0.00      1.02      0.00      0.00     98.98

CPUコア0上でppidloopプログラムを41%の割合で実行し、親プロセス取得のシステムコールを59%の割合で実行していた。

システムコールのラッパー関数

システムコールC言語などの高級言語から直接呼び出せない。アーキテクチャ依存(amd64とかarmとか?)のアセンブリコードを使って呼び出す必要がある。もしシステムコールをラップする関数を内包するライブラリ群がなければ、アーキテクチャ依存のアセンブリソースを書かなくてはいけない。

標準Cライブラリ

GNUプロジェクトが提供するglibcLinuxの標準Cライブラリとして使用する。glibcシステムコールをラップする関数を内包している。またPOSIXという規格に定義されている関数も提供。
プログラムがどのようなライブラリをリンクしているかは以下でlddコマンドでわかる。

% docker run --rm -it golang:1.11.4 /bin/bash
# which go
/usr/local/go/bin/go
# ldd /usr/local/go/bin/go
    linux-vdso.so.1 (0x00007ffef2965000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fe2c2ba2000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe2c2803000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fe2c2dbf000)

第3章プロセス管理

fork関数

同じプロセスの処理を複数のプロセスに分ける際に呼び出される(例:Apacheで子プロセスを生成する)。 子プロセス用メモリ領域を作成し、親プロセスのメモリをコピーする。fork関数はカーネルモードからユーザモードに処理が再び移るときに、親プロセスには子プロセスのプロセスIDを、子プロセスには0を返す。これを利用して親子で処理を分岐させる。

sh main.sh -d 03-process-management/fork
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。 fork()されたときに同じプログラムが複製されて2個同時に動いているような感覚。

I'm parent! my pid is 7 and the pid of my child is 11.
I'm child! my pid is 11.

execve関数

全く別のプログラムを生成するときに呼び出される(例:bashからsleepコマンド実行)。 ます実行ファイルを読み出し、その後現在のプロセスのメモリを上書く。 実行ファイルの中身 * プロセスの実行に必要なコード、データ * コードを含むデータ領域のオフセット(ベースアドレスに加えられるアドレスの数)-> ファイルの情報 * コードを含むデータ領域のサイズ * コードを含むデータ領域のメモリマップ開始アドレス -> コンピュータのメモリのどこにマッピングするか * 最初に実行する命令のメモリアドレス(エントリポイント)

Linuxの実行ファイルはELFというフォーマットを使用する。
エントリポイントのアドレスを得るには「-h」オプション、ファイル内オフセット、サイズ、メモリマップ開始アドレスを得るには「-S」オプション

% docker run --rm -it ubuntu:xenial /bin/bash
# apt-get update
# apt-get install binutils
# readelf -h /bin/sleep
ELF Header:
------(省略)---------------------------------------------
  Entry point address:               0x401760
------(省略)---------------------------------------------

# readelf -S /bin/sleep
There are 29 section headers, starting at offset 0x7370:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
------(省略)--------------------------------------------------------
  [14] .text             PROGBITS         00000000004014e0  000014e0
       0000000000003319  0000000000000000  AX       0     0     16
------(省略)--------------------------------------------------------
  [25] .data             PROGBITS         00000000006071c0  000071c0
       0000000000000074  0000000000000000  WA       0     0     32
------(省略)--------------------------------------------------------

Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

全く新規のプロセスを新規生成する場合には、親となるプロセスからfork()を発行して、復帰後に子プロセスがexec()を発行する。 sh main.sh -d 03-process-management/execve
上述のリポジトリにて、上のコマンドを実行すると以下のような結果を得られました。fork()の直後にexec()が動いています。

sh main.sh -d 03-process-management/execve
I'm parent! my pid is 6 and the pid of my child is 10.
I'm child! my pid is 10.
hello

第4章プロセススケジューラ

Go言語によるWebアプリケーション開発まとめ・感想 チャットアプリ編

オライリー出版のGo言語によるWebアプリケーション開発を読んだ感想 チャット部分編

オライリーから出版されているGo言語によるWebアプリケーション開発を読みました。1章から3章までがWebsocketを使用したチャットアプリケーション開発となるのですが、その部分を読んだまとめ・感想を書いていきます。

www.oreilly.co.jp

github.com

webサーバ公開の基本

アクセスパスと処理を結びつけます。
パスは完全一致ではなく前方一致となるため、注意が必要となります。/を登録した場合は/appとアクセスしても関連付けた処理が実行されます。 結びつけたあとに、ListenAndServeメソッドを使ってWebサーバを開始します。

http.HundleFuncでパスと関数を紐づける

この後に挙げるhttp.Handleとは違い、Handlerのインターフェースを実装した構造体を用意する必要がありません。
その代わり、状態などを持たせることが出来ないため注意です。

http.HandleFunc("/auth/", loginHandler) 

http.HandleでパスとHandlerを紐づける

Handlerのインターフェースを実装した構造体を用意した上で、http.Handle関数の第二引数に渡してあげる。Handler(https://golang.org/pkg/net/http/#Handler)は、ServeHTTP(w http.ResponseWriter, r *http.Request)メソッドのみを持つインターフェースになっている。http.Handle関数にて指定したリクエストパスにマッチするリクエストが来た場合に、ServeHTTPメソッドの中に書いた処理が実行される。

下の例は、返却するhtmlファイルに埋め込むデータオブジェクトを作成し、そのデータオブジェクトをコンパイル済のHtmlテンプレートにapplyする処理。ただし、初回リクエストが来たときにテンプレートファイルをコンパイルする。初回だけの実行を保証するための実装としては、sync.Onceを使用している。起動時にコンパイルしないこの方法を、遅延初期化として本では取り上げられており、セットアップ処理に時間がかかる場合や利用頻度の低い機能では効果的と説明されています。ただファイルが無いなどの状態を検知できないため、template.Mustを使用してグルーバル変数にセットする方法が好まれることもあるとのこと。

http.Handle("/login", &templateHandler{filename: "login.html"})

// テンプレートの読込と出力を受け持つ型。テンプレートのコンパイルは一度で良い。
type templateHandler struct {
    once     sync.Once // 複数のgoroutineが呼び出したとしても、引数として渡した関数が一度しか実行されない。
    filename string
    temp1    *template.Template //コンパイルされたテンプレートへの参照を保持
}

// 同じsync.Onceの値を使用しなくてはいけないためレシーバは必ずポインタでなくてはならない
func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    t.once.Do(func() {
        t.temp1 =
            template.Must(template.ParseFiles(filepath.Join("templates", t.filename)))
    })
    data := map[string]interface{}{
        "Host": r.Host,
    }
    if authCookie, err := r.Cookie("auth"); err == nil {
        data["UserData"] = objx.MustFromBase64(authCookie.Value)
    }
    t.temp1.Execute(w, data)
}

template.Mustを使用してグルーバル変数にセットする方法は以下。この方法使用すると、もしエラーがある場合にはpanicを起こし起動しなくなるため、エラーに気づかない事象を避けることができる。

var temp1 = template.Must(template.ParseFiles(filepath.Join("templates", "login.html")))

以下のコマンドでWebサーバを開始する。

if err := http.ListenAndServe(*addr, nil); err != nil {
   log.Fatal("ListenAndServe:", err)
}

HTTP2をつかった通信をしたいのであれば、以下のようになる。

certFile, _ := filepath.Abs("server.crt")
keyFile, _ := filepath.Abs("server.key")

if err := http.ListenAndServeTLS(*addr, certFile, keyFile, nil); err != nil {
   log.Fatal("ListenAndServe:", err)
}

実装としては割れるのは、http.HandleFuncを使うかhttp.Handleを使うかというところだと思うが、遅延初期化を使わない場合には、http.HandleFuncで事足りるのではと私は感じている。

チャットアプリケーションのモデル化

ここでのチャットアプリケーションの要件は、一つの公開されたチャットルームにクライアントは自動的にログインして会話をするというもの。クライアントとの通信を管理するclientとという構造体とクライアントの入退出の管理やメッセージをブロードキャストする役割を持つroomという構造体で構成する。

client

Client構造体は、各クライアントごとに必要となるwebsocketのコネクションと他クライアントからのメッセージを格納するチャネルを持つ。また、ブラウザから送信されるメッセージをRoomに送るためにRoomへの参照を持っている。
clientのメソッドは各クライアントに対応するため、ServeHTTPメソッドの中で実行される。他クライアントからのメッセージを格納するチャネルをクライアント側で持つのは、言わずもがなWebsocketに書き出すWriteメソッドをクライアント側で持つため(おそらくsocketのコネクションを他ソースコードに使わせずチャネルでやり取りしたかった)。

type client struct {
    socket *websocket.Conn // クライアントのためのWebSocket
    send   chan []byte     // messageが送られるバッファ付きチャネル
    room   *room           // 参加しているチャットルーム
}

func (c *client) read() {
    for {                                                                                                // 無限ループ
        if _, msg, err := c.socket.ReadMessage(); err == nil {
            c.room.forward <- msg                                               // クライアントからメッセージを受信したらブロードキャストするためにRoomに送る 
        } else {
            break
        }
    }
    c.socket.Close()
}

func (c *client) write() {
    for msg := range c.send {    // チャネルが閉じられるまで、チャネルから値を繰り返し受信し続ける。channelがクローズされれば、ループから抜ける。 
        if err := c.socket.WriteMessage(websocket.TextMessage, msg); err != nil {
            break   // WebSocketへの書き込みが失敗するとbreakでforループから抜け出し、 Websocketが閉じられる。
        }
    }
    c.socket.Close()
}

room

room構造体は他のクライアントに転送するためのメッセージを保持するチャネルと入退出を受け取るチャネル、クライアント全体のデータを持つ。
外部から利用しやすいように、newRoom()という関数を作る。run()メソッドはgoルーチンでWEBサーバと並列で実行される。チャネルへのメッセージの処理には、selectを使用している。

type room struct {
    forward chan []byte      // 他のクライアントに転送するためのメッセージを保持するチャネル
    join    chan *client     // チャットに参加しようとするクライアント用
    leave   chan *client     // チャットルームから退室しようとする用のチャネル
    clients map[*client]bool //すべてのクライアントが保持
    tracer  trace.Tracer
}

func newRoom() *room {
    return &room{
        forward: make(chan []byte),
        join:    make(chan *client),
        leave:   make(chan *client),
        clients: make(map[*client]bool),
    }
}

func (r *room) run() {
    for {
        // いずれかのチャネルにメッセージが届くとcaseに合わせて処理を実行
        // case節の処理は同時に実行されないので同時にr.clientに変更を加えることはない
        select {
        case client := <-r.join:
            r.clients[client] = true
        case client := <-r.leave:
            delete(r.clients, client)
            close(client.send)  // client側のチャンネルを閉じたのでforループから抜けてwebsocket通信が閉じられる
        case msg := <-r.forward:
            for client := range r.clients {
                select {
                case client.send <- msg:
                    r.tracer.Trace(" -- クライアントに送信されました")
                default:
                    delete(r.clients, client)
                    close(client.send) // client側のチャンネルを閉じたのでforループから抜けてwebsocket通信が閉じられる
                }
            }

        }
    }
}

Roomでの処理をHTTPハンドラ化

HTTPハンドラ化するにあたり、room構造体はServeHTTPメソッドを実装する必要がある。
Upgraderメソッドによりhttp通信はWebsocket通信へアップグレードされます。そして変数socketにwebcsocket通信のコネクションが入ってきます。

func (r *room) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    socket, err := upgrader.Upgrade(w, req, nil)
    if err != nil {
        log.Fatal("ServeHTTP:", err)
        return
    }
    client := &client{
        socket: socket,
        send:   make(chan []byte, messageBufferSize),
        room:   r,
    }

    r.join <- client   // joinチャンネルに生成したclientをpassing
    defer func() { r.leave <- client }()  // client.readはソケット通信でエラーになったらbreakし、そのclientをleaveチャンネルに通知する。
    go client.write()
    client.read()
}

main関数の流れは以下のようになります。

        http.Handle("/room", r)
        http.Handle("/chat", &templateHandler{filename: "chat.html"})
        go r.run()
        // WEBサーバ起動

認証機能

HTTPハンドラのdecoratorパターン

認証機能を作るにあたり、/roomにアクセスした際の処理を認証用のHTTPハンドラを用意しラップします。次に実行する処理を持つハンドラを認証ハンドラの構造体にフィールドに登録できるようにしておきます。認証用のハンドラは、認証が正常に完了したら、認証用ハンドラの次に実行するハンドラのServeHTTP()メソッドを実行することで処理を渡します。

http.Handle("/chat", MustAuth(&templateHandler{filename: "chat.html"}))

func MustAuth(handler http.Handler) http.Handler {
    return &authHandler{next: handler}
}

func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if _, err := r.Cookie("auth"); err == http.ErrNoCookie {
        // 未認証
        w.Header().Set("Location", "/login")
        w.WriteHeader(http.StatusTemporaryRedirect)
    } else if err != nil {
        panic(err.Error())
    } else {
        // 成功。ラップしたハンドラ呼び出し。
        h.next.ServeHTTP(w, r)
    }
}

このWrapするパターンですが、以下のようにWrapperをhttp.HandleFuncを使って記述することも可能です。
(参考元->https://medium.com/@matryer/the-http-handler-wrapper-technique-in-golang-updated-bc7fbcffa702)

http.Handle("/chat", MustAuth(&templateHandler{filename: "chat.html"}))

func MustAuth(h http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r 
                                          *http.Request) {
    if _, err := r.Cookie("auth"); err == http.ErrNoCookie {
        w.Header().Set("Location", "/login")
        w.WriteHeader(http.StatusTemporaryRedirect)
    } else if err != nil {
        panic(err.Error())
    } else {
        h.ServeHTTP(w, r)
    }
  })
}

wrapする側もされる側もHandleFuncにするなら以下のような形です。 (参考元->https://twinbird-htn.hatenablog.com/entry/2016/06/06/001704)

http.HandleFunc("/chat", MustAuth(templateHandleFunc("chat.html")))
func MustAuth(fn http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if _, err := r.Cookie("auth"); err == http.ErrNoCookie {
        w.Header().Set("Location", "/login")
        w.WriteHeader(http.StatusTemporaryRedirect)
    } else if err != nil {
        panic(err.Error())
    } else {
        fn(w, r)
    }
    }
}

OAuth2

この本ではgomniauthというOSSが利用されていたが、gomniauthを使うと何故かobjx: JSON encode failed with: json: unsupported type: func() stringというgomiauthの中で使用されているライブラリの中で落ちるという現象にあい、ここで時間をとるのも…と思い、golang.org/x/oauth2というパッケージを利用しました。
処理の流れとしては、以下の記事を参考にいたしました。

dev.to

var googleOauthConfig = &oauth2.Config{
    RedirectURL:  "http://localhost:8080/auth/callback/google",
    ClientID:     os.Getenv("OAUTH_ID_GOOGLE"),
    ClientSecret: os.Getenv("OAUTH_KEY_GOOGLE"),
    Scopes:       []string{"https://www.googleapis.com/auth/userinfo.profile"}, // https://developers.google.com/identity/protocols/googlescopes#oauth2v2
    Endpoint:     google.Endpoint,
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
    segs := strings.Split(r.URL.Path, "/")
    action := segs[2]
    // provider := segs[3]

    switch action {
    case "login":
        state, expiration := generateStateOauthCookie(w)
        u := googleOauthConfig.AuthCodeURL(state) // 認可サーバへのURL+クライアントIDやリダイレクト先の情報が入ったQuery
        cookie := http.Cookie{Name: "oauthstate", Value: state, Expires: expiration, Path: "/"}
        http.SetCookie(w, &cookie)
        http.Redirect(w, r, u, http.StatusTemporaryRedirect)
    case "callback":
        oauthState, _ := r.Cookie("oauthstate")

        fmt.Println(r.FormValue("state"))
        fmt.Println(oauthState.Value)
        if r.FormValue("state") != oauthState.Value {
            log.Fatalln("invalid oauth google state") // Cookieにセットしたstateとクエリにセットしたstateが合致しないのはCSRF攻撃を受けている場合がある
            return
        }

        // 正しく認可コードを得ることができたためアクセストークンを要求し、そのアクセストークンを使用しリソースサーバにアクセスできる。
        data, err := getUserDataFromGoogle(r.FormValue("code"))
        if err != nil {
            log.Println(err.Error())
            http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
            return
        }

        var profile Profile
        if err := json.Unmarshal(data, &profile); err != nil {
            fmt.Println("error:", err)
        }

        authCookieValue := objx.New(map[string]interface{}{
            "name": profile.Name,
        }).MustBase64()

        // Cookie は、ステートレスな HTTP プロトコルのためにステートフルな情報を記憶する
        // https://developer.mozilla.org/ja/docs/Web/HTTP/Cookies
        http.SetCookie(w, &http.Cookie{
            Name:  "auth",
            Value: authCookieValue,
            Path:  "/"})
        // Headerの方は map[string][]string
        w.Header()["Location"] = []string{"/chat"}
        w.WriteHeader(http.StatusTemporaryRedirect)
    default:
        w.WriteHeader(http.StatusNotFound)
        fmt.Fprintf(w, "アクション%sには非対応です", action)
    }
}

アバター

SNSから取得するパターン、Gravatarから取得するパターン、画像のアップロードしてもらうパターンの3種ありましたが、前の2つは実装自体は難しくなかったため3つ目の画像のアップロード・提供について書いていきます。 具体的な処理から抽象化していく工程も書いていきたかったのですが、言語化するのが難しかった…。

画像のアップロード

http.HandleFunc("/uploader", uploaderHandler)

func uploaderHandler(w http.ResponseWriter, req *http.Request) {
    userId := req.FormValue("userid") // HTMLフォームの隠しフィールドにセットされたユーザーIDを読み取り
    file, header, err := req.FormFile("avatarFile")
    if err != nil {
        fmt.Fprintln(w, err.Error())
        return
    }
    defer file.Close()
    data, err := ioutil.ReadAll(file)
    if err != nil {
        fmt.Fprintln(w, err.Error())
        return
    }
    filename := filepath.Join("avatars", userId+filepath.Ext(header.Filename)) // 元ファイル拡張子(extension)をjoinする
    err = ioutil.WriteFile(filename, data, 0644)
    if err != nil {
        fmt.Fprintln(w, err.Error())
        return
    }
    fmt.Fprintln(w, "success!!")
}

提供するときには組み込みのファイルサーバ機能を使います。

   // ScripPrefixもFileServerもhttp.Handler型を返す。本の2章で紹介されたDecoratorパターン。
    http.Handle("/avatars/",
        http.StripPrefix("/avatars/", // パスの中から接頭辞(/avatars/の部分を削除)。削除しないと./avatars/avatars/filenameにアクセスしようとする
            http.FileServer(http.Dir("./avatars")))) // 静的ファイルの提供ができる

まとめ

本自体は原本が2014年の出版でかなり古いので、新しい情報とかも見つつ整理していくのが大事だなと思いました。 普通WEBアプリケーションを作成するときは標準パッケージではなくGinとか使われていくのだろうなとは思っているのですが…。
ただメルカリさんのAPIサーバのように、net/httpパッケージだけを見て作成されるパターンもあるので、実際は用途によっての使い分けなんですかね。

メルカリさんのAPIサーバの実装に関する記事↓

tech.mercari.com

Kubernetes完全ガイド読んだ感想・まとめ

青山さんのKubernetes完全ガイド読みました

インプレスさんより出版されているKubernetes完全ガイドを読みました。今まで行き当たりばったりでKubernetesを学んできた自分にとって、ものすごく学びが多かったですし、体系だった知識に整理できた一冊でした。図がたくさんあって理解もスムーズだったように思えます。 Kubernetesにこれから触る人や私みたいにその場しのぎで学んできた方は絶対に読むべき一冊だと思います。
※ 私はAmazon ECSとGKEを使い始めて半年くらいです

book.impress.co.jp

今回Kubernetes入門を見て改めて整理出来た項目を中心に挙げていきたいと思います。

kubectl

Contextの切り替え

kubectlで操作する際に、操作対象のクラスターや認証情報が必要になります。実態はデフォルトだと、$HOME/.kube/configに記載されるらしいです。EKSとGKEを使っている場合に、複数の異なる環境にアクセスする場合にはContextの登録・切り替えが必要になります。

# クラスタ名の登録
kubectl config --kubeconfig=config-demo set-cluster development --server=https://1.2.3.4 --certificate-authority=fake-ca-file

# 認証情報の登録
kubectl config --kubeconfig=config-demo set-credentials developer --client-certificate=fake-cert-file --client-key=fake-key-seefile

# コンテキスト名 / クラスタ名 / namespace / 認証情報
kubectl config --kubeconfig=config-demo set-context dev-frontend --cluster=development --namespace=frontend --user=developer
kubectl config --kubeconfig=config-demo set-context dev-storage --cluster=development --namespace=storage --user=developer
kubectl config --kubeconfig=config-demo set-context exp-scratch --cluster=scratch --namespace=default --user=experimenter

# 設定情報を見る
kubectl config --kubeconfig=config-demo view

# 切り替えコマンド
kubectl config --kubeconfig=config-demo use-context dev-frontend

Configure Access to Multiple Clusters - Kubernetes

kubectlで適用される差分

今まで意識してこなかったと同時に、なぜ意識できなかったのか後悔の多いところです。 さらに言えば、kubectl applyのマニフェストのマージ方法というところになります。 削除項目の差分適用は、新しくapplyされるマニフェストと、以前applyされたマニフェストとの比較で行われますが、追加・更新の項目の差分適用は以前applyされたマニフェストではなく、そこからKubernetesが変更を加えたものとの比較になります。つまり、kubectl get deploy xxxxx -o yamlした結果との比較です。 applyした結果、podのテンプレートと変更があれば、ローリングアップデートが起こりますね。以前適応したマニフェストにrollingUpdateについて書いてない状態から、rollingUpdateでmaxSurge25%、minUnAvailable25%ととしても、デフォルト値としてKubernetes側がセットした値とかわらないため、rollingUpdateは起こりません。

また、削除項目の差分適用が以前applyされたマニフェストとの比較ということで、単純にcreateされたマニフェストから、applyで更新すると削除できない項目が出てきてしまいます。createコマンドを使用するときはkubectl create --save-configを使用すること。

マニフェストファイルの設計

結合度の高いリソースである場合、例えばDeploymentとService、HPA、PDBは同じマニフェストファイルに書いても良いかもしれない。ConfigMapやSecretなどの疎結合のリソースに関してはマニフェストファイルを分けるべき。1つのファイルに書く場合には---で区切る。上から順に適用されていくため、書く順番は考慮されるべきである。
kubectl apply -f ./ -R再帰的にマニフェストを適応させることが出来るらしいので、ディレクトリ構造は極力わかりやすい形に持っていくことも可能。

その他

  • Stern(https://github.com/wercker/stern)導入で複数のpodをログを同時に見ることが可能
  • 環境変数KUBE_EDITORで使用するエディタを指定できる。
  • kubectl copyコマンド
  • kubectl port-forward deployment/xxxxxx 8080:3306でkubectlを実行したインスタンス(もしくはローカルマシン)のポート8080にアクセスすると対象Podの3306番のpodに接続される。
  • デバッグ-v=6-v=8を使う。

Workloadsリソース

Podのデザインパターン

  • サイドカー:メインコンテナに機能を追加する。個人的にはkinesisアクセスログを流すflunetdコンテナとかがこれに当たると思いました。
  • アンバサダー:外部システムとのやり取りの代理を行う。HAProxyコンテナのイメージです。AWSでいうならRDSの死活監視を行ってトラフィックを流すようなもの。
  • アダプタ:外部からのアクセスのインターフェース。JavaアプリケーションとかだったらPrometheus Tomcat Exporterのようなイメージでしょうか。

Rolling Updateの処理

マニフェストをapply -> .spec.template以下の構造体のハッシュ値を計算 -> 同じハッシュ値のReplicaSetが既存していなければ作成 -> ローリングアップデート戦略にもとづいて、現在のReplicaSetのPod数を減らし新しいReplicaSetのPod数を増やしていく。ロールバックは、戻すリビジョンを指定しそれに対応するReplicaSetのPod数を増やし、現在のReplicaSetのPod数を減らしていく。kubectl rollout pause deployment sampleでローリングアップデートを止めることも可能。

Daemon Setのアップデート戦略

OndeleteRolling Updateがある。OndeleteはDaemosetのtemplateが変更されてもPodの更新は行われない。更新する場合には手動でPodをdeleteしオートヒーリングでPodを新たに作成する際に新しい定義となる。apiVersionがapps/v1はデフォルトがRolling Updateですが、それより前のバージョンでのデフォルト値はOndeleteなので注意が必要。
参考: Default apps/v1beta2 StatefulSet and DaemonSet strategy to RollingUpdate · Issue #49604 · kubernetes/kubernetes · GitHub

Stateful Setの特徴

業務では使用したこと無いワークロードであるところのStateful Setですが他のワークロードとの違いが顕著であるように見受けられます。以下に列挙していきます。

  • 作成されるPod名のSuffixは数字のインデックスが付与されたのものになる。
  • scaleすると、今あるpodのIndexに+1された名前のPodが作成される。デフォルトでは、Ready状態になってから次のPodを作成し始める。なので増えるときは1つずつ。ただ、podManagementPolicyをPararellにするとdeploymentと同じように複数同時に起動する。
  • Rolling UpdateはpodManagementPolicyにかかわらず1つずつ行われる。partitionを設定することで一部だけを更新することが可能
  • Podの再起動時にも同じPersistentDiskを使用する

その他

  • Podテンプレートのコンテナ実行時のコマンド指定
kubernetes Dockerfile
command ENTRYPOINT
args CMD
  • Pod全コンテナの/etc/hostsを書き換える機能があり、.spec.hostAliasに指定する
  • kubectl applyしたときに--recordオプション必須つけることでアノテーションにchange-causeが記録される

Discovery&LBリソース

Service

kind type description
ClusterIP ClusterIP クラスタ内のみから疎通性があるInternal Networkに作り出される仮想IP。spec.ports.portがClusterIPで待ち受けるIPアドレス。spec.ports.targetPortが転送先コンテナのPort番号。手動でIPを固定する場合には、spec.ClusterIPを指定する。
ExternalIP ClusterIP typeはClusterIP。特定のKubernetesノードで受信したトラフィックをコンテナに転送する。spec.ExternalIPsで受信するノードのIPを列挙する
NodePort NordPort ExternalIPと似ている。ただしこれは、全ノードからトラフィックを受け付けるイメージ。NodePortで利用できるポート範囲は30000~32767
LoadBalancer LoadBalancer Production環境でクラスタ外からトラフィックを受けるときに使用される。Kubernetesクラスタ外に疎通性のある仮想IPを払い出す。上の外部疎通ServiceはノードがSPOFになるが、これは外部のロードバランサを利用するためにノードの障害に強い。GCPAWSでは、spec.loadBalancerSourceRangesに接続を許可する送信元IPアドレス範囲を指定できる。
Headless Service ClusterIP DNSラウンドロビンを使ったエンドポイントの提供。type:ClusterIPかつspec.ClusterIPがNoneのもの。負荷分散に向かない。
ExternalName Service ExternalName クラスタ内からService名で名前解決するとCNAMEが返ってくる。外部サービスと疎結合にするために使用する。
None-Selector Service * 自分で指定したメンバに負荷分散することが出来る。EndPointとServiceを同じ名前で作ることで紐付けられる。typeは自由だがメリットを考えるとClusterIP一択。Selectorには何も指定しない

Ingress

Ingressリソースは一旦作成すると、その後あんまりにも触る機会が無く忘れてしまいがちです。
L7のロードバランシングを行うリソース。L4のServiceと区別するため別のリソースとして扱われている。大別してクラスタ外のロードバランサを利用するものとクラスタ内のIngress用Podをデプロイするものがある。Ingressは事前に作成されたServiceをバックエンドとして転送を行う仕組みになっています。なのでNodePortのリソースをまず用意します。その後に、IngressリソースのserviceNameの項目で結びつけます。L7なので当然パスベースルーティングもマニフェストで指定することが出来ます。https通信を行う場合には別途でSecretを作成する必要性がある。

その他

  • 正式なFQDNを指定せずに、Service名だけでも解決できるのは、/resolve/confsearch [namespace].svc.cluster.localという文言があるため。

Config & Storageリソース

Secret

1つのSecretの中に複数のKey-Value値。 大体は、マニフェストから作成することが考えられる。base64エンコードした値をマニフェストに埋め込みますが、その状態で管理してもセキュリティなど無いに等しいため、kubesec(https://github.com/shyiko/kubesec)と暗号鍵のマネージドサービスを使って暗号化した状態でリポジトリに保存しておくのが良きかと思う。

apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
data:
  username: YWRtaW4=
  password: MWYyZDFlMmU2N2Rm

利用方法も複数ある。環境変数として渡す手段とVolumeとしてマウントする手段があります。Volumeとしてマウントした場合には動的なSecretの変更を行うことができます。また、それぞれ特定のKeyを渡す手段(spec.containers[].env[].valueFrom.secretKeyRef)とすべてのKeyを渡す手段(spec.containers[].envFrom.secretRef)がある。マニフェストに一覧性を持たせたい場合には前者、マニフェストを冗長にしたくない場合には後者を用いると良さげ。

kind type description
Generic Opaque マニフェストから作成するケースが多そう。DBなどの資格情報はこれで管理する。
TLS kubernetes.io/tls kubectl create secret tls custom-tls-cert --key /path/to/tls.key --cert /path/to/tls.crtで作成。keyが秘密鍵でcertが証明書ファイル。
Dockerレジストリ kubernetes.io/dockerconfigjson Registoryサーバと認証情報を引数で指定して作成する。podテンプレートのspec.containers[].imagePullSecretsで名前を指定する。
Service Account kubernetes.io/service-account-token 自動で作成されるもの。利用者が意識することはほとんどない

ConfigMap

ConfigMapも殆どの場合でマニフェストから作成されるケースが多そう。Secretと違いBase64エンコードしない点に注意。数値はダブルクォートで囲む。渡し方はSecretとほぼ変わらず、spec.containers[].env[].valueFrom.secretKeyRef -> spec.containers[].env[].valueFrom.configMapKeyRef / spec.containers[].envFrom.secretRef -> spec.containers[].envFrom.configMapRefとフィールドの名前が変わったくらい。

Persistent Volume Claim

ややこしい名前のリソースたちの違い

  • Volume
    • 予め用意された利用可能なボリューム
      • EmptyDir : ホスト上の領域を一時的に確保して利用できる。podがterminateされると削除される
      • hostPath : ホスト上の領域をコンテナにマッピングする。セキュリティに注意。
      • gcePersistentDisk : 同プロジェクト同ゾーンの複数のGCE VMから読み取り専用でマウントされる。大体は予めデータを置いている想定かと思われる。
  • Persistent Volume
    • 実はConfig & StorageリソースではなくClusterリソース
    • 予め作成しておく必要性がある
    • Dynamic Provisioningを使用しない場合にはラベルを付与すること
    • アクセスモードが複数あるが、大体はReadWriteOnceかReadOnlyManyが許可されていて、ReadWriteManyは許可されていない。
    • Reclaim PolicyでPersistentVolumeClaimが削除された際のPersistent Volumeの挙動を決められる。
  • PersistentVolumeClaim
    • その名の通りPersistent Volumeを要求するリソース。マニフェストに指定した条件にあうPVの割当をおこなう。
    • podテンプレートの定義のspec.volumes[].persistentVolumeClaim.ClaimNameにPVCの名前を指定する。
    • 予めPVを使用しなくても、動的にPVを作成して割り当てるのがDynamic Provisioning。StorageClassを作成してPVCのマニフェストで指定する必要あり。
    • StatefulSetはPVCの定義がマニフェストに含まれている

その他

  • SecretとConfigMap以外からPodテンプレートで環境変数を渡すには
    • 静的設定
env:
     - name: TZ
        value: Asia/Tokyo
  • podの情報を参照 。env[].valueFrom.fieldRef.fieldPathで指定する。
  • コンテナの情報を参照。env[].valueFrom.resourceFieldRef.containerNameでコンテナの名前、env[].valueFrom.resourceFieldRef.resouceで取得するフィールドを指定する。
  • SubPathマウントで特定のディレクトリをルートとしてマウントすることができる。mountPathがコンテナ側・subPathがボリューム側のディレクトリパスの指定となる。

リソース管理とオートスケーリング

requestsとlimit

requests/limitともに各コンテナに定義する。
この値が適正かどうか判断する際に役に立つのが、Nodeごとにリソース状況。これはkubectl describe node hogehogeで見ることが可能。
Requestsを大きくしすぎず、RequestsとLimitsの間に顕著な差を作らないことで適正なスケールアウトができるようになる。

  • requests
    • 使用するリソースの下限を指定するもの。
    • この値をもとにNodeにPodがスケジューリングされる。
  • Limits
    • 使用するリソースの上限を指定するもの。
    • この値を超えないようにPodの使用するリソース量がコントロールされる。
    • メモリの場合はLimitsを超えるとOOMでコンテナプロセスが殺される。

HPA(水平スケーリング)

30秒に1回の頻度でオートスケーリングすべきかのチェックが走る。必要なレプリカ数の計算は以下で計算される。
マニフェストの中でDeploymentを指定する。

desiredReplicas = ceil[currentReplicas * ( currentMetricValue / desiredMetricValue )]

スケールアウトの式 (最大で3分に1回)

avg(currentMetricValue) / desiredMetricValue > 1.1

スケールインの式(最大で5分に1回)

avg(currentMetricValue) / desiredMetricValue < 0.9

その他

  • LimitRangeでリソースの最小・最大値・デフォルト値を設定可能。新規でPodを作成するときに適用される。
  • OOM KillerによってPodを停止される場合には、QoS Class「Best Effort」「Burstable」「Guaranteed」の順で停止される。RequestsとLimitsが指定されていてなおかつ値が同じ場合には「Guaranteed」となり削除されにくくなる。逆にどちらも指定していない場合には、停止される対象になりやすい。
  • ResouceQuotaによってNameSpaceごとのリソース量・数に制限をかけることができる。

コンテナのライフサイクル

ヘルスチェック

Amazon ECSでもELBを使用したヘルスチェックがありますが、それよりもだいぶ柔軟です。ヘルスチェック方式は3種類あり、コマンドベースのチェック(exec)とHTTPベースのチェック(httpGet)、TCPベースのチェック(tcpSocket)がある。HTTPベースのチェックではホスト名やHeaderを設定できたりもする。
Amazon ECSにおける「healthCheckGracePeriodSeconds(ヘルスチェックまでの猶予期間)」にあたるのがinitialDelaySecondsとなります。

  • Liveness Probe
    • 失敗時にはPodを再起動する。メモリリークなどでPodの再起動なしには状態が回復しずらい場合に使用。
    • 成功と判断するまでのチェック回数であるSuccessThresholdは必ず1
  • Readiness Probe

コンテナライフサイクル

restartPolicyはPodに定義するものでPodが停止した場合にどのような挙動を取るか指定したものです。基本的には、終了コードにかかわらず起動するAlwaysなのではと思うのですが、Jobなどは終了コード0以外で再起動するOnFailureになるのではと思います。Neverは再起動を行わないものです。

Init Containers

初期化処理を行うためのコンテナ。InitContainersの処理が終わらないと、spec.containersで指定したコンテナが起動しない。GCSからファイルをGETしてくる必要性があるときなどに使用している。この機能がAmazon ECSにも欲しい…。

postStart/preStop

コンテナの起動・終了時に行われる処理。postStartは、コンテナのENTRYPOINTとほぼ同じタイミングで実行されるため、どうしてもコンテナ内部で処理をしなくてはいけない場合以外はinitContainersに処理をしてもらったほうが良さそう。
preStopの処理の流れは、この本を読む前にこの記事(Kubernetes: 詳解 Pods の終了 - Qiita)で知っていましたが、改めて別の図も交えてだとよりわかりやすいなと思う。spec.terminationGracePeriodSecondsをpreStop処理にかかる時間を考慮した時間にしないと、デフォルトの30秒がたった時点でSIGKILLコマンド、preStop処理の途中だった場合はSIGTERMコマンドが送られ、その2秒後にSIGKILLが送られる。nginxやapacheなどのWEBサーバでGraceful Shatdownを行う場合に、コマンドだと非同期での実行となる場合があるのでsleepコマンドを入れておくことが望ましい。

その他

  • 親リソース(ReplicaSet)が削除された場合には子リソースのPodを削除するという処理がはいる。この親子関係は、マニフェストmetadata.ownerReferenceから読み取ることが可能。ex) Deployment削除 -> ReplicaSet削除 -> pod削除
  • podを即座に削除したい場合
kubectl delete pod hogehoge --grace-period 0 --force

高度なスケジューリング

Node Affinity

Podが特定のノード上でしか実行できないような条件付ができる。

  • requiredDuringSchedulingIgnoredDuringExecution
    • 必須条件のポリシー
    • この条件を満たさないとスケジューリングされない
    • 検証環境でプリエンプティブインスタンスに起動されると困るものは、priemptive:falseのものを指定したりとか
  • preferredDuringSchedulingIgnoredDuringExecution
    • 優先条件のポリシー
    • この条件を満たさない場合でもスケジューリングされる
    • 優先度の重み(weight)を設定することができる

Node AntiAffinity

これは、matchExpressionで指定できるオペレータでNotがついているものを使えば実現できるもの。

Inter-Pod Affinity

基本的にはNode Affinityと同じだが、topologyKeyというのを指定する必要がある。topologyKeyはスケジューリング対象の範囲を示す。これはホストやゾーンなどを指定でき、matchExpressionのじょうけんを満たすPodのホストもしくはゾーンに配置するという意味になる。

TaintsとTolerations

Podが条件を提示してNode側が許可する形のスケジューリング。node affinityと違う点は他にもあり、考慮されるのがスケジューリングのときだけではないという点がある。

まとめ

本当にめっちゃめちゃ良い本だったので買いましょう。