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章プロセススケジューラ

シュガーラッシュ2感想

シュガーラッシュ2(吹替版)をMX4Dで観に行きました。面白かったので感想書いてみようかなと思っています。Twitterに書いて終わりにしようとも思ったのですが、ちょっと文字数が足りなかったようです。
ネタバレ少なめで書いていきますが、多少はありますのでお気をつけください。
出ている企業とかじっくり見たかったので、配信されるようになったらレンタルして一時停止しながら見ようと思います。

インターネット周りの描写

今回の舞台はインターネットなので、インターネットに関する表現が沢山出てきます。

f:id:jrywm121:20181225023559p:plain
インターネット

Wifiからインターネットに出ていくとき

インターネットに出ていくときとか、ちゃんとカプセル化されるんですよね。「これ絶対ヘッダーついてますよね!送信元(先?)IPアドレス言ってたし、これ絶対ヘッダ付いてるってことですよね!?」と思いながら觀ていました。ラルフがカプセルから顔だけはみ出ちゃっているのは、1フレームの最大サイズにおさまらなかったからなのかなと思ったりしました。

f:id:jrywm121:20181225023403p:plain
パケット?状態のラルフ

途中で加速してスピード早くなったのは、電話回線から光回線に変わったからなんですかね?(詳しくないので知ってる方いたら教えてください)

インターネットの世界

インターネットの世界では、Googleエンパイアステートビル的な一番大きなビルに描かれてます。やっぱりインターネットの中で一番存在感があるというのは世界共通認識のようです。ラルフがゴーグルのお店と勘違いしてたのに笑いました。

f:id:jrywm121:20181225023651p:plain
Google

他にもTwittterやInstagramAmazonなど日本でも馴染みのある企業が沢山並んでましたね。FBとInstagramは資本関係にあるだけに、近いところに立地があるのかとかを見ておけばよかったです。IPアドレスで立地決めているという線もあるかもしれないです。

インターネットならではの表現とかもユニークでした。
人間はインターネットの中ではアバターとして表現されるのですが、そのアバターがインターネットを移動する際に乗っているのがブラウザらしいです。終盤もろもろ有って、その乗り物が破壊されたりしたときに、物理世界ではブラウザがクラッシュしているような描写になっていましたね。みんな同じように見えましたが、もしかしてFirefoxChromeなどで違いが表現されていたのかもと思うと夢があります。

f:id:jrywm121:20181225023811p:plain
ブラウザ

検索バーの自動補完、WEBサイト広告のアニメ映像への落とし込み方とかも面白く感じる要素が多かったです。実際のeBayは特に広告などにあうこと無く、各商品のページにいけました。

作品で大きなカギを握るWEBサイトが、ゲーム以外だとeBayとohmy.disney.comとbuzzzTubeになるのですが、buzzzTubeだけは架空のサイトみたいですね。仕組み的にはほぼYouTubeなのではという感じでしたが、YouTube自体のインターネットの街に存在していたので、名前だけ変更して登場させたというわけでもなさそうです。
結構たくさんのIT企業出てたけど、netflixが見つからなくて、これはディズニーがnetflixからディズニー作品を引き上げて自分でストリーミングサービスやるって言ってたことと関係あるのか…?と勘ぐってしまう部分も。

キャラ周り

オールスター感

MARVELとスターウォーズ持ってるだけあって出せるキャラの幅が広いのもオールスター感を感じてよかったです。
ズートピアの監督さんということでズートピアのキャラは多めになるのかなと思いきやニックだけの出演だったと思います。なんにせよ、一時停止しながら見ないと分からない所ではあります。C3POがなにげにちょっと美味しい役回りだったのがちょっと嬉しかったです。

f:id:jrywm121:20181225024029p:plain

プリンセス

プリンセスの条件のくだりとかも面白くて、ディズニーにおけるプリンセス像の変化なんかも、他に沢山作品見てれば感じられたかもと思ってちょっと悔しい気持ち。 ディズニープリンセス系の論文って調べれば結構あるみたいで、一番新しそうなものを一個つまんで読んでみたけどかなり面白いです。こういうこと研究してらっしゃる方もいるんですね。 https://human.kyotogakuen.ac.jp/pdf/association/2017/i2017_01.pdf

ディズニースタジオ作品たくさん見てらっしゃる方は、最後あたりのシーンとかかなり胸熱ではないでしょうか。

おまけ

MX4D

4Dで觀たのは初めてでしたが、思ったよりかなり揺れます。なみなみと盛ってあるポップコーンはまず食べられないと思います。ただ、揺れなども最初のほうはかなり気になりますが、物語を觀ているうちに徐々に気にならなくなります。ただ、映画が始まる前までの予告編でも揺れるのが想定外で、予告編ってだいたい動きのあるシーンの塊だったりするのでガンガン連続で揺れます。

普通に映画を見る料金(1800円)+ 4Dの料金(1200円) って感じでした。一種のアトラクション的な要素があると思えば妥当な価格設定かなと思います。

勢いで帰ってきてモアナを觀た

ヴァネロペのプリンセス像というのが自分の想像するディズニープリンセスとはかなり変わっていて、どういう変遷を通ってディズニーはこのようなディズニープリンセスにしたのかなと気になり、モアナを帰ってきてから觀ました。 作品としてとてもおもしろかったです。2時間があっという間に感じられましたし、音楽もとても良かったです。映画觀ながら3回は泣きました。

f:id:jrywm121:20181225024205p:plain

まとめ

また一時停止しながらじっくり見たい作品です。 ほぼ映画を見ない生活を送っているので、これを機にちょっとずつディズニー周りから履修してみようかなと思いました。

映画「シュガー・ラッシュ:オンライン」の感想 #シュガラお題



sponsored by 映画「シュガー・ラッシュ:オンライン」(12月21日公開)

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