แƒ แƒแƒ’แƒแƒ  แƒ›แƒฃแƒจแƒแƒแƒ‘แƒก kubectl exec?

แฒจแƒ”แƒœแƒ˜แƒจแƒ•แƒœแƒ. แƒ—แƒแƒ แƒ’แƒ›แƒœแƒ: แƒกแƒขแƒแƒขแƒ˜แƒ˜แƒก แƒแƒ•แƒขแƒแƒ แƒ˜ - แƒ”แƒ แƒ™แƒแƒœ แƒ”แƒ แƒแƒšแƒ˜, แƒ˜แƒœแƒŸแƒ˜แƒœแƒ”แƒ แƒ˜ SAP-แƒ“แƒแƒœ - แƒ˜แƒ–แƒ˜แƒแƒ แƒ”แƒ‘แƒก แƒ—แƒแƒ•แƒ˜แƒก แƒ™แƒ•แƒšแƒ”แƒ•แƒแƒก แƒ’แƒฃแƒœแƒ“แƒ˜แƒก แƒคแƒฃแƒœแƒฅแƒชแƒ˜แƒแƒœแƒ˜แƒ แƒ”แƒ‘แƒ˜แƒก แƒ›แƒ”แƒฅแƒแƒœแƒ˜แƒ–แƒ›แƒ”แƒ‘แƒ˜แƒก แƒจแƒ”แƒกแƒแƒฎแƒ”แƒ‘. kubectl exec, แƒแƒกแƒ” แƒœแƒแƒชแƒœแƒแƒ‘แƒ˜แƒ แƒงแƒ•แƒ”แƒšแƒแƒกแƒ—แƒ•แƒ˜แƒก, แƒ•แƒ˜แƒœแƒช แƒ›แƒฃแƒจแƒแƒแƒ‘แƒก Kubernetes-แƒ—แƒแƒœ. แƒ˜แƒก แƒ›แƒ—แƒ”แƒš แƒแƒšแƒ’แƒแƒ แƒ˜แƒ—แƒ›แƒก แƒแƒฎแƒšแƒแƒ•แƒก Kubernetes-แƒ˜แƒก แƒฌแƒงแƒแƒ แƒแƒก แƒ™แƒแƒ“แƒ˜แƒก (แƒ“แƒ แƒ›แƒแƒกแƒ—แƒแƒœ แƒ“แƒแƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ”แƒ‘แƒฃแƒšแƒ˜ แƒžแƒ แƒแƒ”แƒฅแƒขแƒ”แƒ‘แƒ˜แƒก) แƒฉแƒแƒ›แƒแƒœแƒแƒ—แƒ•แƒแƒšแƒก, แƒ แƒแƒช แƒกแƒแƒจแƒฃแƒแƒšแƒ”แƒ‘แƒแƒก แƒ’แƒแƒซแƒšแƒ”แƒ•แƒ— แƒ’แƒแƒ˜แƒ’แƒแƒ— แƒ—แƒ”แƒ›แƒ แƒ แƒแƒช แƒจแƒ”แƒ˜แƒซแƒšแƒ”แƒ‘แƒ แƒฆแƒ แƒ›แƒแƒ“.

แƒ แƒแƒ’แƒแƒ  แƒ›แƒฃแƒจแƒแƒแƒ‘แƒก kubectl exec?

แƒ”แƒ แƒ— แƒžแƒแƒ แƒแƒกแƒ™แƒ”แƒ•แƒก, แƒ™แƒแƒšแƒ”แƒ’แƒ แƒ›แƒแƒ›แƒ˜แƒแƒฎแƒšแƒแƒ•แƒ“แƒ แƒ“แƒ แƒ›แƒ™แƒ˜แƒ—แƒฎแƒ, แƒ แƒแƒ’แƒแƒ  แƒจแƒ”แƒ›แƒ”แƒกแƒ แƒฃแƒšแƒ”แƒ‘แƒ˜แƒœแƒ แƒ‘แƒ แƒซแƒแƒœแƒ”แƒ‘แƒ แƒžแƒแƒ“แƒจแƒ˜ แƒ™แƒšแƒ˜แƒ”แƒœแƒขแƒ˜-แƒฌแƒแƒ“แƒ˜. แƒ•แƒ”แƒ  แƒ•แƒฃแƒžแƒแƒกแƒฃแƒฎแƒ” แƒ“แƒ แƒฃแƒชแƒ”แƒ‘ แƒ›แƒ˜แƒ•แƒฎแƒ•แƒ“แƒ˜, แƒ แƒแƒ› แƒ›แƒฃแƒจแƒแƒแƒ‘แƒ˜แƒก แƒ›แƒ”แƒฅแƒแƒœแƒ˜แƒ–แƒ›แƒ˜แƒก แƒจแƒ”แƒกแƒแƒฎแƒ”แƒ‘ แƒแƒ แƒแƒคแƒ”แƒ แƒ˜ แƒ•แƒ˜แƒชแƒแƒ“แƒ˜ kubectl exec. แƒ“แƒ˜แƒแƒฎ, แƒ›แƒ” แƒ›แƒฅแƒแƒœแƒ“แƒ แƒ’แƒแƒ แƒ™แƒ•แƒ”แƒฃแƒšแƒ˜ แƒ˜แƒ“แƒ”แƒ”แƒ‘แƒ˜ แƒ›แƒ˜แƒก แƒ›แƒแƒฌแƒงแƒแƒ‘แƒ˜แƒšแƒแƒ‘แƒแƒ–แƒ”, แƒ›แƒแƒ’แƒ แƒแƒ› แƒ›แƒ” แƒแƒ  แƒ•แƒ˜แƒงแƒแƒ•แƒ˜ 100% แƒ“แƒแƒ แƒฌแƒ›แƒฃแƒœแƒ”แƒ‘แƒฃแƒšแƒ˜ แƒ›แƒแƒ— แƒกแƒ˜แƒกแƒฌแƒแƒ แƒ”แƒจแƒ˜ แƒ“แƒ แƒแƒ›แƒ˜แƒขแƒแƒ› แƒ’แƒแƒ“แƒแƒ•แƒฌแƒงแƒ•แƒ˜แƒขแƒ” แƒแƒ› แƒกแƒแƒ™แƒ˜แƒ—แƒฎแƒ˜แƒก แƒ›แƒแƒ’แƒ•แƒแƒ แƒ”แƒ‘แƒ. แƒ‘แƒšแƒแƒ’แƒ”แƒ‘แƒ˜แƒก, แƒ“แƒแƒ™แƒฃแƒ›แƒ”แƒœแƒขแƒแƒชแƒ˜แƒ˜แƒกแƒ แƒ“แƒ แƒฌแƒงแƒแƒ แƒแƒก แƒ™แƒแƒ“แƒ˜แƒก แƒจแƒ”แƒกแƒฌแƒแƒ•แƒšแƒ˜แƒก แƒจแƒ”แƒ›แƒ“แƒ”แƒ’, แƒ‘แƒ”แƒ•แƒ แƒ˜ แƒ แƒแƒ› แƒ•แƒ˜แƒกแƒฌแƒแƒ•แƒšแƒ” แƒ“แƒ แƒแƒ› แƒกแƒขแƒแƒขแƒ˜แƒแƒจแƒ˜ แƒ›แƒ˜แƒœแƒ“แƒ แƒ’แƒแƒ’แƒ˜แƒ–แƒ˜แƒแƒ แƒแƒ— แƒฉแƒ”แƒ›แƒ˜ แƒแƒฆแƒ›แƒแƒฉแƒ”แƒœแƒ”แƒ‘แƒ˜ แƒ“แƒ แƒ’แƒแƒ’แƒ”แƒ‘แƒ. แƒ—แƒฃ แƒ แƒแƒ›แƒ” แƒแƒ แƒแƒกแƒฌแƒแƒ แƒ˜แƒ, แƒ’แƒ—แƒฎแƒแƒ•แƒ— แƒ“แƒแƒ›แƒ˜แƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ“แƒ”แƒ— แƒœแƒแƒ›แƒ”แƒ แƒ–แƒ” Twitter.

แƒกแƒแƒกแƒฌแƒแƒ•แƒšแƒ

