11B-43 NGUYỄN DUY TIẾN
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # K8S (Ku bé) ## Introduction ### Cách phát âm * Kiu-bơ-nấy-tịt-s * Kiu-bơ-nà-tịt-s * Kiu-bơ-nà-địt-s ### Các vấn đề khi chạy docker ở production * mỗi lần muốn có cập nhật mới thì ta phải docker compose down xong rồi up lại, khoảng thời gian giữa lúc down và up thì app của chúng ta bị shutdown, không hoạt động được * auto scale app lên được theo những thông số cụ thể * deploy xong thấy có lỗi thì làm sao ngay lập tức quay lại bản cũ * Còn nữa ### Định nghĩa * K8S là "container orchestration" - hệ thống điều phối container giúp tự động hoá việc triển khai (deploy), scale và quản lý (manage) các ứng dụng chạy trên nền container. * So sánh với docker swarm * với những app và kiến trúc phức tạp thì K8S làm tốt và phù hợp hơn, trong khi Docker Swarm hướng tới sự đơn giản khi sử dụng. * Phiên bản Enterprise của nó là Openshift ### Giúp ích gì * tự biết cách deploy và quản lý app * tự biết cách check logs, check deployment, auto scale,... * kết hợp với CICD để tự động hoá toàn bộ quá trình triển khai app * biết cấu hình các thứ ABCXYZ * Biết K8S nó cho mình làm việc độc lập hơn rất là nhiều, từ viết Dockerfile, rồi viết file manifest K8S để deploy. # Ku bé Cluster { K8S Cluster : 1 tập hợp nhiều node được kết nối với nhau node : 1 máy, 1 VPS, hay 1 server } * Môi trường works on : Ku bé Cluster * k8S điều phối các activities trong cluster: * start, stop, restart, kill các container của bạn * quản lý volume * phân quyền * đưa ra quyết định khi nào cần scale app của bạn lên * load balance (Từ từ sẽ hiểu sau) ## Node * **Nodes, Control Plane $\epsilon$ Cluster** ![](https://hackmd.io/_uploads/SJcf15ce6.png) ### Control Plane (Master node) * Brain of the Cluster * Quản lý cluster, deploy, scale app ### Node * Có thể là máy ảo VPS, physical PC * Mỗi node làm nhiệm vụ như worker bên trong cluster vậy, kiểu như anh công nhân, bên trên - Control Plane bảo gì thì làm nấy #### Node component ##### Kubelet * trưởng phòng, agent, chịu trách nhiệm giao tiếp với bên Control Plan thông qua K8S API * lắng nghe thông tin các Pod, dùng nhiều cơ chế để đảm bảo việc tạo ra các container đúng theo mô tả trong podspec được chạy (running) và không bị lỗi (healthy) * Nó cũng đồng thời cập nhật trạng thái của các container trên node cũng như thông tin của node về control plane. * Ví dụ đơn giản nhất là nếu kubelet bị stop thì node đó sẽ hiển thị trạng thái NotReady trong cluster. ##### kube-proxy * network-proxy chạy trên từng node. Nó quản lý và duy trì các network rule trên các node. ##### Container Runtime * Container Runtime là thứ để chạy container * Trc đó dùng docker, sau đó dùng containerd #### Additional * Deploy app lên K8S cluster = control plane triển khai app on node in cluster * Note: Cluster chạy cho production nên có tối thiểu 3 nodes để đảm bảo tính high-availability * Control Plane chạy trên node gọi là master node ![](https://hackmd.io/_uploads/B1Yrwcix6.png) * 2 loại chính là Master Node (còn gọi là Control Plane) và Worker Node (Data Plane) * worker node: cung cấp tài nguyên cho các app chạy * Master node nếu cần thì cũng dc cấu hình thành worker node * trong nhiều tình huống thực tế ta vẫn phải chạy master node đồng thời là worker node. #### Master node * kube-api-server * etcd * kube-scheduler * kube-controller-manager ##### kube-api-server * kube-api-server cung cấp ra các API của Kubernetes ##### etcd * một CSDL dạng key-value, dùng lưu data của cluster * data: quản lý tài nguyên của k8s : thông tin node, pod, service, deployment, configmap,... * phải có kế hoạch backup DB định kỳ. ##### kube-scheduler * Kiểu như con (pods) sinh ra mà chưa tìm thấy mẹ (gán vào node), thì thằng này là thám tử để tìm mẹ phù hợp cho thằng con (cluster là bệnh viện Hùng Vương =))) * tham số ảnh hưởng tới việc điều phối một Pod vào một node: * Các yêu cầu về tài nguyên cho Pod như RAM/CPU.. * Các ràng buộc về phần cứng phần mềm * Các chỉ định về affinity và anti-affinity.. và nhiều yếu tố khác nữa ##### kube-controler-manager * tổng hợp của nhiều controller khác nhưng chạy chung 1 process: * Node controller: quản lý xem có node nào bị down ko * Job controller: quản lý các job/tasks, sau đó tạo pods để chạy job. * Endpoints controller: quản lý các endpoints việc giao tiếp giữa pods và service * Service Account và Token controller: Tạo ra các account và token API mặc định cho namespace mới. ##### cloud-controller-manager (sử dụng với cloud) #### Thành phần Addon ##### DNS * DNS server lưu trữ các bản ghi cho các service của kubernetes. * hầu hết chúng ta làm việc với các service qua service name, và đương nhiên thì để làm được việc đó thì k8s cần có DNS. ##### Web UI (Dashboard) * Ngoài sử dụng dashboard, thì có nhiều giải pháp tương tự cũng khá phổ biến như sử dụng Rancher, Lens hay k9s.. để có giao diện quản trị với k8s cluster. * theo dõi,quản lý cũng như troubleshoot các ứng dụng chạy trên cluster và chính tình trạng của cluster ## Inside the Node * Khi deploy app, app run on the node of cluster, inside a place/instance called Pod (Pọt) * Pod = instance nhỏ nhất ta can deploy app, monitor pod. ![](https://hackmd.io/_uploads/Bki6W59ea.png) **Big Note: 1 node can Run nhiều Pod => can run 1 or nhiều container** ![](https://hackmd.io/_uploads/HyR9f5cx6.png) * Pod có cả Volume, containerized app * Xác định Pod = Gán IP (cluster IP) Ví dụ trên: 10.10.10.x ### Pod * 1 Pod run in 1 node in cluster, trong 1 pod là 1 tập hợp của 1 hay nhiều containers, các containers đó sẽ dùng chung storage (volume) và network. * Có **file template** để guide cách chạy các containers * naked pod: chạy trực tiếp, pod sẽ không được chạy lại nếu như bị failed. * cách phổ biến nhất : dùng những resource khác như Deployment hay Job để khởi tạo nó, đây cũng là cách mà ta nên dùng. ![](https://hackmd.io/_uploads/S1bbNyWZT.png) ![](https://hackmd.io/_uploads/SyMt4kWZ6.png) * Pod như 1 nhà kho chứa container * ![](https://hackmd.io/_uploads/SkBrBJ--p.png) * cung cấp cho chúng ta thêm nhiều chức năng để quản lý và chạy một container, giúp container của ta chạy tốt hơn là chạy container trực tiếp, như là group tài nguyên của container, check container healthy và restart * kubernetes sẽ quản lý Pod thay vì quản lý container trực tiếp ##### Expose pod * Khi tạo pod thì pod chỉ giao tiếp trong local, phải expose pod mới có thể chọc vào từ bên ngoài * ![](https://hackmd.io/_uploads/rJ_zegWbT.png) * ![](https://hackmd.io/_uploads/HypMxlW-p.png) ##### Tổ chức pod bằng cách sử dụng labels * Dùng label là cách để chúng ta có thể phân chia các pod khác nhau tùy thuộc vào dự án hoặc môi trường. Ví dụ công ty của chúng ta có 3 môi trường là testing, staging, production, nếu chạy pod mà không có đánh label thì chúng ta rất khó để biết pod nào thuộc môi trường nào * Labels là một thuộc tính cặp key-value mà chúng ta gán vào resource ở phần metadata * ![](https://hackmd.io/_uploads/HJ2DPg-Z6.png) ```yaml labels: enviroment: staging # label with key is enviroment and value is staging project: kubernetes-series ``` #### Giải thích file template trong pod: ```yaml apiVersion: v1 kind: Pod metadata: name: nginx spec: containers: - name: nginx image: nginx:1.19 ports: - containerPort: 80 resources: requests: memory: "64Mi" cpu: "250m" limits: memory: "128Mi" cpu: "500m" ``` * template file có tên gọi phổ biến hơn là **manifest file** * file ở **dạng YAML**, đuôi yml hoặc yaml đều được * Ở đầu của mọi manifest file ta cần phải có **apiVersion, kind và metadata** * **spec** là nơi ta chỉ định **hưỡng dẫn Pod chạy các container**, như các bạn thấy bên trong ta có trường containers là 1 array * Ở trên ta chỉ có 1 **item trong array** -> 1 container trong pod * Image là tên image của container, cái này cần phải chính xác * Bên dưới ta có định nghĩa thêm phần **resource** cần thiết cho pod này nữa, **requests** là ta "xin" bao nhiêu cho pod (tối thiểu), limits là tối đa pod được "ăn" bao nhiêu. Lí do: limit resource cho namespace nên buộc ta phải có cái này. #### Additional * 1 pod chỉ nên chạy 1 container, vì các container trong pod sẽ "tightly couple", dính chặt chẽ với nhau, scale sẽ khó, khi 1 container trên pod thay đổi thì cả pod sẽ được deploy lại ### Vòng đời của Pod (Pod lifecycle) #### Tạo Pod ![](https://hackmd.io/_uploads/rJs2Zijxp.png) * Chi tiết luồng tạo Pod: 1. Khi ta thực hiện tạo một Pod mới, thông tường là dùng lệnh kubectl để apply một file yaml là file mô tả chi thiết các thông tin cần thiết cho việc tạo Pod (kubectl apply -f nginx-pod.yaml). Bản chất lệnh kubectl sẽ làm việc với api-server để gọi một api tương ứng cho việc tạo Pod. 2. API server validate yaml file, ok thì ghi data vào etcd. Hệ thống ghi nhận 1 pod mới cần tạo. Sau khi ghi xong vào etcd, phản hồi lại cho client pod đã dc tạo. 3. scheduler sẽ check API server xem có gì mới ko, các pod mới tạo có dc gán vào node nào chưa. Nó phát hiện ra pods mới và tìm node thỏa mãn yêu cầu pod nêu ra và done (ví dụ node1). Thông tin node chạy trên sẽ gửi cho API server 4. API server nhận được thông tin Pod mới được gán vào node1 thì thực hiện update thông tin này và etcd. Lúc nào pod ở trạng thái bound. 5. Đến lượt kubelet trong node ra tay, nó phát hiện node ở trạng thái bound dc bốc lịch vào node (lấy thông tin từ api-server). Nó lấy các thông tin định nghĩ từ pod để chạy container trên node. Sau đó, update lại status cho api-server 6. API sau đó nhận dc tin từ kubelet, ghi vào etcd. Sau đó nó gửi bản tin acknowledgement tới kubelete để báo rằng event này đã được chấp nhận. #### luồng xóa Pod: 1. Người dùng gửi lệnh để xóa Pod 2. Đối tượng Pod trên k8s được cập nhật trạng thái thành "dead" sau một khoảng thời gian gọi là grace-time * Các hành động sau diễn ra song song: * Pod sẽ hiện thị ở trạng thái "Terminating" khi được kiểm tra từ phía client * Kubelet thấy một Pod được đánh dấu là Terminating thì nó bắt đầu thực hiện dừng process của Pod * Endpoint controller theo dõi pod đã được xóa chưa để xóa thông tin pod đó khỏi các endpoint mà nó phục vụ * Nếu pod có định nghĩa một preStop hook, thì nó được gọi tới bên trong pod. Nếu preStop hook vẫn đang chạy mà grace-time đã hết, thì bước (2) sẽ lại được gọi với thời gian grace-time nới thêm là 2 giây. Các bạn có thể tìm hiểu thêm về "Container hook" ở đây. * Process bên trong Pod đã được gửi tín hiệu yêu cầu terminate (TERM signal) * Sau khi grace-time kết thúc, thì mọi process bên trong Pod sẽ bị kill bởi SIGKILL. * Kubelet hoàn thành xóa Pod bằng cách gọi API server và set grace-time bằng 0, nghĩa là yêu cầu xóa ngay lập tức. Lúc này Pod sẽ không còn và client sẽ không thể thấy được Pod này nữa. ### Namespace * Namespace là cách để ta chia tài nguyên của cluster, và nhóm tất cả những resource liên quan lại với nhau, bạn có thể hiểu namespace như là một sub-cluster. * vài namespace đã được tại bởi kube, trong đó có namespace tên là default, kube-system * Cách tổ chức namespace tốt là tạo theo ``` <project_name>:<enviroment> ``` * Có thể xem các pod trong namespace ### ReplicationControllers * resource mà sẽ tạo và quản lý pod, và chắc chắn là số lượng pod nó quản lý không thay đổi và kept running * quản lý pod thông qua labels của pod * Tạo # of pods = replicas #### Why * pod nó sẽ giám sát container và tự động restart lại container khi nó fail * ![](https://hackmd.io/_uploads/HymxVxZZT.png) * trường hợp toàn bộ worker node của chúng ta fail thì sẽ thế nào? pod nó sẽ không thể chạy nữa, và application của chúng ta sẽ downtime với người dùng * chạy cluster với hơn 1 worker node, RC sẽ giúp chúng ta giải quyết vấn đề này * How: Nếu replicas = 1 thì rc sẽ tạo và giám sát 1 pod, nếu 1 node bị die kéo theo pod thì rc sẽ xem pod mình quản lý có trong node đó ko, nếu có thì tạo ra 1 pod y chang ở node khác. * ![](https://hackmd.io/_uploads/HyV9J---p.png) * cách hđ: * ![](https://hackmd.io/_uploads/SJSakWZZ6.png) * TH too few là khi xảy ra node bị die kéo theo pod, khi nó mới dc khởi tạo,... * TH too many khi ta tạo template mới và muốn run rc theo template mới, ta muốn scale down replicas,... #### use case * có thể tăng performance của ứng dụng bằng cách chỉ định số lượng replicas trong RC để RC tạo ra nhiều pod chạy cùng một version của ứng dụng. * ![](https://hackmd.io/_uploads/ByGMZ--b6.png) #### Thực hành * Cấu trúc: * ![](https://hackmd.io/_uploads/BkbP-ZWWa.png) * ![](https://hackmd.io/_uploads/S14Y-Zb-p.png) ##### Các câu lệnh ##### Thay đổi template của pod * có thể thay đổi template của pod và cập nhật lại RC, nhưng nó sẽ không apply cho những thằng pod hiện tại, muốn pod của bạn cập nhật template mới, bạn phải * 1. xóa hết pod để RC tạo ra pod mới * 2. xóa RC và tạo lại ![](https://hackmd.io/_uploads/r1ikGWZbT.png) ### ReplicaSets thay thế RC * RS và RC sẽ hoạt động tương tự nhau. Nhưng RS linh hoạt hơn ở phần label selector, trong khi label selector thằng RC chỉ có thể chọn pod mà hoàn toàn giống với label nó chỉ định, thì thằng RS sẽ cho phép dùng một số expressions hoặc matching để chọn pod nó quản lý. * Ví dụ, thằng RC không thể nào match với pod mà có env=production và env=testing cùng lúc được, trong khi thằng RS có thể, bằng cách chỉ định label selector như env=* * 4 operators: In, NotIn, Exists, DoesNotExist ```yaml apiVersion: apps/v1 kind: ReplicaSet metadata: name: frontend labels: app: myapp spec: replicas: 4 selector: matchLabels: app: myapp-pod template: metadata: labels: app: myapp-pod spec: containers: - name: myapp-container image: nginx:1.19.10 ``` * Thường thì ta rất ít khi, hay cụ thể là gần như không bao giờ phải tự tay tạo ReplicaSet, thay vào đó ta nên dùng Deployment, và Deployment sẽ tự làm điều đó và quản lý nó thay ta ### DaemonSets để chạy chính xác một pod trên một worker node * DaemonSets này sẽ deploy tới mỗi thằng node một pod duy nhất, và chắc chắn có bao nhiêu node sẽ có mấy nhiêu pod, nó sẽ không có thuộc tính replicas * Ứng dụng của thằng DaemonSets này sẽ được dùng trong việc logging và monitoring. * ![](https://hackmd.io/_uploads/SyqNQbZ-a.png) ### Ku bé Services (cho các chị) ![](https://hackmd.io/_uploads/r1VhheN-a.png) * resouce sẽ tạo ra một single, constant point của một nhóm Pod phía sau nó. * Client sẽ mở connection tới service, và connection đó sẽ được dẫn tới một trong những Pod ở phía sau. * ![](https://hackmd.io/_uploads/H1PLSmZ-6.png) * **Vậy service giúp ta những vấn đề gì? Mỗi thằng Pod nó cũng có địa chỉ IP riêng của nó, sao ta không gọi thẳng nó luôn mà thông qua service chi cho mất công?** * Pods are ephemeral: * Khi tạo thằng pod mới tạo ra, nó sẽ có một IP khác với thằng cũ. Nếu ta dùng IP của Pod để tạo connection tới client thì lúc Pod được thay thế với IP khác thì ta phải update lại code. * Multiple Pod run same application * ta sẽ có nhiều pod đang chạy một ứng dụng của chúng ta để tăng performance. Ví dụ khi ta dùng ReplicaSet với replicas = 3, nó sẽ tạo ra 3 Pod. **Vậy làm sao ta biết được nên gửi request tới Pod nào?** * Kubernetes cung cấp cho chúng ta Services resource. Service sẽ tạo ra một endpoint không đổi cho các Pod phía sau, client chỉ cần tương tác với endpoint này. * Services cũng sẽ sử dụng label selectors để chọn Pod mà nó quản lý connection. ![](https://hackmd.io/_uploads/B17fvQ-W6.png) #### ClusterIP (địa chỉ IP ảo) * ![](https://hackmd.io/_uploads/r1VI6eN-6.png) * service mặc định * Đây là loại service sẽ tạo một IP và local DNS mà sẽ có thể truy cập ở bên trong cluster * không thể truy cập từ bên ngoài * tưởng tượng Service nó như kiểu "tổ trưởng dân phố" vậy, người nắm giữ thông tin các cư dân trong phố của mình, nếu ai muốn hỏi gì thì có thể hỏi trực tiếp ông * Trước khi dùng services cho các pods thì thêm labels cho các pods * Problem: Phải lấy clusterIP của pods mới gọi vào dc, nếu pods đổi IP thì sao??? * Ví dụ File cấu hình service: ```yaml apiVersion: v1 kind: Service metadata: name: helloworld spec: type: ClusterIP ports: - name: http protocol: TCP port: 3000 targetPort: 80 selector: app.kubernetes.io/name: helloworld ``` * Note: * type=ClusterIP: mở cho service này chỉ được truy cập trong cluster * protocol=TCP: chỉ cho phép traffic là TCP (các dạng request http thông thường mà ta vẫn hay gọi), hoặc cũng có thể có UDP hay ICMP * port=3000: định nghĩa cho Service này 1 cái port, thì lát nữa ở nơi khác gọi vào service thì sẽ là Service_name:3000 * targetPort=80: đây là cái containerPort của Helloworld mà service này sẽ target vào * selector: áp dụng service này cho 1 tập hợp pod được chỉ định có các labels như ta định nghĩa * Có vài trường hợp bạn có thể sử dụng Kubernetes Proxy để kết nối service của bạn : * Debug service, hoặc kết nối chúng trực tiếp từ laptop vì một vài lí do nào đó * Cho phép các internal traffic, etc * Các service chỉ tồn tại ở một namespace ![](https://hackmd.io/_uploads/H1_PlXzWa.png) * Format goi ten: ```<tên_service>:<port_của_svc>``` * Pod của ta tha hồ restart, scale thoải mái, ta cứ dùng tên service thì vẫn gọi được bình thường. Và đây cũng là cách ta thường làm thật ở production - luôn có service "đứng trước" pod của chúng ta, giao tiếp với những nơi khác đều qua Service. #### Có thể tạo nhiều port cho service * Tương tự thì cũng có thể tạo nhiều port cho Service để target được vào nhiều port của container trong Pod Hello World nhé (targetPod) #### Cách hay hơn để tạo targetPort * người viết cấu hình manifest cho Service cần biết được là "à container HelloWorld có containerPort=80", và từ đó cho Service target vào port 80. * Và cứ mỗi khi containerPort thay đổi thì targetPort bên Service cũng phải thay đổi theo. * Cách: đặt tên cho containerPort để chỉ cần dùng tên đó cho service #### NodePort * ![](https://hackmd.io/_uploads/S1J_gZ4-p.png) * NodePort Service là một trong những cách nguyên thủy nhất đễ kết nối external traffic trực tiếp tới service của bạn. NodePort như cái tên đã ngụ ý, sẽ mở một port trên tất cả các Nodes(VMs), băt cứ traffic nào tới các node này sẽ được chuyển tiếp đến service. * YAML file: ```yaml apiVersion: v1 kind: Service metadata: name: my-nodeport-service spec: selector: app: my-app type: NodePort ports: - name: http port: 80 targetPort: 80 nodePort: 30036 protocol: TCP ``` * Khi nào bạn nên sử dụng NodePort? * Bạn chỉ có thể có một service với mỗi port * Bạn chỉ có thể sử dụng port từ 30000–32767 * Nếu địa chỉ IP của Node, VM thay đổi, bạn sẽ phải xử lí điều đó. #### LoadBalancer * ![](https://hackmd.io/_uploads/SkRDB-VZ6.png) * có thể gửi hầu hết các loại traffic đến nó, như HTTP, TCP, UDP, WebSockets, gRPC. * Khi nào bạn nên sử dụng LoadBalancer? * Một điểm trừ lớn đó là mỗi service bạn expose với một LoadBalancer sẽ lấy luốn IP address của nó, và bạn sẽ phải chi trả cho LoadBalancer với mỗi service được exposed. Điều đó rất đắt đỏ. * ta cũng được 1 public IP , IP đó là IP của load balancer. * chú ý là khi ta set type=LoadBalancer thì K8S cũng sẽ cấp cho Service của chúng ta ClusterIP và expose ra NodePort. * YAML ```yaml apiVersion: v1 kind: Service metadata: name: helloworld spec: type: LoadBalancer # ---- > ở đây ports: - name: http protocol: TCP port: 3000 targetPort: myport selector: app.kubernetes.io/name: helloworld ``` #### Ingress * Ingress không thực sự là một loại service. Thay vào đó, nó đứng trước nhiều services, và hoạt động như một smart router hoặc entry point tới cluster của bạn. * ![](https://hackmd.io/_uploads/BkPn8Z4Za.png) ```yaml apiVersion: extensions/v1beta1 kind: Ingress metadata: name: my-ingress spec: backend: serviceName: other servicePort: 8080 rules: - host: foo.mydomain.com http: paths: - backend: serviceName: foo servicePort: 8080 - host: mydomain.com http: paths: - path: /bar/* backend: serviceName: bar servicePort: 8080 ``` * Khi nào bạn nên sử dụng Ingress? * Ingress có lẽ là cách hiệu quả nhất để expose các service của bạn, nhưng cũng có thể là cách phức tạp nhất. * Ingress là component hữu ích nhất nếu bạn muốn hiển thị nhiều dịch vụ dưới cùng một địa chỉ IP và các dịch vụ này đều sử dụng cùng một giao thức L7 (thường là HTTP). ### Deployment #### Tạo deployment ##### Viết file manifest ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: myapp-deployment labels: app: myapp spec: replicas: 2 selector: matchLabels: app: myapp-pod template: metadata: labels: app: myapp-pod spec: containers: - name: myapp-container image: nginx:1.14.2 ports: - containerPort: 80 name: http resources: requests: memory: "64Mi" cpu: "64m" limits: memory: "128Mi" cpu: "128m" --- apiVersion: v1 kind: Service metadata: name: myapp-svc spec: type: LoadBalancer ports: - name: first-port protocol: TCP port: 8000 targetPort: http selector: app: myapp-pod ``` * Deployment + Service như kiểu cặp bài trùng luôn đi cùng với nhau. 1 cái để deploy pod, 1 cái để expose Pod để nơi khác có thể gọi vào. * Các thành phần trong file: * Dấu '---' dùng để ngăn cách service và deployment nếu ta muốn viết chung trong 1 file * kind: Deployment, đặt label là app:myapp * spec: * replicas : 2 => replica sets tạo 2 pods và quản lý nó * selector: label selector cho Pod * matchLabels: app: myapp-pod => "deployment" này sẽ được apply cho các pod có label match với giá trị app: myapp-pod * template: nơi ta định nghĩa spec cho pod * labels: định nghĩa metadata với labels giống với giá trị cho pod label selector ở trên * Sau đó cấu hình container giống naked pod: tên container, image là gì, expose port 80 (ta cũng đặt tên "phụ" cho nó là http), và thêm resource request. * Sau đó, ta apply: > kubectl apply -f myapp.yml --kubeconfig=kubernetes-config * Check trạng thái của deployment: > kubectl get deploy --kubeconfig=kubernetes-config ``` NAME READY UP-TO-DATE AVAILABLE AGE myapp-deployment 2/2 2 2 8s ``` * READY: đã có bao nhiêu pod đã "ready" cho user gọi vào, như ta thấy thì cả 2/2 pod đã ready * UP-TO-DATE: bao nhiêu pod đã được update để đạt trạng thái mong muốn (2) * AVAILABLE giống READY * Note: mỗi lần deployment chạy lại là 1 lần rollout > kubectl rollout status -w deployment/myapp-deployment --kubeconfig=kubernetes-config deployment "myapp-deployment" successfully rolled out * Deployment đã tạo ra 1 rs để quản lý pods ``` kubectl get rs --kubeconfig=kubernetes-config ------>> NAME DESIRED CURRENT READY AGE myapp-deployment-55f998b87b 2 2 2 3m6s ``` * Get service để truy cập thử app ở external ``` kubectl get svc --kubeconfig=kubernetes-config --- NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE myapp-svc LoadBalancer 10.245.136.215 146.190.7.21 8000:30623/TCP 44m ``` #### Vọc vạch ``` kubectl set image deploy/myapp-deployment myapp-container=nginx:1.16.1 --kubeconfig=kubernetes-config --- deployment.apps/myapp-deployment image updated ``` * Đổi image của container nginx lên 1.16 từ terminal * Các diễn biến ở deployment: * ![](https://hackmd.io/_uploads/r1m9zw9b6.png) * ngay sau khi set image thì K8S sẽ ngay lập tức triển khai pod mới, nhưng các pod cũ vẫn giữ nguyên, đảm bảo trong thời gian đó app của chúng ta không có downtime, user vẫn có thể truy cập thoải mái. * Sau khi deploy pod mới thành công thì K8S sẽ teardown, terminate dần pod cũ đi. * 1 TH: Chú ý rằng trong quá trình deploy, bất kì Pod mới nào READY là cũng sẽ nhận được traffic đẩy vào. Nên có thể dẫn tới trường hợp là trong 1 khoảng thời gian ngắn (1 vài giây), lúc mà Deployment đang update thì cùng lúc tồn tại cả pod cũ và pod mới, dẫn tới việc cùng 1 user nhưng 2 requests có thể đi vào 2 pod chạy 2 version cũ/mới khác nhau. * Ngoài cách update trực tiếp từ terminal thì có thể sửa lại pod spec ở file manifest và apply lại * Hoặc dùng command sau: > kubectl edit deploy myapp-deployment --kubeconfig=kubernetes-config #### Scale pods in rs #### Rollback về version cũ ``` kubectl rollout history deploy/myapp-deployment --kubeconfig=kubernetes-config ---- REVISION CHANGE-CAUSE 1 <none> 2 <none> 3 <none> ``` * check rollout history xem từ đầu tới giờ ta đã deploy mấy lần > kubectl rollout undo deploy/myapp-deployment --kubeconfig=kubernetes-config * Rollback lại version trc đó * K8S sẽ tiến hành tạo các pod mới với image v1.16 và sau đó Terminate các pod v1.19 * ![](https://hackmd.io/_uploads/Bk7zBv9WT.png) > kubectl rollout undo deploy/myapp-deployment --kubeconfig=kubernetes-config --to-revision=2 * Thêm --to-revision=2 để chọn version #### Additional * Apply từ 1 url: > kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.4.0/deploy/static/provider/do/deploy.yaml * apply file manifest tạo ingress nginx trực tiếp từ Github * imagePullSecret để pull image từ private hub * Xoá Deployment * Xóa trực tiếp * kubectl delete deploy myapp-deployment --kubeconfig=kubernetes-config * Xóa qua manifest * kubectl delete -f myapp.yml --kubeconfig=kubernetes-config * Có thể để các name và label giống nhau ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: myapp labels: app: myapp spec: replicas: 4 selector: matchLabels: app: myapp template: metadata: labels: app: myapp spec: containers: - name: myapp image: nginx:1.19.10 ports: - containerPort: 80 name: http resources: requests: memory: "64Mi" cpu: "64m" limits: memory: "128Mi" cpu: "128m" --- apiVersion: v1 kind: Service metadata: name: myapp spec: type: LoadBalancer ports: - name: http protocol: TCP port: 8000 targetPort: http selector: app: myapp ``` ### Configmap và Secret trên Kubernetes #### ConfigMap ##### Viết yaml file configMap configmap.yml ```yaml apiVersion: v1 kind: ConfigMap metadata: name: myapp-config data: app_name: "Hello World" introduction.txt: | My Introduction ``` * Các thành phần: * kind: configMap * app_name là key có value là Hello World * introduction.txt là key (1 file) có nội dung/ key là My Introduction * key-value data * dùng để lưu data ko bảo mật * Có thể lưu string, num, boolean, file text * Thường dùng cho biến mtr > kubectl apply -f configmap.yml --kubeconfig=kubernetes-config * Apply configMap này > kubectl get configmap myapp-config -o yaml --kubeconfig=kubernetes-config * Check xem nội dung bên trong configMap ![](https://hackmd.io/_uploads/HJpUros-T.png) ##### Viết yaml file cho deployment ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: myapp-deployment labels: app: myapp spec: selector: matchLabels: app: myapp-pod template: metadata: labels: app: myapp-pod spec: securityContext: runAsUser: 1001 runAsGroup: 1001 fsGroup: 1001 containers: - name: myapp-container image: maitrungduc1410/configmap-and-secret ports: - containerPort: 3000 name: http resources: requests: memory: "128Mi" cpu: "64m" limits: memory: "750Mi" cpu: "500m" env: - name: APP_NAME valueFrom: configMapKeyRef: name: myapp-config key: app_name volumeMounts: - name: config mountPath: "/app/storage" readOnly: true volumes: - name: config configMap: name: myapp-config items: - key: "introduction.txt" path: "introduction.txt" --- apiVersion: v1 kind: Service metadata: name: myapp-svc spec: type: LoadBalancer ports: - name: http protocol: TCP port: 8080 targetPort: http selector: app: myapp-pod ``` * các thành phần: * securityContext: dùng để define User,group chạy container, nó sẽ thay cái thằng USER ta define o83 dockerfile * env: env trong container * biến APP_NAME lấy giá trị từ ConfigMap có tên là myapp-config và lấy key là app_name để truyền value cho biến APP_NAME * volumes: volume này là của tất cả container dùng chung (ở pod), lấy từ configmap có tên myapp-config và lấy key map nó vào 1 file introduction.txt trong volume * volumeMounts: volume của containers, tham chiếu từ volume của pod vào * name: tên của volume của pod * mountPath: "/app/storage" : mount vào path trong container * readOnly: true : ko cho container ghi đè vào volume này > kubectl apply -f myapp.yml --kubeconfig=kubernetes-config * Kiểm tra các thành phần trong container ta đã tạo: > kubectl exec -it myapp-deployment-995b4d567-8p4fr --kubeconfig=kubernetes-config -- sh * Chui vào container > whoami id -u id -g id * Check securityContext ``` ls -la echo $APP_NAME ls -la storage cat storage/introduction.txt ``` * Check có biến APP_NAME và file introduction.txt ta đã mount từ volume vào chưa ``` kubectl get svc --kubeconfig=kubernetes-config --->>> NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE myapp-svc LoadBalancer 10.245.64.155 167.99.30.23 8080:32101/TCP 3m20s ``` * Get service và chạy ở địa chỉ: 167.99.30.23:8080 #### Secret ##### Viết yaml file cho Secret ```yaml apiVersion: v1 kind: Secret metadata: name: myapp-secret type: Opaque stringData: EMAIL: "admin@test.com" PASSWORD: "123456" secret.txt: | My supersecret ``` * Các thành phần: * type: Opaque => thường dùng * stringData: data kiểu string * Secret có 2 kiểu là stringData (string) và data (base64) * nếu ta dùng stringData, thì lát nữa sau khi apply xong và get thì K8S sẽ show data dạng base64 thôi. ```yaml kubectl apply -f secret.yml --kubeconfig=kubernetes-config --->>> secret/myapp-secret created ``` * Apply secret ``` kubectl get secret --kubeconfig=kubernetes-config --->>> NAME TYPE DATA AGE default-token-4lggr kubernetes.io/service-account-token 3 10m myapp-secret Opaque 3 41s ``` * get secret ##### Viết yaml cho deployment ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: myapp-deployment labels: app: myapp spec: selector: matchLabels: app: myapp-pod template: metadata: labels: app: myapp-pod spec: securityContext: runAsUser: 1001 runAsGroup: 1001 fsGroup: 1001 containers: - name: myapp-container image: maitrungduc1410/configmap-and-secret ports: - containerPort: 3000 name: http resources: requests: memory: "128Mi" cpu: "64m" limits: memory: "750Mi" cpu: "500m" env: - name: APP_NAME valueFrom: configMapKeyRef: name: myapp-config key: app_name - name: EMAIL valueFrom: secretKeyRef: name: myapp-secret key: EMAIL - name: PASSWORD valueFrom: secretKeyRef: name: myapp-secret key: PASSWORD volumeMounts: - name: config mountPath: "/app/storage/introduction.txt" subPath: introduction.txt readOnly: true - name: secret mountPath: "/app/storage/secret.txt" subPath: secret.txt readOnly: true volumes: - name: config configMap: name: myapp-config items: - key: "introduction.txt" path: "introduction.txt" - name: secret secret: secretName: myapp-secret items: - key: secret.txt path: secret.txt --- apiVersion: v1 kind: Service metadata: name: myapp-svc spec: type: LoadBalancer ports: - name: http protocol: TCP port: 8080 targetPort: http selector: app: myapp-pod ``` ```yaml env: - name: APP_NAME valueFrom: configMapKeyRef: name: myapp-config key: app_name - name: EMAIL valueFrom: secretKeyRef: name: myapp-secret key: EMAIL - name: PASSWORD valueFrom: secretKeyRef: name: myapp-secret key: PASSWORD volumeMounts: - name: config mountPath: "/app/storage/introduction.txt" subPath: introduction.txt readOnly: true - name: secret mountPath: "/app/storage/secret.txt" subPath: secret.txt readOnly: true volumes: - name: config configMap: name: myapp-config items: - key: "introduction.txt" path: "introduction.txt" - name: secret secret: secretName: myapp-secret items: - key: secret.txt path: secret.txt ``` * phân tích đoạn khác với configmap: * env: ta định nghĩa thêm 2 biến mtr là EMAIL và PASSWORD, những info quan trọng từ secret * volumes: * thêm 1 volume nữa là secret, có name là myapp-secret, truyền vào file secret.txt * volumeMounts: * subPath => là cái path mà ta định nghĩa ở phần volumes bên dưới * k8s ko cho phép mountPath ở các volume giống nhau => bắt buộc phải dùng thêm subPath để lấy path từ volume chung > kubectl apply -f myapp.yml --kubeconfig=kubernetes-config * apply deployment ##### Note: * Lí do volume của ta không được update "realtime" vào trong container nữa là bởi vì hiện tại ta dùng subPath để mount, và như vậy ta sẽ không nhận được update mới nữa mà phải restart lại deployment. > kubectl rollout restart deploy myapp-deployment --kubeconfig=kubernetes-config * reset deployment #### Lấy toàn bộ secret vào truyền thẳng vào biến môi trường * thường ta phải định nghĩ từng biến mtr và truyền từ đâu,... * tổ chức các file secret/configmap: * file chỉ chứa biến môi trường ra 1 kiểu: ```yaml apiVersion: v1 kind: Secret metadata: name: myapp-secret type: Opaque stringData: EMAIL: "newadmin@gmail.com" PASSWORD: "abcdefg" ``` * còn lại nếu file có chứa nội dung dài, dạng text bất kì thì đưa ra thành các configmap/secret riêng: ```yaml apiVersion: v1 kind: Secret metadata: name: myapp-secret type: Opaque stringData: EMAIL: "newadmin@gmail.com" PASSWORD: "abcdefg" ``` ```yaml containers: - name: myapp-container image: maitrungduc1410/configmap-and-secret envFrom: - secretRef: name: myapp-secret - configMapRef: name: myapp-config ``` ### PersistentVolume và PersistentVolumeClaim trong Kubernetes * Có 2 loại mà ta hay sử dụng nhất: * PersistentVolume: volume ko bị cook khi pod destroyed, có thể mount sang pod khác * Ephemeral Volume: bay màu khi pod destroyed, pod restart vẫn còn * Mỗi lần container bị crash, lỗi,... -> kubelet restart lại container -> mất toàn bộ data -> dùng volume * PersistentVolume (PV): sống dai thành huyền thoại fee fai * PersistentVolumeClaim (PVC): là 1 request để "xin" được sử dụng storage. Pod muốn sử dụng volume thì cần phải có Claim. * Có thể cấu hình read/write/read-write cho volume. * MQH mập mờ (bound): 1 PVC & nhiều PV ; 1 PV & 1 PVC (one-to-many) * yaml file example: ```yaml # PV apiVersion: v1 kind: PersistentVolume metadata: name: block-pv spec: capacity: storage: 10Gi # tối đa 10GB accessModes: - ReadWriteOnce --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: block-pvc spec: accessModes: - ReadWriteOnce # read + write nhưng chỉ dành cho 1 Node trên cluster resources: requests: storage: 10Gi # "xin" hết 10 GB ``` * Các thành phần: * accessModes: ReadWriteOnce => 1 node được dùng PV, PVC * StorageClass là nơi cấp cho ta PV, thường thì các cloud provider đều có StorageClass phù hợp với nhu cầu đọc,ghi dữ liệu nhanh hay chậm của ta. #### Tạo PVC và PV * Tạo file pvc.yml cho PVC: ```yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: demo-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: "2Gi" ``` * Các thành phần: * accessModes: ReadWriteOnce (cho phép read+ write) * storge: 2 gb > kubectl apply -f pvc.yml --kubeconfig=kubernetes-config * apply pvc ```yaml kubectl get pvc --kubeconfig=kubernetes-config ------ NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE demo-pvc Bound pvc-93715847-269c-40bd-9407-7a11a61514a4 2Gi RWO do-block-storage 61s ``` * Các thành phần: * pvc có tên là demo-pvc * Được bound với pv: pvc-93715847-269c-40bd-9407-7a11a61514a4 * tạo bởi storage class là do-block-storage (provider tạo sẵn) * ta dùng các provider lớn, họ sẽ cung cấp sẵn PV được provision (được họ cung cấp và quản lý thay ta), bởi vì những phần cấu hình đó nhiều khi không dễ nên họ sẽ "abstract" phần đó luôn cho ta. * PV là cluster-object, tức là nó không thuộc về namespace, get PV show toàn bộ PV ở cluster #### Mount PVC vào Pod * Viết file yaml deployment: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: demoapp labels: app.kubernetes.io/name: viblo-pv-demo spec: selector: matchLabels: app: demoapp template: metadata: labels: app: demoapp spec: securityContext: # --> ở đây fsGroup: 1001 # --> và đây containers: - name: demoapp image: maitrungduc1410/viblo-k8s-pv-demo:latest ports: - containerPort: 3000 name: http resources: requests: memory: "128Mi" cpu: "64m" limits: memory: "750Mi" cpu: "500m" volumeMounts: - name: data mountPath: /app/storage # readOnly: true volumes: - name: data persistentVolumeClaim: claimName: demo-pvc # readOnly: true # nếu như ta muốn chỉ cho phép read ko write ``` * Các thành phần: * volumes: ta tạo 1 volume tên data lấy dữ liệu từ pvc có tên demo-pvc * voulmeMounts: mount data từ volume data vào mountPath là /app/storage * persistentVolumeClaim ta cũng có thể thêm readOnly: true, nó sẽ ghi đè lên readOnly ở phần volumeMounts * securityContext: * fsgroup * cái này áp dụng cho tất cả container trong pod * Nếu ko chạy này thì có khi mount file volume vào thì có thể bị lỗi permissions > kubectl apply -f deployment.yml --kubeconfig=kubernetes-config #### RECLAIM POLICY ``` kubectl get pv --kubeconfig=kubernetes-config ------ NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pvc-1f23bfc0-5d62-432c-8706-84bc72b1680f 2Gi RWO Delete Bound learnk8s-bde84e/demo-pvc do-block-storage 24m ``` * có thể có 3 giá trị: * Delete(mặc định): xoá PV cùng với PVC khi PVC bị xoá * Retain: xoá PVC, PV vẫn còn. Trường hợp này thường dùng khi data của ta quan trọng, ví dụ database * Recycle: deprecated (lỗi thời) > kubectl patch pv pvc-1f23bfc0-5d62-432c-8706-84bc72b1680f -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}' --kubeconfig=kubernetes-config * Thay đổi reclaim policy từ delete sang retain (phải có quyền quản lý cluster) #### Readonly Volume * thêm readOnly : True vào volumeMounts trong container #### Additional 1. lưu lượng data vượt quá capacity PVC ![](https://hackmd.io/_uploads/Bk_NRf0Wa.png) * Nó sẽ báo lỗi, ko thể write nữa 2. App đang chạy có xoá được PVC * PVC sẽ không bị xoá nếu vẫn còn đang được sử dụng bởi bất kì Pod nào, kể cả nếu admin có cố gắng xoá PV trong lúc đó thì PVC cũng không bị xoá > kubectl delete pvc demo-pvc --kubeconfig=kubernetes-config 3. Tăng capacity của PVC * PVC sẽ không resize cho tới khi nó được Bound vào 1 Pod nào đó, và ngay sau khi Pod của ta lên sóng, thì PVC được bound -> resize từ 1 -> 2Gi * Ở đây ta đang test với trường hợp ta resize PVC trước cả khi ta tạo Deployment. Giờ nếu ta resize khi ta đã có Deployment và app đang chạy ngon thì kịch bản cũng tương tự, ta phải restart Deployment thì PVC mới được update. 4. Giảm capacity của PVC thì data có bị mất ``` kubectl apply -f pvc.yml --kubeconfig=kubernetes-config ------ The PersistentVolumeClaim "demo-pvc" is invalid: spec.resources.requests.storage: Forbidden: field can not be less than previous value ``` * không thể resize xuống nhỏ hơn capacity cũ, tức là kể cả lượng data của ta có ít hơn 1 Gi, nhưng ta cũng không thể resize từ 2Gi xuống 1Gi, điều này nhằm đảm bảo data không bị mất trong quá trình resize

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully