<-- mermaid -->

ネットワークの「ネ」も知らないで開発環境構築しようとしたらどハマりした

こんにちは。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 StackKibana - 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のschemeinternetFacingに設定する必要あり
    • 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に登録されているアドレスを確認
  • 設計図作成

    開発環境の設計図 - 詳細は消しております

  • 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のschemeinternalからinternetFacingに変更
  • ↑をデプロイしようとしたら見事エラーが発生

    • エラー内容: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.ymlsearver.basePath: '/kibana'
      • 本番kibana.ymlelasticsearch.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)
  • UserDataをデバッグするためにSession Manager(以下 SSM)が必要
    • SSMを使用するには以下の条件が必要
    • コンソール画面からは、AWS LinuxもAWS Linux 2もAWS Linuxとだけ表示
    • 既にAWS Linux 2を使用中だったため、以下の権限を追加
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のschemeinternalだが、Kibana用ELBのschemeinternetFacingであるため、Elasticsearch用とKibana用とでSecurity groupは分けた方がセキュリティ上好ましい状態
    • 権限追加したが、相変わらずSSMが使えない状態
    • Kibanaにインターネットからアクセスすると、502 Bad Gatewayが表示される状態
      • 当エラーはTarget GroupのHealth Check結果がUnhealthyであるのが原因であり、SSMで接続し、中身を確認すれば解消の可能性あり
  • 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は使用可能な状態
  • sshUserDataに実装したコマンドのデバッグ開始
    • AWSのドキュメントには、ルートユーザなのでsudoは不要と記載されていたが、実は必要
    • Kibanaを最も新しい7.14.xにしようとしたが、そもそもインストール段階でエラー発生
    • 7.10.2までは正常にインストールされたこと確認済み
    • sudo -i service kibana startしてスタートはするものの、プロセスリストに出力されてない状態
    • エラーログを確認
$ 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

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.ymlserver.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"}

この日は、上司にたくさん助けられました。デバッグ・調査していると、いつの間にかスレに新しいアドバイスのコメントがついてました。 それで、KibanaがElasticsearchにアクセスする必要があり、そのためには、ElasticsearchのELBのSecurity GroupのInboundルールでKibanaからのアクセスを受け付ける様に実装する必要があることを知りました。

在宅ネイティブですが、出社せず、直接会えずとも初めての世界にこうやってチャレンジできるのは、オンラインでもコミュニケーションが十分活性化されているからだと思います。また、Uzabaseのバリューのうち、「渦中の友を助ける」のおかげでもあります。


9/14

  • 404エラー解消のためデバッグ
    • 404エラーになっているパスは /kibana/app/kibana
    • kibana.ymlserver.basePath: '/kibana'にしたのが原因
      • ググってみると、server.basePathはプロキシ設定する際のみ指定するパス
    • server.basePath:をコメントアウトして解決

image.png (218.3 kB)


ウェブブラウザーからKibanaに接続し、何日も「502 Bad Gateway」を見ていたのですが、初めてKibanaのロゴを見たときはとても嬉しく、パソコンの前で「Yes!!」と大声で叫びました。この時点の私は、2週前の自分に比べて知識の量も、それらへの理解度もレベルアップしており、これがまさに「成長の喜び」だろうな、と実感しました。


終わりに

このタスクは、今年私に最も勉強になった大きな挑戦タスクでした。情報系の基本知識がずらりと解説されている本も読んでみたのですが、やはり直接実の業務で使ってみるのが一番覚えやすいと思いました。例えば、クラウド環境でのネットワーク構成、各リソースの役割と効果に関しての理解、それらをCDKで実装する方法など。

ちなみに、このタスクが終わる頃、先輩から「触って学ぶクラウドインフラ Amazon Web Services 基礎からのネットワーク&サーバー構築」をお勧めされました。読んで開発環境で試してみたのですが、既に試行錯誤した部分に対する説明を添えて実践できるので、本の説明から吸収できる情報のクオリティが全く違うことに気づきました。

これらの知識は今でもエラー調査、他のタスク遂行時にも役立っており、もう1段階私は成長したなと感じております。 未知の領域に挑戦できる環境である・そのチャンスが掴めるというのはとても大切であることを改めて実感しました。

これからは、NewsPicksの検索精度向上にどんどん挑戦していきますので、お楽しみにしてください!

Page top