MacBook-แƒ–แƒ” แƒ™แƒšแƒแƒกแƒขแƒ”แƒ แƒ˜แƒก แƒจแƒ”แƒกแƒแƒฅแƒ›แƒœแƒ”แƒšแƒแƒ“, แƒ›แƒ” แƒ™แƒšแƒแƒœแƒ˜แƒ แƒ”แƒ‘แƒ แƒ›แƒแƒ•แƒแƒฎแƒ“แƒ˜แƒœแƒ” ecomm-integration-ballerina/kubernetes-cluster. แƒจแƒ”แƒ›แƒ“แƒ”แƒ’ แƒ’แƒแƒ•แƒแƒกแƒฌแƒแƒ แƒ” แƒ™แƒ•แƒแƒœแƒซแƒ”แƒ‘แƒ˜แƒก IP แƒ›แƒ˜แƒกแƒแƒ›แƒแƒ แƒ—แƒ”แƒ‘แƒ˜ kubelet'a แƒ™แƒแƒœแƒคแƒ˜แƒ’แƒฃแƒ แƒแƒชแƒ˜แƒแƒจแƒ˜, แƒ แƒแƒ“แƒ’แƒแƒœ แƒœแƒแƒ’แƒฃแƒšแƒ˜แƒกแƒฎแƒ›แƒ”แƒ•แƒ˜ แƒžแƒแƒ แƒแƒ›แƒ”แƒขแƒ แƒ”แƒ‘แƒ˜ แƒแƒ  แƒ˜แƒซแƒšแƒ”แƒแƒ“แƒ แƒกแƒแƒจแƒฃแƒแƒšแƒ”แƒ‘แƒแƒก. kubectl exec. แƒแƒ›แƒ˜แƒก แƒ›แƒ—แƒแƒ•แƒแƒ แƒ˜ แƒ›แƒ˜แƒ–แƒ”แƒ–แƒ˜แƒก แƒจแƒ”แƒกแƒแƒฎแƒ”แƒ‘ แƒ›แƒ”แƒขแƒ˜ แƒจแƒ”แƒ’แƒ˜แƒซแƒšแƒ˜แƒแƒ— แƒฌแƒแƒ˜แƒ™แƒ˜แƒ—แƒฎแƒแƒ— แƒแƒฅ.

  • แƒœแƒ”แƒ‘แƒ˜แƒกแƒ›แƒ˜แƒ”แƒ แƒ˜ แƒ›แƒแƒœแƒฅแƒแƒœแƒ = แƒฉแƒ”แƒ›แƒ˜ MacBook
  • แƒซแƒ˜แƒ แƒ˜แƒ—แƒแƒ“แƒ˜ แƒ™แƒ•แƒแƒœแƒซแƒ˜ IP = 192.168.205.10
  • IP แƒ›แƒฃแƒจแƒ แƒ™แƒ•แƒแƒœแƒซแƒ˜ = 192.168.205.11
  • API แƒกแƒ”แƒ แƒ•แƒ”แƒ แƒ˜แƒก แƒžแƒแƒ แƒขแƒ˜ = 6443

แƒ™แƒแƒ›แƒžแƒแƒœแƒ”แƒœแƒขแƒ”แƒ‘แƒ˜

แƒ แƒแƒ’แƒแƒ  แƒ›แƒฃแƒจแƒแƒแƒ‘แƒก kubectl exec?

  • kubectl exec แƒžแƒ แƒแƒชแƒ”แƒกแƒ˜: แƒ แƒแƒ“แƒ”แƒกแƒแƒช แƒ•แƒแƒ™แƒ”แƒ—แƒ”แƒ‘แƒ— "kubectl exec..." แƒžแƒ แƒแƒชแƒ”แƒกแƒ˜ แƒ˜แƒฌแƒงแƒ”แƒ‘แƒ. แƒแƒ›แƒ˜แƒก แƒ’แƒแƒ™แƒ”แƒ—แƒ”แƒ‘แƒ แƒจแƒ”แƒ’แƒ˜แƒซแƒšแƒ˜แƒแƒ— แƒœแƒ”แƒ‘แƒ˜แƒกแƒ›แƒ˜แƒ”แƒ  แƒ›แƒแƒœแƒฅแƒแƒœแƒแƒ–แƒ” K8s API แƒกแƒ”แƒ แƒ•แƒ”แƒ แƒ–แƒ” แƒฌแƒ•แƒ“แƒแƒ›แƒ˜แƒ—. แฒจแƒ”แƒœแƒ˜แƒจแƒ•แƒœแƒ. แƒ—แƒแƒ แƒ’แƒ›แƒแƒœแƒ˜: แƒ’แƒแƒ แƒ“แƒ แƒแƒ›แƒ˜แƒกแƒ, แƒ™แƒแƒœแƒกแƒแƒšแƒ˜แƒก แƒฉแƒแƒ›แƒแƒœแƒแƒ—แƒ•แƒแƒšแƒจแƒ˜, แƒแƒ•แƒขแƒแƒ แƒ˜ แƒ˜แƒงแƒ”แƒœแƒ”แƒ‘แƒก แƒ™แƒแƒ›แƒ”แƒœแƒขแƒแƒ แƒก "แƒœแƒ”แƒ‘แƒ˜แƒกแƒ›แƒ˜แƒ”แƒ แƒ˜ แƒ›แƒแƒœแƒฅแƒแƒœแƒ", แƒ แƒแƒช แƒ’แƒฃแƒšแƒ˜แƒกแƒฎแƒ›แƒแƒ‘แƒก, แƒ แƒแƒ› แƒจแƒ”แƒ›แƒ“แƒ”แƒ’แƒ˜ แƒ‘แƒ แƒซแƒแƒœแƒ”แƒ‘แƒ”แƒ‘แƒ˜ แƒจแƒ”แƒ˜แƒซแƒšแƒ”แƒ‘แƒ แƒจแƒ”แƒกแƒ แƒฃแƒšแƒ“แƒ”แƒก แƒœแƒ”แƒ‘แƒ˜แƒกแƒ›แƒ˜แƒ”แƒ  แƒแƒกแƒ”แƒ— แƒแƒžแƒแƒ แƒแƒขแƒ–แƒ”, แƒ แƒแƒ›แƒ”แƒšแƒกแƒแƒช แƒแƒฅแƒ•แƒก แƒฌแƒ•แƒ“แƒแƒ›แƒ Kubernetes-แƒ–แƒ”.
  • api แƒกแƒ”แƒ แƒ•แƒ”แƒ แƒ˜: แƒ™แƒแƒ›แƒžแƒแƒœแƒ”แƒœแƒขแƒ˜ แƒ›แƒ—แƒแƒ•แƒแƒ  แƒ™แƒ•แƒแƒœแƒซแƒ–แƒ”, แƒ แƒแƒ›แƒ”แƒšแƒ˜แƒช แƒฃแƒ–แƒ แƒฃแƒœแƒ•แƒ”แƒšแƒงแƒแƒคแƒก แƒฌแƒ•แƒ“แƒแƒ›แƒแƒก Kubernetes API-แƒ–แƒ”. แƒ”แƒก แƒแƒ แƒ˜แƒก แƒกแƒแƒ™แƒแƒœแƒขแƒ แƒแƒšแƒ แƒ—แƒ•แƒ˜แƒ—แƒ›แƒคแƒ แƒ˜แƒœแƒแƒ•แƒ˜แƒก แƒฌแƒ˜แƒœแƒ แƒ‘แƒแƒšแƒ แƒ™แƒฃแƒ‘แƒ”แƒ แƒœแƒ”แƒขแƒ”แƒกแƒจแƒ˜.
  • แƒ™แƒฃแƒ‘แƒ”แƒšแƒ”แƒขแƒ˜: แƒแƒ’แƒ”แƒœแƒขแƒ˜, แƒ แƒแƒ›แƒ”แƒšแƒ˜แƒช แƒ›แƒฃแƒจแƒแƒแƒ‘แƒก แƒ™แƒšแƒแƒกแƒขแƒ”แƒ แƒ˜แƒก แƒงแƒ•แƒ”แƒšแƒ แƒ™แƒ•แƒแƒœแƒซแƒ–แƒ”. แƒ˜แƒก แƒฃแƒ–แƒ แƒฃแƒœแƒ•แƒ”แƒšแƒงแƒแƒคแƒก แƒ™แƒแƒœแƒขแƒ”แƒ˜แƒœแƒ”แƒ แƒ”แƒ‘แƒ˜แƒก แƒ›แƒฃแƒจแƒแƒแƒ‘แƒแƒก แƒžแƒแƒ“แƒจแƒ˜.
  • แƒ™แƒแƒœแƒขแƒ”แƒ˜แƒœแƒ”แƒ แƒ˜แƒก แƒ›แƒฃแƒจแƒแƒแƒ‘แƒ˜แƒก แƒ“แƒ แƒ (แƒ™แƒแƒœแƒขแƒ”แƒ˜แƒœแƒ”แƒ แƒ˜แƒก แƒ’แƒแƒจแƒ•แƒ”แƒ‘แƒ˜แƒก แƒ“แƒ แƒ): แƒžแƒ แƒแƒ’แƒ แƒแƒ›แƒฃแƒšแƒ˜ แƒฃแƒ–แƒ แƒฃแƒœแƒ•แƒ”แƒšแƒงแƒแƒคแƒ, แƒ แƒแƒ›แƒ”แƒšแƒ˜แƒช แƒžแƒแƒกแƒฃแƒฎแƒ˜แƒกแƒ›แƒ’แƒ”แƒ‘แƒ”แƒšแƒ˜แƒ แƒ™แƒแƒœแƒขแƒ”แƒ˜แƒœแƒ”แƒ แƒ”แƒ‘แƒ˜แƒก แƒ’แƒแƒจแƒ•แƒ”แƒ‘แƒแƒ–แƒ”. แƒ›แƒแƒ’แƒแƒšแƒ˜แƒ—แƒ”แƒ‘แƒ˜: Docker, CRI-O, แƒ™แƒแƒœแƒขแƒ”แƒ˜แƒœแƒ”แƒ แƒ˜โ€ฆ
  • kernel: OS แƒ‘แƒ˜แƒ แƒ—แƒ•แƒ˜ แƒ›แƒฃแƒจแƒ แƒ™แƒ•แƒแƒœแƒซแƒ–แƒ”; แƒžแƒแƒกแƒฃแƒฎแƒ˜แƒกแƒ›แƒ’แƒ”แƒ‘แƒ”แƒšแƒ˜แƒ แƒžแƒ แƒแƒชแƒ”แƒกแƒ˜แƒก แƒ›แƒแƒ แƒ—แƒ•แƒแƒ–แƒ”.
  • แƒกแƒแƒ›แƒ˜แƒ–แƒœแƒ” (แƒกแƒแƒ›แƒ˜แƒ–แƒœแƒ”) แƒ™แƒแƒœแƒขแƒ”แƒ˜แƒœแƒ”แƒ แƒ˜: แƒ™แƒแƒœแƒขแƒ”แƒ˜แƒœแƒ”แƒ แƒ˜, แƒ แƒแƒ›แƒ”แƒšแƒ˜แƒช แƒแƒ แƒ˜แƒก pod-แƒ˜แƒก แƒœแƒแƒฌแƒ˜แƒšแƒ˜ แƒ“แƒ แƒ›แƒฃแƒจแƒแƒแƒ‘แƒก แƒ”แƒ แƒ—-แƒ”แƒ แƒ— แƒ›แƒฃแƒจแƒ แƒ™แƒ•แƒแƒœแƒซแƒ–แƒ”.

