Advertisement
Advertisement

More Related Content

Slideshows for you(20)

Viewers also liked(20)

Advertisement

Similar to Kubernetesにまつわるエトセトラ(主に苦労話)(20)

Advertisement

Kubernetesにまつわるエトセトラ(主に苦労話)

  1. 2 0 1 6 年 7 月 2 4 日 井 上 誠 一 郎 Demystifying Kubernetes
  2. 自己紹介: 井上誠一郎 • 元アリエルネットワークCTO (先月まで) • ワークスアプリケーションズ エグゼクティブフェロー • 主な著書 • 「P2P教科書」 • 「パーフェクトJava」 • 「パーフェクトJavaScript」 • 「実践JS サーバサイドJavaScript入門」 • 「パーフェクトJava EE」(来月出版)
  3. 今日のセッションの目的 •複雑怪奇なKubernetesをひ もといていきます •Kubernetesのバージョンは v1.2.6ベースで説明します
  4. 複雑さをひもとくための工夫 •Kubernetes固有の概念/用語 がたくさんあるので、説明の ために簡易化していきます •概念を下から積み上げていき ます
  5. Kubernetes理解に必要な知識(私見) Dockerの理解 Dockerのネットワークの理解 flanneldの理解 コンテナとpodの関係の理解 podとサービスの関係の理解 Kubernetesのネットワークの理解 (DNSとルーティング) Kubernetesのツールの理解 この順序で説明
  6. コンテナに関しての簡易化 • 理屈で言えばコンテナはひとつのOS相当なので、 ひとつのコンテナ内で多数のプロセス、たとえ ばロードバランサ、アプリケーションサーバ、 データベースすべてを動かせます • しかし、Kubernetesの思想的には、ひとつのコ ンテナで動くプロセスの数を最小限にして、代 わりに、コンテナ群を管理します • 今回の説明上も、ひとつのコンテナ上で動くの はひとつのプロセス、というモデルを前提にし ます(Kubernetesの必須要件ではありませんが)
  7. Kubernetesの動作の分解の前に •そもそもKubernetesは何を してくれるのか? •Kubernetesを使うと何がう れしいのか?
  8. Kubernetesがやってくれること • 複数ホストにコンテナをデプロイ • コンテナ間のネットワーク管理(名前 解決含む) • コンテナの死活監視 • コンテナの負荷分散 • コンテナのリソースアロケーション
  9. • 複数ホストにコンテナをデプロイ • どのホストにどのコンテナ(=プロセス)を配備するかを隠蔽 • コンテナ間のネットワーク管理(名前解決含む) • サービスディスカバリ相当の機能 • コンテナの死活監視 • コンテナ(=プロセス)が死んだら、コンテナを自動で新規起動 • コンテナの負荷分散 • 同一機能の複数コンテナ(=プロセス)へのアクセスをバランシン グする機能(あまりリッチではない) • コンテナのリソースアロケーション • コンテナごとにCPUリソースやメモリリソースの割り当てを指定 できる機能(あまりリッチではない) Kubernetesがやってくれること
  10. Kubernetesがない世界 プロセスA プロセスB 依存 実行環境 開発者 プロセスB プロセスB プロセスB プロセスB deploy LB プロセスA LB プロセスA configuration (個々のエンドポイントを設定)
  11. Kubernetesがある世界 プロセスA プロセスB 依存 実行環境 開発者 プロセスB プロセスB プロセスB LB プロセスA Kubernetes プロセスB群のサービス名を定義 プロセスAにサービス名を与える
  12. 用語と概念の整理
  13. • Dockerプロセスが動いているOSをホスト(マシン)と呼 びます • ひとつのホストの上で、コンテナが複数稼働します • Kubernetesの世界から見ると、ホストマシンが物理マ シンか仮想マシンかはどうでもいい話です。気にしない でください • 同様に、ホストマシンがネットワークのどこにいるか (プライベートネットワーク or グローバルIPを持つ)も Kubernetesの世界からはスコープ外なので、気にしな くて結構です ホストマシンについて
  14. • Kubernetesで最初に混乱するのが、Dockerのネット ワークまわりです • flanneldの世界とKubernetesの話が混ざると混乱する ので、話を分離します • まずflanneldだけに話を限定します Dockerのネットワークまわりの話とflanneld
  15. • flanneldがないと、あるホストの上で動い ているコンテナは、他のホストの上で動 いているコンテナのIPアドレスにアクセ スできません • 正確に言えば、ある設定をすれば、相手 側ホストのIPアドレス経由で、リモート のコンテナにアクセスできます • ただ、基本的には少々面倒な世界です flanneldの役割(1)
  16. • flanneldは各ホスト上で動くデーモンプロセスです。 flanneldがいれば、ホスト群の上のコンテナ群はコンテ ナ自身のIPアドレスを使って相互にアクセスできます • コンテナたちは一意なIPアドレスを持つようになります • 一見すると、ホスト間のコーディネーションが必要そうですが、 仕組みはもっと単純で、flanneldプロセス群が同じデータストア (etcd)でルーティングテーブルを共有しているだけです • なお、この領域には、Docker Swarmなど類似機能の他 技術も存在します flanneldの役割(2)
  17. 1. (そもそもDockerのネットワーク補助のためなの で)Docker自身をインストール 2. 共有データストアとしてetcdが必要なので、どこかに etcdを起動(etcd自体は分散KVSですが、動作確認だ けであれば、単体でどこかで起動していれば充分) 3. etcdにflanneldが使うネットワークアドレスを登録 (どのネットワークアドレスを使うかは利用者の自由) 例 $ etcdctl set /coreos.com/network/config '{ "Network": "10.1.0.0/16" }‘ flanneldを動かす手順の概要(1)
  18. 4. 各ホストでflanneldデーモンプロセスを起動 例 同一ホストでetcdが動いている場合は単に $ sudo bin/flanneld 別ホスト(IPアドレスが10.140.0.14)でetcdが動いている 場合は $ sudo bin/flanneld -etcd-endpoints 'http://10.140.0.14:4001,http://10.140.0.14:2379' flanneldを動かす手順の概要(2)
  19. 5. 個々のflanneldは、どのサブネットを確保したかの情報を /run/flannel/subnet.env に書き出す 例 $ cat /run/flannel/subnet.env FLANNEL_NETWORK=10.1.0.0/16 FLANNEL_SUBNET=10.1.19.1/24 FLANNEL_MTU=1432 FLANNEL_IPMASQ=true 6. dockerプロセスに対して、上記のサブネットを使うように指示し て起動 $ source /run/flannel/subnet.env $ sudo docker -d --bip=${FLANNEL_SUBNET} --mtu=${FLANNEL_MTU} flanneldを動かす手順の概要(3)
  20. ifconfigでdocker0やflanneldのネットワークアドレスを確認(出力例 は抜粋) (下記例の場合、このホスト上のコンテナ群は 10.1.19.0/24 のネット ワークを構成) $ ifconfig docker0 Link encap:Ethernet HWaddr 02:42:a5:18:b3:73 inet addr:10.1.19.1 Bcast:0.0.0.0 Mask:255.255.255.0 flannel0 Link encap:UNSPEC HWaddr 00-00-00-00-00-00-00-00- 00-00-00-00-00-00-00-00 inet addr:10.1.19.0 P-t-P:10.1.19.0 Mask:255.255.0.0 flanneldの動作確認(1)
  21. ルーティングテーブルの確認: $ netstat -rn Kernel IP routing table Destination Gateway Genmask Flags MSS Window irtt Iface 0.0.0.0 10.140.0.1 0.0.0.0 UG 0 0 0 ens4 10.1.0.0 0.0.0.0 255.255.0.0 U 0 0 0 flannel0 10.1.19.0 0.0.0.0 255.255.255.0 U 0 0 0 docker0 10.140.0.1 0.0.0.0 255.255.255.255 UH 0 0 0 ens4 他のホスト上のコンテナのIPアドレス(たとえば10.1.79.2)に向けて接続できれば OK flanneldの動作確認(2)
  22. • なかなか面倒... • まず、dockerのデーモンプロセス起動より先に flanneldプロセスの起動が必要 • かつ、flanneldプロセス起動時に、etcdの固定アドレス を外から与える必要がある • 更に、flanneldの書き出した /run/flannel/subnet.env の結果を、dockerのデーモンプロセスの起動時に渡す 必要がある OS起動時のflanneldの自動起動(1)
  23. systemdであれば $ sudo vi /lib/systemd/system/docker.service で EnvironmentFile=-/run/flannel/subnet.env を追記(先頭のハイフンの意味は、ファイルが存在しなければ 無視する、というオプション) #下記に書き換え ExecStart=/usr/bin/docker daemon -H fd:// $DOCKER_OPTS ExecStart=/usr/bin/docker daemon -H fd:// $DOCKER_OPTS - -bip=${FLANNEL_SUBNET} --mtu=${FLANNEL_MTU} OS起動時のflanneldの自動起動(2)
  24. • Kubernetes理解のための、flanneldの最小理解 • flanneldを使うと、Kubernetes管理下にいる各コ ンテナは一意なIPアドレスを持ち、相互にリーチャ ブルになる • flanneldネットワークは、プライベートネットワー クなので、Kubernetesの外側からは叩けない • このネットワークは、ホストのIPアドレスのネット ワークとは別物 • (後述する)KubernetesのサービスのIPアドレスのネッ トワークとも別物(ここがわかりづらい) ここまでのまとめ
  25. flanneldをいったん頭から追い出して、 Kubernetesの概念の整理をします
  26. • ノードはKubernetes固有の用語です • 頭が混乱したら、「ホストマシン = ノー ド」と考えても、大きくは外しません • より正確には、 • ノードは、「マスタノード」と「ワー カーノード」の2種類に分けられます ホストマシンとノードについて(1)
  27. • ワーカーノードは、Dockerプロセスが稼働するホスト で、その上でKubernetes管理下のコンテナ群が稼働し ます。こちらは「ワーカーノード = ホスト」の理解で問 題ありません • マスタノードのほうは、いくつかのKubernetesのサー バプロセス群のことです • これらのプロセスはコンテナ上で動く必要はありません • 「マスタノード」という用語はややミスリーディングに思います (マスタプロセスのほうが適切) # 古い文書ではワーカーノードがMinionと呼ばれていま す ホストマシンとノードについて(2)
  28. • podはコンテナをグループ化したKubernetes固有の概念です • あるpodインスタンス内のコンテナは、同一のホスト上で稼働しま す。かつ、(Dockerネットワーク的な意味で)同じIPアドレスを共有 します • 密結合なプロセス群、言い換えると、死ぬ時は一緒に死んでほしい ようなプロセス群をpodにまとめます • しかし、今日の説明上は、ひとつのpod内にはひとつのコンテナ、 というモデルにします • 既に説明したように、ひとつのコンテナにひとつのプロセスのモデ ルにしているので、今日の説明では、ひとつのpodはひとつのプロ セスと対応します • (pauseという特別なプロセスのコンテナがありますが本質的ではないので 説明を割愛します) コンテナとpod
  29. • レプリケーションコントローラ(以下rcと略)は、podを複数インス タンス化するKubernetes固有の概念です • 実運用上、podを単一インスタンスで使ってもKubernetesの旨味 が少ないので、必要なレプリカ数を指定したrcを設定するのが普通 です • rcは指定したレプリカ数分のpodをインスタンス化します。今回の 文脈では、レプリカ数分のプロセス起動になります • プロセスがどのホストで起動するかは実行時に決まります (Kubernetesが空いているホストを見つけます) • rcは指定レプリカ数分のpodを維持します。つまり、仮にどこかの pod(=コンテナ=プロセス)が死ぬと、自動で新しいpodが起動しま す podとレプリケーションコントローラ(=rc)
  30. • Kubernetes v1.3以降、rcがレプリカセットと Deploymentという新しい概念で置換されていくようで す • 今日の説明は rc のまま進めます fyi, rcとレプリカセットとDeployment
  31. • rcの機能により、podは複数インスタンス状態になります • 今回の場合、同じプログラムから複数プロセスが起動すると考えてください • かつこれらのプロセスがどのホストで動くかは実行時に決まるので、個々の プロセスのIPアドレスは実行時に決まります • サービスは、これらの複数インスタンスに対して単一IPアドレスを 割り当てるKubernetesの機能です • 結果的に、サービスのIPアドレスに向けたアクセスが、背後の複数 podへ割り振られるのでロードバランサのように振る舞います • 内部的には、各ワーカーノード上で動くkube-proxyというプロセ スがiptablesにエントリを追加することで、サービスのIPアドレス を作っています • サービスのIPアドレスはifconfigなどでは見えないアドレスです(後述) podとサービス
  32. • Kubernetesの各サービスのIPアドレスの名前解決にはDNSを 使います • SkyDNSというDNS実装が(事実上)Kubernetesに組み込まれ ています • 実行時に新しいサービスが起動すると、サービス名とIPアド レスのエントリが自動でSkyDNSに登録されます(登録は kube2dnsというプロセスが担当) • 各pod上のプロセスは、サービス名さえわかればサービスに アクセスできます(いわゆるサービスディスカバリ相当の機能 になります) • サービス名の命名や、アプリにサービス名をどう与えるかは、 アプリケーション開発者の責務です 名前解決とSkyDNS
  33. 他に知っておくと良い細々したこと:
  34. •Kubernetesの管理用コマンド ラインツールです •利用例は後ほど具体例の中で 紹介します kubectl
  35. • 分散KVS • Kubernetesのマスタプロセスが使うデータストアです • SkyDNSおよびflanneldもそれぞれetcdをデータストア として使います。 • 今回の説明においては、etcdとは、どこかにあるただ のデータストアと思えば充分です • etcdはコンテナ上にある必要もないですし、マスタノードや ワーカーノード上で必ずしも動かす必要はありません • 今日の説明では、(便宜上)マスタノード上でコンテナを使って etcdを起動します etcd
  36. •etcdの管理用コマンドライン ツールです •利用例は後ほど具体例の中で 紹介します etcdctl
  37. • Kubernetesの各種プロセスをコマンドラインの第一引 数で呼び分けられるプログラムです • たとえば hyperkube kubelet と起動すると kubelet プ ロセスを起動できます • 利用は必須ではないですが、ラクなので今回は hyperkubeを使います hyperkube
  38. • マスタプロセス群は、apiserverやcontroller-managerや schedulerなどがあります • これらは今後Kubernetesのバージョンが上がると色々と変化 すると思うので、一群のプロセス群があるということだけ理 解すれば充分です • 唯一気にすべきプロセスはapiserverです • kubectlはapiserverのREST APIを叩くプログラムです。apiserverの アドレスを引数で与える必要があります(同一ホストで動いていれば 省略可能) • apiserverはデータストアとしてetcdを使うので、apiserver起動の 前にetcd起動が必要で、かつapiserverは起動時にetcdのアドレス を知っている必要があります • 他のKubernetesプロセスたちは、起動時に、apiserverプロセスの アドレスを知っている必要があります Kubernetesのマスタプロセス群
  39. • dockerデーモンプロセス: これがないとコンテナが動か せないので当然必要です • kubelet: ワーカーノードをワーカーノードたらしめるプ ロセスです。pod(=コンテナ)の起動などを担います • kube-proxy: サービスのIPアドレスを管理します(内部的 にiptablesを操作) • flanneld: 異なるホスト上のコンテナ同士をネットワー ク的につなぎます(説明済み) 各ワーカーノード上で動くプロセス群
  40. Kubernetesを動かしてみます
  41. • 単体ノード上で動かす手順 • http://kubernetes.io/docs/getting-started-guides/docker/ • http://kubernetes.io/docs/getting-started-guides/docker- multinode/deployDNS/ • 複数ノードでの手順 • http://kubernetes.io/docs/getting-started-guides/docker- multinode/master/ • http://kubernetes.io/docs/getting-started-guides/docker- multinode/worker/ 手順は下記を参考
  42. • ふたつのホストを使います • ホストのOSは Ubuntu 16.04 ですが、可能な限りディス トリビューション依存のない説明をします • 両方のホストがワーカーノードで、かつ片方のホスト上 で(コンテナを使って)マスタプロセス群を動かします • ワーカーノード上のプロセス群(kubeletやkube-proxy) も、コンテナを使って動かします • これらのプロセスは、コンテナで動かすことが必須では ありません。将来、普通にapt-getでインストールでき るようになれば、そのほうがシンプルです サンプルの構成
  43. 1. 全体の準備 2. マスタノードになるホスト上での作業 3. ワーカーノードになるホスト上での作業 4. 自作アプリのサービス化 手順のオーバービュー
  44. kubectlコマンドをインストールします 基本的には、kubectlコマンドはどのマシンにインストールしても構いま せん(マスタプロセスのIPアドレスにリーチャブルな場所であれば)。 $ export K8S_VERSION=1.2.6 $ curl http://storage.googleapis.com/kubernetes- release/release/v${K8S_VERSION}/bin/linux/amd64/kubectl > kubectl $ sudo mv kubectl /usr/local/bin/ $ sudo chmod +x /usr/local/bin/kubectl マスタプロセスと異なるホストにインストールした場合、kubectlのコマ ンドラインオプションの-sでマスタプロセスのホストのIPアドレスとポー ト(10.140.0.14:8080)を指定する必要があります(あるいは kubeconfig ファイルで設定) $ kubectl -s 10.140.0.14:8080 cluster-info 全体の準備
  45. • 前述したように、マスタノードとい う用語はややミスリーディングです • いくつかのマスタプロセスを起動す るノードを、単にマスタノードと呼 びます • 今回の場合、このホストはワーカー ノードにもなります マスタノードになるホスト
  46. Docker自身のインストール $ sudo apt-get update; sudo apt-get -y upgrade; sudo apt-get -y install docker.io 仮にホストでetcdが動いていると邪魔するので、プロセ スがいないことを確認 仮に動いていたら、止めておく $ sudo systemctl stop etcd $ sudo systemctl disable etcd マスタノードになるホスト上での作業(1)
  47. 環境変数設定(利便性のため) $ export MASTER_IP=10.140.0.14 # ホストのIPアド レス。ifconfigで確認 $ export K8S_VERSION=1.2.6 $ export ETCD_VERSION=2.2.5 $ export FLANNEL_VERSION=0.5.5 $ export FLANNEL_IFACE=ens4 # ifconfigで確認 $ export FLANNEL_IPMASQ=true マスタノードになるホスト上での作業(2)
  48. • flanneldを動かします。その前にflanneldが依存する etcdを動かします • これらはコンテナで動かす必要はないですが、ラクをす るために両方ともコンテナで動かします • ややトリッキーですが、flanneldとetcd専用のDocker デーモンプロセスを起動します • 手順の中の可変項目は、flanneldが使うネットワークア ドレスの部分("10.1.0.0/16")です。これは好きに決めら れます マスタノードになるホスト上での作業(3)
  49. 専用Dockerデーモンプロセスの起動 $ sudo sh -c 'docker daemon -H unix:///var/run/docker-bootstrap.sock -p /var/run/docker-bootstrap.pid --iptables=false -- ip-masq=false --bridge=none -- graph=/var/lib/docker-bootstrap 2> /var/log/docker- bootstrap.log 1> /dev/null &' マスタノードになるホスト上での作業(4)
  50. etcdプロセスの起動(コンテナ内) $ sudo docker -H unix:///var/run/docker-bootstrap.sock run -d --net=host ¥ gcr.io/google_containers/etcd-amd64:${ETCD_VERSION} ¥ /usr/local/bin/etcd ¥ --listen-client- urls=http://127.0.0.1:4001,http://${MASTER_IP}:4001 ¥ --advertise-client-urls=http://${MASTER_IP}:4001 ¥ --data-dir=/var/etcd/data マスタノードになるホスト上での作業(5)
  51. etcdへの初期データ投入 $ sudo docker -H unix:///var/run/docker- bootstrap.sock run ¥ --net=host ¥ gcr.io/google_containers/etcd- amd64:${ETCD_VERSION} ¥ etcdctl set /coreos.com/network/config '{ "Network": "10.1.0.0/16" }' マスタノードになるホスト上での作業(6)
  52. 通常Dockerデーモンを一時停止 $ sudo systemctl stop docker flanneldプロセスの起動(コンテナ内) $ sudo docker -H unix:///var/run/docker-bootstrap.sock run -d ¥ --net=host --privileged ¥ -v /dev/net:/dev/net ¥ quay.io/coreos/flannel:${FLANNEL_VERSION} ¥ /opt/bin/flanneld ¥ --ip-masq=${FLANNEL_IPMASQ} ¥ --iface=${FLANNEL_IFACE} マスタノードになるホスト上での作業(7)
  53. flanneldのサブネットのネットワークアドレスを(通常の)Dockerデーモンプロセ スに伝えます $ sudo docker -H unix:///var/run/docker-bootstrap.sock exec flanneldコン テナの出力ハッシュ値(=コンテナID) cat /run/flannel/subnet.env 入力例 $ sudo docker -H unix:///var/run/docker-bootstrap.sock exec 195ea9f70770ac20a3f04e02c240fb24a74e1d08ef749f162beab5ee8c905734 cat /run/flannel/subnet.env 出力例 FLANNEL_NETWORK=10.1.0.0/16 FLANNEL_SUBNET=10.1.19.1/24 FLANNEL_MTU=1432 FLANNEL_IPMASQ=true マスタノードになるホスト上での作業(8)
  54. $ sudo vi /lib/systemd/system/docker.service で ExecStart=/usr/bin/docker daemon -H fd:// $DOCKER_OPTS --bip=10.1.19.1/24 --mtu=1432 と書き換え マスタノードになるホスト上での作業(9)
  55. Dockerデーモンプロセスを再起動します $ sudo /sbin/ifconfig docker0 down $ sudo brctl delbr docker0 $ sudo systemctl daemon-reload $ sudo systemctl restart docker マスタノードになるホスト上での作業(10)
  56. Docker再起動前の確認(抜粋) $ ifconfig docker0 Link encap:Ethernet HWaddr 02:42:25:65:c5:f3 inet addr:10.1.20.1 Bcast:0.0.0.0 Mask:255.255.255.0 Docker再起動後の確認(抜粋) $ ifconfig docker0 Link encap:Ethernet HWaddr 02:42:a5:18:b3:73 inet addr:10.1.19.1 Bcast:0.0.0.0 Mask:255.255.255.0 flannel0 Link encap:UNSPEC HWaddr 00-00-00-00-00-00-00-00-00- 00-00-00-00-00-00-00 inet addr:10.1.19.0 P-t-P:10.1.19.0 Mask:255.255.0.0 マスタノードになるホスト上での作業(11)
  57. Docker再起動後のルーティングテーブルの確認 $ netstat -rn Kernel IP routing table Destination Gateway Genmask Flags MSS Window irtt Iface 0.0.0.0 10.140.0.1 0.0.0.0 UG 0 0 0 ens4 10.1.0.0 0.0.0.0 255.255.0.0 U 0 0 0 flannel0 10.1.19.0 0.0.0.0 255.255.255.0 U 0 0 0 docker0 10.140.0.1 0.0.0.0 255.255.255.255 UH 0 0 0 ens4 マスタノードになるホスト上での作業(12)
  58. マスタプロセス群およびワーカーノードに必要なプロセス群(kubeletと kube-proxyなど)をhyperkubeで起動します $ sudo docker run ¥ --volume=/:/rootfs:ro --volume=/sys:/sys:ro ¥ --volume=/var/lib/docker/:/var/lib/docker:rw ¥ --volume=/var/lib/kubelet/:/var/lib/kubelet:rw ¥ --volume=/var/run:/var/run:rw ¥ --net=host --privileged=true --pid=host -d ¥ gcr.io/google_containers/hyperkube-amd64:v${K8S_VERSION} ¥ /hyperkube kubelet --allow-privileged=true ¥ --api-servers=http://localhost:8080 ¥ --v=2 --address=0.0.0.0 --enable-server ¥ --hostname-override=127.0.0.1 ¥ --config=/etc/kubernetes/manifests-multi --containerized ¥ --cluster-dns=10.0.0.10 --cluster-domain=cluster.local マスタノードになるホスト上での作業(13)
  59. Kubernetesのマスタプロセスの動作確認 $ kubectl cluster-info Kubernetes master is running at http://localhost:8080 マスタノードになるホスト上での作業(14)
  60. SkyDNSをpodとして動かします。 $ curl http://kubernetes.io/docs/getting-started- guides/docker-multinode/skydns.yaml.in > skydns.yaml.in $ export DNS_REPLICAS=1 $ export DNS_DOMAIN=cluster.local # 自分で決めてよいドメイ ン名 $ export DNS_SERVER_IP=10.0.0.10 # 自分で決めてよいDNSサー バのIPアドレス(KubernetesのサービスとしてIPアドレス) $ sed -e "s/{{ pillar¥['dns_replicas'¥] }}/${DNS_REPLICAS}/g;s/{{ pill ar¥['dns_domain'¥] }}/${DNS_DOMAIN}/g;s/{{ pillar¥['dns_serve r'¥] }}/${DNS_SERVER_IP}/g" skydns.yaml.in > ./skydns.yaml マスタノードになるホスト上での作業(15)
  61. rcおよびサービスの作成(skydns.yamlの中身はrcとサービ ス) $ kubectl create -f ./skydns.yaml マスタノードになるホスト上での作業(15)
  62. $ kubectl cluster-info Kubernetes master is running at http://localhost:8080 KubeDNS is running at http://localhost:8080/api/v1/proxy/namespaces/kube- system/services/kube-dns 確認(このfailureの原因は不明) $ curl http://localhost:8080/api/v1/proxy/namespaces/kube-system/services/kube-dns { "kind": "Status", "apiVersion": "v1", "metadata": {}, "status": "Failure", "message": "no endpoints available for service ¥"kube-dns¥"", "reason": "ServiceUnavailable", "code": 503 } SkyDNSの動作確認(1)
  63. $ kubectl get --all-namespaces svc NAMESPACE NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE default kubernetes 10.0.0.1 <none> 443/TCP 2m kube-system kube-dns 10.0.0.10 <none> 53/UDP,53/TCP 1m $ kubectl get --all-namespaces ep NAMESPACE NAME ENDPOINTS AGE default kubernetes 10.140.0.14:6443 2m kube-system kube-dns 10.1.19.2:53,10.1.19.2:53 1m SkyDNSの動作確認(2)
  64. $ dig @10.0.0.10 cluster.local. (抜粋) ;; ANSWER SECTION: cluster.local. 30 IN A 10.1.19.2 cluster.local. 30 IN A 127.0.0.1 cluster.local. 30 IN A 10.0.0.10 cluster.local. 30 IN A 10.0.0.1 $ dig @10.1.19.2 cluster.local. も動くが、このIPアドレスは状況によって変わりうるので依存しては いけない SkyDNSの動作確認(3)
  65. • 最初のホスト上でflanneldを設定しました • このホスト上でマスタプロセス群を起動 しました(apiserverなど) • このホストをワーカーノードにしました (kubeletとkube-proxyの起動) • SkyDNSをKubernetesのサービスとして 起動しました(10.0.0.10のIPアドレスでア クセス可能) ここまでのまとめ
  66. Docker自身のインストール $ sudo apt-get update; sudo apt-get -y upgrade; sudo apt-get -y install docker.io 環境変数設定(利便性のため) $ export MASTER_IP=10.140.0.14 # マスタノードのホ ストのIPアドレス $ export K8S_VERSION=1.2.6 $ export FLANNEL_VERSION=0.5.5 $ export FLANNEL_IFACE=ens4 # ifconfigで確認 $ export FLANNEL_IPMASQ=true ワーカーノードになるホスト上での作業(1)
  67. • マスタノードと同じく、flanneldをコンテナで起動します • etcdはマスタノード上のetcdを参照します • flanneldの起動はマスタノードと同じ手順です 専用Dockerデーモンプロセスの起動 $ sudo sh -c 'docker daemon -H unix:///var/run/docker- bootstrap.sock -p /var/run/docker-bootstrap.pid -- iptables=false --ip-masq=false --bridge=none -- graph=/var/lib/docker-bootstrap 2> /var/log/docker- bootstrap.log 1> /dev/null &' 通常Dockerデーモンを一時停止 $ sudo systemctl stop docker ワーカーノードになるホスト上での作業(2)
  68. flanneldプロセスの起動(コンテナ内) $ sudo docker -H unix:///var/run/docker- bootstrap.sock run -d ¥ --net=host --privileged -v /dev/net:/dev/net ¥ quay.io/coreos/flannel:${FLANNEL_VERSION} ¥ /opt/bin/flanneld ¥ --ip-masq=${FLANNEL_IPMASQ} ¥ --etcd-endpoints=http://${MASTER_IP}:4001 ¥ --iface=${FLANNEL_IFACE} ワーカーノードになるホスト上での作業(3)
  69. flanneldのサブネットのネットワークアドレスを(通常の)Dockerデーモンプロセスに 伝えます $ sudo docker -H unix:///var/run/docker-bootstrap.sock exec flanneldコンテナ の出力ハッシュ値(=コンテナID) cat /run/flannel/subnet.env 出力例 FLANNEL_NETWORK=10.1.0.0/16 FLANNEL_SUBNET=10.1.79.1/24 FLANNEL_MTU=1432 FLANNEL_IPMASQ=true $ sudo vi /lib/systemd/system/docker.service で ExecStart=/usr/bin/docker daemon -H fd:// $DOCKER_OPTS --bip=10.1.79.1/24 -- mtu=1432 と書き換え ワーカーノードになるホスト上での作業(4)
  70. Dockerデーモンプロセスを再起動します $ sudo /sbin/ifconfig docker0 down $ sudo brctl delbr docker0 $ sudo systemctl daemon-reload $ sudo systemctl restart docker ワーカーノードになるホスト上での作業(5)
  71. 確認はifconfigおよびnetstat -rnで実施 再起動後のルーティングテーブルの確認 $ netstat -rn Kernel IP routing table Destination Gateway Genmask Flags MSS Window irtt Iface 0.0.0.0 10.140.0.1 0.0.0.0 UG 0 0 0 ens4 10.1.0.0 0.0.0.0 255.255.0.0 U 0 0 0 flannel0 10.1.79.0 0.0.0.0 255.255.255.0 U 0 0 0 docker0 10.140.0.1 0.0.0.0 255.255.255.255 UH 0 0 0 ens4 ワーカーノードになるホスト上での作業(6)
  72. ワーカーノードに必要なプロセス(kubelet)をhyperkubeで起動します $ sudo docker run ¥ --volume=/:/rootfs:ro --volume=/sys:/sys:ro ¥ --volume=/dev:/dev ¥ --volume=/var/lib/docker/:/var/lib/docker:rw ¥ --volume=/var/lib/kubelet/:/var/lib/kubelet:rw ¥ --volume=/var/run:/var/run:rw ¥ --net=host --privileged=true --pid=host -d ¥ gcr.io/google_containers/hyperkube-amd64:v${K8S_VERSION} ¥ /hyperkube kubelet ¥ --allow-privileged=true --api-servers=http://${MASTER_IP}:8080 ¥ --v=2 --address=0.0.0.0 --enable-server --containerized ¥ --cluster-dns=10.0.0.10 --cluster-domain=cluster.local ワーカーノードになるホスト上での作業(7)
  73. ワーカーノードに必要なプロセス(kube-proxy)を hyperkubeで起動します $ sudo docker run -d --net=host --privileged ¥ gcr.io/google_containers/hyperkube- amd64:v${K8S_VERSION} ¥ /hyperkube proxy ¥ --master=http://${MASTER_IP}:8080 --v=2 ワーカーノードになるホスト上での作業(8)
  74. •ふたつ目のホスト上でflanneldを 設定しました •このホストをワーカーノードにし ました(kubeletとkube-proxyの 起動) ここまでのまとめ
  75. $ kubectl -s 10.140.0.14:8080 cluster-info Kubernetes master is running at 10.140.0.14:8080 KubeDNS is running at 10.140.0.14:8080/api/v1/proxy/namespaces/kube- system/services/kube-dns サービスの確認 $ kubectl -s 10.140.0.14:8080 get --all-namespaces svc NAMESPACE NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE default kubernetes 10.0.0.1 <none> 443/TCP 47m kube-system kube-dns 10.0.0.10 <none> 53/UDP,53/TCP 45m Kubernetesの基本的な動作確認(1)
  76. ノードの確認 $ kubectl get nodes NAME STATUS AGE 127.0.0.1 Ready 52m ubuntu16k5 Ready 16m Kubernetesの基本的な動作確認(2)
  77. 自作Node.jsアプリをKubernetes上の サービスとして動かしてみる
  78. server.js => var http = require('http'); var handleRequest = function(request, response) { response.writeHead(200); response.end("Hello World"); } var www = http.createServer(handleRequest); www.listen(8888); サンプルアプリケーションのDockerイメー ジを準備(1)
  79. Dockerfile => FROM node:latest EXPOSE 8888 COPY server.js . CMD node server.js $ sudo docker build -t mynode:latest . $ sudo docker images # 確認 サンプルアプリケーションのDockerイメー ジを準備(2)
  80. このDockerイメージを複数ホストから取得できるために レジストリに登録する必要があります (自前でdockerレジストリを立てるのがベストですが、今 回はDockerHubで代用します) $ docker login $ docker tag mynode:latest guest/mynode # guest の部分はDockerHubのログインID $ docker push guest/mynode Dockerイメージをレジストリに登録
  81. mynode.yaml => apiVersion: v1 kind: ReplicationController metadata: name: my-node spec: replicas: 2 template: metadata: labels: app: sample spec: containers: - name: mynode image: guest/mynode ports: - containerPort: 8888 rc用の設定ファイル ReplicationController(rc)の設定ファイル このrcの識別名(開発者の命名) レプリケーション数の指定 Dockerイメージの指定(on DockerHub) ラベル(keyとvalue両方とも開発者の命名)
  82. mynode-svc.yaml => apiVersion: v1 kind: Service metadata: name: frontend labels: app: sample spec: ports: - port: 8888 selector: app: sample サービス用の設定ファイル(selectorで mynode.yamlを参照する関係) サービス(svc)の設定ファイル このサービスの識別名(開発者の命名) ラベル(開発者の命名) rc(やpod)のラベルでセレクト
  83. 起動は $ kubectl create -f mynode.yaml $ kubectl create -f mynode-svc.yaml 削除する時は $ kubectl delete -f mynode-svc.yaml $ kubectl delete -f mynode.yaml 設定ファイルによる起動および削除
  84. まずrcを起動(=暗黙にpodを起動)してみます $ kubectl create -f mynode.yaml rcを起動(=暗黙にpodを起動)
  85. podの確認 $ kubectl get --all-namespaces po NAMESPACE NAME READY STATUS RESTARTS AGE default k8s-master-127.0.0.1 4/4 Running 0 50m default k8s-proxy-127.0.0.1 1/1 Running 0 50m default my-node-ejvv9 1/1 Running 0 10s default my-node-lm62r 1/1 Running 0 10s kube-system kube-dns-v10-suqsw 4/4 Running 0 48m rcの確認 $ kubectl get --all-namespaces rc NAMESPACE NAME DESIRED CURRENT AGE default my-node 2 2 36s kube-system kube-dns-v10 1 1 49m podとrcの確認
  86. サービス(svc)はまだ存在していません $ kubectl get --all-namespaces svc NAMESPACE NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE default kubernetes 10.0.0.1 <none> 443/TCP 51m kube-system kube-dns 10.0.0.10 <none> 53/UDP,53/TCP 49m エンドポイント(ep)もまだ存在しません $ kubectl get --all-namespaces ep NAMESPACE NAME ENDPOINTS AGE default kubernetes 10.140.0.14:6443 51m kube-system kube-dns 10.1.19.2:53,10.1.19.2:53 49m サービスとエンドポイントの確認
  87. サービスを起動してみます $ kubectl create -f mynode-svc.yaml サービス(svc)の起動
  88. サービス確認 $ kubectl get --all-namespaces svc NAMESPACE NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE default frontend 10.0.0.206 <none> 8888/TCP 21s default kubernetes 10.0.0.1 <none> 443/TCP 53m kube-system kube-dns 10.0.0.10 <none> 53/UDP,53/TCP 51m エンドポイント確認 $ kubectl get --all-namespaces ep NAMESPACE NAME ENDPOINTS AGE default frontend 10.1.19.3:8888,10.1.79.2:8888 58s default kubernetes 10.140.0.14:6443 53m kube-system kube-dns 10.1.19.2:53,10.1.19.2:53 52m サービスとエンドポイントの確認
  89. サービスのIPアドレスにアクセス $ curl http://10.0.0.206:8888 Hello World アプリの動作確認
  90. Podの確認 $ kubectl describe po my-node-ejvv9 Name: my-node-ejvv9 Namespace: default Node: ubuntu16k5/10.140.0.15 Start Time: Wed, 20 Jul 2016 09:23:11 +0000 Labels: app=sample Status: Running IP: 10.1.79.2 Controllers: ReplicationController/my-node Containers: mynode: Container ID: docker://daf3fb9f2217ef40226aab040bd64d6f6f0d5bdcd0318e5628e9af07adcb650 4 Image: guest/mynode 以下略 podの詳細
  91. レプリケーションの確認 $ kubectl describe rc my-node Name: my-node Namespace: default Image(s): guest/mynode Selector: app=sample Labels: app=sample Replicas: 2 current / 2 desired Pods Status: 2 Running / 0 Waiting / 0 Succeeded / 0 Failed No volumes. Events: FirstSeen LastSeen Count From SubobjectPath Type Reason Message --------- -------- ----- ---- ----------- -- -------- ------ ------- 5m 5m 1 {replication-controller } Normal SuccessfulCreate Created pod: my-node-ejvv9 5m 5m 1 {replication-controller } Normal SuccessfulCreate Created pod: my-node-lm62r レプリケーション(rc)の確認
  92. サービスの確認 $ kubectl describe svc frontend Name: frontend Namespace: default Labels: app=sample Selector: app=sample Type: ClusterIP IP: 10.0.0.206 Port: <unset> 8888/TCP Endpoints: 10.1.19.3:8888,10.1.79.2:8888 Session Affinity: None No events. サービスの詳細
  93. エンドポイントの確認(:コンテナのIPアドレスの確認) $ kubectl describe ep frontend Name: frontend Namespace: default Labels: app=sample Subsets: Addresses: 10.1.19.3,10.1.79.2 NotReadyAddresses: <none> Ports: Name Port Protocol ---- ---- -------- <unset> 8888 TCP No events. エンドポイントの確認
  94. $ kubectl scale --replicas=5 rc/my-node replicationcontroller "my-node" scaled rcのスケールアウト検証
  95. $ kubectl get --all-namespaces rc NAMESPACE NAME DESIRED CURRENT AGE default my-node 5 5 7m kube-system kube-dns-v10 1 1 55m rcのスケールアウト検証の確認(1)
  96. $ kubectl get --all-namespaces po NAMESPACE NAME READY STATUS RESTARTS AGE default k8s-master-127.0.0.1 4/4 Running 0 57m default k8s-proxy-127.0.0.1 1/1 Running 0 57m default my-node-3blsf 1/1 Running 0 39s default my-node-a2015 1/1 Running 0 39s default my-node-ejvv9 1/1 Running 0 7m default my-node-lm62r 1/1 Running 0 7m default my-node-u5gxt 1/1 Running 0 39s kube-system kube-dns-v10-suqsw 4/4 Running 0 56m rcのスケールアウト検証の確認(2)
  97. $ kubectl get --all-namespaces ep NAMESPACE NAME ENDPOINTS AGE default frontend 10.1.19.3:8888,10.1.19.4:8888,10.1.79.2:8888 + 2 more... 5m default kubernetes 10.140.0.14:6443 58m kube-system kube-dns 10.1.19.2:53,10.1.19.2:53 56m rcのスケールアウト検証の確認(3)
  98. $ kubectl describe ep frontend Name: frontend Namespace: default Labels: app=sample Subsets: Addresses: 10.1.19.3,10.1.19.4,10.1.79.2,10.1.79.3,10.1.79.4 NotReadyAddresses: <none> Ports: Name Port Protocol ---- ---- -------- <unset> 8888 TCP No events. 片方のホストに2つ(10.1.19.3,10.1.19.4)、もうひとつのホストに3つ (10.1.79.2,10.1.79.3,10.1.79.4)のpodがある RCのスケールアウト検証の確認(4)
  99. podが増えようと、サービスとしての見え方は不変 $ kubectl get --all-namespaces svc NAMESPACE NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE default frontend 10.0.0.206 <none> 8888/TCP 5m default kubernetes 10.0.0.1 <none> 443/TCP 58m kube-system kube-dns 10.0.0.10 <none> 53/UDP,53/TCP 56m rcのスケールアウト検証の確認(5)
  100. $ kubectl describe po my-node-3blsf Name: my-node-3blsf Namespace: default Node: ubuntu16k5/10.140.0.15 Start Time: Wed, 20 Jul 2016 09:29:54 +0000 (略) 個々のpodの物理位置の確認(Nodeの行)
  101. $ kubectl exec -it my-node-3blsf sh => 別ホスト上のpodへもログイン可能なので、Dockerレ ベルのコンテナへのログイン(docker exec -it)より便利 です 特定podへログイン
  102. $ kubectl logs my-node-3blsf tailfも可能 $ kubectl logs -f my-node-3blsf 特定podのログの確認
  103. $ sudo docker ps |grep mynode 9b8ecdb7a42f guest/mynode "/bin/sh -c 'node ser" 15 minutes ago Up 15 minutes k8s_mynode.6062cb3_my-node-a2015_default_841729f4-4e5c-11e6- 930a-42010a8c000e_a8947e68 50e8cd85abec guest/mynode "/bin/sh -c 'node ser" 21 minutes ago Up 21 minutes k8s_mynode.6062cb3_my-node-lm62r_default_94564430-4e5b-11e6- 930a-42010a8c000e_bcf722c3 fyi, Dockerレベルでコンテナの詳細を確認 $ sudo docker inspect 9b8ecdb7a42f (略) fyi, Dockerレベルで確認
  104. • たとえばDockerレベルでコンテナを落としたり、ホス トOSからDockerコンテナ上のプロセスをkill • たとえば特定のマシン(VMインスタンス)を落とすテス ト => レプリケーションの数が維持される 特定のpodを落とすテスト
  105. • 自作アプリをサービスとして起動しました • rcにより、pod(=コンテナ=プロセス)を複数インスタン ス化しました • podのインスタンス数がどうあれ、サービスにはサービ スのIPアドレスで常にアクセス可能です • 個々のpodの障害調査をしたければ、シェルで直接ログ インしたりログの監視が可能です ここまでのまとめ
  106. Kubernetesのネットワーク
  107. 各pod内からはサービス名 つまり curl http://frontend:8888 でお互いが見えます => DNSが返すIPアドレスは、サービスのIPアドレス (DockerコンテナのIPアドレスでもないし、ホストのIPア ドレスでもない) 名前解決(DNS)
  108. $ dig @10.0.0.10 frontend.default.svc.cluster.local. (略) ;; ANSWER SECTION: frontend.default.svc.cluster.local. 30 IN A 10.0.0.206 => DNSのFQDNは frontend.default.svc.cluster.local. フォーマット: サービス名.ネームスペース名.svc.cluster.local. ホストからの名前解決
  109. • ここまでの話: • DNSでサービス名からサービスのIPアドレスがひけるようになり ました • ここからの話: • このサービスのIPアドレスから、サービスにひもづくpodにたど りつく必要があります • rcのメカニズムにより、サービスにひもづくpodは複数ありうる ので、どこかのpodにたどりつく必要があります • 正確に言うと、pod内のコンテナにたどりつく必要があります ネットワーク詳解
  110. • サービスのIPアドレスはifconfigの世界には存在しないアドレ スです • iptablesで、サービスのIPアドレスからpod(=コンテナ)のア ドレスへ宛先アドレスを書き換えます • rcのメカニズムで複数のpodがある場合、iptablesのロード バランス機能で振り分けます • iptablesにサービスのIPアドレスを追加するのはkube-proxy の役割です • pod(=コンテナ)のアドレス宛のパケットをホストのIPアドレ ス宛に変えるのはflanneldの役割です • (このルーティングテーブルはetcdに入っています) ネットワークの動作概要
  111. 10.0.0.206宛のパケットはiptablesでランダムに5つのどれかに行きます(rcのレプリカ数が5の場合): $ sudo iptables-save | grep 10.0.0.206 -A KUBE-SERVICES -d 10.0.0.206/32 -p tcp -m comment --comment "default/frontend: cluster IP" -m tcp - -dport 8888 -j KUBE-SVC-GYQQTB6TY565JPRW $ sudo iptables-save |grep KUBE-SVC-GYQQTB6TY565JPRW :KUBE-SVC-GYQQTB6TY565JPRW - [0:0] -A KUBE-SERVICES -d 10.0.0.206/32 -p tcp -m comment --comment "default/frontend: cluster IP" -m tcp - -dport 8888 -j KUBE-SVC-GYQQTB6TY565JPRW -A KUBE-SVC-GYQQTB6TY565JPRW -m comment --comment "default/frontend:" -m statistic --mode random -- probability 0.20000000019 -j KUBE-SEP-IABZAQPI4OCAAEYI -A KUBE-SVC-GYQQTB6TY565JPRW -m comment --comment "default/frontend:" -m statistic --mode random -- probability 0.25000000000 -j KUBE-SEP-KOOQP76EBZUHPEOS -A KUBE-SVC-GYQQTB6TY565JPRW -m comment --comment "default/frontend:" -m statistic --mode random -- probability 0.33332999982 -j KUBE-SEP-R2LUGYH3W6MZDZRV -A KUBE-SVC-GYQQTB6TY565JPRW -m comment --comment "default/frontend:" -m statistic --mode random -- probability 0.50000000000 -j KUBE-SEP-RHTBT7WLGW2VONI3 -A KUBE-SVC-GYQQTB6TY565JPRW -m comment --comment "default/frontend:" -j KUBE-SEP-DSHEFNPOTRMM5FWS iptablesを確認(1)
  112. $ sudo iptables-save |grep KUBE-SEP-DSHEFNPOTRMM5FWS :KUBE-SEP-DSHEFNPOTRMM5FWS - [0:0] -A KUBE-SEP-DSHEFNPOTRMM5FWS -s 10.1.79.4/32 -m comment --comment "default/frontend:" -j KUBE-MARK-MASQ -A KUBE-SEP-DSHEFNPOTRMM5FWS -p tcp -m comment --comment "default/frontend:" -m tcp -j DNAT --to-destination 10.1.79.4:8888 -A KUBE-SVC-GYQQTB6TY565JPRW -m comment --comment "default/frontend:" -j KUBE-SEP-DSHEFNPOTRMM5FWS => ある確率で 10.0.0.206:8888宛のパケットは 10.1.79.4:8888 宛のパケットへ変換される iptablesを確認(2)
  113. $ netstat -rn Kernel IP routing table Destination Gateway Genmask Flags MSS Window irtt Iface 0.0.0.0 10.140.0.1 0.0.0.0 UG 0 0 0 ens4 10.1.0.0 0.0.0.0 255.255.0.0 U 0 0 0 flannel0 10.1.19.0 0.0.0.0 255.255.255.0 U 0 0 0 docker0 10.140.0.1 0.0.0.0 255.255.255.255 UH 0 0 0 ens4 => 10.1.79.4:8888 宛のパケットはflanneldへ行く ホストのルーティングテーブルを確認
  114. $ etcdctl ls --recursive /coreos.com/network/subnets /coreos.com/network/subnets/10.1.19.0-24 /coreos.com/network/subnets/10.1.79.0-24 $ etcdctl get /coreos.com/network/subnets/10.1.79.0-24 {"PublicIP":"10.140.0.15"} => "10.140.0.15"は、10.1.79.0/24のpod(コンテナ)が動 いているホストのIPアドレス flanneldのルーティングテーブルを確認
  115. •iptablesでサービスのIPアドレス 宛のパケットがpodのIPアドレス 宛になる •pod宛のパケットはflanneldの力 で、そのpodが動いているホスト 宛のパケットになる ここまでのまとめ
  116. 10.0.0.206をホストの外から叩けるようにするには、NodePort(or LoadBalancer)でポートを外に見せる mynode-svc.yaml => apiVersion: v1 kind: Service metadata: name: frontend labels: app: sample spec: type: NodePort ports: - port: 8888 selector: app: sample ホストの外にポートを公開するには 動作は異なりますが、感覚的には Dockerでポートエクスポートする感じ
  117. • Kubernetesは初見でかなり複雑怪奇に見えますが、き ちんと分解すればなんとか理解できる(はず) • ちょっとした考察 • おかしなことが起きた時、プロセスは潔く死んだほうが Kubernetesに優しそうです • iptablesを使っている限り、負荷分散が(ラウンドロビンより) リッチになる見込みがない部分は賛否両論あるかもしれません (LBとしては、ある種の割り切り) まとめ
  118. • メインのプログラミング言語: Java、JavaScript、Swift(一 部) • 使っているミドルウェアの言語: Scala(Spark、Kafka)、 Go(Kubernetes) • OS(Linux)、JVM、アルゴリズム、ミドルウェア、ネットワー ク、ブラウザそれぞれの専門家は歓迎します • 本物の機械学習ができるデータがあります(会社組織という アーキテクチャが分析対象) • 大規模開発(東京、大阪、上海、シンガポール、チェンナイ) なので、日本なんてどうでもいいと思うタフさ(鈍感さ)と抽 象化して大枠をとらえる力があると良いと思います 最後にJTFのスポンサーなので人材募集 (株式会社ワークスアプリケーションズ)
Advertisement