こんにちは。NewsPicksのアルゴリズム開発チームの崔(チェ)です。2020年4月新卒入社し、現在は検索エンジン周りの開発に携わっております。今回は、開発環境に必要なインフラを構築しようとしてどはまりしたお話をお伝えしようと思います。もし同じポイントでハマっていたり、NewsPicksでは何に挑戦できるか気になっている方に少しでもお役に立てれば嬉しいです。
前置き
私は文系大学 → 情報系大学院(自然言語処理) → エンジニアのキャリア持ちなのですが、NewsPicksに入社してからは様々なタスクに挑戦してきております。その中でも、今年下半期からは全くの未経験の検索エンジンのメンテナンス及び精度向上タスクに挑戦させていただいております。
NewsPicksでは検索のためElasticsearchを使用中なのですが、開発環境に精度検証に必要なKibanaという便利なツールがなかったため、サーバー構築・インターネット接続設定などから進める必要がありました。ネットワークの「ネ」の字も知らない状態でスタートしたため、約2週間ほぼ全てのハマりポイントに着実にハマりました。当時の記録をSlackに毎日スレにして残しておきました。今回はそれらをまとめた「どハマり日記」をお伝えします。よろしくお願いします。
そもそもElasticsearchとKibanaとは
Elasitcsearchとは、Apache Luceneをベースにしたオープンソースの検索エンジンです。Kibanaとは、Elasticsearchに格納されているデータをブラウザを用いて可視化、分析するためのツールです。 もしより詳しい情報が欲しい方は、Elasticsearch - The heart of the free and open Elastic StackやKibana - Your window to the Elastic Stackの説明を読んでみることをお勧めします。本としては、「Elastic Stack 実践ガイド [Elasticsearch/Kibana編]」というのもあります。
どハマり日記スタート
8/31
- 現在の開発環境に何があり何がないか調査
- 既にKibanaが動いている本番を参考に図を作成
ワクワク半分、心配半分でタスク着手しました。どれぐらいかかるか、タスクの見積もりもできない状況でした。
とにかく今をしろう、という気持ち。
9/1
- 既にAWS Cloud Development Kit(以下 AWS CDK)で実装されていた検索サーバーのスタックをデプロイ
- Kibana用のEC2インスタンスとElastic Load Balancer(以下 ELB)を実装
Amazon EC2インスタンス:仮想サーバー
Elastic Load Balancing:外部からのトラフィックを、うまく自動的に分散してくれるサービス
スタックをデプロイしてみたのは、今できているものを私は使えるかという確認のためです。
EC2インスタンスとELBをまず実装しようと思ったのは、前日まとめた図から見えた一番わかりやすい差であったからで、特にこれらのコンポーネントが何を成すのかまではちゃんと理解できてませんでした。
9/2
- ELBを実装しデプロイを試みるも、エラー発生
- エラー内容:
The maximum number of load balancers has been reached (Service: AmazonElasticLoadBalancing; Status Code: 400; Error Code: TooManyLoadBalancers)
- エラー内容:
- コンソールから確認
- リジョンごとにLoad Balancer(以下 LB)数が制限されており、開発環境に作られているLBがその最大数に達していること確認
- AWS service quotasにて最大数増大申請
Load Balancing:外部からのトラフィックを複数のサーバーに分散してくれる仕組み
入社直後のタスクで、「バケット数が最大数に達したため作れません」というエラーを経験したことがあったので、この日のエラーはすぐ解決できました。
9/3
シニアエンジニアに相談し、不足しているリソースを確認
- Kibanaは外部のインターネットからアクセスするので、LBの
scheme
をinternetFacing
に設定する必要あり - Kibana用EC2インスタンスのSubnet実装が必要
- Kibana用のEC2インスタンスのSecurity GroupのInboundルールに、Kibana用LBからのアクセス許容が必要
- Kibana用LBをおく新たなSubnetが必要
- 開発環境のCIDRブロックのルールを確認
- Internet Gatewayを向いているRoute tableを割り当て
- Kibana用LBのInboundルールで社内用VPNからのアクセス許容が必要
- コンソールのVPCセクションから
managed prefix lists
に登録されているアドレスを確認
- コンソールのVPCセクションから
- Kibanaは外部のインターネットからアクセスするので、LBの
設計図作成
Amazon Route 53に追加したアドレスからアクセスを許容
- Kibana用LBのSecurity groupを作成し、社内用VPNからのアクセスを許容
- Kibana用EC2インスタンスのSecurity groupを作成し、LBからのアクセスを許容
- Kibana用Subnetは、Elasticsearch用のものと共有
- Kibana用のLBのためのパブリックSubnetを作成
Subnets:特定Virtual Private Cloud(以下 VPC)のIPアドレス範囲で、さらに細分化したCIDRブロック
Security Groups:防火壁
Inbound(又は Ingress)ルール:アクセスを許容する対象を決めるルール
Classless Inter-Domain Routing(CIDR)表記:ネットワーク部のビット長を「/ビット長」で示す2進数の表記法
CIDRブロック:CIDRによりアドレスをRoute Tableにグループ化して格納したアドレスのグループで、IPアドレスを二進法で表したときの先頭の数ビットが同一
Amazon Route 53:DNSサーバーを構成するためのサービスで、取得したドメイン名を設定することで独自のドメイン名が利用可能
パブリックSubnet:外部のインターネットからアクセスできるSubnet
Network Address Translation(以下 NAT):IPアドレスを変換する装置
NAT Gateway:プライベートSubnetに存在するホストがインターネットと通信する際、NATが持つパブリックIPアドレスに置換し送・受信を可能にする仕組み
Internet Gateway:VPC とインターネットとの間の通信を可能にする VPC コンポーネント
開発環境の構築・整備などを担っているチームの方に相談し、自分が知っていることとやろうとしていることの方向性を確かめた日でした。正直相談当時は、知らない用語や概念ばかりでしたが、MTGの時間は限られておりましたので、とにかくSlackのスレにメモりました。聞き取れたものをメモっとけば、探すのはいくらでもできるはずですので。
それと、大事なのは、正しいメモを残すために、きちんと最後に自分が理解した(表面だけかもしれませんが)内容で問題ないか確認をとってMTGを終えることです。
9/6
- Kibana用ELBの
scheme
をinternal
からinternetFacing
に変更aws-elasticloadbalancingv2
のApplicationLoadBalancer
クラスのprops
はscheme
キーでの指定ではなく、internetFacing
キーにboolean値を渡しての実装が必要
↑をデプロイしようとしたら見事エラーが発生
- エラー内容:
HTTPS Listener needs at least one certificate
- Route 53に登録されているドメイン名についているSecure Sockets Layer(以下 SSL)証明書のARNをつけて解決
- エラー内容:
シニアエンジニアに再度相談
- 踏み台サーバーでエラー発生
- エラー内容:
Not enough IP space available in subnet-***. ELB requires at least 8 free IP addresses in each subnet. (Service: AmazonElasticLoadBalancing; Status Code: 400; Error Code: InvalidSubnet; ...)
- エラー内容:
- ↑既に作られている踏み台サーバーのNameTagがルールに従ってなかったため、修正したのが原因
- NameTagが異なっているせいで、新たにリソースを作成しようとし、IPスペースが不足な状態
- AWS CloudFormationにデプロイ・作成されている検索回りのCDKスタックをデストロイし新たにデプロイすることで解決
- 踏み台サーバーでエラー発生
SSL:インターネット上でブラウザとサーバがデータのやり取りをする際、その通信を暗号化し、送・受信させるプロトコル
SSL証明書(又はサーバー証明書):「通信の暗号化」や「Webサイトの運営者・運営組織の実在証明」を証明するもの
SSL証明書というものを知っていてデバッグできたわけではありません。なぜなのか知らないときはとにかく本番でちゃんと動いているものと確認を取りました。
このSSL証明書というものを探そうと、コンソールからRoute 53やEC2のページを彷徨いました。そしたら、今まで「いったい皆さんはどこからOOの機能が立ち上がっているアドレスを知るのだろう」と疑問に思っていたことが解けました!
9/7
(午後のみ稼働)
- Kibanaのプロセスをサーバー内に立てる実装スタート
UserData
を利用kibana.yml
をコマンドで編集する方法を調査sed
コマンドについて勉強
この日からKibanaの内部プロセスの実装を始めました。この時までは、sed
コマンドにあれほど悩まされるとは知りもしませんでした。"
'
は要注意です!
9/8
UserData
の実装で502エラーが発生- 本番で使用中のKibanaについて当時作成された社内ドキュメントを参考に原因調査
- 本番
kibana.yml
のsearver.basePath: '/kibana'
- 本番
kibana.yml
のelasticsearch.url: 'http://${dnsPrefix}Elasticsearch.${domainName}:9200'
- 本番
sed
コマンドが間違っていることにより設定が正しくないのが原因kibana.yml
原本を改めて確認したらelasticsearch.url
というキーはもうなくなっており、elasticsearch.hosts: [${}]
になっていたので修正UserData
を修正しデプロイし直したとしても、既に立っているEC2インスタンスが立ち直るわけではないので、terminate
が必要- 本番ではnginxを使いプロキシサーバーにしており、
HTTP:80
をlistenするので良かったが、開発環境ではnginxが不要であるため、Kibana用のEC2インスタンス・ELBの、Security GroupのInboundルールを合わせる必要あり- ELBのSecurity GroupのInboundルール
HTTPS:443
(社内VPN)HTTP:80
(社内VPN)
- EC2インスタンスのSecurity GroupのInboundルール
HTTP:5601
(Kibana)
- ELBのSecurity GroupのInboundルール
- 本番で使用中のKibanaについて当時作成された社内ドキュメントを参考に原因調査
UserData
をデバッグするためにSession Manager(以下 SSM)が必要- SSMを使用するには以下の条件が必要
- OSにSSMをサポートするAgentがインストールされてるか
- 権限を持っているか
- コンソール画面からは、AWS LinuxもAWS Linux 2もAWS Linuxとだけ表示
- 既にAWS Linux 2を使用中だったため、以下の権限を追加
- SSMを使用するには以下の条件が必要
new iam.PolicyStatement({ actions: [ "ssm:StartSession", "ssm:SendCommand", "ssm:GetConnectionStatus", "ssm:DescribeInstanceInformation", "ssm:TerminateSession", "ssm:ResumeSession", ], resources: ["*"], }),
プロキシサーバー:クライアントとネットワークサーバーの間で、データを処理し、間接的アクセスを可能にするサーバー
この日は(と言っても他の日もですが)他のタスクと並行でやっておりました。でも、両方とも頭で覚えるだけでは消化できない情報量でしたので、まとめ方を悩みました。選んだ方法は、スレの中に「子スレ」としてコメントをつけといて、それを編集・更新し続けることです。一つのスレの中に、複数のスレができてかつまとめられているのでおすすめです!
9/9
- サーバーがKibanaのポート番号の5601をlistenしない状態
サーバーが5601をlistenするには、中にKibanaのプロセスがちゃんと立っている必要あり
シニアエンジニアに再度相談
- Elasticsearch用ELBのSecurity GroupにKibana用EC2インスタンスから9200ポート(Elasticsearch用ポート番号)へのアクセスを許容する必要あり
- Elasticsearch用EC2インスタンスには、Elasticsearch用ELBでのみアクセスできるようにしたいが、直接のアクセスが可能な状態
- Elasticsearch用ELBの
scheme
はinternal
だが、Kibana用ELBのscheme
はinternetFacing
であるため、Elasticsearch用とKibana用とでSecurity groupは分けた方がセキュリティ上好ましい状態 - 権限追加したが、相変わらずSSMが使えない状態
- AWS側が提供するAmazonSSMManagedInstanceCoreポリシーをアタッチした方が良し
- Kibanaにインターネットからアクセスすると、502 Bad Gatewayが表示される状態
- 当エラーはTarget GroupのHealth Check結果が
Unhealthy
であるのが原因であり、SSMで接続し、中身を確認すれば解消の可能性あり
- 当エラーはTarget GroupのHealth Check結果が
- Elasticsearch用ELBのSecurity GroupにKibana用EC2インスタンスから9200ポート(Elasticsearch用ポート番号)へのアクセスを許容する必要あり
lsof
コマンドでやはり5601をlistenしてないことが発覚UserData
に実装した何らかのコマンドがエラーになり、Kibanaのプロセスが正しく動作できてないことが原因- Kibana用ELBのTargetGroupのEC2インスタンスも
Unhealthy
な状態
Peering:インターネットサービスの提供者同士でネットワークを繋ぎ、トラフィックを交換するためのもの
Transmission Controll Protocol(以下 TCP):正しくデータが送られたことを保証するプロトコル
Security GroupのInboundルールには、コンソールから確認できるアクセス情報として、「Port range」 「Source」があります。「Source」から「Port range」へのアクセスを許すのがInboundルールの役割だということがちゃんと理解できた日です。つまり、Security Groupがちゃんと理解できた初めての日だと言ってもいいです。
ちなみに、CDKでは「Source」をpeer=ec2.Peer.ipv4(...),
、「Port range」をconnection=ec2.Port.tcp(...)
と実装します。
9/10
- AmazonSSMManagedInstanceCoreポリシーをアタッチすることでSSMは使用可能な状態
ssh
しUserData
に実装したコマンドのデバッグ開始- AWSのドキュメントには、ルートユーザなので
sudo
は不要と記載されていたが、実は必要 - Kibanaを最も新しい7.14.xにしようとしたが、そもそもインストール段階でエラー発生
- 7.10.2までは正常にインストールされたこと確認済み
sudo -i service kibana start
してスタートはするものの、プロセスリストに出力されてない状態- エラーログを確認
- AWSのドキュメントには、ルートユーザなので
$ sudo bash -c "less /var/log/kibana/kibana.stderr" FATAL Error: listen EADDRNOTAVAIL: address not available **.**.***.***:5601 FATAL Error: listen EADDRNOTAVAIL: address not available **.**.***.***:5601 FATAL Error: listen EADDRNOTAVAIL: address not available **.**.***.***:5601
- ElasticsearchとKibanaを同じバージョンにする必要があることに気づき、6.xをインストール
- やはり
sed
コマンドがエラーの原因
- やはり
やっとSSMを使ってサーバー接続ができました。一体その内部で何が起きているのか気になっており、SSMで接続さえできればデバッグはすぐできる!くらいの勢いでタスクを進めておりました。 これもネットワーク周りの知識が熟知されていれば、SSM通さずとも見当がついたかもしれませんが、私はどうもわからなかったので、やっと光が見えた感じでした。
9/13
sed
コマンドを修正し、Kibanaの起動に成功- 「触って学ぶクラウドインフラ Amazon Web Services 基礎からのネットワーク&サーバー構築」を参考にデバッグ
sh-4.2$ sudo lsof -i -n -P ... node 2962 kibana 18u IPv4 128124 0t0 TCP ***.*.*.*:5601 (LISTEN) node 2962 kibana 19u IPv4 128322 0t0 TCP **.**.**.**:****->**.**.**.**:9200 (ESTABLISHED) ... sh-4.2$ ps aufx | grep kibana ssm-user 3020 0.0 0.0 110588 1544 pts/1 S+ 01:09 0:00 \_ grep kibana kibana 2962 77.5 4.8 1459728 385300 pts/1 Sl 01:08 0:24 /usr/share/kibana/bin/../node/bin/node --no-warnings --max-http-header-size=65536 /usr/share/kibana/bin/../src/cli -c /etc/kibana/kibana.yml
sh-4.2$ sudo netstat -antp | fgrep LISTEN tcp 0 0 :::22 :::* LISTEN 2072/sshd tcp 0 0 :::49765 :::* LISTEN 1805/rpc.statd tcp 0 0 :::111 :::* LISTEN 1784/rpcbind ...
- これで502エラーは解消されたものの、TargetGroupsは相変わらず
Unhealthy
な状態 - 標準出力のログを確認
- Elasticsearchとの通信がうまく行ってないのが原因の可能性あり
- 標準エラーは302、browserからは404エラーが発生
{"type":"log","@timestamp":"2021-09-13T01:09:15Z","tags":["license","info","xpack"],"pid":2962,"message":"Imported license information from Elasticsearch for the [monitoring] cluster: mode: basic | status: active"} {"type":"log","@timestamp":"2021-09-13T01:09:16Z","tags":["reporting","warning"],"pid":2962,"message":"Generating a random key for xpack.reporting.encryptionKey. To prevent pending reports from failing on restart, please set xpack.reporting.encryptionKey in kibana.yml"} {"type":"log","@timestamp":"2021-09-13T01:09:16Z","tags":["status","plugin:reporting@6.8.6","info"],"pid":2962,"state":"green","message":"Status changed from uninitialized to green - Ready","prevState":"uninitialized","prevMsg":"uninitialized"} {"type":"log","@timestamp":"2021-09-13T01:09:16Z","tags":["info","task_manager"],"pid":2962,"message":"Installing .kibana_task_manager index template version: 6080699."} {"type":"log","@timestamp":"2021-09-13T01:09:16Z","tags":["info","task_manager"],"pid":2962,"message":"Installed .kibana_task_manager index template: version 6080699 (API version 1)"} {"type":"log","@timestamp":"2021-09-13T01:09:17Z","tags":["info","migrations"],"pid":2962,"message":"Creating index .kibana_1."} {"type":"log","@timestamp":"2021-09-13T01:09:17Z","tags":["info","migrations"],"pid":2962,"message":"Pointing alias .kibana to .kibana_1."} {"type":"log","@timestamp":"2021-09-13T01:09:17Z","tags":["info","migrations"],"pid":2962,"message":"Finished in 275ms."}{"type":"log","@timestamp":"2021-09-13T01:09:17Z","tags":["listening","info"],"pid":2962,"message":"Server running at http://localhost:5601"} {"type":"log","@timestamp":"2021-09-13T01:09:18Z","tags":["status","plugin:spaces@6.8.6","info"],"pid":2962,"state":"green","message":"Status changed from yellow to green - Ready","prevState":"yellow","prevMsg":"Waiting for Elasticsearch"}
- Elasticsearch用EC2インスタンスにはELBからのアクセスのみ許したいので、ELBの
ホスト名:9200
を設定ファイルのelasticsearch.hosts:
の値にする必要あり- 間違っていた値:
elasticsearch.hosts: ["http://${dnsPrefix}Kibana.${props.config.zoneName}:9200"]
- 間違っていた値:
- 標準出力のログに変化がなかったので、上司に相談し、curlでリクエスト
sh-4.2$ curl http://{ELBのDNS名}:9200 { "name" : "ip-**-**-**-**", "cluster_name" : "**", "cluster_uuid" : "**", "version" : { "number" : "6.x", "build_flavor" : "default", "build_type" : "rpm", "build_hash" : "**", "build_date" : "2019-12-13T17:11:52.013738Z", "build_snapshot" : false, "lucene_version" : "7.7.2", "minimum_wire_compatibility_version" : "5.6.0", "minimum_index_compatibility_version" : "5.0.0" }, "tagline" : "You Know, for Search" }
- Kibanaと通信ができてないレスポンスについて上司に相談
これが問題です。 0.0.0.0:5601 じゃないとだめです。
sh-4.2$ curl **.**.**.**:5601 curl: (7) Failed to connect to **.**.**.** port 5601: Connection refused sh-4.2$ curl http://**.**.**.**:5601 curl: (7) Failed to connect to **.**.**.** port 5601: Connection refused
kibana.yml
のserver.host:
の値を"0.0.0.0"
にしたら解決
node 3646 kibana 18u IPv4 22966 0t0 TCP *:5601 (LISTEN) node 3646 kibana 19u IPv4 23000 0t0 TCP **.**.**.**:*****->**.**.**.**:9200 (ESTABLISHED) ...
sh-4.2$ curl http://**.**.**.**:5601 sh-4.2$ curl **.**.**.**:5601
- それでも302エラーが治らないので、原因を上司と調査
- Elasticsearchへの通信は全て200が帰ってくること確認
sh-4.2$ curl -I http://{ELBのDNS名}:9200 HTTP/1.1 200 OK Date: Mon, 13 Sep 2021 08:00:14 GMT Content-Type: application/json; charset=UTF-8 Content-Length: 503 Connection: keep-alive
sh-4.2$ curl -u {user}:{pw} http://{ELBのDNS名}:9200 { "name" : "ip-**-**-**-**",", "cluster_name" : "**", "cluster_uuid" : "**", "version" : { "number" : "6.x", "build_flavor" : "default", "build_type" : "rpm", "build_hash" : "**", "build_date" : "2019-12-13T17:11:52.013738Z", "build_snapshot" : false, "lucene_version" : "7.7.2", "minimum_wire_compatibility_version" : "5.6.0", "minimum_index_compatibility_version" : "5.0.0" }, "tagline" : "You Know, for Search" }
sh-4.2$ curl -u {user}:{pw} http://{ELBのDNS名}:9200/_cat/health 1631520212 08:03:32 *** green 1 1 2 2 0 0 0 0 - 100.0%
- この時点での標準出力はこんな感じ
{Kibana用IPアドレス}:5601, connection: close
{"type":"response","@timestamp":"2021-09-13T08:07:59Z","tags":[],"pid":4769,"method":"get","statusCode":302,"req":{"url":"/","method":"get","headers":{"host":"**.**.**.**:5601","connection":"close","user-agent":"ELB-HealthChecker/2.0","accept-encoding":"gzip, compressed"},"remoteAddress":"**.**.**.**","userAgent":"**.**.**.**"},"res":{"statusCode":302,"responseTime":3,"contentLength":9},"message":"GET / 302 3ms - 9.0B"}
- TargetGroupが相変わらず
Unhealthy
なのはパスが間違ったのが原因の可能性あり
この日は、上司にたくさん助けられました。デバッグ・調査していると、いつの間にかスレに新しいアドバイスのコメントがついてました。 それで、KibanaがElasticsearchにアクセスする必要があり、そのためには、ElasticsearchのELBのSecurity GroupのInboundルールでKibanaからのアクセスを受け付ける様に実装する必要があることを知りました。
在宅ネイティブですが、出社せず、直接会えずとも初めての世界にこうやってチャレンジできるのは、オンラインでもコミュニケーションが十分活性化されているからだと思います。また、Uzabaseのバリューのうち、「渦中の友を助ける」のおかげでもあります。
9/14
- 404エラー解消のためデバッグ
- 404エラーになっているパスは
/kibana/app/kibana
kibana.yml
のserver.basePath: '/kibana'
にしたのが原因- ググってみると、
server.basePath
はプロキシ設定する際のみ指定するパス
- ググってみると、
server.basePath:
をコメントアウトして解決
- 404エラーになっているパスは
ウェブブラウザーからKibanaに接続し、何日も「502 Bad Gateway」を見ていたのですが、初めてKibanaのロゴを見たときはとても嬉しく、パソコンの前で「Yes!!」と大声で叫びました。この時点の私は、2週前の自分に比べて知識の量も、それらへの理解度もレベルアップしており、これがまさに「成長の喜び」だろうな、と実感しました。
終わりに
このタスクは、今年私に最も勉強になった大きな挑戦タスクでした。情報系の基本知識がずらりと解説されている本も読んでみたのですが、やはり直接実の業務で使ってみるのが一番覚えやすいと思いました。例えば、クラウド環境でのネットワーク構成、各リソースの役割と効果に関しての理解、それらをCDKで実装する方法など。
ちなみに、このタスクが終わる頃、先輩から「触って学ぶクラウドインフラ Amazon Web Services 基礎からのネットワーク&サーバー構築」をお勧めされました。読んで開発環境で試してみたのですが、既に試行錯誤した部分に対する説明を添えて実践できるので、本の説明から吸収できる情報のクオリティが全く違うことに気づきました。
これらの知識は今でもエラー調査、他のタスク遂行時にも役立っており、もう1段階私は成長したなと感じております。 未知の領域に挑戦できる環境である・そのチャンスが掴めるというのはとても大切であることを改めて実感しました。
これからは、NewsPicksの検索精度向上にどんどん挑戦していきますので、お楽しみにしてください!