แƒ แƒ แƒแƒฆแƒ›แƒแƒ•แƒแƒฉแƒ˜แƒœแƒ”

1. แƒแƒฅแƒขแƒ˜แƒ•แƒแƒ‘แƒ แƒ™แƒšแƒ˜แƒ”แƒœแƒขแƒ˜แƒก แƒ›แƒฎแƒแƒ แƒ”แƒก

แƒจแƒ”แƒฅแƒ›แƒ”แƒœแƒ˜แƒ— แƒžแƒแƒ“แƒ˜ แƒกแƒแƒฎแƒ”แƒšแƒ—แƒ แƒกแƒ˜แƒ•แƒ แƒชแƒ”แƒจแƒ˜ default:

// any machine
$ kubectl run exec-test-nginx --image=nginx

แƒจแƒ”แƒ›แƒ“แƒ”แƒ’ แƒฉแƒ•แƒ”แƒœ แƒ•แƒแƒกแƒ แƒฃแƒšแƒ”แƒ‘แƒ— exec แƒ‘แƒ แƒซแƒแƒœแƒ”แƒ‘แƒแƒก แƒ“แƒ แƒ•แƒ”แƒšแƒแƒ“แƒ”แƒ‘แƒ˜แƒ— 5000 แƒฌแƒแƒ›แƒก แƒจแƒ”แƒ›แƒ“แƒ’แƒแƒ›แƒ˜ แƒ“แƒแƒ™แƒ•แƒ˜แƒ แƒ•แƒ”แƒ‘แƒ˜แƒกแƒ—แƒ•แƒ˜แƒก:

// any machine
$ kubectl exec -it exec-test-nginx-6558988d5-fgxgg -- sh
# sleep 5000

แƒฉแƒœแƒ“แƒ”แƒ‘แƒ kubectl แƒžแƒ แƒแƒชแƒ”แƒกแƒ˜ (แƒฉแƒ•แƒ”แƒœแƒก แƒจแƒ”แƒ›แƒ—แƒฎแƒ•แƒ”แƒ•แƒแƒจแƒ˜ pid=8507):

// any machine
$ ps -ef |grep kubectl
501  8507  8409   0  7:19PM ttys000    0:00.13 kubectl exec -it exec-test-nginx-6558988d5-fgxgg -- sh

แƒ—แƒฃ แƒจแƒ”แƒ•แƒแƒ›แƒแƒฌแƒ›แƒ”แƒ‘แƒ— แƒžแƒ แƒแƒชแƒ”แƒกแƒ˜แƒก แƒฅแƒกแƒ”แƒšแƒฃแƒ  แƒแƒฅแƒขแƒ˜แƒ•แƒแƒ‘แƒแƒก, แƒแƒฆแƒ›แƒแƒ•แƒแƒฉแƒ”แƒœแƒ—, แƒ แƒแƒ› แƒ›แƒแƒก แƒแƒฅแƒ•แƒก แƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ”แƒ‘แƒ˜ api-แƒกแƒ”แƒ แƒ•แƒ”แƒ แƒ—แƒแƒœ (192.168.205.10.6443):

// any machine
$ netstat -atnv |grep 8507
tcp4       0      0  192.168.205.1.51673    192.168.205.10.6443    ESTABLISHED 131072 131768   8507      0 0x0102 0x00000020
tcp4       0      0  192.168.205.1.51672    192.168.205.10.6443    ESTABLISHED 131072 131768   8507      0 0x0102 0x00000028

แƒ›แƒแƒ“แƒ˜แƒ— แƒจแƒ”แƒ•แƒฎแƒ”แƒ“แƒแƒ— แƒ™แƒแƒ“แƒก. Kubectl แƒฅแƒ›แƒœแƒ˜แƒก POST แƒ›แƒแƒ—แƒฎแƒแƒ•แƒœแƒแƒก exec แƒฅแƒ•แƒ”แƒ แƒ”แƒกแƒฃแƒ แƒกแƒ˜แƒ— แƒ“แƒ แƒแƒ’แƒ–แƒแƒ•แƒœแƒ˜แƒก REST แƒ›แƒแƒ—แƒฎแƒแƒ•แƒœแƒแƒก:

              req := restClient.Post().
                        Resource("pods").
                        Name(pod.Name).
                        Namespace(pod.Namespace).
                        SubResource("exec")
                req.VersionedParams(&corev1.PodExecOptions{
                        Container: containerName,
                        Command:   p.Command,
                        Stdin:     p.Stdin,
                        Stdout:    p.Out != nil,
                        Stderr:    p.ErrOut != nil,
                        TTY:       t.Raw,
                }, scheme.ParameterCodec)

                return p.Executor.Execute("POST", req.URL(), p.Config, p.In, p.Out, p.ErrOut, t.Raw, sizeQueue)

(kubectl/pkg/cmd/exec/exec.go)

แƒ แƒแƒ’แƒแƒ  แƒ›แƒฃแƒจแƒแƒแƒ‘แƒก kubectl exec?

2. แƒแƒฅแƒขแƒ˜แƒ•แƒแƒ‘แƒ แƒกแƒแƒ›แƒแƒ’แƒ˜แƒกแƒขแƒ แƒ แƒ™แƒ•แƒแƒœแƒซแƒ˜แƒก แƒ›แƒฎแƒแƒ แƒ”แƒก

แƒฉแƒ•แƒ”แƒœ แƒแƒกแƒ”แƒ•แƒ” แƒจแƒ”แƒ’แƒ•แƒ˜แƒซแƒšแƒ˜แƒ แƒ“แƒแƒ•แƒแƒ™แƒ•แƒ˜แƒ แƒ“แƒ”แƒ— แƒ›แƒแƒ—แƒฎแƒแƒ•แƒœแƒแƒก api-แƒกแƒ”แƒ แƒ•แƒ”แƒ แƒ˜แƒก แƒ›แƒฎแƒแƒ แƒ”แƒก:

handler.go:143] kube-apiserver: POST "/api/v1/namespaces/default/pods/exec-test-nginx-6558988d5-fgxgg/exec" satisfied by gorestful with webservice /api/v1
upgradeaware.go:261] Connecting to backend proxy (intercepting redirects) https://192.168.205.11:10250/exec/default/exec-test-nginx-6558988d5-fgxgg/exec-test-nginx?command=sh&input=1&output=1&tty=1
Headers: map[Connection:[Upgrade] Content-Length:[0] Upgrade:[SPDY/3.1] User-Agent:[kubectl/v1.12.10 (darwin/amd64) kubernetes/e3c1340] X-Forwarded-For:[192.168.205.1] X-Stream-Protocol-Version:[v4.channel.k8s.io v3.channel.k8s.io v2.channel.k8s.io channel.k8s.io]]

แƒ’แƒแƒ˜แƒ—แƒ•แƒแƒšแƒ˜แƒกแƒฌแƒ˜แƒœแƒ”แƒ—, แƒ แƒแƒ› HTTP แƒ›แƒแƒ—แƒฎแƒแƒ•แƒœแƒ แƒจแƒ”แƒ˜แƒชแƒแƒ•แƒก แƒžแƒ แƒแƒขแƒแƒ™แƒแƒšแƒ˜แƒก แƒชแƒ•แƒšแƒ˜แƒšแƒ”แƒ‘แƒ˜แƒก แƒ›แƒแƒ—แƒฎแƒแƒ•แƒœแƒแƒก. SPDY แƒกแƒแƒจแƒฃแƒแƒšแƒ”แƒ‘แƒแƒก แƒแƒซแƒšแƒ”แƒ•แƒก stdin/stdout/stderr/spdy-error-แƒ˜แƒก แƒชแƒแƒšแƒ™แƒ”แƒฃแƒšแƒ˜ "แƒœแƒแƒ™แƒแƒ“แƒ”แƒ‘แƒ˜แƒก" แƒ›แƒฃแƒšแƒขแƒ˜แƒžแƒšแƒ”แƒฅแƒกแƒ˜แƒ แƒ”แƒ‘แƒแƒก แƒ”แƒ แƒ— TCP แƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ–แƒ”.

API แƒกแƒ”แƒ แƒ•แƒ”แƒ แƒ˜ แƒ˜แƒฆแƒ”แƒ‘แƒก แƒ›แƒแƒ—แƒฎแƒแƒ•แƒœแƒแƒก แƒ“แƒ แƒ’แƒแƒ แƒ“แƒแƒฅแƒ›แƒœแƒ˜แƒก แƒ›แƒแƒก PodExecOptions:

// PodExecOptions is the query options to a Pod's remote exec call
type PodExecOptions struct {
        metav1.TypeMeta

        // Stdin if true indicates that stdin is to be redirected for the exec call
        Stdin bool

        // Stdout if true indicates that stdout is to be redirected for the exec call
        Stdout bool

        // Stderr if true indicates that stderr is to be redirected for the exec call
        Stderr bool

        // TTY if true indicates that a tty will be allocated for the exec call
        TTY bool

        // Container in which to execute the command.
        Container string

        // Command is the remote command to execute; argv array; not executed within a shell.
        Command []string
}

(pkg/apis/core/types.go)

แƒกแƒแƒญแƒ˜แƒ แƒ แƒ›แƒแƒฅแƒ›แƒ”แƒ“แƒ”แƒ‘แƒ”แƒ‘แƒ˜แƒก แƒจแƒ”แƒกแƒแƒกแƒ แƒฃแƒšแƒ”แƒ‘แƒšแƒแƒ“, api-แƒกแƒ”แƒ แƒ•แƒ”แƒ แƒ›แƒ แƒฃแƒœแƒ“แƒ แƒ˜แƒชแƒแƒ“แƒ”แƒก แƒ แƒแƒ›แƒ”แƒš แƒžแƒแƒ“แƒก แƒฃแƒœแƒ“แƒ แƒ“แƒแƒฃแƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ“แƒ”แƒก:

// ExecLocation returns the exec URL for a pod container. If opts.Container is blank
// and only one container is present in the pod, that container is used.
func ExecLocation(
        getter ResourceGetter,
        connInfo client.ConnectionInfoGetter,
        ctx context.Context,
        name string,
        opts *api.PodExecOptions,
) (*url.URL, http.RoundTripper, error) {
        return streamLocation(getter, connInfo, ctx, name, opts, opts.Container, "exec")
}

(pkg/registry/core/pod/strategy.go)

แƒ แƒ แƒ—แƒฅแƒ›แƒ แƒฃแƒœแƒ“แƒ, แƒกแƒแƒ‘แƒแƒšแƒแƒ แƒฌแƒ”แƒ แƒขแƒ˜แƒšแƒ˜แƒก แƒจแƒ”แƒกแƒแƒฎแƒ”แƒ‘ แƒ›แƒแƒœแƒแƒชแƒ”แƒ›แƒ”แƒ‘แƒ˜ แƒแƒฆแƒ”แƒ‘แƒฃแƒšแƒ˜แƒ แƒ™แƒ•แƒแƒœแƒซแƒ˜แƒก แƒจแƒ”แƒกแƒแƒฎแƒ”แƒ‘ แƒ˜แƒœแƒคแƒแƒ แƒ›แƒแƒชแƒ˜แƒ˜แƒกแƒ’แƒแƒœ:

        nodeName := types.NodeName(pod.Spec.NodeName)
        if len(nodeName) == 0 {
                // If pod has not been assigned a host, return an empty location
                return nil, nil, errors.NewBadRequest(fmt.Sprintf("pod %s does not have a host assigned", name))
        }
        nodeInfo, err := connInfo.GetConnectionInfo(ctx, nodeName)

(pkg/registry/core/pod/strategy.go)

แƒฐแƒแƒ! แƒ™แƒฃแƒ‘แƒ”แƒšแƒ”แƒขแƒก แƒแƒฎแƒšแƒ แƒแƒฅแƒ•แƒก แƒžแƒแƒ แƒขแƒ˜ (node.Status.DaemonEndpoints.KubeletEndpoint.Port) แƒ แƒแƒ›แƒ”แƒšแƒ—แƒแƒœแƒแƒช API แƒกแƒ”แƒ แƒ•แƒ”แƒ แƒก แƒจแƒ”แƒฃแƒซแƒšแƒ˜แƒ แƒ“แƒแƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ”แƒ‘แƒ:

// GetConnectionInfo retrieves connection info from the status of a Node API object.
func (k *NodeConnectionInfoGetter) GetConnectionInfo(ctx context.Context, nodeName types.NodeName) (*ConnectionInfo, error) {
        node, err := k.nodes.Get(ctx, string(nodeName), metav1.GetOptions{})
        if err != nil {
                return nil, err
        }

        // Find a kubelet-reported address, using preferred address type
        host, err := nodeutil.GetPreferredNodeAddress(node, k.preferredAddressTypes)
        if err != nil {
                return nil, err
        }

        // Use the kubelet-reported port, if present
        port := int(node.Status.DaemonEndpoints.KubeletEndpoint.Port)
        if port <= 0 {
                port = k.defaultPort
        }

        return &ConnectionInfo{
                Scheme:    k.scheme,
                Hostname:  host,
                Port:      strconv.Itoa(port),
                Transport: k.transport,
        }, nil
}

(pkg/kubelet/client/kubelet_client.go)

แƒ“แƒแƒ™แƒฃแƒ›แƒ”แƒœแƒขแƒแƒชแƒ˜แƒ˜แƒ“แƒแƒœ Master-Node Communication > Master to Cluster > apiserver to kubelet:

แƒ”แƒก แƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ”แƒ‘แƒ˜ แƒฌแƒงแƒ“แƒ”แƒ‘แƒ kubelet-แƒ˜แƒก HTTPS แƒ‘แƒแƒšแƒ แƒฌแƒ”แƒ แƒขแƒ˜แƒšแƒจแƒ˜. แƒœแƒแƒ’แƒฃแƒšแƒ˜แƒกแƒฎแƒ›แƒ”แƒ•แƒแƒ“, apiserver แƒแƒ  แƒแƒ›แƒแƒฌแƒ›แƒ”แƒ‘แƒก kubelet-แƒ˜แƒก แƒกแƒ”แƒ แƒ—แƒ˜แƒคแƒ˜แƒ™แƒแƒขแƒก, แƒ แƒแƒช แƒ™แƒแƒ•แƒจแƒ˜แƒ แƒก แƒ“แƒแƒฃแƒชแƒ•แƒ”แƒšแƒก แƒฎแƒ“แƒ˜แƒก "แƒแƒ“แƒแƒ›แƒ˜แƒแƒœแƒ˜แƒก แƒจแƒฃแƒแƒ’แƒฃแƒšแƒจแƒ˜ แƒจแƒ”แƒขแƒ”แƒ•แƒ”แƒ‘แƒ˜แƒก" (MITM) แƒ“แƒ แƒกแƒแƒฎแƒ˜แƒคแƒแƒ—แƒ แƒแƒ แƒแƒกแƒแƒœแƒ“แƒ แƒ“แƒ/แƒแƒœ แƒกแƒแƒฏแƒแƒ แƒ แƒฅแƒกแƒ”แƒšแƒ”แƒ‘แƒจแƒ˜ แƒ›แƒฃแƒจแƒแƒแƒ‘แƒ.

แƒแƒฎแƒšแƒ API แƒกแƒ”แƒ แƒ•แƒ”แƒ แƒ›แƒ แƒ˜แƒชแƒ˜แƒก แƒกแƒแƒ‘แƒแƒšแƒแƒ แƒฌแƒ”แƒ แƒขแƒ˜แƒšแƒ˜ แƒ“แƒ แƒแƒ›แƒงแƒแƒ แƒ”แƒ‘แƒก แƒ™แƒแƒ•แƒจแƒ˜แƒ แƒก:

// Connect returns a handler for the pod exec proxy
func (r *ExecREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
        execOpts, ok := opts.(*api.PodExecOptions)
        if !ok {
                return nil, fmt.Errorf("invalid options object: %#v", opts)
        }
        location, transport, err := pod.ExecLocation(r.Store, r.KubeletConn, ctx, name, execOpts)
        if err != nil {
                return nil, err
        }
        return newThrottledUpgradeAwareProxyHandler(location, transport, false, true, true, responder), nil
}

(pkg/registry/core/pod/rest/subresources.go)

แƒ•แƒœแƒแƒฎแƒแƒ—, แƒ แƒ แƒฎแƒ“แƒ”แƒ‘แƒ แƒ›แƒ—แƒแƒ•แƒแƒ  แƒ™แƒ•แƒแƒœแƒซแƒ–แƒ”.

แƒžแƒ˜แƒ แƒ•แƒ”แƒšแƒ˜, แƒ’แƒแƒแƒ แƒ™แƒ•แƒ˜แƒ”แƒ— แƒกแƒแƒ›แƒฃแƒจแƒแƒ แƒ™แƒ•แƒแƒœแƒซแƒ˜แƒก IP. แƒฉแƒ•แƒ”แƒœแƒก แƒจแƒ”แƒ›แƒ—แƒฎแƒ•แƒ”แƒ•แƒแƒจแƒ˜, แƒ”แƒก แƒแƒ แƒ˜แƒก 192.168.205.11:

// any machine
$ kubectl get nodes k8s-node-1 -o wide
NAME         STATUS   ROLES    AGE   VERSION   INTERNAL-IP      EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION      CONTAINER-RUNTIME
k8s-node-1   Ready    <none>   9h    v1.15.3   192.168.205.11   <none>        Ubuntu 16.04.6 LTS   4.4.0-159-generic   docker://17.3.3

แƒจแƒ”แƒ›แƒ“แƒ”แƒ’ แƒ“แƒแƒแƒงแƒ”แƒœแƒ”แƒ— แƒ™แƒฃแƒ‘แƒ”แƒšแƒ”แƒขแƒ˜แƒก แƒžแƒแƒ แƒขแƒ˜ (แƒฉแƒ•แƒ”แƒœแƒก แƒจแƒ”แƒ›แƒ—แƒฎแƒ•แƒ”แƒ•แƒแƒจแƒ˜ 10250):

// any machine
$ kubectl get nodes k8s-node-1 -o jsonpath='{.status.daemonEndpoints.kubeletEndpoint}'
map[Port:10250]

แƒแƒฎแƒšแƒ แƒ“แƒ แƒแƒ แƒจแƒ”แƒแƒ›แƒแƒฌแƒ›แƒแƒ— แƒฅแƒกแƒ”แƒšแƒ˜. แƒแƒ แƒ˜แƒก แƒ—แƒฃ แƒแƒ แƒ แƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ˜ แƒ›แƒฃแƒจแƒ แƒ™แƒ•แƒแƒœแƒซแƒ—แƒแƒœ (192.168.205.11)? แฒ”แƒก แƒแƒ แƒ˜แƒก! แƒ—แƒฃ โ€žแƒ›แƒแƒ™แƒšแƒแƒ•โ€œ แƒžแƒ แƒแƒชแƒ”แƒกแƒก exec, แƒ’แƒแƒฅแƒ แƒ”แƒ‘แƒ, แƒแƒ›แƒ˜แƒขแƒแƒ› แƒ•แƒ˜แƒชแƒ˜, แƒ แƒแƒ› แƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ˜ แƒ“แƒแƒ›แƒงแƒแƒ แƒ“แƒ api-แƒกแƒ”แƒ แƒ•แƒ”แƒ แƒ˜แƒก แƒ›แƒ˜แƒ”แƒ  แƒจแƒ”แƒกแƒ แƒฃแƒšแƒ”แƒ‘แƒฃแƒšแƒ˜ exec แƒ‘แƒ แƒซแƒแƒœแƒ”แƒ‘แƒ˜แƒก แƒจแƒ”แƒ“แƒ”แƒ’แƒแƒ“.

// master node
$ netstat -atn |grep 192.168.205.11
tcp        0      0 192.168.205.10:37870    192.168.205.11:10250    ESTABLISHED
โ€ฆ

แƒ แƒแƒ’แƒแƒ  แƒ›แƒฃแƒจแƒแƒแƒ‘แƒก kubectl exec?

แƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ˜ kubectl-แƒกแƒ แƒ“แƒ api-แƒกแƒ”แƒ แƒ•แƒ”แƒ แƒก แƒจแƒแƒ แƒ˜แƒก แƒฏแƒ”แƒ  แƒ™แƒ˜แƒ“แƒ”แƒ• แƒฆแƒ˜แƒแƒ. แƒ’แƒแƒ แƒ“แƒ แƒแƒ›แƒ˜แƒกแƒ, แƒแƒ แƒกแƒ”แƒ‘แƒแƒ‘แƒก แƒ™แƒ˜แƒ“แƒ”แƒ• แƒ”แƒ แƒ—แƒ˜ แƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ˜, แƒ แƒแƒ›แƒ”แƒšแƒ˜แƒช แƒแƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ”แƒ‘แƒก api-แƒกแƒ”แƒ แƒ•แƒ”แƒ แƒกแƒ แƒ“แƒ แƒ™แƒฃแƒ‘แƒ”แƒšแƒ”แƒขแƒก.

3. แƒแƒฅแƒขแƒ˜แƒ•แƒแƒ‘แƒ แƒ›แƒฃแƒจแƒ แƒ™แƒ•แƒแƒœแƒซแƒ–แƒ”

แƒแƒฎแƒšแƒ แƒ›แƒแƒ“แƒ˜แƒ— แƒ“แƒแƒ•แƒฃแƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ“แƒ”แƒ— แƒ›แƒฃแƒจแƒ แƒ™แƒ•แƒแƒœแƒซแƒก แƒ“แƒ แƒ•แƒœแƒแƒฎแƒแƒ— แƒ แƒ แƒฎแƒ“แƒ”แƒ‘แƒ แƒ›แƒแƒกแƒ–แƒ”.

แƒฃแƒžแƒ˜แƒ แƒ•แƒ”แƒšแƒ”แƒก แƒงแƒแƒ•แƒšแƒ˜แƒกแƒ, แƒ•แƒฎแƒ”แƒ“แƒแƒ•แƒ—, แƒ แƒแƒ› แƒ›แƒแƒกแƒ—แƒแƒœ แƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ˜แƒช แƒ›แƒงแƒแƒ แƒ“แƒ”แƒ‘แƒ (แƒ›แƒ”แƒแƒ แƒ” แƒฎแƒแƒ–แƒ˜); 192.168.205.10 แƒแƒ แƒ˜แƒก แƒ›แƒ—แƒแƒ•แƒแƒ แƒ˜ แƒ™แƒ•แƒแƒœแƒซแƒ˜แƒก IP:

 // worker node
  $ netstat -atn |grep 10250
  tcp6       0      0 :::10250                :::*                    LISTEN
  tcp6       0      0 192.168.205.11:10250    192.168.205.10:37870    ESTABLISHED

แƒ แƒแƒช แƒจแƒ”แƒ”แƒฎแƒ”แƒ‘แƒ แƒฉแƒ•แƒ”แƒœแƒก แƒ’แƒฃแƒœแƒ“แƒก sleep? แƒฐแƒแƒ แƒ˜, แƒ˜แƒกแƒ˜แƒช แƒ˜แƒฅ แƒแƒ แƒ˜แƒก!

 // worker node
  $ ps -afx
  ...
  31463 ?        Sl     0:00      _ docker-containerd-shim 7d974065bbb3107074ce31c51f5ef40aea8dcd535ae11a7b8f2dd180b8ed583a /var/run/docker/libcontainerd/7d974065bbb3107074ce31c51
  31478 pts/0    Ss     0:00          _ sh
  31485 pts/0    S+     0:00              _ sleep 5000
  โ€ฆ

แƒ›แƒแƒ’แƒ แƒแƒ› แƒ›แƒแƒ˜แƒชแƒแƒ“แƒ”แƒ—, แƒ แƒแƒ’แƒแƒ  แƒ›แƒแƒแƒฎแƒ”แƒ แƒฎแƒ แƒ™แƒฃแƒ‘แƒ”แƒšแƒ”แƒขแƒ›แƒ แƒ”แƒก? Kubelet-แƒก แƒแƒฅแƒ•แƒก แƒ“แƒ”แƒ›แƒแƒœแƒ˜, แƒ แƒแƒ›แƒ”แƒšแƒ˜แƒช แƒฎแƒกแƒœแƒ˜แƒก แƒฌแƒ•แƒ“แƒแƒ›แƒแƒก API-แƒ–แƒ” แƒžแƒแƒ แƒขแƒ˜แƒก แƒ›แƒ”แƒจแƒ•แƒ”แƒแƒ‘แƒ˜แƒ— api-แƒกแƒ”แƒ แƒ•แƒ”แƒ แƒ˜แƒก แƒ›แƒแƒ—แƒฎแƒแƒ•แƒœแƒ”แƒ‘แƒ˜แƒกแƒ—แƒ•แƒ˜แƒก:

// Server is the library interface to serve the stream requests.
type Server interface {
        http.Handler

        // Get the serving URL for the requests.
        // Requests must not be nil. Responses may be nil iff an error is returned.
        GetExec(*runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error)
        GetAttach(req *runtimeapi.AttachRequest) (*runtimeapi.AttachResponse, error)
        GetPortForward(*runtimeapi.PortForwardRequest) (*runtimeapi.PortForwardResponse, error)

        // Start the server.
        // addr is the address to serve on (address:port) stayUp indicates whether the server should
        // listen until Stop() is called, or automatically stop after all expected connections are
        // closed. Calling Get{Exec,Attach,PortForward} increments the expected connection count.
        // Function does not return until the server is stopped.
        Start(stayUp bool) error
        // Stop the server, and terminate any open connections.
        Stop() error
}

(pkg/kubelet/server/streaming/server.go)

Kubelet แƒ˜แƒ—แƒ•แƒšแƒ˜แƒก แƒžแƒแƒกแƒฃแƒฎแƒ˜แƒก แƒกแƒแƒ‘แƒแƒšแƒแƒ แƒฌแƒ”แƒ แƒขแƒ˜แƒšแƒก exec แƒ›แƒแƒ—แƒฎแƒแƒ•แƒœแƒ”แƒ‘แƒ˜แƒกแƒ—แƒ•แƒ˜แƒก:

func (s *server) GetExec(req *runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) {
        if err := validateExecRequest(req); err != nil {
                return nil, err
        }
        token, err := s.cache.Insert(req)
        if err != nil {
                return nil, err
        }
        return &runtimeapi.ExecResponse{
                Url: s.buildURL("exec", token),
        }, nil
}

(pkg/kubelet/server/streaming/server.go)

แƒœแƒฃ แƒแƒ‘แƒœแƒ”แƒ•แƒ—. แƒ˜แƒก แƒแƒ  แƒแƒ‘แƒ แƒฃแƒœแƒ”แƒ‘แƒก แƒ‘แƒ แƒซแƒแƒœแƒ”แƒ‘แƒ˜แƒก แƒจแƒ”แƒ“แƒ”แƒ’แƒก, แƒแƒ แƒแƒ›แƒ”แƒ“ แƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ˜แƒก แƒ‘แƒแƒšแƒ แƒฌแƒ”แƒ แƒขแƒ˜แƒšแƒก:

type ExecResponse struct {
        // Fully qualified URL of the exec streaming server.
        Url                  string   `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
        XXX_NoUnkeyedLiteral struct{} `json:"-"`
        XXX_sizecache        int32    `json:"-"`
}

(cri-api/pkg/apis/runtime/v1alpha2/api.pb.go)

Kubelet แƒแƒฎแƒแƒ แƒชแƒ˜แƒ”แƒšแƒ”แƒ‘แƒก แƒ˜แƒœแƒขแƒ”แƒ แƒคแƒ”แƒ˜แƒกแƒก RuntimeServiceClient, แƒ แƒแƒ›แƒ”แƒšแƒ˜แƒช Container Runtime Interface-แƒ˜แƒก แƒœแƒแƒฌแƒ˜แƒšแƒ˜แƒ (แƒแƒ›แƒแƒ–แƒ” แƒ›แƒ”แƒขแƒ˜ แƒ“แƒแƒ•แƒฌแƒ”แƒ แƒ”แƒ—, แƒ›แƒแƒ’แƒแƒšแƒ˜แƒ—แƒแƒ“, แƒแƒฅ - แƒ“แƒแƒแƒฎแƒš. แƒ—แƒแƒ แƒ’แƒ›แƒแƒœแƒ˜.):

แƒ’แƒ แƒซแƒ”แƒšแƒ˜ แƒกแƒ˜แƒ cri-api-แƒ“แƒแƒœ kubernetes/kubernetes-แƒจแƒ˜

// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type RuntimeServiceClient interface {
        // Version returns the runtime name, runtime version, and runtime API version.
        Version(ctx context.Context, in *VersionRequest, opts ...grpc.CallOption) (*VersionResponse, error)
        // RunPodSandbox creates and starts a pod-level sandbox. Runtimes must ensure
        // the sandbox is in the ready state on success.
        RunPodSandbox(ctx context.Context, in *RunPodSandboxRequest, opts ...grpc.CallOption) (*RunPodSandboxResponse, error)
        // StopPodSandbox stops any running process that is part of the sandbox and
        // reclaims network resources (e.g., IP addresses) allocated to the sandbox.
        // If there are any running containers in the sandbox, they must be forcibly
        // terminated.
        // This call is idempotent, and must not return an error if all relevant
        // resources have already been reclaimed. kubelet will call StopPodSandbox
        // at least once before calling RemovePodSandbox. It will also attempt to
        // reclaim resources eagerly, as soon as a sandbox is not needed. Hence,
        // multiple StopPodSandbox calls are expected.
        StopPodSandbox(ctx context.Context, in *StopPodSandboxRequest, opts ...grpc.CallOption) (*StopPodSandboxResponse, error)
        // RemovePodSandbox removes the sandbox. If there are any running containers
        // in the sandbox, they must be forcibly terminated and removed.
        // This call is idempotent, and must not return an error if the sandbox has
        // already been removed.
        RemovePodSandbox(ctx context.Context, in *RemovePodSandboxRequest, opts ...grpc.CallOption) (*RemovePodSandboxResponse, error)
        // PodSandboxStatus returns the status of the PodSandbox. If the PodSandbox is not
        // present, returns an error.
        PodSandboxStatus(ctx context.Context, in *PodSandboxStatusRequest, opts ...grpc.CallOption) (*PodSandboxStatusResponse, error)
        // ListPodSandbox returns a list of PodSandboxes.
        ListPodSandbox(ctx context.Context, in *ListPodSandboxRequest, opts ...grpc.CallOption) (*ListPodSandboxResponse, error)
        // CreateContainer creates a new container in specified PodSandbox
        CreateContainer(ctx context.Context, in *CreateContainerRequest, opts ...grpc.CallOption) (*CreateContainerResponse, error)
        // StartContainer starts the container.
        StartContainer(ctx context.Context, in *StartContainerRequest, opts ...grpc.CallOption) (*StartContainerResponse, error)
        // StopContainer stops a running container with a grace period (i.e., timeout).
        // This call is idempotent, and must not return an error if the container has
        // already been stopped.
        // TODO: what must the runtime do after the grace period is reached?
        StopContainer(ctx context.Context, in *StopContainerRequest, opts ...grpc.CallOption) (*StopContainerResponse, error)
        // RemoveContainer removes the container. If the container is running, the
        // container must be forcibly removed.
        // This call is idempotent, and must not return an error if the container has
        // already been removed.
        RemoveContainer(ctx context.Context, in *RemoveContainerRequest, opts ...grpc.CallOption) (*RemoveContainerResponse, error)
        // ListContainers lists all containers by filters.
        ListContainers(ctx context.Context, in *ListContainersRequest, opts ...grpc.CallOption) (*ListContainersResponse, error)
        // ContainerStatus returns status of the container. If the container is not
        // present, returns an error.
        ContainerStatus(ctx context.Context, in *ContainerStatusRequest, opts ...grpc.CallOption) (*ContainerStatusResponse, error)
        // UpdateContainerResources updates ContainerConfig of the container.
        UpdateContainerResources(ctx context.Context, in *UpdateContainerResourcesRequest, opts ...grpc.CallOption) (*UpdateContainerResourcesResponse, error)
        // ReopenContainerLog asks runtime to reopen the stdout/stderr log file
        // for the container. This is often called after the log file has been
        // rotated. If the container is not running, container runtime can choose
        // to either create a new log file and return nil, or return an error.
        // Once it returns error, new container log file MUST NOT be created.
        ReopenContainerLog(ctx context.Context, in *ReopenContainerLogRequest, opts ...grpc.CallOption) (*ReopenContainerLogResponse, error)
        // ExecSync runs a command in a container synchronously.
        ExecSync(ctx context.Context, in *ExecSyncRequest, opts ...grpc.CallOption) (*ExecSyncResponse, error)
        // Exec prepares a streaming endpoint to execute a command in the container.
        Exec(ctx context.Context, in *ExecRequest, opts ...grpc.CallOption) (*ExecResponse, error)
        // Attach prepares a streaming endpoint to attach to a running container.
        Attach(ctx context.Context, in *AttachRequest, opts ...grpc.CallOption) (*AttachResponse, error)
        // PortForward prepares a streaming endpoint to forward ports from a PodSandbox.
        PortForward(ctx context.Context, in *PortForwardRequest, opts ...grpc.CallOption) (*PortForwardResponse, error)
        // ContainerStats returns stats of the container. If the container does not
        // exist, the call returns an error.
        ContainerStats(ctx context.Context, in *ContainerStatsRequest, opts ...grpc.CallOption) (*ContainerStatsResponse, error)
        // ListContainerStats returns stats of all running containers.
        ListContainerStats(ctx context.Context, in *ListContainerStatsRequest, opts ...grpc.CallOption) (*ListContainerStatsResponse, error)
        // UpdateRuntimeConfig updates the runtime configuration based on the given request.
        UpdateRuntimeConfig(ctx context.Context, in *UpdateRuntimeConfigRequest, opts ...grpc.CallOption) (*UpdateRuntimeConfigResponse, error)
        // Status returns the status of the runtime.
        Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error)
}

(cri-api/pkg/apis/runtime/v1alpha2/api.pb.go)
แƒ˜แƒก แƒฃแƒ‘แƒ แƒแƒšแƒแƒ“ แƒ˜แƒงแƒ”แƒœแƒ”แƒ‘แƒก gRPC แƒ›แƒ”แƒ—แƒแƒ“แƒ˜แƒก แƒ’แƒแƒ›แƒแƒกแƒแƒซแƒแƒฎแƒ”แƒ‘แƒšแƒแƒ“ Container Runtime Interface-แƒ˜แƒก แƒ›แƒ”แƒจแƒ•แƒ”แƒแƒ‘แƒ˜แƒ—:

type runtimeServiceClient struct {
        cc *grpc.ClientConn
}

(cri-api/pkg/apis/runtime/v1alpha2/api.pb.go)

func (c *runtimeServiceClient) Exec(ctx context.Context, in *ExecRequest, opts ...grpc.CallOption) (*ExecResponse, error) {
        out := new(ExecResponse)
        err := c.cc.Invoke(ctx, "/runtime.v1alpha2.RuntimeService/Exec", in, out, opts...)
        if err != nil {
                return nil, err
        }
        return out, nil
}

(cri-api/pkg/apis/runtime/v1alpha2/api.pb.go)

Container Runtime แƒžแƒแƒกแƒฃแƒฎแƒ˜แƒกแƒ›แƒ’แƒ”แƒ‘แƒ”แƒšแƒ˜แƒ แƒ’แƒแƒœแƒฎแƒแƒ แƒชแƒ˜แƒ”แƒšแƒ”แƒ‘แƒแƒ–แƒ” RuntimeServiceServer:

แƒ’แƒ แƒซแƒ”แƒšแƒ˜ แƒกแƒ˜แƒ cri-api-แƒ“แƒแƒœ kubernetes/kubernetes-แƒจแƒ˜

// RuntimeServiceServer is the server API for RuntimeService service.
type RuntimeServiceServer interface {
        // Version returns the runtime name, runtime version, and runtime API version.
        Version(context.Context, *VersionRequest) (*VersionResponse, error)
        // RunPodSandbox creates and starts a pod-level sandbox. Runtimes must ensure
        // the sandbox is in the ready state on success.
        RunPodSandbox(context.Context, *RunPodSandboxRequest) (*RunPodSandboxResponse, error)
        // StopPodSandbox stops any running process that is part of the sandbox and
        // reclaims network resources (e.g., IP addresses) allocated to the sandbox.
        // If there are any running containers in the sandbox, they must be forcibly
        // terminated.
        // This call is idempotent, and must not return an error if all relevant
        // resources have already been reclaimed. kubelet will call StopPodSandbox
        // at least once before calling RemovePodSandbox. It will also attempt to
        // reclaim resources eagerly, as soon as a sandbox is not needed. Hence,
        // multiple StopPodSandbox calls are expected.
        StopPodSandbox(context.Context, *StopPodSandboxRequest) (*StopPodSandboxResponse, error)
        // RemovePodSandbox removes the sandbox. If there are any running containers
        // in the sandbox, they must be forcibly terminated and removed.
        // This call is idempotent, and must not return an error if the sandbox has
        // already been removed.
        RemovePodSandbox(context.Context, *RemovePodSandboxRequest) (*RemovePodSandboxResponse, error)
        // PodSandboxStatus returns the status of the PodSandbox. If the PodSandbox is not
        // present, returns an error.
        PodSandboxStatus(context.Context, *PodSandboxStatusRequest) (*PodSandboxStatusResponse, error)
        // ListPodSandbox returns a list of PodSandboxes.
        ListPodSandbox(context.Context, *ListPodSandboxRequest) (*ListPodSandboxResponse, error)
        // CreateContainer creates a new container in specified PodSandbox
        CreateContainer(context.Context, *CreateContainerRequest) (*CreateContainerResponse, error)
        // StartContainer starts the container.
        StartContainer(context.Context, *StartContainerRequest) (*StartContainerResponse, error)
        // StopContainer stops a running container with a grace period (i.e., timeout).
        // This call is idempotent, and must not return an error if the container has
        // already been stopped.
        // TODO: what must the runtime do after the grace period is reached?
        StopContainer(context.Context, *StopContainerRequest) (*StopContainerResponse, error)
        // RemoveContainer removes the container. If the container is running, the
        // container must be forcibly removed.
        // This call is idempotent, and must not return an error if the container has
        // already been removed.
        RemoveContainer(context.Context, *RemoveContainerRequest) (*RemoveContainerResponse, error)
        // ListContainers lists all containers by filters.
        ListContainers(context.Context, *ListContainersRequest) (*ListContainersResponse, error)
        // ContainerStatus returns status of the container. If the container is not
        // present, returns an error.
        ContainerStatus(context.Context, *ContainerStatusRequest) (*ContainerStatusResponse, error)
        // UpdateContainerResources updates ContainerConfig of the container.
        UpdateContainerResources(context.Context, *UpdateContainerResourcesRequest) (*UpdateContainerResourcesResponse, error)
        // ReopenContainerLog asks runtime to reopen the stdout/stderr log file
        // for the container. This is often called after the log file has been
        // rotated. If the container is not running, container runtime can choose
        // to either create a new log file and return nil, or return an error.
        // Once it returns error, new container log file MUST NOT be created.
        ReopenContainerLog(context.Context, *ReopenContainerLogRequest) (*ReopenContainerLogResponse, error)
        // ExecSync runs a command in a container synchronously.
        ExecSync(context.Context, *ExecSyncRequest) (*ExecSyncResponse, error)
        // Exec prepares a streaming endpoint to execute a command in the container.
        Exec(context.Context, *ExecRequest) (*ExecResponse, error)
        // Attach prepares a streaming endpoint to attach to a running container.
        Attach(context.Context, *AttachRequest) (*AttachResponse, error)
        // PortForward prepares a streaming endpoint to forward ports from a PodSandbox.
        PortForward(context.Context, *PortForwardRequest) (*PortForwardResponse, error)
        // ContainerStats returns stats of the container. If the container does not
        // exist, the call returns an error.
        ContainerStats(context.Context, *ContainerStatsRequest) (*ContainerStatsResponse, error)
        // ListContainerStats returns stats of all running containers.
        ListContainerStats(context.Context, *ListContainerStatsRequest) (*ListContainerStatsResponse, error)
        // UpdateRuntimeConfig updates the runtime configuration based on the given request.
        UpdateRuntimeConfig(context.Context, *UpdateRuntimeConfigRequest) (*UpdateRuntimeConfigResponse, error)
        // Status returns the status of the runtime.
        Status(context.Context, *StatusRequest) (*StatusResponse, error)
}

(cri-api/pkg/apis/runtime/v1alpha2/api.pb.go)
แƒ แƒแƒ’แƒแƒ  แƒ›แƒฃแƒจแƒแƒแƒ‘แƒก kubectl exec?

แƒ—แƒฃ แƒแƒกแƒ”แƒ, แƒฉแƒ•แƒ”แƒœ แƒฃแƒœแƒ“แƒ แƒ“แƒแƒ•แƒ˜แƒœแƒแƒฎแƒแƒ— แƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ˜ แƒ™แƒฃแƒ‘แƒ”แƒšแƒ”แƒขแƒกแƒ แƒ“แƒ แƒ™แƒแƒœแƒขแƒ”แƒ˜แƒœแƒ”แƒ แƒ˜แƒก แƒ’แƒแƒจแƒ•แƒ”แƒ‘แƒแƒก แƒจแƒแƒ แƒ˜แƒก, แƒแƒ แƒ? แƒจแƒ”แƒ•แƒแƒ›แƒแƒฌแƒ›แƒแƒ—.

แƒ’แƒแƒฃแƒจแƒ•แƒ˜แƒ— แƒ”แƒก แƒ‘แƒ แƒซแƒแƒœแƒ”แƒ‘แƒ exec แƒ‘แƒ แƒซแƒแƒœแƒ”แƒ‘แƒ˜แƒก แƒฌแƒ˜แƒœ แƒ“แƒ แƒจแƒ”แƒ›แƒ“แƒ”แƒ’ แƒ“แƒ แƒœแƒแƒฎแƒ”แƒ— แƒ’แƒแƒœแƒกแƒฎแƒ•แƒแƒ•แƒ”แƒ‘แƒ. แƒฉแƒ”แƒ›แƒก แƒจแƒ”แƒ›แƒ—แƒฎแƒ•แƒ”แƒ•แƒแƒจแƒ˜ แƒ’แƒแƒœแƒกแƒฎแƒ•แƒแƒ•แƒ”แƒ‘แƒแƒ:

// worker node
$ ss -a -p |grep kubelet
...
u_str  ESTAB      0      0       * 157937                * 157387                users:(("kubelet",pid=5714,fd=33))
...

แƒฐแƒ›... แƒแƒฎแƒแƒšแƒ˜ unix แƒกแƒแƒ™แƒ”แƒขแƒ˜แƒก แƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ˜ แƒ™แƒฃแƒ‘แƒ”แƒšแƒ”แƒขแƒก (pid=5714) แƒ“แƒ แƒ แƒแƒฆแƒแƒช แƒฃแƒชแƒœแƒแƒ‘แƒก แƒจแƒแƒ แƒ˜แƒก. แƒ แƒ แƒจแƒ”แƒ˜แƒซแƒšแƒ”แƒ‘แƒ แƒ˜แƒงแƒแƒก? แƒ›แƒแƒ แƒ—แƒแƒšแƒ˜แƒ, แƒ”แƒก แƒแƒ แƒ˜แƒก Docker (pid=1186)!

// worker node
$ ss -a -p |grep 157387
...
u_str  ESTAB      0      0       * 157937                * 157387                users:(("kubelet",pid=5714,fd=33))
u_str  ESTAB      0      0      /var/run/docker.sock 157387                * 157937                users:(("dockerd",pid=1186,fd=14))
...

แƒ แƒแƒ’แƒแƒ แƒช แƒ’แƒแƒฎแƒกแƒแƒ•แƒ—, แƒ”แƒก แƒแƒ แƒ˜แƒก แƒ“แƒแƒ™แƒ”แƒ แƒ˜แƒก แƒ“แƒ”แƒ›แƒแƒœแƒ˜แƒก แƒžแƒ แƒแƒชแƒ”แƒกแƒ˜ (pid=1186), แƒ แƒแƒ›แƒ”แƒšแƒ˜แƒช แƒแƒกแƒ แƒฃแƒšแƒ”แƒ‘แƒก แƒฉแƒ•แƒ”แƒœแƒก แƒ‘แƒ แƒซแƒแƒœแƒ”แƒ‘แƒแƒก:

// worker node
$ ps -afx
...
 1186 ?        Ssl    0:55 /usr/bin/dockerd -H fd://
17784 ?        Sl     0:00      _ docker-containerd-shim 53a0a08547b2f95986402d7f3b3e78702516244df049ba6c5aa012e81264aa3c /var/run/docker/libcontainerd/53a0a08547b2f95986402d7f3
17801 pts/2    Ss     0:00          _ sh
17827 pts/2    S+     0:00              _ sleep 5000
...

4. แƒแƒฅแƒขแƒ˜แƒ•แƒแƒ‘แƒ แƒ™แƒแƒœแƒขแƒ”แƒ˜แƒœแƒ”แƒ แƒ˜แƒก แƒ›แƒฃแƒจแƒแƒแƒ‘แƒ˜แƒก แƒ“แƒ แƒแƒก

แƒ›แƒแƒ“แƒ˜แƒ— แƒจแƒ”แƒ•แƒแƒ›แƒแƒฌแƒ›แƒแƒ— CRI-O แƒฌแƒงแƒแƒ แƒแƒก แƒ™แƒแƒ“แƒ˜, แƒ แƒแƒ—แƒ แƒ’แƒแƒ•แƒ˜แƒ’แƒแƒ— แƒ แƒ แƒฎแƒ“แƒ”แƒ‘แƒ. แƒ“แƒแƒ™แƒ”แƒ แƒจแƒ˜ แƒšแƒแƒ’แƒ˜แƒ™แƒ แƒ›แƒกแƒ’แƒแƒ•แƒกแƒ˜แƒ.

แƒแƒ แƒกแƒ”แƒ‘แƒแƒ‘แƒก แƒกแƒ”แƒ แƒ•แƒ”แƒ แƒ˜, แƒ แƒแƒ›แƒ”แƒšแƒ˜แƒช แƒžแƒแƒกแƒฃแƒฎแƒ˜แƒกแƒ›แƒ’แƒ”แƒ‘แƒ”แƒšแƒ˜แƒ แƒ’แƒแƒœแƒฎแƒแƒ แƒชแƒ˜แƒ”แƒšแƒ”แƒ‘แƒแƒ–แƒ” RuntimeServiceServer:

// Server implements the RuntimeService and ImageService
type Server struct {
        config          libconfig.Config
        seccompProfile  *seccomp.Seccomp
        stream          StreamService
        netPlugin       ocicni.CNIPlugin
        hostportManager hostport.HostPortManager

        appArmorProfile string
        hostIP          string
        bindAddress     string

        *lib.ContainerServer
        monitorsChan      chan struct{}
        defaultIDMappings *idtools.IDMappings
        systemContext     *types.SystemContext // Never nil

        updateLock sync.RWMutex

        seccompEnabled  bool
        appArmorEnabled bool
}

(cri-o/server/server.go)

// Exec prepares a streaming endpoint to execute a command in the container.
func (s *Server) Exec(ctx context.Context, req *pb.ExecRequest) (resp *pb.ExecResponse, err error) {
        const operation = "exec"
        defer func() {
                recordOperation(operation, time.Now())
                recordError(operation, err)
        }()

        resp, err = s.getExec(req)
        if err != nil {
                return nil, fmt.Errorf("unable to prepare exec endpoint: %v", err)
        }

        return resp, nil
}

(cri-o/server/container_exec.go)

แƒฏแƒแƒญแƒ•แƒ˜แƒก แƒ‘แƒแƒšแƒแƒก, แƒ™แƒแƒœแƒขแƒ”แƒ˜แƒœแƒ”แƒ แƒ˜แƒก แƒ’แƒแƒจแƒ•แƒ”แƒ‘แƒ˜แƒก แƒ“แƒ แƒ แƒแƒกแƒ แƒฃแƒšแƒ”แƒ‘แƒก แƒ‘แƒ แƒซแƒแƒœแƒ”แƒ‘แƒแƒก แƒ›แƒฃแƒจแƒ แƒ™แƒ•แƒแƒœแƒซแƒ–แƒ”:

// ExecContainer prepares a streaming endpoint to execute a command in the container.
func (r *runtimeOCI) ExecContainer(c *Container, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan remotecommand.TerminalSize) error {
        processFile, err := prepareProcessExec(c, cmd, tty)
        if err != nil {
                return err
        }
        defer os.RemoveAll(processFile.Name())

        args := []string{rootFlag, r.root, "exec"}
        args = append(args, "--process", processFile.Name(), c.ID())
        execCmd := exec.Command(r.path, args...)
        if v, found := os.LookupEnv("XDG_RUNTIME_DIR"); found {
                execCmd.Env = append(execCmd.Env, fmt.Sprintf("XDG_RUNTIME_DIR=%s", v))
        }
        var cmdErr, copyError error
        if tty {
                cmdErr = ttyCmd(execCmd, stdin, stdout, resize)
        } else {
                if stdin != nil {
                        // Use an os.Pipe here as it returns true *os.File objects.
                        // This way, if you run 'kubectl exec <pod> -i bash' (no tty) and type 'exit',
                        // the call below to execCmd.Run() can unblock because its Stdin is the read half
                        // of the pipe.
                        r, w, err := os.Pipe()
                        if err != nil {
                                return err
                        }
                        go func() { _, copyError = pools.Copy(w, stdin) }()

                        execCmd.Stdin = r
                }
                if stdout != nil {
                        execCmd.Stdout = stdout
                }
                if stderr != nil {
                        execCmd.Stderr = stderr
                }

                cmdErr = execCmd.Run()
        }

        if copyError != nil {
                return copyError
        }
        if exitErr, ok := cmdErr.(*exec.ExitError); ok {
                return &utilexec.ExitErrorWrapper{ExitError: exitErr}
        }
        return cmdErr
}

(cri-o/internal/oci/runtime_oci.go)

แƒ แƒแƒ’แƒแƒ  แƒ›แƒฃแƒจแƒแƒแƒ‘แƒก kubectl exec?

แƒกแƒแƒ‘แƒแƒšแƒแƒแƒ“, แƒ‘แƒ˜แƒ แƒ—แƒ•แƒ˜ แƒแƒกแƒ แƒฃแƒšแƒ”แƒ‘แƒก แƒ‘แƒ แƒซแƒแƒœแƒ”แƒ‘แƒ”แƒ‘แƒก:

แƒ แƒแƒ’แƒแƒ  แƒ›แƒฃแƒจแƒแƒแƒ‘แƒก kubectl exec?

แƒจแƒ”แƒฎแƒกแƒ”แƒœแƒ”แƒ‘แƒ”แƒ‘แƒ˜

  • API แƒกแƒ”แƒ แƒ•แƒ”แƒ แƒก แƒแƒกแƒ”แƒ•แƒ” แƒจแƒ”แƒฃแƒซแƒšแƒ˜แƒ แƒ™แƒฃแƒ‘แƒ”แƒšแƒ”แƒขแƒ—แƒแƒœ แƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ˜แƒก แƒ˜แƒœแƒ˜แƒชแƒ˜แƒแƒšแƒ˜แƒ–แƒแƒชแƒ˜แƒ.
  • แƒจแƒ”แƒ›แƒ“แƒ”แƒ’แƒ˜ แƒ™แƒแƒ•แƒจแƒ˜แƒ แƒ”แƒ‘แƒ˜ แƒจแƒ”แƒœแƒแƒ แƒฉแƒฃแƒœแƒ”แƒ‘แƒฃแƒšแƒ˜แƒ แƒ˜แƒœแƒขแƒ”แƒ แƒแƒฅแƒขแƒ˜แƒฃแƒšแƒ˜ exec แƒกแƒ”แƒกแƒ˜แƒ˜แƒก แƒ“แƒแƒกแƒ แƒฃแƒšแƒ”แƒ‘แƒแƒ›แƒ“แƒ”:
    • kubectl-แƒกแƒ แƒ“แƒ api-แƒกแƒ”แƒ แƒ•แƒ”แƒ แƒก แƒจแƒแƒ แƒ˜แƒก;
    • api-แƒกแƒ”แƒ แƒ•แƒ”แƒ แƒกแƒ แƒ“แƒ kubectl-แƒก แƒจแƒแƒ แƒ˜แƒก;
    • แƒ™แƒฃแƒ‘แƒ”แƒšแƒ”แƒขแƒกแƒ แƒ“แƒ แƒ™แƒแƒœแƒขแƒ”แƒ˜แƒœแƒ”แƒ แƒ˜แƒก แƒ’แƒแƒจแƒ•แƒ”แƒ‘แƒแƒก แƒจแƒแƒ แƒ˜แƒก.
  • Kubectl แƒแƒœ api-แƒกแƒ”แƒ แƒ•แƒ”แƒ แƒ˜ แƒ•แƒ”แƒ แƒแƒคแƒ”แƒ แƒก แƒฃแƒจแƒ•แƒ”แƒ‘แƒก แƒ›แƒฃแƒจแƒ แƒ™แƒ•แƒแƒœแƒซแƒ”แƒ‘แƒ–แƒ”. Kubelet-แƒก แƒจแƒ”แƒฃแƒซแƒšแƒ˜แƒ แƒแƒฌแƒแƒ แƒ›แƒแƒแƒก, แƒ›แƒแƒ’แƒ แƒแƒ› แƒ˜แƒก แƒแƒกแƒ”แƒ•แƒ” แƒฃแƒ แƒ—แƒ˜แƒ”แƒ แƒ—แƒฅแƒ›แƒ”แƒ“แƒ”แƒ‘แƒก แƒ™แƒแƒœแƒขแƒ”แƒ˜แƒœแƒ”แƒ แƒ˜แƒก แƒ›แƒฃแƒจแƒแƒแƒ‘แƒ˜แƒก แƒ“แƒ แƒแƒก แƒแƒ› แƒ›แƒแƒฅแƒ›แƒ”แƒ“แƒ”แƒ‘แƒ”แƒ‘แƒ˜แƒกแƒ—แƒ•แƒ˜แƒก.

แƒ แƒ”แƒกแƒฃแƒ แƒกแƒ”แƒ‘แƒ˜

PS แƒ›แƒ—แƒแƒ แƒ’แƒ›แƒœแƒ”แƒšแƒ˜แƒกแƒ’แƒแƒœ

แƒแƒกแƒ”แƒ•แƒ” แƒฌแƒแƒ˜แƒ™แƒ˜แƒ—แƒฎแƒ”แƒ— แƒฉแƒ•แƒ”แƒœแƒก แƒ‘แƒšแƒแƒ’แƒ–แƒ”:

แƒฌแƒงแƒแƒ แƒ: www.habr.com

แƒแƒฎแƒแƒšแƒ˜ แƒ™แƒแƒ›แƒ”แƒœแƒขแƒแƒ แƒ˜แƒก แƒ“แƒแƒ›แƒแƒขแƒ”แƒ‘แƒ