[{"content":"Nomad experiment Setup environment Setup enviroment with consul, dnsmasq and nomad through ansible: ./ansible_run.sh install.yml Consul status:\nNomad status:\nAccess the UI with ACL: Consul UI:\nNomad UI:\nRun example nginx job Add nginx.conf into consul key/value: export CONSUL_HTTP_TOKEN=$(sudo cat /etc/consul.d/data/consul.token | awk \u0026#39;/SecretID/ {print $NF}\u0026#39;) consul kv put example/nginx.conf \u0026#34;events { worker_connections 1024; } http { server { listen 8081; server_name localhost; location / { return 200 \u0026#39;Hello from Nomad \u0026amp; Consul!\u0026#39;; add_header Content-Type text/plain; } } }\u0026#34; Create nginx.hcl job, get nginx.conf from consul with template stanza: template { data = \u0026lt;\u0026lt;EOF {{ key \u0026#34;local/nginx/nginx.conf\u0026#34; }} EOF destination = \u0026#34;local/nginx.conf\u0026#34; change_mode = \u0026#34;restart\u0026#34; } Bind mount nginx.conf to containers: config { image = \u0026#34;nginx:alpine\u0026#34; ports = [\u0026#34;http\u0026#34;] volumes = [ \u0026#34;local/nginx.conf:/etc/nginx/nginx.conf\u0026#34;, ] } Set port mapping: network { port \u0026#34;http\u0026#34; { static = 8082 to = 8081 } } In there, static is the host port (8082), it’s mapping to the container port (8081). We have already set the nginx service running on port 8081 in the nginx.conf file in consul.\nRegister services to Consul service { name = \u0026#34;example-nginx\u0026#34; port = \u0026#34;http\u0026#34; } Run job: export NOMAD_TOKEN=$(sudo cat /etc/nomad.d/data/nomad.token | awk \u0026#39;/Secret ID/ {print $NF}\u0026#39;) nomad run -detach jobs/example.hcl Testing: docker ps -a Stdout:\nCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES afd5f072ed1f nginx:latest \u0026#34;/docker-entrypoint.…\u0026#34; 30 seconds ago Up 29 seconds 80/tcp, 192.168.0.3:8082-\u0026gt;8081/tcp, 192.168.0.3:8082-\u0026gt;8081/udp nginx-1c0ad89d-494c-becc-4a90-b694545fb4ca Go to the brower: http://192.168.0.3:8082\nOr you can access through consul service with format: \u0026lt;service_name\u0026gt;.service.\u0026lt;consul_domain\u0026gt;\nGo to the brower: http://example-nginx.service.demo:8082\nValidate the DNS record:\ndig @127.0.0.1 example-nginx.service.demo SRV Run microservice jobs In this the tutorial, we will deploy the flask app, redis cache with nginx gateway.\nOne nginx which redirect 80 –\u0026gt; flask app on 8000 Flask app, which records in a redis database a number of view A redis database, on port 6380 Note that services communicate with each other through the host network.\nBuild flask app docker image on nodes running nomad: make build Put configs to Consul KV: export CONSUL_HTTP_TOKEN=$(sudo cat /etc/consul.d/data/consul.token | awk \u0026#39;/SecretID/ {print $NF}\u0026#39;) consul kv put NGINX_CONFIG \u0026#34; worker_processes 1; events { worker_connections 1024; } http { sendfile on; server { listen 80; location / { proxy_pass http://app-flask.service.demo:8000; add_header Content-Type text/plain; } } }\u0026#34; consul kv put REDIS_URL \u0026#34;redis://db-redis.service.demo:6380/0\u0026#34; Run nomad jobs: nomad run -detach jobs/redis.hcl nomad run -detach jobs/webapp.hcl Testing: Check docker containers\ndocker ps -a Go to the brower: http://app-nginx.service.demo\nIt’s working.\nYou can get full source code here!\n","permalink":"https://hoangph3.github.io/posts/nomad/","summary":"Deploy and orchestrate containerized services using HashiCorp Nomad and Consul.","title":"Scheduling Services with HashiCorp Nomad"},{"content":"Factory Method Factory method là pattern giúp khởi tạo các đối tượng thông qua một interface chung, qua đó:\nChe giấu quá trình xử lý logic của phương thức khởi tạo. Giảm sự phụ thuộc, dễ dàng mở rộng. \u0026#34;\u0026#34;\u0026#34; Đoạn code này không sử dụng Factory Method \u0026#34;\u0026#34;\u0026#34; class FrenchLocalizer: \u0026#34;\u0026#34;\u0026#34;it simply returns the french version \u0026#34;\u0026#34;\u0026#34; def __init__(self): self.translations = {\u0026#34;car\u0026#34;: \u0026#34;voiture\u0026#34;, \u0026#34;bike\u0026#34;: \u0026#34;bicyclette\u0026#34;, \u0026#34;cycle\u0026#34;:\u0026#34;cyclette\u0026#34;} def localize(self, msg): \u0026#34;\u0026#34;\u0026#34;change the message using translations\u0026#34;\u0026#34;\u0026#34; return self.translations.get(msg, msg) class SpanishLocalizer: \u0026#34;\u0026#34;\u0026#34;it simply returns the spanish version\u0026#34;\u0026#34;\u0026#34; def __init__(self): self.translations = {\u0026#34;car\u0026#34;: \u0026#34;coche\u0026#34;, \u0026#34;bike\u0026#34;: \u0026#34;bicicleta\u0026#34;, \u0026#34;cycle\u0026#34;:\u0026#34;ciclo\u0026#34;} def localize(self, msg): \u0026#34;\u0026#34;\u0026#34;change the message using translations\u0026#34;\u0026#34;\u0026#34; return self.translations.get(msg, msg) class EnglishLocalizer: \u0026#34;\u0026#34;\u0026#34;Simply return the same message\u0026#34;\u0026#34;\u0026#34; def localize(self, msg): return msg if __name__ == \u0026#34;__main__\u0026#34;: # Business logic f = FrenchLocalizer() e = EnglishLocalizer() s = SpanishLocalizer() msg = \u0026#34;car\u0026#34; lang = \u0026#34;French\u0026#34; if lang == \u0026#34;French\u0026#34;: print(f.localize(msg)) elif lang == \u0026#34;English\u0026#34;: print(e.localize(msg)) elif lang == \u0026#34;Spanish\u0026#34;: print(s.localize(msg)) else: raise ValueError() voiture Với đoạn code trên, ta có thể nhận thấy các vấn đề như sau:\nCode khởi tạo đối tượng nằm ở phía client, bao gồm: FrenchLocalizer, EnglishLocalizer, SpanishLocalizer. Như vậy sau này cứ mỗi lần phía thư viện có update mới, chẳng hạn như support thêm VietnameseLocalizer, JapaneseLocalizer hay ChineseLocalizer, … thì phía client cũng phải update thêm code khởi tạo để có thể sử dụng được. Tùy thuộc vào bussiness logic mà chúng ta sẽ tạo ra các object tương ứng, nếu logic này nằm ở nhiều nơi khác nhau thì nó sẽ bị lặp đi lặp lại. Đặc biệt là khi chúng ta muốn thay đổi, chỉnh sửa hay mở rộng, ta đều phải sửa tất cả những nơi có logic đó gây mất thời gian và dễ bị sót hay lỗi. Giải pháp Gom các business logic để khởi tạo object vào một nơi trong chương trình, gọi dân dã là đóng hàm, và nó chính là factory method. Khi đó, người sử dụng (client) đạt được mục đích tạo mới object và không cần quan tâm đến cách nó được tạo ra (vì factory method đã che giấu logic này). \u0026#34;\u0026#34;\u0026#34; Đoạn code áp dụng Factory Method \u0026#34;\u0026#34;\u0026#34; class FrenchLocalizer: \u0026#34;\u0026#34;\u0026#34;it simply returns the french version \u0026#34;\u0026#34;\u0026#34; def __init__(self): self.translations = {\u0026#34;car\u0026#34;: \u0026#34;voiture\u0026#34;, \u0026#34;bike\u0026#34;: \u0026#34;bicyclette\u0026#34;, \u0026#34;cycle\u0026#34;:\u0026#34;cyclette\u0026#34;} def localize(self, msg): \u0026#34;\u0026#34;\u0026#34;change the message using translations\u0026#34;\u0026#34;\u0026#34; return self.translations.get(msg, msg) class SpanishLocalizer: \u0026#34;\u0026#34;\u0026#34;it simply returns the spanish version\u0026#34;\u0026#34;\u0026#34; def __init__(self): self.translations = {\u0026#34;car\u0026#34;: \u0026#34;coche\u0026#34;, \u0026#34;bike\u0026#34;: \u0026#34;bicicleta\u0026#34;, \u0026#34;cycle\u0026#34;:\u0026#34;ciclo\u0026#34;} def localize(self, msg): \u0026#34;\u0026#34;\u0026#34;change the message using translations\u0026#34;\u0026#34;\u0026#34; return self.translations.get(msg, msg) class EnglishLocalizer: \u0026#34;\u0026#34;\u0026#34;Simply return the same message\u0026#34;\u0026#34;\u0026#34; def localize(self, msg): return msg def Factory(language=\u0026#34;English\u0026#34;): \u0026#34;\u0026#34;\u0026#34;Factory Method\u0026#34;\u0026#34;\u0026#34; localizers = { \u0026#34;French\u0026#34;: FrenchLocalizer, \u0026#34;English\u0026#34;: EnglishLocalizer, \u0026#34;Spanish\u0026#34;: SpanishLocalizer, } return localizers[language]() if __name__ == \u0026#34;__main__\u0026#34;: message = \u0026#34;car\u0026#34; lang = \u0026#34;French\u0026#34; fn = Factory(lang) print(fn.localize(message)) voiture Factory method có ưu điểm gì? Chúng ta có một super class với nhiều class con và dựa trên đầu vào, chúng ta cần trả về một class con. Mô hình này giúp chúng ta đưa trách nhiệm (hay logic if else) của việc khởi tạo một class từ phía người dùng (client) sang lớp Factory là một nơi trong chương trình (đáp ứng Single Responsibility Principle). Nhờ việc che giấu hay kiểm soát logic khởi tạo, chúng ta có thể tiết kiệm tài nguyên bằng cách sử dụng lại đối tượng hiện có thay vì tạo mới chúng mỗi lần (nghe có vẻ liên quan đến Singleton): Bạn cần nơi để lưu trữ tất cả các đối tượng đã tạo. Khi ai đó yêu cầu một đối tượng, chương trình sẽ thực hiện tìm kiếm đối tượng đó trong pool. …và trả về cho code client. Nếu không có đối tượng, chương trình sẽ tạo ra một đối tượng mới (và thêm nó vào pool). Chúng ta không thể biết được liệu sau này còn có class con nào không. Khi cần mở rộng, hãy tạo subclass và thêm vào Factory Method để khởi tạo subclass này mà không làm ảnh hưởng đến code client hiện tại (đáp ứng Open/Closed Principle). Ví dụ: # Tạo subclass class VietnameseLocalizer: \u0026#34;\u0026#34;\u0026#34;it simply returns the spanish version\u0026#34;\u0026#34;\u0026#34; def __init__(self): self.translations = {\u0026#34;car\u0026#34;: \u0026#34;xe hơi\u0026#34;, \u0026#34;bike\u0026#34;: \u0026#34;xe đạp\u0026#34;, \u0026#34;cycle\u0026#34;:\u0026#34;xích lô\u0026#34;} def localize(self, msg): \u0026#34;\u0026#34;\u0026#34;change the message using translations\u0026#34;\u0026#34;\u0026#34; return self.translations.get(msg, msg) # Thêm vào Factory Method def Factory(language=\u0026#34;English\u0026#34;): \u0026#34;\u0026#34;\u0026#34;Factory Method\u0026#34;\u0026#34;\u0026#34; localizers = { \u0026#34;French\u0026#34;: FrenchLocalizer, \u0026#34;English\u0026#34;: EnglishLocalizer, \u0026#34;Spanish\u0026#34;: SpanishLocalizer, \u0026#34;Vietnamese\u0026#34;: VietnameseLocalizer } return localizers[language]() message = \u0026#34;car\u0026#34; lang = \u0026#34;Vietnamese\u0026#34; fn = Factory(lang) print(lang) print(fn.localize(message)) Vietnamese xe hơi Một cách khác tạo factory method sử dụng staticmethod\nclass Factory: _localizers = { \u0026#34;French\u0026#34;: FrenchLocalizer, \u0026#34;English\u0026#34;: EnglishLocalizer, \u0026#34;Spanish\u0026#34;: SpanishLocalizer, \u0026#34;Vietnamese\u0026#34;: VietnameseLocalizer } @staticmethod def create_localizer(lang: str): return Factory._localizers[lang]() fn = Factory.create_localizer(\u0026#34;Vietnamese\u0026#34;) fn.localize(message) \u0026#39;xe hơi\u0026#39; Ứng dụng Hệ thống quản lý tài liệu: có nhiều loại tài liệu khác nhau như Word, PDF, Excel, … Sử dụng Factory Method giúp chúng ta tạo ra các đối tượng tài liệu tương ứng mà không cần phải biết trước loại tài liệu cụ thể. Hệ thống thanh toán: hệ thống thanh toán hỗ trợ nhiều phương thức thanh toán khác nhau như thẻ ATM, thẻ tín dụng, PayPal, … Sử dụng Factory Method sẽ giúp chúng ta tạo ra đối tượng xử lý thanh toán tương ứng với phương thức thanh toán mà người dùng lựa chọn. Ứng dụng đồ họa: ứng dụng có thể xử lý với nhiều loại hình ảnh khác nhau (JPG, PNG, BMP, …). Một Factory Method có thể được sử dụng để tạo ra các đối tượng hình ảnh tương ứng tùy thuộc vào định dạng của file ảnh được mở. Kết nối database: sử dụng Factory Method để tạo ra các đối tượng kết nối với cơ sở dữ liệu cụ thể (MySQL, PostgreSQL, SQL Server, …), giúp chúng ta dễ dàng mở rộng khi có các cơ sở dữ liệu mới (Mongo, Elasticsearch, …) Abstract Factory Method Abstract Factory Pattern còn được gọi là Factory of Factories, nó cũng tương tự như Factory Pattern, nhưng cung cấp một mức trừu tượng hơn (abstract) để tạo ra các đối tượng liên quan đến nhau.\nHãy ví dụ với hệ điều hành, mỗi hệ điều hành sẽ hiển thị UI style khác nhau từ button, dock, … Bạn sẽ không muốn một chương trình hiển thị MacOS control khi đang chạy Windows. Đó chính là lý do mà Abstract Factory ra đời, nó hoạt động như sau:\nKiểm tra hệ điều hành hiện tại. Tạo ra đối tượng Factory từ class tương ứng với hệ điều hành. Các phần còn lại sử dụng đối tượng Factory này để tạo ra các phần tử UI tương thích với hệ điều hành đó. Bằng cách tiếp cận này, mỗi lần thêm biến thể của phần tử UI trong ứng dụng, chúng ta chỉ cần tạo một class Factory mới tạo ra các phần tử UI này và sửa đổi một chút code khởi tạo để ứng dụng chọn class đó khi thích hợp.\n\u0026#34;\u0026#34;\u0026#34; Code with Abstract Factory Method \u0026#34;\u0026#34;\u0026#34; from __future__ import annotations from abc import ABC, abstractmethod class Button(ABC): \u0026#34;\u0026#34;\u0026#34; Mỗi phần tử UI của từng OS phải có base interface. Ở đây ta tạo Button với các biến thể khác nhau: WindowsButton, MacOSButton \u0026#34;\u0026#34;\u0026#34; @abstractmethod def render(self) -\u0026gt; str: pass class WindowsButton(Button): def render(self) -\u0026gt; str: return f\u0026#34;This is {__class__.__name__}\u0026#34; class MacOSButton(Button): def render(self) -\u0026gt; str: return f\u0026#34;This is {__class__.__name__}\u0026#34; \u0026#34;\u0026#34;\u0026#34; Thực hiện tương tự với các phần tử UI khác, ví dụ: dock \u0026#34;\u0026#34;\u0026#34; class Dock(ABC): \u0026#34;\u0026#34;\u0026#34; Ở đây ta tạo Dock với các biến thể khác nhau: WindowsDock, MacOSDock \u0026#34;\u0026#34;\u0026#34; @abstractmethod def render(self) -\u0026gt; None: pass class WindowsDock(Dock): def render(self) -\u0026gt; str: return f\u0026#34;This is {__class__.__name__}\u0026#34; class MacOSDock(Dock): def render(self) -\u0026gt; str: return f\u0026#34;This is {__class__.__name__}\u0026#34; \u0026#34;\u0026#34;\u0026#34; Abstract Factory là interface với các methods trả về Abstract Products. Những Products trong này có liên quan đến nhau dựa trên concept nào đó, ví dụ: cùng OS \u0026#34;\u0026#34;\u0026#34; class AbstractFactory(ABC): @abstractmethod def create_button(self) -\u0026gt; Button: pass @abstractmethod def create_dock(self) -\u0026gt; Dock: pass class WindowsFactory(AbstractFactory): \u0026#34;\u0026#34;\u0026#34; Concrete Factories override các methods để trả về các Products thuộc cùng 1 biến thể: Windows UI \u0026#34;\u0026#34;\u0026#34; def create_button(self): return WindowsButton() def create_dock(self): return WindowsDock() class MacOSFactory(AbstractFactory): \u0026#34;\u0026#34;\u0026#34; Concrete Factories override các methods để trả về các Products thuộc cùng 1 biến thể: MacOS UI \u0026#34;\u0026#34;\u0026#34; def create_button(self): return MacOSButton() def create_dock(self): return MacOSDock() \u0026#34;\u0026#34;\u0026#34; Create Factory method \u0026#34;\u0026#34;\u0026#34; class GUIFactory: _os = { \u0026#34;macos\u0026#34;: MacOSFactory, \u0026#34;windows\u0026#34;: WindowsFactory, } @staticmethod def create_factory(name: str): return GUIFactory._os[name]() \u0026#34;\u0026#34;\u0026#34; Code client \u0026#34;\u0026#34;\u0026#34; # Client class Application: def __init__(self, factory: GUIFactory): self.factory = factory def create_ui(self): button = self.factory.create_button() print(button.render()) dock = self.factory.create_dock() print(dock.render()) if __name__ == \u0026#34;__main__\u0026#34;: factory = GUIFactory.create_factory(\u0026#34;macos\u0026#34;) app = Application(factory) app.create_ui() print() factory = GUIFactory.create_factory(\u0026#34;windows\u0026#34;) app = Application(factory) app.create_ui() This is MacOSButton This is MacOSDock This is WindowsButton This is WindowsDock Áp dụng Abstract Factory khi nào? Ban đầu khi mới phát triển, chúng ta có thể chỉ cần dùng Factory Method. Tuy nhiên khi dự án ngày càng mở rộng, càng có nhiều loại đối tượng hơn, thì chúng ta nên sử dụng Abstract Factory để tách nhiều biến thể của một nhóm sản phẩm (products) thành một Factory. Khi đó:\nCác sản phẩm lấy từ một factory sẽ tương thích với nhau. Tránh được kết hợp quá chặt chẽ giữa code client và concrete product, bạn có thể thêm các biến thể mới vào chương trình, mà không làm ảnh hưởng đến code client hiện tại (✔️ Open/Closed Principle.) Builder Builder là pattern cho phép chúng ta khởi tạo các object phức tạp theo từng bước. bằng cách sử dụng cùng một mã xây dựng, chúng ta có thể tạo ra các loại và cách biểu diễn khác nhau của đối tượng một cách dễ dàng.\nBuilder có ưu điểm gì? Sử dụng Builder để loại bỏ các “hàm khởi tạo khổng lồ” với quá nhiều tham số, chỉ dùng những bước build đối tượng thực sự cần. Sử dụng Builder để tạo ra những cây Composite và các đối tượng phức tạp khác, bằng cách bỏ qua một số bước hoặc gọi đệ quy. Single Responsibility Principle: tách code khởi tạo phức tạp khỏi logic nghiệp vụ của sản phẩm. Bằng cách sử dụng class Director, bạn sẽ ẩn hoàn toàn chi tiết khởi tạo của sản phẩm với code client. Code client chỉ cần liên kết với Director, rồi nhận hàm khởi tạo từ director và kết quả từ builder. \u0026#34;\u0026#34;\u0026#34; Builder Design Pattern Mục đích: Cho phép khởi tạo một đối tượng phức tạp theo từng bước, nhằm tạo ra các kiểu và cách thể hiện khác nhau của một đối tượng bằng cách sử dụng cùng một mã xây dựng. \u0026#34;\u0026#34;\u0026#34; from __future__ import annotations from abc import ABC, abstractmethod from typing import Any \u0026#34;\u0026#34;\u0026#34; Xây dựng Product object. Với những builder cụ thể thì có thể tạo ra các đối tượng không liên quan, hay nói cách khác là có thể không cùng một interface. \u0026#34;\u0026#34;\u0026#34; class PizzaHut: def __init__(self) -\u0026gt; None: self.parts = [] def add(self, part: Any) -\u0026gt; None: self.parts.append(part) def list_parts(self) -\u0026gt; None: print(f\u0026#34;Product parts: {\u0026#39;, \u0026#39;.join(self.parts)}\u0026#34;, end=\u0026#34;\u0026#34;) \u0026#34;\u0026#34;\u0026#34; Khởi tạo Builder interface với các methods giúp tạo Product objects theo các bước \u0026#34;\u0026#34;\u0026#34; class Builder(ABC): @property @abstractmethod def product(self) -\u0026gt; None: pass @abstractmethod def produce_size(self) -\u0026gt; None: pass @abstractmethod def produce_sauce(self) -\u0026gt; None: pass @abstractmethod def produce_cheese(self) -\u0026gt; None: pass @abstractmethod def produce_topping(self) -\u0026gt; None: pass class PizzaHutBuilder(Builder): def __init__(self) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34; Reset builder trước khi xây dựng Product object \u0026#34;\u0026#34;\u0026#34; self.reset() def reset(self) -\u0026gt; None: self._product = PizzaHut() @property def product(self) -\u0026gt; PizzaHut: \u0026#34;\u0026#34;\u0026#34; Một một builder cụ thể (ở đây là PizzaHutBuilder) sẽ có những methods riêng để xây dựng đối tượng. Tại vì mỗi một builder khác nhau sẽ có những methods khác nhau Vì vậy, chúng ta không nên khai báo trong interface Builder. Thay vào đó, khi tạo một builder cụ thể chúng ta sẽ thực hiện override các methods này. * Note: Sau khi trả về kết quả, builder sẽ đc reset để bắt đầu sản xuất một Product (object) khác. \u0026#34;\u0026#34;\u0026#34; product = self._product self.reset() return product def produce_size(self) -\u0026gt; None: self._product.add(\u0026#34;20\u0026#34;) def produce_sauce(self) -\u0026gt; None: self._product.add(\u0026#34;Tomato\u0026#34;) def produce_cheese(self) -\u0026gt; None: self._product.add(\u0026#34;Ricotta\u0026#34;) def produce_topping(self) -\u0026gt; None: self._product.add(\u0026#34;Pepperoni\u0026#34;) \u0026#34;\u0026#34;\u0026#34; Director sẽ chịu trách nhiệm xây dựng đối tượng theo một trình tự cụ thể. Tuy nhiên chỉ là Optional vì client có thể điều khiển builder. \u0026#34;\u0026#34;\u0026#34; class Director: def __init__(self) -\u0026gt; None: self._builder = None @property def builder(self) -\u0026gt; Builder: return self._builder @builder.setter def builder(self, builder: Builder) -\u0026gt; None: \u0026#34;\u0026#34;\u0026#34; Director vẫn chấp nhận với mọi yêu cầu xây dựng tượng từ client. \u0026#34;\u0026#34;\u0026#34; self._builder = builder \u0026#34;\u0026#34;\u0026#34; Director hỗ trợ một số methods để xây dựng đối tượng default \u0026#34;\u0026#34;\u0026#34; def build_minimal_viable_product(self) -\u0026gt; None: # Pizza chỉ có sốt self.builder.produce_sauce() def build_full_featured_product(self) -\u0026gt; None: # Pizza full topping self.builder.produce_sauce() self.builder.produce_cheese() self.builder.produce_topping() if __name__ == \u0026#34;__main__\u0026#34;: \u0026#34;\u0026#34;\u0026#34; Client code tạo builder object, chuyển cho director để thực hiện xây dựng, sau đó trả về kết quả từ builder object. \u0026#34;\u0026#34;\u0026#34; director = Director() builder = PizzaHutBuilder() director.builder = builder print(\u0026#34;Standard basic product: \u0026#34;) director.build_minimal_viable_product() builder.product.list_parts() print(\u0026#34;\\n\u0026#34;) print(\u0026#34;Standard full featured product: \u0026#34;) director.build_full_featured_product() builder.product.list_parts() print(\u0026#34;\\n\u0026#34;) # Builder pattern vẫn work mà không cần Director. print(\u0026#34;Custom product: \u0026#34;) builder.produce_size() builder.produce_cheese() builder.produce_sauce() builder.product.list_parts() Standard basic product: Product parts: Tomato Standard full featured product: Product parts: Tomato, Ricotta, Pepperoni Custom product: Product parts: 20, Ricotta, Tomato Prototype Prototype là một pattern nhằm mục đích giảm số lượng class được sử dụng cho một ứng dụng. Nó cho phép bạn sao chép các đối tượng hiện có một cách độc lập với việc triển khai cụ thể các lớp của chúng, đặc biệt là khi việc tạo đối tượng là một nhiệm vụ tốn kém về thời gian và tài nguyên và đã tồn tại một đối tượng tương tự. Phương pháp này cung cấp cách sao chép đối tượng ban đầu và sau đó sửa đổi nó theo nhu cầu của chúng ta.\nVấn đề Giả sử chúng ta có một object và muốn tạo ra một bản sao của nó, đơn giản nhất là:\nKhởi tạo object mới có cùng lớp Lấy giá trị từ tất cả các trường của đối tượng gốc và gán nó sang cho đối tượng mới. Tuy nhiên không phải tất cả object đều có thể sao chép theo cách này vì có thể một vài trường của nó là private, không thể truy cập từ bên ngoài object.\nChưa kể nữa là code của bạn sẽ trở nên phụ thuộc vào lớp đó (vi phạm Dependency inversion principle), và đôi khi bạn chỉ biết interface của đối tượng chứ không biết đến lớp cụ thể, khi đó các tham số trong các method sẽ chấp nhận bất kỳ object nào theo interface đấy.\nĐể clone một object thì chúng ta có thể sử dụng shadow copy hoặc deep copy.\nShadow copy: chỉ sao chép cấu trúc cao nhất (top-level) của object chứ không tạo ra các bản sao của các object lồng nhau của nó (nested objects), khi đó nested objects sẽ giữ references. Deep copy: sao chép cấu trúc cao nhất (top-level) và tất cả các nested objects để tạo ra các bản sao hoàn toàn độc lập. Ứng dụng Database Connection Pooling: sử dụng prototypes để tạo và reuse connection. Configuration Handling: sử dụng prototypes để quản lý danh sách các cấu hình, để đảm bảo khi copy sang chạy ở môi trường khác thì ko bị mismatch. Machine Learning Initialization: sử dụng prototypes để khởi tạo models, weights pretrained khi fine-tuning, tiết kiệm thời gian training. Distributed system: sử dụng prototypes để duy trì tính toàn vẹn và đồng nhất dữ liệu của distributed databases, hay với mô hình microservices thì các service khi scale sẽ sử dụng prototypes để copy các cấu hình hoặc trạng thái để duy trì tính nhất quán và phục vụ các yêu cầu của người dùng. \u0026#34;\u0026#34;\u0026#34; Prototype pattern \u0026#34;\u0026#34;\u0026#34; from abc import abstractmethod, ABC class Person(ABC): \u0026#34;\u0026#34;\u0026#34; Prototype abstract \u0026#34;\u0026#34;\u0026#34; def __init__(self, name): self._name = name def set_name(self, name: str): self._name = name def get_name(self): return self._name @abstractmethod def display(self): pass @abstractmethod def clone(self): \u0026#34;\u0026#34;\u0026#34; abstract method để clone đối tượng \u0026#34;\u0026#34;\u0026#34; pass import copy \u0026#34;\u0026#34;\u0026#34; Teacher is Concrete Prototype \u0026#34;\u0026#34;\u0026#34; class Teacher(Person): def __init__(self, name, course): super().__init__(name) self._course = course def set_course(self, course: str): self._course = course def get_course(self): return self._course def display(self): print(\u0026#34;Teacher was cloned:\u0026#34;) print(\u0026#34;------------------:\u0026#34;) print(f\u0026#34;Name: {self._name}\u0026#34;) print(f\u0026#34;Who Teaches: {self._course}\\n\u0026#34;) def clone(self): return copy.copy(self) \u0026#34;\u0026#34;\u0026#34; Student is Concrete Prototype \u0026#34;\u0026#34;\u0026#34; class Student(Person): def __init__(self, name, teacher: Teacher): super().__init__(name) self._teacher = teacher def display(self): print(\u0026#34;Student was cloned:\u0026#34;) print(\u0026#34;------------------:\u0026#34;) print(f\u0026#34;Student Name: {self._name}\u0026#34;) print(f\u0026#34;Enrolled in: {self._teacher.get_course()}\u0026#34;) print(f\u0026#34;Taught by: {self._teacher.get_name()}\\n\u0026#34;) def clone(self): return copy.copy(self) \u0026#34;\u0026#34;\u0026#34; Client code \u0026#34;\u0026#34;\u0026#34; if __name__ == \u0026#34;__main__\u0026#34;: teacher = Teacher(\u0026#34;Ian Goodfellow\u0026#34;, \u0026#34;Deep Learning\u0026#34;) teacher_clone = teacher.clone() teacher_clone.display() student = Student(\u0026#34;Hoang\u0026#34;, teacher=teacher_clone) student_clone = student.clone() student_clone.display() # Modify teacher clone teacher_clone.set_course(\u0026#34;Machine Learning\u0026#34;) student_clone.display() Teacher was cloned: ------------------: Name: Ian Goodfellow Who Teaches: Deep Learning Student was cloned: ------------------: Student Name: Hoang Enrolled in: Deep Learning Taught by: Ian Goodfellow Student was cloned: ------------------: Student Name: Hoang Enrolled in: Machine Learning Taught by: Ian Goodfellow Do sử dụng shadow copy, nên khi gọi student.clone(), nó vẫn giữ reference đến object teacher_clone và thông tin display sẽ bị thay đổi theo. Vậy nếu sử dụng deepcopy thì sao?\nimport copy \u0026#34;\u0026#34;\u0026#34; Teacher is Concrete Prototype \u0026#34;\u0026#34;\u0026#34; class Teacher(Person): def __init__(self, name, course): super().__init__(name) self._course = course def set_course(self, course: str): self._course = course def get_course(self): return self._course def display(self): print(\u0026#34;Teacher was cloned:\u0026#34;) print(\u0026#34;------------------:\u0026#34;) print(f\u0026#34;Name: {self._name}\u0026#34;) print(f\u0026#34;Who Teaches: {self._course}\\n\u0026#34;) def clone(self): return copy.deepcopy(self) \u0026#34;\u0026#34;\u0026#34; Student is Concrete Prototype \u0026#34;\u0026#34;\u0026#34; class Student(Person): def __init__(self, name, teacher: Teacher): super().__init__(name) self._teacher = teacher def display(self): print(\u0026#34;Student was cloned:\u0026#34;) print(\u0026#34;------------------:\u0026#34;) print(f\u0026#34;Student Name: {self._name}\u0026#34;) print(f\u0026#34;Enrolled in: {self._teacher.get_course()}\u0026#34;) print(f\u0026#34;Taught by: {self._teacher.get_name()}\\n\u0026#34;) def clone(self): return copy.deepcopy(self) \u0026#34;\u0026#34;\u0026#34; Client code \u0026#34;\u0026#34;\u0026#34; if __name__ == \u0026#34;__main__\u0026#34;: teacher = Teacher(\u0026#34;Ian Goodfellow\u0026#34;, \u0026#34;Deep Learning\u0026#34;) teacher_clone = teacher.clone() teacher_clone.display() student = Student(\u0026#34;Hoang\u0026#34;, teacher=teacher_clone) student_clone = student.clone() student_clone.display() # Modify teacher clone teacher_clone.set_course(\u0026#34;Machine Learning\u0026#34;) student_clone.display() Teacher was cloned: ------------------: Name: Ian Goodfellow Who Teaches: Deep Learning Student was cloned: ------------------: Student Name: Hoang Enrolled in: Deep Learning Taught by: Ian Goodfellow Student was cloned: ------------------: Student Name: Hoang Enrolled in: Deep Learning Taught by: Ian Goodfellow Thông tin display được giữ nguyên -\u0026gt; deepcopy đã clone ra các bản sao hoàn toàn độc lập.\nSingleton Singleton pattern cho phép bạn tạo một instance duy nhất của một class trong suốt vòng đời của chương trình, với mục đích:\nHạn chế chế quyền truy cập đồng thời vào một tài nguyên được chia sẻ (vd: database connections) Tạo một điểm truy cập toàn cầu cho một tài nguyên (vd: load balancing) Non Thread Safe class Singleton: \u0026#34;\u0026#34;\u0026#34; Implement Singleton với staticmethod \u0026#34;\u0026#34;\u0026#34; __instance: dict = None @staticmethod def get_instance(): if not Singleton.__instance: return Singleton() return Singleton.__instance def __init__(self): if Singleton.__instance: raise Exception(\u0026#34;This class cannot initialize\u0026#34;) else: Singleton.__instance = self if __name__ == \u0026#34;__main__\u0026#34;: s1 = Singleton() s2 = Singleton.get_instance() s3 = Singleton.get_instance() assert id(s1) == id(s2) assert id(s2) == id(s3) class Singleton: \u0026#34;\u0026#34;\u0026#34; Implement Singleton với magic method __new__ \u0026#34;\u0026#34;\u0026#34; _instance = None def __new__(cls): \u0026#34;\u0026#34;\u0026#34; Create a new instance of the Singleton class if one does not already exist. Returns: Singleton: The unique instance of the Singleton class. \u0026#34;\u0026#34;\u0026#34; if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance if __name__ == \u0026#34;__main__\u0026#34;: s1 = Singleton() s2 = Singleton() assert id(s1) == id(s2) \u0026#34;\u0026#34;\u0026#34; Implement Singleton với decorator pattern \u0026#34;\u0026#34;\u0026#34; def singleton(cls): \u0026#34;\u0026#34;\u0026#34; A decorator to implement the Singleton design pattern. \u0026#34;\u0026#34;\u0026#34; instances = {} # Dictionary to store instances of different classes def get_instance(*args, **kwargs): \u0026#34;\u0026#34;\u0026#34; Create a new instance if it doesn\u0026#39;t exist, or return the existing one. Args: *args: Positional arguments for class initialization. **kwargs: Keyword arguments for class initialization. Returns: cls: The unique instance of the class. \u0026#34;\u0026#34;\u0026#34; if cls not in instances: # Create a new instance and store it instances[cls] = cls(*args, **kwargs) return instances[cls] # Return the existing instance # Return the closure function for class instantiation return get_instance @singleton # Applying the singleton decorator class SingletonClass: def __init__(self, data): self.data = data def display(self): print(f\u0026#34;Singleton instance with data: {self.data}\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: s1 = SingletonClass(\u0026#34;instance 1\u0026#34;) s2 = SingletonClass(\u0026#34;instance 2\u0026#34;) assert s1.display() == s2.display() Singleton instance with data: instance 1 Singleton instance with data: instance 1 class SingletonMeta(type): _instances = {} \u0026#34;\u0026#34;\u0026#34; Implement Singleton với metaclass _instances được khai báo ở đây là class attribute, nó được share (dùng chung) cho các instance khởi tạo từ class này. \u0026#34;\u0026#34;\u0026#34; def __call__(cls, *args, **kwargs): \u0026#34;\u0026#34;\u0026#34; Possible changes to the value of the `__init__` argument do not affect the returned instance. \u0026#34;\u0026#34;\u0026#34; if cls not in cls._instances: instance = super().__call__(*args, **kwargs) cls._instances[cls] = instance return cls._instances[cls] class Singleton(metaclass=SingletonMeta): def some_business_logic(self): pass if __name__ == \u0026#34;__main__\u0026#34;: s1 = Singleton() s2 = Singleton() assert id(s1) == id(s2) Thread Safe from threading import Lock, Thread class SingletonMeta(type): _instances = {} _lock: Lock = Lock() \u0026#34;\u0026#34;\u0026#34; We now have a lock object that will be used to synchronize threads during first access to the Singleton. \u0026#34;\u0026#34;\u0026#34; def __call__(cls, *args, **kwargs): \u0026#34;\u0026#34;\u0026#34; Possible changes to the value of the `__init__` argument do not affect the returned instance. \u0026#34;\u0026#34;\u0026#34; # Now, imagine that the program has just been launched. # Since there\u0026#39;s no Singleton instance yet, multiple threads can # simultaneously pass the previous conditional and reach this # point almost at the same time. The first of them will acquire # lock and will proceed further, while the rest will wait here. with cls._lock: # The first thread to acquire the lock, reaches this # conditional, goes inside and creates the Singleton # instance. Once it leaves the lock block, a thread that # might have been waiting for the lock release may then # enter this section. But since the Singleton field is # already initialized, the thread won\u0026#39;t create a new # object. if cls not in cls._instances: instance = super().__call__(*args, **kwargs) cls._instances[cls] = instance return cls._instances[cls] class Singleton(metaclass=SingletonMeta): value: str = None \u0026#34;\u0026#34;\u0026#34; We\u0026#39;ll use this property to prove that our Singleton really works. \u0026#34;\u0026#34;\u0026#34; def __init__(self, value: str) -\u0026gt; None: self.value = value def some_business_logic(self): \u0026#34;\u0026#34;\u0026#34; Finally, any singleton should define some business logic, which can be executed on its instance. \u0026#34;\u0026#34;\u0026#34; def test_singleton(value: str) -\u0026gt; None: singleton = Singleton(value) print(singleton.value) if __name__ == \u0026#34;__main__\u0026#34;: print(\u0026#34;If you see the same value, then singleton was reused (yay!)\\n\u0026#34; \u0026#34;If you see different values, \u0026#34; \u0026#34;then 2 singletons were created (booo!!)\\n\\n\u0026#34; \u0026#34;RESULT:\\n\u0026#34;) process1 = Thread(target=test_singleton, args=(\u0026#34;FOO\u0026#34;,)) process2 = Thread(target=test_singleton, args=(\u0026#34;BAR\u0026#34;,)) process1.start() process2.start() If you see the same value, then singleton was reused (yay!) If you see different values, then 2 singletons were created (booo!!) RESULT: FOO FOO You can get full source code here!\n","permalink":"https://hoangph3.github.io/posts/design_patterns_creational/","summary":"Implement creational design patterns such as Factory, Builder and Singleton in Python.","title":"Creational Design Patterns in Python"},{"content":"Chain of Responsibility Trong thực tế, một hệ thống thường có rất nhiều giai đoạn xử lý phải xử lý các yêu cầu gửi đến. Mỗi giai đoạn có một trách nhiệm cụ thể và thứ tự thực hiện các giai đoạn này là rất quan trọng. Đó chính là lý do mà chúng ta có thể sử dụng Chain of Responsibility, nó cho phép chúng ta xử lý các yêu cầu theo một chuỗi (chain) công việc cụ thể.\nChuỗi công việc ở đây không đơn thuần chỉ là một chuỗi thực hiện tuần tự (Basic Chain), mà nó có thể có một số biến thể khác như:\nBidirectional Chain: forward and backward directions. Hierarchical Chain: tổ chức dưới dạng hierarchical structure, có chứa các sub-chains khác. Dynamic Chain: chain có thể thay đổi tùy thuộc vào kết quả lúc chạy chương trình, điều kiện, … Một số hệ thống sử dụng Chain of Responsibility như:\nMiddleware trong web frameworks Workflow Automation Financial Transaction Processing Data Transformation Pipelines Ví dụ sau đây triển khai Middleware trong web frameworks, áp dụng Basic Chain:\nfrom abc import ABC, abstractmethod # Section 1: Abstract Middleware Base class Middleware(ABC): \u0026#34;\u0026#34;\u0026#34;Abstract Middleware class serving as the base of the chain.\u0026#34;\u0026#34;\u0026#34; @abstractmethod def handle_request(self, request): \u0026#34;\u0026#34;\u0026#34;Handle the request; must be implemented by concrete classes.\u0026#34;\u0026#34;\u0026#34; pass # Section 2: Concrete Middleware Implementations class AuthenticationMiddleware(Middleware): \u0026#34;\u0026#34;\u0026#34;Middleware responsible for user authentication.\u0026#34;\u0026#34;\u0026#34; def handle_request(self, request): \u0026#34;\u0026#34;\u0026#34;Handle authentication or pass to the next middleware in the chain.\u0026#34;\u0026#34;\u0026#34; if self.authenticate(request): print(\u0026#34;Authentication middleware: Authenticated successfully\u0026#34;) # Pass the request to the next middleware or handler in the chain. return super().handle_request(request) else: print(\u0026#34;Authentication middleware: Authentication failed\u0026#34;) # Stop the chain if authentication fails. return None def authenticate(self, request): \u0026#34;\u0026#34;\u0026#34;Implement authentication logic here.\u0026#34;\u0026#34;\u0026#34; # Return True if authentication is successful, else False. pass class LoggingMiddleware(Middleware): \u0026#34;\u0026#34;\u0026#34;Middleware responsible for logging requests.\u0026#34;\u0026#34;\u0026#34; def handle_request(self, request): \u0026#34;\u0026#34;\u0026#34;Handle request logging and pass to the next middleware in the chain.\u0026#34;\u0026#34;\u0026#34; print(\u0026#34;Logging middleware: Logging request\u0026#34;) # Pass the request to the next middleware or handler in the chain. return super().handle_request(request) class DataValidationMiddleware(Middleware): \u0026#34;\u0026#34;\u0026#34;Middleware responsible for data validation.\u0026#34;\u0026#34;\u0026#34; def handle_request(self, request): \u0026#34;\u0026#34;\u0026#34;Handle data validation or pass to the next middleware in the chain.\u0026#34;\u0026#34;\u0026#34; if self.validate_data(request): print(\u0026#34;Data Validation middleware: Data is valid\u0026#34;) # Pass the request to the next middleware or handler in the chain. return super().handle_request(request) else: print(\u0026#34;Data Validation middleware: Invalid data\u0026#34;) # Stop the chain if data validation fails. return None def validate_data(self, request): \u0026#34;\u0026#34;\u0026#34;Implement data validation logic here.\u0026#34;\u0026#34;\u0026#34; # Return True if data is valid, else False. pass # Section 3: Request Handling Class and Client Code # Chain class to handle the final request and manage middleware. class Chain: def __init__(self): self.middlewares = [] def add_middleware(self, middleware): self.middlewares.append(middleware) def handle_request(self, request): for middleware in self.middlewares: request = middleware.handle_request(request) if request is None: print(\u0026#34;Request processing stopped.\u0026#34;) break # Client code to create and configure the middleware chain. if __name__ == \u0026#34;__main__\u0026#34;: # Create middleware instances. auth_middleware = AuthenticationMiddleware() logging_middleware = LoggingMiddleware() data_validation_middleware = DataValidationMiddleware() # Create the chain and add middleware. chain = Chain() chain.add_middleware(auth_middleware) chain.add_middleware(logging_middleware) chain.add_middleware(data_validation_middleware) # Simulate an HTTP request. http_request = {\u0026#34;user\u0026#34;: \u0026#34;username\u0026#34;, \u0026#34;data\u0026#34;: \u0026#34;valid_data\u0026#34;} chain.handle_request(http_request) Authentication middleware: Authentication failed Request processing stopped. Command Command Method cho phép bạn đóng gói các yêu cầu hoặc hoạt động vào một đối tượng riêng biệt, cho phép bạn thực hiện các thao tác như di chuyển, hoặc gửi các yêu cầu một cách dễ dàng mà không cần biết chi tiết về thực hiện cụ thể của nó.\nfrom abc import ABC, abstractmethod # Command interface class Command(ABC): @abstractmethod def execute(self): pass # Receiver class Light: def turn_on(self): print(\u0026#34;Light is on\u0026#34;) def turn_off(self): print(\u0026#34;Light is off\u0026#34;) # Concrete commands class TurnOnCommand(Command): def __init__(self, light: Light): self.light = light def execute(self): self.light.turn_on() class TurnOffCommand(Command): def __init__(self, light: Light): self.light = light def execute(self): self.light.turn_off() # Invoker class RemoteControl: def __init__(self): self.command = None def set_command(self, command: Command): self.command = command def press_button(self): self.command.execute() # Client code if __name__ == \u0026#34;__main__\u0026#34;: light = Light() turn_on = TurnOnCommand(light) turn_off = TurnOffCommand(light) remote = RemoteControl() remote.set_command(turn_on) remote.press_button() # Output: Light is on remote.set_command(turn_off) remote.press_button() # Output: Light is off Light is on Light is off Trong đoạn code trên:\nCommand là một interface, chỉ có method execute() để thực thi command. Các lớp cụ thể như TurnOnCommand và TurnOffCommand này là các Concrete Command để thực hiện các hành động cụ thể, được tạo từ Client. RemoteControl có nhiệm vụ kích hoạt các hành động (Invoker: nhận các commands được client giao cho). Light có nhiệm vụ nhận và thực thi các hành động (Receiver: chứa một số logic nghiệp vụ). Iterator Iterator là giúp chúng ta duyệt phần tử của một tập hợp mà không để lộ dạng cơ bản của nó (list, stack, tree, …)\nclass Iterator: \u0026#34;\u0026#34;\u0026#34;Step 1: Create the Iterator interface.\u0026#34;\u0026#34;\u0026#34; def __iter__(self): \u0026#34;\u0026#34;\u0026#34;Defines the __iter__() method to return self as an iterator.\u0026#34;\u0026#34;\u0026#34; raise NotImplementedError def __next__(self): \u0026#34;\u0026#34;\u0026#34;Defines the __next__() method to retrieve elements sequentially.\u0026#34;\u0026#34;\u0026#34; raise NotImplementedError class MyIterator(Iterator): \u0026#34;\u0026#34;\u0026#34;Step 2: Implement a Concrete Iterator.\u0026#34;\u0026#34;\u0026#34; def __init__(self, data): self.data = data self.index = 0 def __iter__(self): return self # Returning self as an iterator def __next__(self): if self.index \u0026gt;= len(self.data): raise StopIteration value = self.data[self.index] self.index += 1 return value class Collection: \u0026#34;\u0026#34;\u0026#34;Step 3: Create the Collection interface.\u0026#34;\u0026#34;\u0026#34; def create_iterator(self): \u0026#34;\u0026#34;\u0026#34;Method to create an Iterator compatible with the collection.\u0026#34;\u0026#34;\u0026#34; raise NotImplementedError class MyCollection(Collection): \u0026#34;\u0026#34;\u0026#34;Step 4: Implement a Concrete Collection.\u0026#34;\u0026#34;\u0026#34; def __init__(self): self.data = [] def add(self, value): self.data.append(value) def create_iterator(self): return MyIterator(self.data) # Client code if __name__ == \u0026#34;__main__\u0026#34;: \u0026#34;\u0026#34;\u0026#34;Demonstrate the usage of the implemented Iterator Pattern.\u0026#34;\u0026#34;\u0026#34; # Creating a collection my_collection = MyCollection() my_collection.add(1) my_collection.add(2) my_collection.add(3) # Creating an iterator for the collection my_iterator = my_collection.create_iterator() # Using the iterator to traverse through the elements for element in my_iterator: print(element) 1 2 3 Mediator Mediator pattern giúp chúng ta hạn chế việc giao tiếp trực tiếp giữa các đối tượng, buộc nó giao tiếp thông qua đối tượng mediator. Khi đó, bạn có thể:\nQuản lý giao tiếp: các đối tượng muốn trao đổi với nhau sẽ phải thông qua một người trung gian, đó chính là mediator. Giảm sự phụ thuộc: do các đối tượng chỉ giao tiếp qua trung gian là mediator nên chúng nó sẽ không quá gắn bó và phụ thuộc nhau, giúp việc thay đổi trở nên dễ dàng hơn. Tuy nhiên cũng cần chú ý rằng, mediator chịu trách nhiệm giao tiếp giữa các đối tượng với nhau nên dần theo thời gian nó sẽ đảm nhiệm rất nhiều nhiệm vụ và trở thành God object (một trong những anti-pattern). Khi đó chúng ta có thể tách nhỏ mediator này thành các mediator nhỏ hơn.\nTrong thực tế, Message Broker hoạt động như một Mediator, nó sẽ đóng vai trò làm trung gian, cho phép các dịch vụ của hệ thống gửi và nhận message mà không cần biết và kết nối trực tiếp với nhau. Hãy xem đoạn code sau:\nfrom abc import ABC, abstractmethod class Participant: \u0026#34;\u0026#34;\u0026#34; Represents a message participant. \u0026#34;\u0026#34;\u0026#34; def __init__(self, mediator, name): self._mediator = mediator self.name = name def send_message(self, message): \u0026#34;\u0026#34;\u0026#34; Sends a message through the mediator. \u0026#34;\u0026#34;\u0026#34; self._mediator.send_message(message, self) def receive_message(self, message): \u0026#34;\u0026#34;\u0026#34; Receives and processes messages from the mediator. \u0026#34;\u0026#34;\u0026#34; print(f\u0026#34;{self.name} received message: {message}\u0026#34;) class MessageBroker(ABC): \u0026#34;\u0026#34;\u0026#34; Mediator interface (Message Broker) declares message handling methods. \u0026#34;\u0026#34;\u0026#34; @abstractmethod def send_message(self, message, participant): \u0026#34;\u0026#34;\u0026#34; Sends a message to a participant. \u0026#34;\u0026#34;\u0026#34; pass class ConcreteMessageBroker(MessageBroker): \u0026#34;\u0026#34;\u0026#34; Concrete Message Broker manages message passing between participants. \u0026#34;\u0026#34;\u0026#34; def __init__(self): self._participants = [] def add_participant(self, participant): \u0026#34;\u0026#34;\u0026#34; Adds a participant to the broker. \u0026#34;\u0026#34;\u0026#34; self._participants.append(participant) def send_message(self, message, participant): \u0026#34;\u0026#34;\u0026#34; Sends a message to all participants except the sender. \u0026#34;\u0026#34;\u0026#34; for p in self._participants: # if p != participant: p.receive_message(message) # Client code if __name__ == \u0026#34;__main__\u0026#34;: # Create message broker message_broker = ConcreteMessageBroker() # Create participants and link them to the broker participant1 = Participant(message_broker, \u0026#34;Participant 1\u0026#34;) participant2 = Participant(message_broker, \u0026#34;Participant 2\u0026#34;) # Add participants to the broker message_broker.add_participant(participant1) message_broker.add_participant(participant2) # Send messages through participants participant1.send_message(\u0026#34;Hello from Participant 1\u0026#34;) print() participant2.send_message(\u0026#34;Hi from Participant 2\u0026#34;) Participant 1 received message: Hello from Participant 1 Participant 2 received message: Hello from Participant 1 Participant 1 received message: Hi from Participant 2 Participant 2 received message: Hi from Participant 2 Memento Memento pattern được sử dụng trong việc lưu trữ trạng thái của một đối tượng để sau này có thể khôi phục lại trạng thái đó mà không làm thay đổi cấu trúc của đối tượng đó.\nTrong Memento pattern, có ba thành phần chính:\nOriginator (Nguyên tác): Đây là đối tượng mà trạng thái cần được lưu trữ. Nó có thể tạo ra một memento để lưu trữ trạng thái hiện tại của nó và cung cấp một phương thức để khôi phục lại trạng thái từ memento. Memento (Bản ghi nhớ): Là một đối tượng chứa trạng thái của nguyên tác tại một thời điểm cụ thể. Memento không nên bị sửa đổi từ bên ngoài, chỉ nguyên tác mới có quyền truy cập vào nó. Caretaker: Là đối tượng được sử dụng để quản lý và duy trì các memento. Nó không biết về cấu trúc nội bộ của memento, chỉ biết cách lưu trữ và khôi phục lại chúng. Memento được ứng dụng trong rất nhiều phần mềm như:\nGit: lưu trữ commit, branch, cho phép checkout version, … Text editor: lưu trữ các action, cho phép undo, … Database: cho phép rollback transaction, … class Memento: def __init__(self, state): self._state = state def get_state(self): return self._state class Originator: _state = None def set_state(self, state): print(f\u0026#34;Setting state to {state}\u0026#34;) self._state = state def save_to_memento(self): print(\u0026#34;Saving state to Memento\u0026#34;) return Memento(self._state) def restore_from_memento(self, memento): self._state = memento.get_state() print(f\u0026#34;Restored state to {self._state}\u0026#34;) class Caretaker: _mementos = [] def add_memento(self, memento): print(\u0026#34;Adding Memento to the list\u0026#34;) self._mementos.append(memento) def get_memento(self, index): print(\u0026#34;Getting Memento from the list\u0026#34;) return self._mementos[index] # Client code if __name__ == \u0026#34;__main__\u0026#34;: originator = Originator() caretaker = Caretaker() # Thiết lập trạng thái ban đầu của originator originator.set_state(\u0026#34;State 1\u0026#34;) # Lưu trạng thái hiện tại vào memento và thêm vào caretaker caretaker.add_memento(originator.save_to_memento()) # Thiết lập trạng thái mới cho originator originator.set_state(\u0026#34;State 2\u0026#34;) # Lưu trạng thái mới vào một memento mới và thêm vào caretaker caretaker.add_memento(originator.save_to_memento()) # Khôi phục trạng thái trước đó originator.restore_from_memento(caretaker.get_memento(0)) # Output: \u0026#34;State 1\u0026#34; Setting state to State 1 Saving state to Memento Adding Memento to the list Setting state to State 2 Saving state to Memento Adding Memento to the list Getting Memento from the list Restored state to State 1 Observer Observer pattern cho phép nhiều đối tượng nhận được cập nhật khi có thay đổi xảy ra ở đối tượng khác mà chúng đang quan sát. Trong thực tế ví dụ như Youtube, kênh sẽ thông báo cho các subscribers khi có video mới, live stream, …\nObserver khá giống với Mediator, đều cố gắng tạo ra sự giao tiếp một cách gián tiếp giữa các đối tượng. Trong khi Mediator loại bỏ các kết nối trực tiếp giữa các thành phần, thiết lập một mediator duy nhất làm trung gian, thì Observer linh hoạt hơn khi cho phép chủ động subscribe hoặc unsubscribe người nhận.\nVí dụ sau đây triển khai một ChatRoom, tất cả mọi người cùng join vào một room (group) sẽ nhận được thông báo khi có message từ một người bất kỳ gửi đến.\nfrom abc import ABC, abstractmethod # Step 1: The ChatRoom - Publisher class ChatRoom: def __init__(self): self._participants = set() def join(self, participant): \u0026#34;\u0026#34;\u0026#34;Adds a new participant to the chat room.\u0026#34;\u0026#34;\u0026#34; self._participants.add(participant) def leave(self, participant): \u0026#34;\u0026#34;\u0026#34;Removes a participant from the chat room.\u0026#34;\u0026#34;\u0026#34; self._participants.remove(participant) def broadcast(self, message): \u0026#34;\u0026#34;\u0026#34;Sends a message to all participants in the chat room.\u0026#34;\u0026#34;\u0026#34; for participant in self._participants: participant.receive(message) # Step 2: Participant - Subscriber Interface class Participant(ABC): @abstractmethod def receive(self, message): \u0026#34;\u0026#34;\u0026#34;Abstract method for receiving messages.\u0026#34;\u0026#34;\u0026#34; pass # Step [3]: ChatMember - Concrete Subscribers class ChatMember(Participant): def __init__(self, name): self.name = name def receive(self, message): \u0026#34;\u0026#34;\u0026#34;Receives and displays the message.\u0026#34;\u0026#34;\u0026#34; print(f\u0026#34;{self.name} received: {message}\u0026#34;) # Step [4]: Client if __name__ == \u0026#34;__main__\u0026#34;: # Create a chat room general_chat = ChatRoom() # Create participants user1 = ChatMember(\u0026#34;User1\u0026#34;) user2 = ChatMember(\u0026#34;User2\u0026#34;) user3 = ChatMember(\u0026#34;User3\u0026#34;) # Participants join the chat room general_chat.join(user1) general_chat.join(user2) general_chat.join(user3) # Send a message to the chat room general_chat.broadcast(\u0026#34;Welcome to the chat!\u0026#34;) User1 received: Welcome to the chat! User3 received: Welcome to the chat! User2 received: Welcome to the chat! State State pattern giúp chúng ta quản lý hành vi của đối tượng khi trạng thái của nó thay đổi.\nTrong thực tế, tại bất kỳ thời điểm nào cũng có một hữu hạn trạng thái (state) mà chương trình có thể có. Với từng trạng thái cụ thể, chương trình sẽ có các hành vi khác nhau, nó có thể chuyển từ trạng thái này sang trạng thái khác ngay lập tức, hoặc giữ nguyên trạng thái đó, … Quy luật chuyển đổi này gọi là transitions, nó hữu hạn và có thể định trước.\nCách đơn giản nhất mà chúng ta có thể nghĩ ra ngay đó chính là xử lý logic nghiệp vụ cho từng trạng thái (if-else) trong một class duy nhất, tuy nhiên khi logic nghiệp vụ phát sinh thêm khiến số lượng trạng thái và quá trình chuyển đổi tăng lên thì câu lệnh if-else rất khó bảo trì và dễ mắc lỗi. Đó chính là lý do mà chúng ta nên sử dụng State pattern, giúp đóng gói các trạng thái này trong các đối tượng riêng biệt và chuyển đổi giữa chúng một cách liền mạch.\nTrong State pattern, có ba thành phần chính:\nContext: Là đối tượng chứa trạng thái hiện tại và gọi các method tương ứng với trạng thái đó. State (Interface): lớp base chứa các method chung cho tất cả các trạng thái. Concrete States (Trạng thái cụ thể): triển khai các method để thực hiện các hành vi cụ thể cho mỗi trạng thái. Trong ví dụ sau, hãy triển khai State pattern cho hệ thống thanh toán thương mại điện tử.\n# Step 1: Define the State Interface class CheckoutState: \u0026#34;\u0026#34;\u0026#34;Interface defining the methods for various checkout states.\u0026#34;\u0026#34;\u0026#34; def add_item(self, item): \u0026#34;\u0026#34;\u0026#34;Add an item to the cart.\u0026#34;\u0026#34;\u0026#34; pass def review_cart(self): \u0026#34;\u0026#34;\u0026#34;Review the cart contents.\u0026#34;\u0026#34;\u0026#34; pass def enter_shipping_info(self, info): \u0026#34;\u0026#34;\u0026#34;Enter shipping information.\u0026#34;\u0026#34;\u0026#34; pass def process_payment(self): \u0026#34;\u0026#34;\u0026#34;Process the payment.\u0026#34;\u0026#34;\u0026#34; pass \u0026#34;\u0026#34;\u0026#34; Triển khai các State cụ thể, ở đây chúng ta có 4 trạng thái cụ thể, thể hiện các giai đoạn khác nhau khi thanh toán: - EmptyCartState: giỏ hàng trống - ItemAddedState: thêm sản phẩm vào giỏ - CartReviewedState: Check lại giỏ hàng - ShippingInfoEnteredState: thêm thông tin vận chuyển Với mỗi trạng thái sẽ triển khai hành vi cụ thể của nó. \u0026#34;\u0026#34;\u0026#34; # Step 2: Create Concrete State Classes class EmptyCartState(CheckoutState): \u0026#34;\u0026#34;\u0026#34;Concrete state representing an empty cart.\u0026#34;\u0026#34;\u0026#34; def add_item(self, item): \u0026#34;\u0026#34;\u0026#34;Add an item to the cart and transition to ItemAddedState.\u0026#34;\u0026#34;\u0026#34; print(\u0026#34;Item added to the cart.\u0026#34;) return ItemAddedState() def review_cart(self): \u0026#34;\u0026#34;\u0026#34;Display inability to review an empty cart.\u0026#34;\u0026#34;\u0026#34; print(\u0026#34;Cannot review an empty cart.\u0026#34;) def enter_shipping_info(self, info): \u0026#34;\u0026#34;\u0026#34;Display inability to enter shipping info with an empty cart.\u0026#34;\u0026#34;\u0026#34; print(\u0026#34;Cannot enter shipping info with an empty cart.\u0026#34;) def process_payment(self): \u0026#34;\u0026#34;\u0026#34;Display inability to process payment with an empty cart.\u0026#34;\u0026#34;\u0026#34; print(\u0026#34;Cannot process payment with an empty cart.\u0026#34;) class ItemAddedState(CheckoutState): \u0026#34;\u0026#34;\u0026#34;Concrete state representing a cart with added items.\u0026#34;\u0026#34;\u0026#34; def add_item(self, item): \u0026#34;\u0026#34;\u0026#34;Add an item to the cart.\u0026#34;\u0026#34;\u0026#34; print(\u0026#34;Item added to the cart.\u0026#34;) def review_cart(self): \u0026#34;\u0026#34;\u0026#34;Review cart contents and transition to CartReviewedState.\u0026#34;\u0026#34;\u0026#34; print(\u0026#34;Reviewing cart contents.\u0026#34;) return CartReviewedState() def enter_shipping_info(self, info): \u0026#34;\u0026#34;\u0026#34;Display inability to enter shipping info without reviewing the cart.\u0026#34;\u0026#34;\u0026#34; print(\u0026#34;Cannot enter shipping info without reviewing the cart.\u0026#34;) def process_payment(self): \u0026#34;\u0026#34;\u0026#34;Display inability to process payment without entering shipping info.\u0026#34;\u0026#34;\u0026#34; print(\u0026#34;Cannot process payment without entering shipping info.\u0026#34;) class CartReviewedState(CheckoutState): \u0026#34;\u0026#34;\u0026#34;Concrete state representing a reviewed cart.\u0026#34;\u0026#34;\u0026#34; def add_item(self, item): \u0026#34;\u0026#34;\u0026#34;Display inability to add items after reviewing the cart.\u0026#34;\u0026#34;\u0026#34; print(\u0026#34;Cannot add items after reviewing the cart.\u0026#34;) def review_cart(self): \u0026#34;\u0026#34;\u0026#34;Display that the cart has already been reviewed.\u0026#34;\u0026#34;\u0026#34; print(\u0026#34;Cart already reviewed.\u0026#34;) def enter_shipping_info(self, info): \u0026#34;\u0026#34;\u0026#34;Enter shipping information and transition to ShippingInfoEnteredState.\u0026#34;\u0026#34;\u0026#34; print(\u0026#34;Entering shipping information.\u0026#34;) return ShippingInfoEnteredState(info) def process_payment(self): \u0026#34;\u0026#34;\u0026#34;Display inability to process payment without entering shipping info.\u0026#34;\u0026#34;\u0026#34; print(\u0026#34;Cannot process payment without entering shipping info.\u0026#34;) class ShippingInfoEnteredState(CheckoutState): \u0026#34;\u0026#34;\u0026#34;Concrete state representing the entry of shipping information.\u0026#34;\u0026#34;\u0026#34; def __init__(self, info): self.info = info def add_item(self, item): \u0026#34;\u0026#34;\u0026#34;Display inability to add items after entering shipping info.\u0026#34;\u0026#34;\u0026#34; print(\u0026#34;Cannot add items after entering shipping info.\u0026#34;) def review_cart(self): \u0026#34;\u0026#34;\u0026#34;Display inability to review cart after entering shipping info.\u0026#34;\u0026#34;\u0026#34; print(\u0026#34;Cannot review cart after entering shipping info.\u0026#34;) def enter_shipping_info(self, info): \u0026#34;\u0026#34;\u0026#34;Display that shipping information has already been entered.\u0026#34;\u0026#34;\u0026#34; print(\u0026#34;Shipping information already entered.\u0026#34;) def process_payment(self): \u0026#34;\u0026#34;\u0026#34;Process payment with the entered shipping info.\u0026#34;\u0026#34;\u0026#34; print(\u0026#34;Processing payment with the entered shipping info.\u0026#34;) \u0026#34;\u0026#34;\u0026#34; CheckoutContext: class đóng vai trò là người quản lý quá trình thanh toán. - Theo dõi trạng thái hiện tại. - Thực hiện hành vi của trạng thái. - Đảm bảo sự chuyển đổi liền mạch giữa các trạng thái. \u0026#34;\u0026#34;\u0026#34; # Step 3: Create the Context Class class CheckoutContext: \u0026#34;\u0026#34;\u0026#34;Context class managing the checkout states.\u0026#34;\u0026#34;\u0026#34; def __init__(self): self.current_state = EmptyCartState() def add_item(self, item): \u0026#34;\u0026#34;\u0026#34;Add an item to the cart, updating the current state.\u0026#34;\u0026#34;\u0026#34; self.current_state = self.current_state.add_item(item) def review_cart(self): \u0026#34;\u0026#34;\u0026#34;Review the cart, updating the current state.\u0026#34;\u0026#34;\u0026#34; self.current_state = self.current_state.review_cart() def enter_shipping_info(self, info): \u0026#34;\u0026#34;\u0026#34;Enter shipping information, updating the current state.\u0026#34;\u0026#34;\u0026#34; self.current_state = self.current_state.enter_shipping_info(info) def process_payment(self): \u0026#34;\u0026#34;\u0026#34;Process the payment, updating the current state.\u0026#34;\u0026#34;\u0026#34; self.current_state.process_payment() # Step 4: Example of Usage if __name__ == \u0026#34;__main__\u0026#34;: cart = CheckoutContext() cart.add_item(\u0026#34;Product 1\u0026#34;) cart.review_cart() cart.enter_shipping_info(\u0026#34;123 Main St, City\u0026#34;) cart.process_payment() Item added to the cart. Reviewing cart contents. Entering shipping information. Processing payment with the entered shipping info. Strategy Strategy pattern giúp chúng ta gom nhóm các thuật toán mà có thể thay thế cho nhau, cho phép phía client có thể chuyển đổi thuật toán một cách linh hoạt (Dynamic Algorithm Switching).\nỨng dụng: Hệ thống đặt xe: ví dụ bạn muốn đến sân bay, bạn có thể bắt xe máy, xe 4 chỗ, xe 7 chỗ. Các phương tiện của bạn ở đây là strategy. Bạn có thể chọn một trong các strategy dựa vào các nhân tố như ví tiền hay thời gian. Hệ thống giao dịch: khi chúng ta muốn thực hiện một giao dịch thì chắc hẳn chúng ta cũng đã thực hiện một số chiến lược (strategy) nào đó để đưa ra quyết định, ví dụ: Moving Average, Mean Reversion, … Chúng ta có thể chọn một trong các Strategy này dựa vào các nhân tố như ví tiền, đồ thị xu hướng, giá cả, … Chính sách giảm giá của các cửa hàng: bất kỳ chính sách nào cũng phải có chiến lược (strategy), ví dụ: discount X%, discount X% + quà tặng, discount khi mua với số lượng lớn, tặng phiếu mua hàng lần tiếp theo, … Một vài usecase sử dụng trong thực tế:\nPython's sort function: hàm này cho phép truyền vào custom comparison function. Sklearn ML Library: module preprocessing data hỗ trợ nhiều strategy khác nhau để xử lý dữ liệu: mean removal, variance scaling, standardization, … TensorFlow ML Library: module optimizers hỗ trợ nhiều strategy khác nhau để chạy gradient descent: adam, adagrad, … Tại sao lại cần đến Strategy? Cứ mỗi khi thêm thuật toán mới, tư duy đơn giản nhất là chúng ta sẽ copy code của thuật toán cũ, sau đó sửa lại logic của thuật toán mới là work. Nếu ứng dụng nhỏ với vài ba thuật toán thì có thể chấp nhận được, tuy nhiên lâu dần nó sẽ nảy sinh ra vấn đề khi lượng thuật toán tăng lên:\nDuplicate code Quá trình copy code và sửa đổi dễ nhầm lẫn và sinh ra bug. Có những phần tính toán chung nằm trong các thuật toán, nếu có bug hay cần sửa đổi ở phần này thì phải sửa lại ở tất cả các lớp thuật toán. Triển khai Strategy pattern Strategy Interface: chứa interface thực thi một strategy cụ thể Concrete Strategy: triển khai strategy cụ thể Context: refer đến một strategy cụ thể Hãy xem ví dụ triển khai strategy bên dưới cho hệ thống giao dịch (trading system):\n# Step 1: Create the Strategy Interface class TradingStrategy(ABC): @abstractmethod def execute_trade(self, data): pass # Step 2: Create Concrete Strategies class MovingAverageStrategy(TradingStrategy): def execute_trade(self, data): # Calculate Moving Average (Simple example for illustration) window_size = 3 # Adjust as needed moving_average = sum(data[-window_size:]) / window_size return f\u0026#34;Executing Moving Average Trading Strategy. Moving Average: {moving_average:.2f}\u0026#34; class MeanReversionStrategy(TradingStrategy): def execute_trade(self, data): # Calculate Mean Reversion (Simple example for illustration) mean_value = sum(data) / len(data) deviation = data[-1] - mean_value return f\u0026#34;Executing Mean Reversion Trading Strategy. Deviation from Mean: {deviation:.2f}\u0026#34; # Step 3: Create the Context class TradingContext: def __init__(self, strategy): self._strategy = strategy def set_strategy(self, strategy): self._strategy = strategy def execute_trade(self, data): return self._strategy.execute_trade(data) # Main Section to Showcase Usage if __name__ == \u0026#34;__main__\u0026#34;: # Sample data for trading trading_data = [50, 55, 45, 60, 50] # Create concrete strategy objects moving_average_strategy = MovingAverageStrategy() mean_reversion_strategy = MeanReversionStrategy() # Create context with a default strategy trading_context = TradingContext(moving_average_strategy) # Execute the default strategy result = trading_context.execute_trade(trading_data) print(result) # Output: Executing Moving Average Trading Strategy. Moving Average: 51.67 # Switch to a different strategy at runtime trading_context.set_strategy(mean_reversion_strategy) # Execute the updated strategy result = trading_context.execute_trade(trading_data) print(result) # Output: Executing Mean Reversion Trading Strategy. Deviation from Mean: -1.00 Executing Moving Average Trading Strategy. Moving Average: 51.67 Executing Mean Reversion Trading Strategy. Deviation from Mean: -2.00 Template Method Template Method giúp chúng ta định nghĩa các interface của thuật toán ở lớp cha (superclass) - cái này gọi là abstraction, còn về chi tiết cách thực hiện thì lớp con sẽ triển khai.\nTại sao lại cần đến Template method? Giả sử chúng ta có một hệ thống datamining để xử lý dữ liệu của công ty, phiên bản đầu tiên ứng dụng làm việc với file doc, trong các phiên bản tiếp theo nó hỗ trợ thêm các định dạng khác: csv, json, pdf, xml, … Và lúc đó chúng ta nhận ra rằng, code xử lý và phân tích dữ liệu với các loại định dạng file là hoàn toàn giống nhau, chỉ khác nhau ở bước tiền xử lý dữ liệu tương ứng với từng loại định dạng file, và chúng ta hoàn toàn có thể loại bỏ sự trùng lặp code này.\nTư tưởng của Template method khá giống với Strategy, chỉ khác ở chỗ:\nTemplate Method dựa trên sự kế thừa: cho phép thay đổi các phần của một thuật toán bằng cách mở rộng các phần đó trong các lớp con. Strategy dựa trên cấu tạo: cho phép thay đổi các phần trong hành vi của đối tượng bằng cách cung cấp cho đối tượng các strategy khác nhau tương ứng với hành vi đó. Template Method hoạt động ở cấp độ lớp, vì vậy nó là tĩnh. Strategy hoạt động ở cấp độ đối tượng, cho phép bạn chuyển đổi hành vi khi chạy chương trình (run-time).\nHạn chế của template method ❌ Các template method có thể khó bảo trì hơn khi chúng có nhiều bước hơn.\n❌ Áp dụng template method có thể đã khiến chúng ta vi phạm nguyên tắc Liskov Substitution, khi mà việc triển khai Concrete Template của class con đã chặn bước triển khai mặc định của class cha, khi đó chúng ta không thể dùng class con để thay thế (substitute) cho class cha được nữa.\nTriển khai Template method pattern Hãy xem ví dụ triển khai template method bên dưới cho hệ thống web scraper, xử lý với nhiều loại data input khác nhau (url, html, …):\nfrom abc import ABC, abstractmethod import requests import urllib from bs4 import BeautifulSoup # Step 1: Abstract Class (Web Scraper Template) class WebScraperTemplate(ABC): @abstractmethod def send_request(self, url): \u0026#34;\u0026#34;\u0026#34;Abstract method for sending HTTP requests.\u0026#34;\u0026#34;\u0026#34; pass @abstractmethod def parse_html(self, content): \u0026#34;\u0026#34;\u0026#34;Abstract method for parsing HTML content.\u0026#34;\u0026#34;\u0026#34; pass @abstractmethod def extract_data(self, soup): \u0026#34;\u0026#34;\u0026#34;Abstract method for extracting data from parsed HTML.\u0026#34;\u0026#34;\u0026#34; pass # Template method orchestrating the steps def scrape_website(self, url): \u0026#34;\u0026#34;\u0026#34;The template method for web scraping.\u0026#34;\u0026#34;\u0026#34; content = self.send_request(url) soup = self.parse_html(content) data = self.extract_data(soup) return data # Step 2: Concrete Class 1 (RequestsAndBeautifulSoupWebScraper) class RequestsAndBeautifulSoupWebScraper(WebScraperTemplate): def send_request(self, url): \u0026#34;\u0026#34;\u0026#34;Concrete implementation for sending HTTP requests using requests library.\u0026#34;\u0026#34;\u0026#34; response = requests.get(url) return response.content def parse_html(self, content): \u0026#34;\u0026#34;\u0026#34;Concrete implementation for parsing HTML content using BeautifulSoup.\u0026#34;\u0026#34;\u0026#34; soup = BeautifulSoup(content, \u0026#39;html.parser\u0026#39;) return soup def extract_data(self, soup): \u0026#34;\u0026#34;\u0026#34;Concrete implementation for extracting data from parsed HTML.\u0026#34;\u0026#34;\u0026#34; # Example: Extracting all hyperlinks from the webpage links = [a[\u0026#39;href\u0026#39;] for a in soup.find_all(\u0026#39;a\u0026#39;, href=True)] return links # Step 3: Concrete Class 2 (UrllibAndRegexWebScraper) class UrllibAndRegexWebScraper(WebScraperTemplate): def send_request(self, url): \u0026#34;\u0026#34;\u0026#34;Concrete implementation for sending HTTP requests using urllib.\u0026#34;\u0026#34;\u0026#34; with urllib.request.urlopen(url) as response: return response.read() def parse_html(self, content): \u0026#34;\u0026#34;\u0026#34;Concrete implementation for parsing HTML content using custom parser.\u0026#34;\u0026#34;\u0026#34; # Custom HTML parsing logic return content def extract_data(self, content): \u0026#34;\u0026#34;\u0026#34;Concrete implementation for extracting data using regex.\u0026#34;\u0026#34;\u0026#34; import re # Example: Extracting all email addresses from the webpage emails = re.findall(r\u0026#39;\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b\u0026#39;, content.decode(\u0026#39;utf-8\u0026#39;)) return emails # Main Section (Client Code) if __name__ == \u0026#34;__main__\u0026#34;: # Creating instances of the concrete classes web_scraper_requests_beautifulsoup = RequestsAndBeautifulSoupWebScraper() web_scraper_urllib_regex = UrllibAndRegexWebScraper() # Using the template method to scrape websites target_url = \u0026#34;https://amirlavasani.vercel.app/\u0026#34; print(\u0026#34;Scraped Data using RequestsAndBeautifulSoupWebScraper:\u0026#34;) data_requests_beautifulsoup = web_scraper_requests_beautifulsoup.scrape_website(target_url) for link in data_requests_beautifulsoup: print(link) print(\u0026#34;\\nScraped Data using UrllibAndRegexWebScraper:\u0026#34;) data_urllib_regex = web_scraper_urllib_regex.scrape_website(target_url) for email in data_urllib_regex: print(email) Scraped Data using RequestsAndBeautifulSoupWebScraper: / /blog /projects /about /resume / /blog /projects /about /resume /about /projects /blog/lora-a-revolution-in-fine-tuning /blog/lora-a-revolution-in-fine-tuning /tags/ai /tags/deep-learning /tags/paper /tags/llm /tags/fine-tuning /blog/lora-a-revolution-in-fine-tuning /blog/attention-is-all-you-need /blog/attention-is-all-you-need /tags/ai /tags/deep-learning /tags/attention /tags/paper /tags/transformers /blog/attention-is-all-you-need /blog/beyond-recognition-openAIs-whisper /blog/beyond-recognition-openAIs-whisper /tags/openai /tags/deep-learning /tags/asr /tags/paper /blog/beyond-recognition-openAIs-whisper /blog mailto:amirm.lavasani@gmail.com https://github.com/AmirLavasani https://www.linkedin.com/in/amir-lavasani/ https://twitter.com/AmirMLavasani / https://nextjs.org/ https://tailwindcss.com/ Scraped Data using UrllibAndRegexWebScraper: amirm.lavasani@gmail.com amirm.lavasani@gmail.com Visitor Visitor là pattern cho phép một “visitor” (khách thăm) thêm các phép toán lên các phần tử (element) của một cấu trúc đối tượng mà không thay đổi các lớp của các phần tử đó. Hãy tưởng tượng visitor như một khách thăm đến nhà bạn và thực hiện các hoạt động khác nhau trong mỗi phòng mà không thay đổi cấu trúc của ngôi nhà.\nTại sao lại cần đến Visitor? Giả sử bạn đang cần phát triển một module ghi log cho một hệ thống, cách đơn giản nhất là thêm code logging cho những class mà cần ghi log, tuy nhiên nó sẽ vi phạm nguyên tắc open-closed khi mà chúng ta sửa trực tiếp trên code hiện tại\nBây giờ chúng ta thử áp dụng Visitor, chúng ta sẽ đặt hành vi mới (ở đây là ghi log) vào một class riêng biệt được gọi là visitor, thay vì cố gắng tích hợp nó vào các class hiện có. Lúc này, đối tượng gốc cần ghi log sẽ không cần phải thực hiện hành vi ghi log mà sẽ ủy quyền cho một method của Visitor dưới dạng tham số, cung cấp cho method này quyền truy cập vào các dữ liệu cần thiết có trong đối tượng gốc để có thể ghi log được.\nTư tưởng của Visitor khá giống với Command, đều cho phép đối tượng thực hiện các hành vi trên các đối tượng khác nhau thuộc các lớp khác nhau, chỉ khác nhau ở chỗ:\nCommand tập trung vào việc đóng gói (encapsulating) các yêu cầu dưới dạng đối tượng, giúp chúng ta dễ dàng quản lý các hành động trong chương trình. Visitor tập trung vào việc mở rộng, thêm các hành vi (thuật toán) mới vào các lớp mà không ảnh hưởng đến lớp hiện có. Triển khai Visitor Để triển khai visitor, chúng ta cần làm hai thứ:\nTriển khai method Visit() được quản lý bởi Visitor, có quyền truy cập vào các phần tử (element). Triển khai method Accept() trong các đối tượng gốc để accept các visitor. Như vậy là chúng ta vẫn phải thay đổi các class gốc (thêm method Accept()), tuy nhiên sự thay đổi này là nhỏ và không hề ảnh hưởng đến code đang chạy, cũng không phải thay đổi theo một số hành vi cụ thể (ghi log, tính toán, …), và đặc biệt là sau này nếu có thêm hành vi thì chúng ta sẽ không phải update code một lần nữa, chỉ cần thêm visitor là được.\nHãy xem ví dụ dưới đây triển khai module export document:\n# Step 1: Define the Visitor Interface class Exporter: def export_paragraph(self, paragraph): pass def export_heading(self, heading): pass def export_image(self, image): pass # Step 2: Implement Concrete Visitors class HTMLExporter(Exporter): def export_paragraph(self, paragraph): return f\u0026#34;\u0026lt;p\u0026gt;{paragraph.content}\u0026lt;/p\u0026gt;\u0026#34; def export_heading(self, heading): return f\u0026#34;\u0026lt;h{heading.level}\u0026gt;{heading.text}\u0026lt;/h{heading.level}\u0026gt;\u0026#34; def export_image(self, image): return f\u0026#39;\u0026lt;img src=\u0026#34;{image.source}\u0026#34; alt=\u0026#34;{image.alt}\u0026#34;\u0026gt;\u0026#39; class PDFExporter(Exporter): def export_paragraph(self, paragraph): return f\u0026#34;PDF Paragraph: {paragraph.content}\u0026#34; def export_heading(self, heading): return f\u0026#34;PDF Heading ({heading.level}): {heading.text}\u0026#34; def export_image(self, image): return f\u0026#39;PDF Image: {image.alt}\u0026#39; # Step 3: Define Element Interface class Element: def accept(self, visitor): pass # Step 4: Implement Concrete Elements class Paragraph(Element): def __init__(self, content): self.content = content def accept(self, visitor): return visitor.export_paragraph(self) class Heading(Element): def __init__(self, level, text): self.level = level self.text = text def accept(self, visitor): return visitor.export_heading(self) class Image(Element): def __init__(self, source, alt): self.source = source self.alt = alt def accept(self, visitor): return visitor.export_image(self) # Step 5: Client Code class Document: def __init__(self, elements): self.elements = elements def accept(self, visitor): result = [] for element in self.elements: result.append(element.accept(visitor)) return result # Usage html_exporter = HTMLExporter() pdf_exporter = PDFExporter() document = Document([ Paragraph(\u0026#34;This is a paragraph.\u0026#34;), Heading(1, \u0026#34;Main Heading\u0026#34;), Image(\u0026#34;image.jpg\u0026#34;, \u0026#34;A beautiful image\u0026#34;) ]) # Export to HTML html_result = document.accept(html_exporter) print(\u0026#34;HTML Export:\u0026#34;) print(\u0026#39;\\n\u0026#39;.join(html_result)) print(\u0026#34;\\n\u0026#34;) # Export to PDF pdf_result = document.accept(pdf_exporter) print(\u0026#34;PDF Export:\u0026#34;) print(\u0026#39;\\n\u0026#39;.join(pdf_result)) HTML Export: \u0026lt;p\u0026gt;This is a paragraph.\u0026lt;/p\u0026gt; \u0026lt;h1\u0026gt;Main Heading\u0026lt;/h1\u0026gt; \u0026lt;img src=\u0026#34;image.jpg\u0026#34; alt=\u0026#34;A beautiful image\u0026#34;\u0026gt; PDF Export: PDF Paragraph: This is a paragraph. PDF Heading (1): Main Heading PDF Image: A beautiful image You can get full source code here!\n","permalink":"https://hoangph3.github.io/posts/design_patterns_behavior/","summary":"Implement behavioral design patterns such as Strategy, Observer and Command in Python.","title":"Behavioral Design Patterns in Python"},{"content":"Single-responsibility principle (SRP) Một class chỉ nên thực hiện một công việc. Nếu một class mà có nhiều hơn một công việc thì khi đó chúng ta nên tách ra mỗi class thực hiện một công việc khác nhau.\n# file_manager_srp.py from pathlib import Path from zipfile import ZipFile class FileManager: def __init__(self, filename): self.path = Path(filename) def read(self, encoding=\u0026#34;utf-8\u0026#34;): return self.path.read_text(encoding) def write(self, data, encoding=\u0026#34;utf-8\u0026#34;): self.path.write_text(data, encoding) def compress(self): with ZipFile(self.path.with_suffix(\u0026#34;.zip\u0026#34;), mode=\u0026#34;w\u0026#34;) as archive: archive.write(self.path) def decompress(self): with ZipFile(self.path.with_suffix(\u0026#34;.zip\u0026#34;), mode=\u0026#34;r\u0026#34;) as archive: archive.extractall() Class trên vi phạm nguyên tắc SRP, do nó cho phép thực hiện 2 nhiệm vụ khác nhau:\nĐọc ghi file. Nén/giải nén file. Hai nhiệm vụ này độc lập (có thể theo ý kiến chủ quan), không nên để chung trong cùng 1 class mà nên tách ra để dễ quản lý hơn.\n# file_manager_srp.py from pathlib import Path from zipfile import ZipFile class FileManager: def __init__(self, filename): self.path = Path(filename) def read(self, encoding=\u0026#34;utf-8\u0026#34;): return self.path.read_text(encoding) def write(self, data, encoding=\u0026#34;utf-8\u0026#34;): self.path.write_text(data, encoding) class ZipFileManager: def __init__(self, filename): self.path = Path(filename) def compress(self): with ZipFile(self.path.with_suffix(\u0026#34;.zip\u0026#34;), mode=\u0026#34;w\u0026#34;) as archive: archive.write(self.path) def decompress(self): with ZipFile(self.path.with_suffix(\u0026#34;.zip\u0026#34;), mode=\u0026#34;r\u0026#34;) as archive: archive.extractall() *** Note:\nClass thực hiện một nhiệm vụ duy nhất không nhất thiết phải chỉ có chứa 1 method duy nhất, mà quan trọng là nhiệm vụ cốt lõi của nó.\nNhư ở trên, class FileManager đảm nhiệm việc quản lý tệp, trong khi ZipFileManager xử lý việc nén và giải nén tệp với định dạng ZIP.\nTại sao phải cần SRP?\nGiả sử code chúng ta có 5 module khác nhau, trong đó 4 module cần quản lý file (đọc ghi), 1 module còn lại quản lý nén/giải nén file ZIP. Sau một thời gian chạy, ta thấy module quản lý nén/giải nén file có bug -\u0026gt; cần thực hiện fix.\nNếu không áp dụng SRP, ta sẽ phải upcode class FileManager và đồng bộ việc upcode này cho cả 5 module đang sử dụng nó. Nếu áp dụng SRP, ta chỉ cần upcode mỗi class ZipFileManager cho 1 module, 4 module kia chỉ đọc ghi file nên không cần phải tác động gì. Tuy nhiên, không phải lúc nào cũng nên áp dụng nguyên tắc này vào code. Một trường hợp hay gặp trong thực tế là các class dạng Helper hay Utilities, đều vi phạm SRP. Nếu số lượng hàm ít, ta vẫn có thể cho tất cả các hàm này vào 1 class, nhưng cũng cần lưu ý rằng khi Helper có thêm nhiều chức năng, thì ta nên tách Helper thành các Helper nhỏ hơn, ví dụ: UserHelper, DatabaseHelper, DataHelper, …\nOpen-Closed Principle (OCP) Các entities (classes, modules, functions) phải “open” để mở rộng, nhưng phải “close” để sửa đổi.\nVí dụ chúng ta nên mở rộng class (dùng kế thừa) thay vì sửa đổi class gốc, tại vì việc sửa đổi trên class cũ sẽ có thể gây ra bug khi các module khác đang sử dụng class cũ.\nclass Animal: def __init__(self, name: str): self.name = name def get_name(self) -\u0026gt; str: pass animals = [ Animal(\u0026#39;lion\u0026#39;), Animal(\u0026#39;mouse\u0026#39;) ] def animal_sound(animals: list): for animal in animals: if animal.name == \u0026#39;lion\u0026#39;: print(\u0026#39;roar\u0026#39;) elif animal.name == \u0026#39;mouse\u0026#39;: print(\u0026#39;squeak\u0026#39;) animal_sound(animals) roar squeak Hàm animal_sound không tuân theo nguyên tắc OCP, vì nó không thể mở rộng (open) đối với các loại động vật mới.\nDễ thấy là cứ thêm một động vật mới thì chúng ta lại phải đi sửa đổi function gốc, logic sẽ trở nên ngày càng phức tạp với nhiều câu lệnh if else lặp đi lặp lại. Vậy áp dụng OCP như thế nào?\nclass Animal: def __init__(self, name: str): self.name = name def get_name(self) -\u0026gt; str: pass def make_sound(self): pass class Lion(Animal): def make_sound(self): return \u0026#39;roar\u0026#39; class Mouse(Animal): def make_sound(self): return \u0026#39;squeak\u0026#39; class Snake(Animal): def make_sound(self): return \u0026#39;hiss\u0026#39; def animal_sound(animals: list): for animal in animals: print(animal.make_sound()) animals = [ Lion(\u0026#39;lion\u0026#39;), Mouse(\u0026#39;mouse\u0026#39;), Snake(\u0026#39;snake\u0026#39;) ] animal_sound(animals) roar squeak hiss Bằng cách áp dụng nguyên tắc OCP, mọi loại con vật đều có method make_sound, được mở rộng từ class Animal.\nBây giờ nếu có thêm một con vật mới, tất cả những gì chúng ta cần làm là thêm con vật mới vào mảng.\nHãy xem một ví dụ khác:\nclass Discount: def __init__(self, customer, price): self.customer = customer self.price = price def give_discount(self): if self.customer == \u0026#39;fav\u0026#39;: return self.price * 0.2 if self.customer == \u0026#39;vip\u0026#39;: return self.price * 0.4 Method give_discount sẽ cần phải update nếu như bạn có thêm nhiều đối tượng khách hàng mới, điều này vi phạm nguyên tắc OCP\nThay vào đó, chúng ta sẽ mở rộng bằng cách tạo class mới với method mới.\nclass Discount: def __init__(self, customer, price): self.customer = customer self.price = price def get_discount(self): # discount 20% return self.price * 0.2 class VIPDiscount(Discount): def get_discount(self): # discount 40% return super().get_discount() * 2 class SuperVIPDiscount(VIPDiscount): def get_discount(self): # discount 80% return super().get_discount() * 2 Việc áp dụng nguyên tắc OCP sẽ phát sinh thêm nhiều class. Tuy nhiên ta sẽ không cần phải test lại các class cũ, thay vào đó chỉ cần test các class mới với các method mới.\nNếu sửa trực tiếp vào class hay method cũ, ta sẽ cần phải test lại cả những logic cũ để xem sau khi thêm code mới thì nó có hoạt động như cũ không -\u0026gt; tốn thời gian, có thể gây ra bug.\nLiskov Substitution Principle (LSP) Nguyên tắc này nói đến việc các class con có thể thay thế class cha (base) mà chương trình vẫn hoạt động đúng.\n# shapes_lsp.py class Rectangle: def __init__(self, width, height): self.width = width self.height = height def calculate_area(self): print(self.__dict__) return self.width * self.height class Square(Rectangle): def __init__(self, side): super().__init__(side, side) def __setattr__(self, key, value): super().__setattr__(key, value) if key in (\u0026#34;width\u0026#34;, \u0026#34;height\u0026#34;): self.__dict__[\u0026#34;width\u0026#34;] = value self.__dict__[\u0026#34;height\u0026#34;] = value Trong toán học, hình vuông là trường hợp đặc biệt của hình chữ nhật. Do đó, với class Square, ta có thể kế thừa class base Rectangle như trên, với hai thuộc tính width và height đều bằng side.\nrec = Rectangle(width=2, height=3) print(rec.calculate_area()) square = Square(side=4) print(square.calculate_area()) {\u0026#39;width\u0026#39;: 2, \u0026#39;height\u0026#39;: 3} 6 {\u0026#39;width\u0026#39;: 4, \u0026#39;height\u0026#39;: 4} 16 Đoạn code trên không có gì sai, tuy nhiên nó vi phạm nguyên tắc LSP, khi mà chúng ta không thể thay thế Rectangle instance bằng những Square instance khác.\nTrong lập trình, tránh bê nguyên các mối quan hệ của các object ngoài đời sống vào code. Mặc dù hình vuông là trường hợp đặc biệt của hình chữ nhật trong toán học, tuy nhiên ở đây chúng không nên có quan hệ cha-con mà chỉ nên là quan hệ anh-em (kế thừa từ một class base khác và đều có method calculate_area) như sau:\n# shapes_lsp.py from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def calculate_area(self): pass class Rectangle(Shape): def __init__(self, width, height): self.width = width self.height = height def calculate_area(self): return self.width * self.height class Square(Shape): def __init__(self, side): self.side = side def calculate_area(self): return self.side ** 2 Bằng cách áp dụng LSP, ta không cần quan tâm đối tượng thuộc loại nào, miễn là nó có method calculate_area thì code vẫn sẽ hoạt động.\ndef get_total_area(shapes): return sum(shape.calculate_area() for shape in shapes) get_total_area([Rectangle(10, 5), Square(5)]) 75 Interface Segregation Principle (ISP) Nguyên tắc này nói đến việc không nên ép buộc các class phải có các methods mà nó ko cần dùng đến.\nNói cách khác, nếu một class không sử dụng các attributes hay methods (gọi chung là interfaces) thì các methods và attributes đó sẽ được tách riêng thành các class cụ thể hơn.\n\u0026#34;\u0026#34;\u0026#34; Base class \u0026#34;\u0026#34;\u0026#34; class IShape: def draw_square(self): raise NotImplementedError def draw_rectangle(self): raise NotImplementedError def draw_circle(self): raise NotImplementedError \u0026#34;\u0026#34;\u0026#34; Subclasses \u0026#34;\u0026#34;\u0026#34; class Circle(IShape): def draw_square(self): pass def draw_rectangle(self): pass def draw_circle(self): pass class Square(IShape): def draw_square(self): pass def draw_rectangle(self): pass def draw_circle(self): pass class Rectangle(IShape): def draw_square(self): pass def draw_rectangle(self): pass def draw_circle(self): pass Khi triển khai như trên, class Rectangle chứa các methods (draw_circle và draw_square) mà nó không dùng đến, tương tự với class Square với các methods draw_circle, draw_ectangle, và class Circle với các methods draw_square, draw_rectangle.\nĐể đáp ứng nguyên tắc ISP, ta sẽ sửa lại như sau:\nclass IShape: def draw(self): raise NotImplementedError class Circle(IShape): def draw(self): pass class Square(IShape): def draw(self): pass class Rectangle(IShape): def draw(self): pass Ví dụ với trường hợp đa kế thừa:\n# printers_isp.py from abc import ABC, abstractmethod \u0026#34;\u0026#34;\u0026#34; Base classes \u0026#34;\u0026#34;\u0026#34; class Printer(ABC): @abstractmethod def print(self, document): pass class Fax(ABC): @abstractmethod def fax(self, document): pass class Scanner(ABC): @abstractmethod def scan(self, document): pass \u0026#34;\u0026#34;\u0026#34; Subclasses \u0026#34;\u0026#34;\u0026#34; class OldPrinter(Printer): def print(self, document): print(f\u0026#34;Printing {document} in black and white...\u0026#34;) class NewPrinter(Printer, Fax, Scanner): def print(self, document): print(f\u0026#34;Printing {document} in color...\u0026#34;) def fax(self, document): print(f\u0026#34;Faxing {document}...\u0026#34;) def scan(self, document): print(f\u0026#34;Scanning {document}...\u0026#34;) Với triển khai như trên, ta đã tách biệt được các interfaces (Interface Segregation), bây giờ Printer, Fax và Scanner là các class base cung cấp các interfaces thỏa mãn nguyên tắc SRP (chỉ thực hiện một nhiệm vụ duy nhất).\nNhư vậy với OldPrinter chỉ cần kết thừa interface Printer, trong khi đó NewPrinter sẽ đa kế thừa từ các interfaces Printer, Fax và Scanner để có đủ các methods cần thiết.\nDependency Inversion Principle (DIP) Nguyên tắc này yêu cầu hai việc:\nCác mô-đun cấp cao không nên phụ thuộc vào các mô-đun cấp thấp. Cả hai nên phụ thuộc vào abstractions. Abstractions không nên phụ thuộc vào chi tiết (details), chi tiết nên phụ thuộc vào abstractions. Sẽ có những thời điểm trong quá trình phát triển, ứng dụng của chúng ta sẽ có nhiều rất module, các module cấp cao phụ thuộc vào các module cấp thấp để chạy. Hãy xem ví dụ sau:\nclass FrontEnd: def __init__(self, back_end): self.back_end = back_end def display_data(self): data = self.back_end.get_data_from_database() print(\u0026#34;Display data:\u0026#34;, data) class BackEnd: def get_data_from_database(self): return \u0026#34;Data from the database\u0026#34; Trong ví dụ trên, class Frontend bị phụ thuộc vào cách triển khai cụ thể của class Backend, chính sự liên kết chặt chẽ này khiến nó khó mở rộng.\nGiả sử bây giờ chúng ta cần đọc dữ liệu từ một nguồn khác như REST API chẳng hạn, thì sẽ triển khai thế nào?\nThêm method mới get_data_from_api trong class Backend Sửa class Frontend để có thể đọc dữ liệu từ method get_data_from_api -\u0026gt; vi phạm open-closed principle. Bằng cách áp dụng DIP, ta sẽ làm cho class Frontend phụ thuộc vào abstractions, thay vì phụ thuộc vào class Backend được triển khai cụ thể.\nfrom abc import ABC, abstractmethod class FrontEnd: def __init__(self, data_source): self.data_source = data_source def display_data(self): data = self.data_source.get_data() return f\u0026#34;Display data: {data}\u0026#34; class DataSource(ABC): @abstractmethod def get_data(self): pass class Database(DataSource): def get_data(self): return \u0026#34;Data from the database\u0026#34; class API(DataSource): def get_data(self): return \u0026#34;Data from the API\u0026#34; # Run db_front_end = FrontEnd(Database()) print(db_front_end.display_data()) api_front_end = FrontEnd(API()) print(api_front_end.display_data()) Display data: Data from the database Display data: Data from the API Thêm một ví dụ khác:\nclass IFood: def bake(self): raise NotImplemented def eat(self): raise NotImplemented class Pizza(IFood): def bake(self): print(\u0026#34;pizza was baked\u0026#34;) def eat(self): print(\u0026#34;pizza was ate\u0026#34;) class Bread(IFood): def bake(self): print(\u0026#34;bread was baked\u0026#34;) def eat(self): print(\u0026#34;bread was ate\u0026#34;) class Production: def __init__(self, food: IFood): self.food = food def produce(self): self.food.bake() def consume(self): self.food.eat() if __name__ == \u0026#39;__main__\u0026#39;: pizza = Pizza() bread = Bread() p = Production(pizza) p.produce() p.consume() b = Production(bread) b.produce() b.consume() pizza was baked pizza was ate bread was baked bread was ate Ở đây ta có các module cấp thấp là Bread và Pizza, module cấp cao là Production. 2 module này giao tiếp với nhau bằng interface IFood, giúp cho chương trình trở lên linh hoạt hơn. Module Production chỉ cần sử dụng các method trong IFood mà không bị ràng buộc hay cần quan tâm object nào sẽ được truyền vào. Ta có thể truyền vào pizza hoặc bread.\nYou can get full source code here!\n","permalink":"https://hoangph3.github.io/posts/solid_principles/","summary":"Walk through the five SOLID object-oriented design principles with runnable Python examples.","title":"SOLID Principles in Python"},{"content":"Adapter Method Adapter hiểu đơn giản là một lớp chuẩn hóa, nhằm để các đối tượng không tương tích có thể tương thích với nhau.\nVí dụ, chúng ta đang sử dụng điện thế 220V, trong khi các thiết bị điện tử chỉ tương thích với điện thế 15V. Chính vì thế các cục sạc đã được thiết kế có adapter để có thể tương thích với điện thế dân dụng 220V.\nMột ví dụ khác, bạn có một module xử lý với dữ liệu dạng .json. Sau một thời gian, lượng dữ liệu tăng lên với nhiều định dạng khác nhau: .txt, .xml, .csv, … Thay vì bạn phải sửa module xử lý dữ liệu của mình, thì giờ chúng ta sẽ viết thêm một cục Adapter để parse toàn bộ dữ liệu sang dạng chỉ json.\nimport json class JsonView: def display(self): doc = {\u0026#34;text\u0026#34;: \u0026#34;FOO\u0026#34;} print(\u0026#34;Json object:\u0026#34;, doc,\u0026#39;\\n\u0026#39;) class TextView: def display(self): print(\u0026#34;Text string:\u0026#34;, \u0026#34;FOO\u0026#34;,\u0026#39;\\n\u0026#39;) class Adapter(JsonView): \u0026#34;\u0026#34;\u0026#34; The Adapter converts text string to json object \u0026#34;\u0026#34;\u0026#34; def __init__(self, adaptee: TextView) -\u0026gt; None: self.adaptee = adaptee def display(self, text: str): doc = {\u0026#34;text\u0026#34;: text} print(\u0026#34;Adapter:\u0026#34;, doc,\u0026#39;\\n\u0026#39;) if __name__ == \u0026#34;__main__\u0026#34;: target = JsonView() target.display() adaptee = TextView() adaptee.display() adapter = Adapter(adaptee) adapter.display(\u0026#34;BAR\u0026#34;) Json object: {\u0026#39;text\u0026#39;: \u0026#39;FOO\u0026#39;} Text string: FOO Adapter: {\u0026#39;text\u0026#39;: \u0026#39;BAR\u0026#39;} Bridge Method Bridge method giúp bạn tách một lớp khổng lồ hoặc một tập hợp lớp có quan hệ gần gũi với nhau thành hai hệ thống phân cấp lớp riêng biệt là - abstraction (trừu tượng) và implementation (triển khai) - có thể phát triển độc lập với nhau. Method này có concept gần giống như nguyên tắc Single Responsibility, vì nó tách rời phần abstraction khỏi việc triển khai (implementation), thằng nào làm nhiệm vụ của thằng đó.\nVí dụ sau đây, chúng ta muốn xây dựng hệ thống điều khiển remote cho nhiều loại thiết bị điện tử: TV, DVD, … với các tính năng và lệnh khác nhau, thì triển khai như thế nào?\nfrom abc import ABC, abstractmethod # Abstraction class RemoteControl: \u0026#34;\u0026#34;\u0026#34; Xây dựng abstract interface, reference đến một đối tượng implementation (ở đây là device) \u0026#34;\u0026#34;\u0026#34; def __init__(self, device): self._device = device @abstractmethod def toggle_power(self): self._device.toggle_power() @abstractmethod def volume_up(self): self._device.volume_up() @abstractmethod def volume_down(self): self._device.volume_down() # Implementation class Device: \u0026#34;\u0026#34;\u0026#34; Xây dựng implementation interface \u0026#34;\u0026#34;\u0026#34; @abstractmethod def toggle_power(self): pass @abstractmethod def volume_up(self): pass @abstractmethod def volume_down(self): pass # Concrete Implementations for Devices class TV(Device): \u0026#34;\u0026#34;\u0026#34; Xây dựng một Concrete Implementation cụ thể, ở đây là TV \u0026#34;\u0026#34;\u0026#34; def toggle_power(self): print(\u0026#34;TV: Toggling power\u0026#34;) def volume_up(self): print(\u0026#34;TV: Volume up\u0026#34;) def volume_down(self): print(\u0026#34;TV: Volume down\u0026#34;) class DVDPlayer(Device): \u0026#34;\u0026#34;\u0026#34; Thêm một Concrete Implementation khác, ở đây là DVD \u0026#34;\u0026#34;\u0026#34; def toggle_power(self): print(\u0026#34;DVD Player: Toggling power\u0026#34;) def volume_up(self): print(\u0026#34;DVD Player: Volume up\u0026#34;) def volume_down(self): print(\u0026#34;DVD Player: Volume down\u0026#34;) # Abstraction and Refined Abstraction class BasicRemoteControl(RemoteControl): def toggle_power(self): print(\u0026#34;Basic Remote: Press power button\u0026#34;) super().toggle_power() class AdvancedRemoteControl(RemoteControl): \u0026#34;\u0026#34;\u0026#34; Mở rộng abstract với các method khác. \u0026#34;\u0026#34;\u0026#34; def mute(self): print(\u0026#34;Advanced Remote: Mute button pressed\u0026#34;) self._device.volume_down() # Client Code if __name__ == \u0026#34;__main__\u0026#34;: tv = TV() dvd_player = DVDPlayer() basic_remote_tv = BasicRemoteControl(tv) advanced_remote_dvd = AdvancedRemoteControl(dvd_player) basic_remote_tv.toggle_power() basic_remote_tv.volume_up() advanced_remote_dvd.toggle_power() advanced_remote_dvd.mute() Basic Remote: Press power button TV: Toggling power TV: Volume up DVD Player: Toggling power Advanced Remote: Mute button pressed DVD Player: Volume down Sau này, chúng ta có thể dễ dàng thêm device mới, loại remote mới mà không cần thay đổi code hiện tại, sẽ dễ dàng bảo trì và mở rộng hơn.\nComposite Method Composite method cho phép chúng ta sắp xếp các đối tượng thành cấu trúc Tree. Khi đó:\nBạn có thể xử lý các đối tượng theo cùng một cách mà không phân biệt chúng là đối tượng đơn lẻ (leaf) hay là thành phần của một cấu trúc lớn hơn (composite). Dễ dàng thêm mới các thành phần: Bạn có thể thêm hoặc loại bỏ các thành phần từ cấu trúc cây mà không cần thay đổi code hiện tại, giúp code trở nên linh hoạt và dễ bảo trì. Ví dụ sau đây biểu diễn một cây hệ thống tệp (file system tree):\nFileSystemComponent là một interface chung cho cả file và directory. File là một leaf class, biểu diễn một file trong hệ thống tệp. Directory là một composite class, biểu diễn một thư mục trong hệ thống tệp, có thể chứa cả các file và thư mục con. Tiến hành tạo các file và thư mục, sau đó thêm chúng vào các thư mục khác nhau. Cuối cùng, chúng ta tạo ra một thư mục gốc (root dir) chứa các thư mục và file đã tạo. from abc import ABC, abstractmethod # Component interface class FileSystemComponent: @abstractmethod def display(self): pass # Leaf class class File(FileSystemComponent): def __init__(self, name): self.name = name def display(self): \u0026#34;\u0026#34;\u0026#34; File là đơn vị nhỏ nhất (child element), nên chỉ display chính nó \u0026#34;\u0026#34;\u0026#34; print(\u0026#34;File:\u0026#34;, self.name) # Composite class class Directory(FileSystemComponent): def __init__(self, name): self.name = name self.components = [] def add_component(self, component): \u0026#34;\u0026#34;\u0026#34; Thêm một component mới \u0026#34;\u0026#34;\u0026#34; self.components.append(component) def remove_component(self, component): \u0026#34;\u0026#34;\u0026#34; Xóa bỏ một component \u0026#34;\u0026#34;\u0026#34; self.components.remove(component) def display(self): \u0026#34;\u0026#34;\u0026#34; Display tất cả các components con \u0026#34;\u0026#34;\u0026#34; print(\u0026#34;Directory:\u0026#34;, self.name) for component in self.components: component.display() # Client code if __name__ == \u0026#34;__main__\u0026#34;: # Create files file1 = File(\u0026#34;file1.txt\u0026#34;) file2 = File(\u0026#34;file2.txt\u0026#34;) file3 = File(\u0026#34;file3.txt\u0026#34;) # Create directories dir1 = Directory(\u0026#34;Directory 1\u0026#34;) dir2 = Directory(\u0026#34;Directory 2\u0026#34;) # Add files to directories dir1.add_component(file1) dir1.add_component(file2) dir2.add_component(file3) # Create composite directory root_dir = Directory(\u0026#34;Root Directory\u0026#34;) root_dir.add_component(dir1) root_dir.add_component(dir2) # Display the file system tree root_dir.display() Directory: Root Directory Directory: Directory 1 File: file1.txt File: file2.txt Directory: Directory 2 File: file3.txt Như chúng ta có thể thấy, khi gọi root_dir.display(), composite sẽ đệ quy gọi vào các component nhỏ hơn và các leaf để hiển thị cây hệ thống tệp.\nDecorator Method Decorator method cho phép chúng ta mở rộng hành vi của một đối tượng mà không cần phải thay đổi mã nguồn gốc của nó. Việc này có thể được thực hiện bằng cách truyền đối tượng gốc qua chuỗi các decorator, trong đó mỗi decorator là một function cung cấp hành vi cần bổ sung vào đổi tượng gốc.\nDưới đây là một ví dụ đơn giản sử dụng decorator để xử lý text:\ndef uppercase_decorator(func): def wrapper(text): result = func(text) return result.upper() return wrapper @uppercase_decorator def greet(name): return f\u0026#34;Hello, {name}!\u0026#34; print(greet(\u0026#34;Alice\u0026#34;)) HELLO, ALICE! Trong ví dụ trên, uppercase_decorator là một hàm decorator. Nó nhận một hàm khác làm đối số và trả về một hàm mới mở rộng hành vi của hàm ban đầu. Khi chúng ta gọi hàm greet, nó sẽ được thực thi thông qua hàm decorator và kết quả được chuyển thành chữ in hoa.\nMột ví dụ khác khi sử dụng decorator để ghi log:\nimport time def log_writer(log_file): def decorator(func): def wrapper(*args, **kwargs): start_time = time.time() result = func(*args, **kwargs) total_time = time.time() - start_time output = f\u0026#34;Result: {result}, Time: {round(total_time, 4)}\\n\u0026#34; with open(log_file, \u0026#39;a\u0026#39;) as f: f.write(output) return output return wrapper return decorator @log_writer(\u0026#34;test.txt\u0026#34;) def test(): import random time.sleep(random.randint(0, 1)) return random.randrange(0, 10) @log_writer(\u0026#34;test2.txt\u0026#34;) def test2(): import random time.sleep(random.randint(1, 2)) return random.choice([\u0026#34;apple\u0026#34;, \u0026#34;banana\u0026#34;, \u0026#34;cherry\u0026#34;]) if __name__ == \u0026#34;__main__\u0026#34;: for _ in range(3): test() test2() !cat test.txt \u0026amp;\u0026amp; echo $\u0026#39;\\n\u0026#39; \u0026amp;\u0026amp; cat test2.txt Result: 4, Time: 0.0 Result: 3, Time: 1.0011 Result: 9, Time: 0.0 Result: apple, Time: 1.0007 Result: cherry, Time: 1.0011 Result: cherry, Time: 2.0021 Trong ví dụ trên, log_writer là một hàm decorator, nhận tham số log_file truyền vào. Decorator này chạy qua hàm chính, lấy kết quả và tính thời gian thực thi để ghi vào file log. Sau này chúng ta muốn ghi log khi chạy qua một hàm bất kỳ thì chỉ cần thêm hàm decorator log_writer là được.\nFacade Method Facade method cho phép chúng ta triển khai interface giao tiếp giữa client và các hệ thống con một cách dễ dàng.\nVí dụ trong thực tế, một chiếc máy giặt có thể giặt, xả hay vắt quần áo nhưng tất cả các công việc đều riêng biệt. Khi đó chúng ta sẽ phải cần đến một hệ thống có thể tự động toàn bộ công việc mà không cần can thiệp, đó chính là Facade.\n\u0026#34;\u0026#34;\u0026#34;Facade pattern with an example of WashingMachine\u0026#34;\u0026#34;\u0026#34; class Washing: \u0026#39;\u0026#39;\u0026#39;Subsystem # 1\u0026#39;\u0026#39;\u0026#39; def wash(self): print(\u0026#34;Washing...\u0026#34;) class Rinsing: \u0026#39;\u0026#39;\u0026#39;Subsystem # 2\u0026#39;\u0026#39;\u0026#39; def rinse(self): print(\u0026#34;Rinsing...\u0026#34;) class Spinning: \u0026#39;\u0026#39;\u0026#39;Subsystem # 3\u0026#39;\u0026#39;\u0026#39; def spin(self): print(\u0026#34;Spinning...\u0026#34;) class WashingMachine: \u0026#39;\u0026#39;\u0026#39;Facade\u0026#39;\u0026#39;\u0026#39; def __init__(self): self.washing = Washing() self.rinsing = Rinsing() self.spinning = Spinning() def startWashing(self): self.washing.wash() self.rinsing.rinse() self.spinning.spin() \u0026#34;\u0026#34;\u0026#34; client code \u0026#34;\u0026#34;\u0026#34; if __name__ == \u0026#34;__main__\u0026#34;: washingMachine = WashingMachine() washingMachine.startWashing() Washing... Rinsing... Spinning... Khi nào thì sử dụng Facade? Gom nhóm chức năng lại để client dễ sử dụng, thay vì phải tìm hiểu quy trình xử lý của chương trình. Đóng gói nhiều chức năng, che giấu thuật toán phức tạp. Xây dựng một interface đơn giản, dễ sử dụng mà không bị phụ thuộc quá nhiều vào hệ thống con. Rủi ro khi sử dụng Facade? Facade của bạn có thể trở lên quá lớn, làm quá nhiều nhiệm vụ với nhiều hàm chức năng trong nó -\u0026gt; phá vỡ quy tắc SOLID. Dư thừa, khi mà hệ thống của bạn không quá phức tạp, ít hệ thống con. Proxy Method Proxy Method cho phép chúng ta tạo ra một lớp trung gian (proxy) để kiểm soát quyền truy cập vào đối tượng thực sự, hay nói cách khác nó cung cấp một đối tượng thay thế cho một đối tượng thực sự, từ đó cho phép kiểm soát hoặc mở rộng hành vi của đối tượng đó một cách linh hoạt.\nVí dụ trong thực tế, thẻ ATM là thứ “đại diện” cho tiền mặt trong tài khoản ngân hàng của chúng ta, nó chính là Proxy để kiểm soát và quản lý quyền truy cập vào đối tượng cụ thể (ở đây chính là tiền mặt). Hay như nginx cũng là một proxy để điều hướng traffic từ port 80, 443 vào các service tương ứng.\nỨng dụng Kiểm soát truy cập: proxy kiểm tra xem ai có quyền truy cập hay không, ví dụ: authen, nginx, … Quản lý hiệu năng: proxy lưu trữ các kết quả đã được tính toán trước đó, tránh việc tính lại nhiều lần, ví dụ: caching, … \u0026#34;\u0026#34;\u0026#34; Ví dụ sử dụng Proxy để làm cache \u0026#34;\u0026#34;\u0026#34; import time # Define the interface for the Real Subject class DatabaseQuery: def execute_query(self, query): pass # Real Subject: Represents the actual database class RealDatabaseQuery(DatabaseQuery): def execute_query(self, query): print(f\u0026#34;Executing query: {query}\u0026#34;) # Simulate a database query and return the results return f\u0026#34;Results for query: {query}\\n\u0026#34; # Proxy: Caching Proxy for Database Queries class CacheProxy(DatabaseQuery): def __init__(self, real_database_query, cache_duration_seconds): self._real_database_query = real_database_query self._cache = {} self._cache_duration = cache_duration_seconds def execute_query(self, query): if query in self._cache and time.time() - self._cache[query][\u0026#34;timestamp\u0026#34;] \u0026lt;= self._cache_duration: # Return cached result if it\u0026#39;s still valid print(f\u0026#34;CacheProxy: Returning cached result for query: {query}\u0026#34;) return self._cache[query][\u0026#34;result\u0026#34;] else: # Execute the query and cache the result result = self._real_database_query.execute_query(query) self._cache[query] = {\u0026#34;result\u0026#34;: result, \u0026#34;timestamp\u0026#34;: time.time()} return result # Client code if __name__ == \u0026#34;__main__\u0026#34;: # Create the Real Subject real_database_query = RealDatabaseQuery() # Create the Cache Proxy with a cache duration of 5 seconds cache_proxy = CacheProxy(real_database_query, cache_duration_seconds=5) # Perform database queries, some of which will be cached print(cache_proxy.execute_query(\u0026#34;SELECT * FROM table1\u0026#34;)) print(cache_proxy.execute_query(\u0026#34;SELECT * FROM table2\u0026#34;)) time.sleep(3) # Sleep for 3 seconds # Should return cached result print(cache_proxy.execute_query(\u0026#34;SELECT * FROM table1\u0026#34;)) print(cache_proxy.execute_query(\u0026#34;SELECT * FROM table3\u0026#34;)) Executing query: SELECT * FROM table1 Results for query: SELECT * FROM table1 Executing query: SELECT * FROM table2 Results for query: SELECT * FROM table2 CacheProxy: Returning cached result for query: SELECT * FROM table1 Results for query: SELECT * FROM table1 Executing query: SELECT * FROM table3 Results for query: SELECT * FROM table3 \u0026#34;\u0026#34;\u0026#34; Ví dụ sử dụng Proxy để kiểm soát truy cập: validation, protection \u0026#34;\u0026#34;\u0026#34; class College: \u0026#39;\u0026#39;\u0026#39;Resource-intensive object\u0026#39;\u0026#39;\u0026#39; def studyingInCollege(self): print(\u0026#34;Studying In College....\\n\u0026#34;) class CollegeProxy: \u0026#39;\u0026#39;\u0026#39;Relatively less resource-intensive proxy acting as middleman. Instantiates a College object only if there is no fee due.\u0026#39;\u0026#39;\u0026#39; def __init__(self): self.feeBalance = 1000 self.college = None def studyingInCollege(self): print(f\u0026#34;Proxy in action. Checking fee balance: {self.feeBalance}\u0026#34;) if self.feeBalance \u0026lt;= 500: # If the balance is less than 500, let him study. self.college = College() self.college.studyingInCollege() else: # Otherwise, don\u0026#39;t instantiate the college object. print(\u0026#34;Your fee balance is greater than 500, first pay the fee.\\n\u0026#34;) # Client code if __name__ == \u0026#34;__main__\u0026#34;: # Instantiate the Proxy collegeProxy = CollegeProxy() # Client attempting to study in the college at the default balance of 1000. # Logically, since he / she cannot study with such balance, # there is no need to make the college object. collegeProxy.studyingInCollege() # Altering the balance of the student collegeProxy.feeBalance = 100 # Client attempting to study in college at the balance of 100. Should succeed. collegeProxy.studyingInCollege() Proxy in action. Checking fee balance: 1000 Your fee balance is greater than 500, first pay the fee. Proxy in action. Checking fee balance: 100 Studying In College.... Flyweight Method Flyweight Method cho phép chúng ta giảm thiểu số lượng object mà chương trình yêu cầu khi đang chạy, hiểu nôm na là đối tượng Flyweight có thể được share cho các đối tượng, và khi đó chúng ta sẽ không thể phân biệt giữa một object và một Flyweight object.\nCác bước triển khai Flyweight method:\nXây dựng các phần được chia sẻ, không thể thay đổi của một đối tượng. Xây dựng các phần có thể thay đổi, theo ngữ cảnh cụ thể của một đối tượng. Ví dụ thực tế, giả sử chúng ta đang xây dựng một trình soạn thảo văn bản đơn giản và muốn biểu diễn các ký tự dưới dạng đối tượng. Tuy nhiên, thay vì tạo một đối tượng riêng cho từng ký tự, chúng ta sẽ sử dụng mẫu Flyweight để chia sẻ thông tin chung về ký tự.\nclass CharFlyweight: def __init__(self, char): self.char = char class CharFactory: char_flyweights = {} @staticmethod def get_char(char): \u0026#34;\u0026#34;\u0026#34; Kiểm tra char object đã được khởi tạo hay chưa. - Đã khởi tạo -\u0026gt; trả về object đã khởi tạo, không tạo thêm để tiết kiệm bộ nhớ - Chưa khởi tạo -\u0026gt; khởi tạo -\u0026gt; lưu vào cache để sau dùng lại. \u0026#34;\u0026#34;\u0026#34; if char not in CharFactory.char_flyweights: CharFactory.char_flyweights[char] = CharFlyweight(char) return CharFactory.char_flyweights[char] class Character: def __init__(self, char, font_size): # Thông tin về ký tự được chia sẻ self.char_flyweight = CharFactory.get_char(char) # Thông tin về font size thì có thể thay đổi self.font_size = font_size def render(self): print(f\u0026#34;Object id: {id(self.char_flyweight)}, Character: {self.char_flyweight.char}, Font Size: {self.font_size}\\n\u0026#34;) # Client code if __name__ == \u0026#34;__main__\u0026#34;: characters = [] characters.append(Character(\u0026#39;A\u0026#39;, 12)) characters.append(Character(\u0026#39;B\u0026#39;, 13)) characters.append(Character(\u0026#39;A\u0026#39;, 14)) # Reusing \u0026#39;A\u0026#39; from flyweight for character in characters: character.render() Object id: 139701667453152, Character: A, Font Size: 12 Object id: 139701666563264, Character: B, Font Size: 13 Object id: 139701667453152, Character: A, Font Size: 14 Đi đến đây chúng ta có thể thấy Flyweight khá giống với Singleton, ở chỗ nó sử dụng lại đối tượng đã được khởi tạo. Tuy nhiên cần lưu ý như sau:\nFlyweight sẽ giống với Singleton nếu chúng ta có thể giảm tất cả các trạng thái được chia sẻ của đối tượng cuống còn 1 đối tượng. Singleton chỉ có duy nhất một instance, trong khi Flyweight có thể có nhiều instance với các trạng thái nội tại khác nhau (Intrinsic state). Singleton có thể thay đổi hoặc bất biến (thay đổi trong trường hợp trong class Singleton chúng ta có các method setter), trong khi đó Flyweight là bất biến. You can get full source code here!\n","permalink":"https://hoangph3.github.io/posts/design_patterns_structural/","summary":"Implement structural design patterns such as Adapter, Decorator and Facade in Python.","title":"Structural Design Patterns in Python"},{"content":"demo app - developing with ansible To start VMs Step 1: Create hosts server\nvagrant up Step 2: Checking connection\nsshpass -p vagrant ssh vagrant@192.168.56.10 sshpass -p vagrant ssh vagrant@192.168.56.11 With ansible-playbook Creating file inventory\n[all] 192.168.56.10 ansible_user=vagrant ansible_ssh_pass=vagrant 192.168.56.11 ansible_user=vagrant ansible_ssh_pass=vagrant Use tasks to play in playbook-01.yml.\n--- - hosts: all tasks: - name: Print message debug: msg: Hello Ansible ansible-playbook -i inventory playbook-01.yml PLAY [all] ********************************************************************* TASK [Gathering Facts] ********************************************************* ok: [192.168.56.11] ok: [192.168.56.10] TASK [Print message] *********************************************************** ok: [192.168.56.10] =\u0026gt; { \u0026#34;msg\u0026#34;: \u0026#34;Hello Ansible\u0026#34; } ok: [192.168.56.11] =\u0026gt; { \u0026#34;msg\u0026#34;: \u0026#34;Hello Ansible\u0026#34; } PLAY RECAP ********************************************************************* 192.168.56.10 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 192.168.56.11 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 Use vars to defines a list of variables in playbook-02.yml.\n--- - hosts: all vars: - username: hoang - dir: /home/hoang tasks: - name: Print variables debug: msg: \u0026#34; Username: {{ username }}, Home dir: {{ dir }} \u0026#34; ansible-playbook -i inventory playbook-02.yml PLAY [all] ********************************************************************* TASK [Gathering Facts] ********************************************************* ok: [192.168.56.11] ok: [192.168.56.10] TASK [Print variables] ********************************************************* ok: [192.168.56.10] =\u0026gt; { \u0026#34;msg\u0026#34;: \u0026#34;Username: hoang, Home dir: /home/hoang \u0026#34; } ok: [192.168.56.11] =\u0026gt; { \u0026#34;msg\u0026#34;: \u0026#34;Username: hoang, Home dir: /home/hoang \u0026#34; } PLAY RECAP ********************************************************************* 192.168.56.10 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 192.168.56.11 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 To access system information, use filter parameter to provide a pattern, you can get any variable in JSON output and show off on msg of debug in playbook-03.yml.\n--- - hosts: all tasks: - name: print facts debug: msg: \u0026#34;IPv4 address: {{ ansible_default_ipv4.address }}\u0026#34; ansible all -i inventory -m setup ansible all -i inventory -m setup -a \u0026#34;filter=*ipv4*\u0026#34; ansible-playbook -i inventory playbook-03.yml PLAY [all] ********************************************************************* TASK [Gathering Facts] ********************************************************* ok: [192.168.56.11] ok: [192.168.56.10] TASK [print facts] ************************************************************* ok: [192.168.56.10] =\u0026gt; { \u0026#34;msg\u0026#34;: \u0026#34;IPv4 address: 10.0.2.15\u0026#34; } ok: [192.168.56.11] =\u0026gt; { \u0026#34;msg\u0026#34;: \u0026#34;IPv4 address: 10.0.2.15\u0026#34; } PLAY RECAP ********************************************************************* 192.168.56.10 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 192.168.56.11 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 Use when in playbook to run tasks with condition, the variable in the condition was predefined in vars in playbook-04.yml.\n--- - hosts: all vars: - create_user_file: yes - user: vagrant tasks: - name: create file for user file: path: /home/{{ user }}/tmp.txt state: touch when: create_user_file ansible-playbook -i inventory playbook-04.yml PLAY [all] ********************************************************************* TASK [Gathering Facts] ********************************************************* ok: [192.168.56.11] ok: [192.168.56.10] TASK [create file for user] **************************************************** changed: [192.168.56.11] changed: [192.168.56.10] PLAY RECAP ********************************************************************* 192.168.56.10 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 192.168.56.11 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 Now we can verify the file was created.\nsshpass -p vagrant ssh vagrant@192.168.56.10 cd /home/vagrant/ ls -l total 0 -rw-rw-r-- 1 vagrant vagrant 0 Mar 3 16:26 tmp.txt Use register in playbook to create a new variable and assigns it with the output obtained from a command.\nBecause Ansible will interrupt a play if the command you’re using to evaluate a condition fails. For that reason, you’ll need to include an ignore_errors directive set to yes in said task, and this will make Ansible move on to the next task and continue the play. We can see clearly with playbook-05.yml.\n--- - hosts: all vars: - user: vagrant - filename: tmp tasks: - name: Check if file already exists command: ls /home/{{ user }}/{{ filename }} register: file_exists ignore_errors: yes - name: create file for user file: path: /home/{{ user }}/{{ filename }} state: touch when: file_exists is failed - name: show message if file exists debug: msg: The user file already exists. when: file_exists is succeeded ansible-playbook -i inventory playbook-05.yml PLAY [all] ********************************************************************* TASK [Gathering Facts] ********************************************************* ok: [192.168.56.11] ok: [192.168.56.10] TASK [Check if file already exists] ******************************************** fatal: [192.168.56.11]: FAILED! =\u0026gt; {\u0026#34;changed\u0026#34;: true, \u0026#34;cmd\u0026#34;: [\u0026#34;ls\u0026#34;, \u0026#34;/home/vagrant/tmp\u0026#34;], \u0026#34;delta\u0026#34;: \u0026#34;0:00:00.004377\u0026#34;, \u0026#34;end\u0026#34;: \u0026#34;2022-03-03 15:42:05.367117\u0026#34;, \u0026#34;msg\u0026#34;: \u0026#34;non-zero return code\u0026#34;, \u0026#34;rc\u0026#34;: 2, \u0026#34;start\u0026#34;: \u0026#34;2022-03-03 15:42:05.362740\u0026#34;, \u0026#34;stderr\u0026#34;: \u0026#34;ls: cannot access \u0026#39;/home/vagrant/tmp\u0026#39;: No such file or directory\u0026#34;, \u0026#34;stderr_lines\u0026#34;: [\u0026#34;ls: cannot access \u0026#39;/home/vagrant/tmp\u0026#39;: No such file or directory\u0026#34;], \u0026#34;stdout\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;stdout_lines\u0026#34;: []} ...ignoring fatal: [192.168.56.10]: FAILED! =\u0026gt; {\u0026#34;changed\u0026#34;: true, \u0026#34;cmd\u0026#34;: [\u0026#34;ls\u0026#34;, \u0026#34;/home/vagrant/tmp\u0026#34;], \u0026#34;delta\u0026#34;: \u0026#34;0:00:00.005670\u0026#34;, \u0026#34;end\u0026#34;: \u0026#34;2022-03-03 15:42:05.495959\u0026#34;, \u0026#34;msg\u0026#34;: \u0026#34;non-zero return code\u0026#34;, \u0026#34;rc\u0026#34;: 2, \u0026#34;start\u0026#34;: \u0026#34;2022-03-03 15:42:05.490289\u0026#34;, \u0026#34;stderr\u0026#34;: \u0026#34;ls: cannot access \u0026#39;/home/vagrant/tmp\u0026#39;: No such file or directory\u0026#34;, \u0026#34;stderr_lines\u0026#34;: [\u0026#34;ls: cannot access \u0026#39;/home/vagrant/tmp\u0026#39;: No such file or directory\u0026#34;], \u0026#34;stdout\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;stdout_lines\u0026#34;: []} ...ignoring TASK [create file for user] **************************************************** changed: [192.168.56.10] changed: [192.168.56.11] TASK [show message if file exists] ********************************************* skipping: [192.168.56.11] skipping: [192.168.56.10] PLAY RECAP ********************************************************************* 192.168.56.10 : ok=3 changed=2 unreachable=0 failed=0 skipped=1 rescued=0 ignored=1 192.168.56.11 : ok=3 changed=2 unreachable=0 failed=0 skipped=1 rescued=0 ignored=1 Then, re-run playbook, you’ll get a different result because the file is already exists.\nansible-playbook -i inventory playbook-05.yml PLAY [all] ********************************************************************* TASK [Gathering Facts] ********************************************************* ok: [192.168.56.11] ok: [192.168.56.10] TASK [Check if file already exists] ******************************************** changed: [192.168.56.11] changed: [192.168.56.10] TASK [create file for user] **************************************************** skipping: [192.168.56.10] skipping: [192.168.56.11] TASK [show message if file exists] ********************************************* ok: [192.168.56.10] =\u0026gt; { \u0026#34;msg\u0026#34;: \u0026#34;The user file already exists.\u0026#34; } ok: [192.168.56.11] =\u0026gt; { \u0026#34;msg\u0026#34;: \u0026#34;The user file already exists.\u0026#34; } PLAY RECAP ********************************************************************* 192.168.56.10 : ok=3 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0 192.168.56.11 : ok=3 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0 Use loop in playbook to avoid repeating the task several times. By default Ansible sets the loop variable item for each loop. Let see in playbook-06.yml.\n--- - hosts: all tasks: - name: creates users files file: path: /tmp/ansible-{{ item }} state: touch loop: - root - hoang - hanh ansible-playbook -i inventory playbook-06.yml PLAY [all] ********************************************************************* TASK [Gathering Facts] ********************************************************* ok: [192.168.56.11] ok: [192.168.56.10] TASK [creates users files] ***************************************************** changed: [192.168.56.11] =\u0026gt; (item=root) changed: [192.168.56.10] =\u0026gt; (item=root) changed: [192.168.56.10] =\u0026gt; (item=hoang) changed: [192.168.56.11] =\u0026gt; (item=hoang) changed: [192.168.56.10] =\u0026gt; (item=hanh) changed: [192.168.56.11] =\u0026gt; (item=hanh) PLAY RECAP ********************************************************************* 192.168.56.10 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 192.168.56.11 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 With privilege escalation, such as to run a command with extended permissions (ex: sudo), you’ll need to include a become directive set to yes in playbook-07.yml.\n--- - hosts: all become: yes tasks: - name: Update apt cache apt: update_cache: yes ansible-playbook -i inventory playbook-07.yml PLAY [all] ********************************************************************* TASK [Gathering Facts] ********************************************************* ok: [192.168.56.11] ok: [192.168.56.10] TASK [Update apt cache] ******************************************************** changed: [192.168.56.10] changed: [192.168.56.11] PLAY RECAP ********************************************************************* 192.168.56.10 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 192.168.56.11 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 To provide privilege escalation password, you can use the following command with flag -K (–ask-become-pass).\nansible-playbook -i inventory playbook-07.yml -K BECOME password: PLAY [all] ********************************************************************* TASK [Gathering Facts] ********************************************************* ok: [192.168.56.11] ok: [192.168.56.10] TASK [Update apt cache] ******************************************************** ok: [192.168.56.11] ok: [192.168.56.10] PLAY RECAP ********************************************************************* 192.168.56.10 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 192.168.56.11 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 You can also change which user you want to switch to while executing a task or play. To do that, set the become_user directive to the name of the remote user you want to switch to in playbook-08.yml.\n--- - hosts: all become: yes vars: user: \u0026#34;{{ ansible_env.SUDO_USER }}\u0026#34; tasks: - name: Create root file file: path: /tmp/file_of_root state: touch - name: Create {{ user }} file become_user: \u0026#34;{{ user }}\u0026#34; file: path: /tmp/file_of_{{ user }} state: touch ansible-playbook -i inventory playbook-08.yml PLAY [all] ********************************************************************* TASK [Gathering Facts] ********************************************************* ok: [192.168.56.11] ok: [192.168.56.10] TASK [Create root file] ******************************************************** changed: [192.168.56.11] changed: [192.168.56.10] TASK [Create vagrant file] ***************************************************** changed: [192.168.56.10] changed: [192.168.56.11] PLAY RECAP ********************************************************************* 192.168.56.10 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 192.168.56.11 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 Now we can verify file ownership information.\nsshpass -p vagrant ssh vagrant@192.168.56.10 ls -la /tmp/file_of_* -rw-r--r-- 1 root root 0 Mar 3 16:03 /tmp/file_of_root -rw-rw-r-- 1 vagrant vagrant 0 Mar 3 16:03 /tmp/file_of_vagrant Use apt in playbook to install and manage system packages. To install a package, you can set package state to present or latest (default is present). Let see in playbook-09.yml.\n--- - hosts: all become: yes tasks: - name: Update apt cache and make sure Vim is installed apt: name: vim state: latest update_cache: yes ansible-playbook -i inventory playbook-09.yml PLAY [all] ********************************************************************* TASK [Gathering Facts] ********************************************************* ok: [192.168.56.11] ok: [192.168.56.10] TASK [Update apt cache and make sure Vim is installed] ************************* ok: [192.168.56.11] ok: [192.168.56.10] PLAY RECAP ********************************************************************* 192.168.56.10 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 192.168.56.11 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 Now we can verify Vim package have installed.\nsshpass -p vagrant ssh vagrant@192.168.56.11 vim --version | head -n1 VIM - Vi IMproved 8.0 (2016 Sep 12, compiled Jan 20 2022 02:47:53) When you want to remove a package, you must set the package state to absent.\n--- - hosts: all become: yes tasks: - name: Removing Vim package apt: name: vim state: absent Now we can verify Vim package have removed.\nsshpass -p vagrant ssh vagrant@192.168.56.11 vim --version -bash: vim: command not found When installing multiple packages, you can use a loop and provide an array containing the names of the packages you want to install in playbook-10.yml.\n--- - hosts: all become: yes tasks: - name: Update apt cache and make sure Vim, Curl and Unzip are installed apt: name: \u0026#34;{{ item }}\u0026#34; update_cache: yes loop: - vim - curl - unzip ansible-playbook -i inventory playbook-10.yml PLAY [all] ********************************************************************* TASK [Gathering Facts] ********************************************************* ok: [192.168.56.11] ok: [192.168.56.10] TASK [Update apt cache and make sure Vim, Curl and Unzip are installed] ******** changed: [192.168.56.10] =\u0026gt; (item=vim) ok: [192.168.56.10] =\u0026gt; (item=curl) ok: [192.168.56.10] =\u0026gt; (item=unzip) changed: [192.168.56.11] =\u0026gt; (item=vim) ok: [192.168.56.11] =\u0026gt; (item=curl) ok: [192.168.56.11] =\u0026gt; (item=unzip) PLAY RECAP ********************************************************************* 192.168.56.10 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 192.168.56.11 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 Templates allow you to create new files on the nodes using predefined models based on the Jinja2 templating system. Ansible templates are typically saved as .tpl files and support the use of variables, loops, and conditional expressions.\nNow we will create new template file called landing-page.html.j2\n\u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt; {{ page_title }} \u0026lt;/title\u0026gt; \u0026lt;meta name=\u0026#34;description\u0026#34; content=\u0026#34;Created with Ansible\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt; {{ page_title }} \u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt; {{ page_description }} \u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; This template uses two variables that must be provided whenever the template is applied in a playbook: page_title and page_description. We can see in playbook-11.yml\n--- - hosts: all become: yes vars: page_title: Meme Wibu page_description: Wibu is the best. tasks: - name: Install Nginx apt: name: nginx state: latest - name: Apply Page Template template: src: files/landing-page.html.j2 dest: /var/www/html/index.nginx-debian.html - name: Allow all access to tcp port 80 ufw: rule: allow port: \u0026#39;80\u0026#39; proto: tcp ansible-playbook -i inventory playbook-11.yml On localhost, you can access landing page with server public ip (192.168.56.10 or 192.168.56.11)\ncurl 192.168.56.11 \u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Meme Wibu\u0026lt;/title\u0026gt; \u0026lt;meta name=\u0026#34;description\u0026#34; content=\u0026#34;Created with Ansible\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Meme Wibu\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;Wibu is the best.\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; In Ansible, handlers are special tasks that only get executed when triggered via the notify directive. Handlers are executed at the end of the play, once all tasks are finished. So, handlers are typically used to start, reload, restart, and stop services. If your playbook involves changing configuration files, there is a high chance that you’ll need to restart a service so that the changes take effect.\nThe following playbook-12.yml replaces the default document root in Nginx’s configuration file using the built-in Ansible module replace. This module looks for patterns in a file based on a regular expression defined by regexp, and then replaces any matches found with the content defined by replace. The task then sends a notification to the Restart Nginx handler for a restart as soon as possible.\n--- - hosts: all become: yes vars: page_title: Master Wibu page_description: No wibu No fun. doc_root: /var/www/wibu tasks: - name: Install Nginx apt: name: nginx state: latest - name: Make sure new doc root exists file: path: \u0026#34;{{ doc_root }}\u0026#34; state: directory mode: \u0026#39;0755\u0026#39; - name: Apply Page Template template: src: files/landing-page.html.j2 dest: \u0026#34;{{ doc_root }}/index.html\u0026#34; - name: Replace document root on default Nginx configuration replace: path: /etc/nginx/sites-available/default regexp: \u0026#39;(\\s+)root /var/www/html;(\\s+.*)?$\u0026#39; replace: \\g\u0026lt;1\u0026gt;root {{ doc_root }};\\g\u0026lt;2\u0026gt; notify: Restart Nginx - name: Allow all access to tcp port 80 ufw: rule: allow port: \u0026#39;80\u0026#39; proto: tcp handlers: - name: Restart Nginx service: name: nginx state: restarted ansible-playbook -i inventory playbook-12.yml PLAY [all] ********************************************************************* TASK [Gathering Facts] ********************************************************* ok: [192.168.56.10] ok: [192.168.56.11] TASK [Install Nginx] *********************************************************** ok: [192.168.56.10] ok: [192.168.56.11] TASK [Make sure new doc root exists] ******************************************* changed: [192.168.56.10] changed: [192.168.56.11] TASK [Apply Page Template] ***************************************************** changed: [192.168.56.11] changed: [192.168.56.10] TASK [Replace document root on default Nginx configuration] ******************** changed: [192.168.56.11] changed: [192.168.56.10] TASK [Allow all access to tcp port 80] ***************************************** ok: [192.168.56.10] ok: [192.168.56.11] RUNNING HANDLER [Restart Nginx] ************************************************ changed: [192.168.56.11] changed: [192.168.56.10] PLAY RECAP ********************************************************************* 192.168.56.10 : ok=7 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 192.168.56.11 : ok=7 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 On a fresh installation of Nginx, the document root is located at /var/www/html, but we move the location to /var/www/wibu by using replace if matched pattern in regexp. This causes a change in Nginx configuration, so you’ll see the Restart Nginx handler being executed just before the end of the play.\nNow if you re-run playbook, you’ll get a different result because the document root was moved to /var/www/wibu, not matched pattern in regexp. So the Restart Nginx not being executed.\nPLAY [all] ********************************************************************* TASK [Gathering Facts] ********************************************************* ok: [192.168.56.11] ok: [192.168.56.10] TASK [Install Nginx] *********************************************************** ok: [192.168.56.10] ok: [192.168.56.11] TASK [Make sure new doc root exists] ******************************************* ok: [192.168.56.10] ok: [192.168.56.11] TASK [Apply Page Template] ***************************************************** ok: [192.168.56.10] ok: [192.168.56.11] TASK [Replace document root on default Nginx configuration] ******************** ok: [192.168.56.10] ok: [192.168.56.11] TASK [Allow all access to tcp port 80] ***************************************** ok: [192.168.56.11] ok: [192.168.56.10] PLAY RECAP ********************************************************************* 192.168.56.10 : ok=6 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 192.168.56.11 : ok=6 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 If you go to your browser and access the server’s IP address now, you’ll see the following page:\ncurl 192.168.56.11 \u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Master Wibu\u0026lt;/title\u0026gt; \u0026lt;meta name=\u0026#34;description\u0026#34; content=\u0026#34;Created with Ansible\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Master Wibu\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;No wibu No fun.\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; After all, now we’ll use what we have seen so far to create a playbook that automates setting up a remote Nginx server to host a static HTML website on Ubuntu 20.04.\nFirst of all, we create a demo website in nginx_demo/resources.\ntotal 20 drwxr-xr-x 3 ph3 ph3 4096 Mar 3 13:42 . drwxr-xr-x 4 ph3 ph3 4096 Mar 3 13:31 .. -rw-r--r-- 1 ph3 ph3 1441 Jul 29 2021 about.html drwxr-xr-x 2 ph3 ph3 4096 Jul 29 2021 images -rw-r--r-- 1 ph3 ph3 3165 Jul 29 2021 index.html You’ll now set up the Nginx template that is necessary to configure the remote web server in files/nginx.conf.j2.\nserver { listen 80; root {{ document_root }}/{{ app_root }}; index index.html index.htm; server_name {{ server_name }}; location / { default_type \u0026#34;text/html\u0026#34;; try_files $uri.html $uri $uri/ =404; } } This template file contains an Nginx server block configuration for a static HTML website. It uses three variables: document_root, app_root, and server_name. Now we’ll define these variables in playbook-nginx.yml.\n--- - hosts: all become: yes vars: server_name: \u0026#34;{{ ansible_default_ipv4.address }}\u0026#34; document_root: /var/www app_root: resources tasks: - name: Update apt cache and install Nginx apt: name: nginx state: latest update_cache: yes - name: Copy website files to the server\u0026#39;s document root copy: src: \u0026#34;{{ app_root }}\u0026#34; dest: \u0026#34;{{ document_root }}\u0026#34; mode: preserve - name: Apply Nginx template template: src: files/nginx.conf.j2 dest: /etc/nginx/sites-available/default notify: Restart Nginx - name: Enable new site file: src: /etc/nginx/sites-available/default dest: /etc/nginx/sites-enabled/default state: link notify: Restart Nginx - name: Allow all access to tcp port 80 ufw: rule: allow port: \u0026#39;80\u0026#39; proto: tcp handlers: - name: Restart Nginx service: name: nginx state: restarted ansible-playbook -i inventory nginx_demo/playbook-nginx.yml If you go to your browser and access your server’s hostname or IP address you should now see the following page:\nThanks and Best Regards !!!\nYou can get full source code here!\n","permalink":"https://hoangph3.github.io/posts/ansible/","summary":"Automate server provisioning and configuration with Ansible playbooks and roles.","title":"Ansible Automation Tutorial"},{"content":"Bootstrapping a Basic Cluster Create kubernetes cluster using kubeadm and ansible from scratch Starting Virtual Machine Step 1: Create master and worker server (–provision flag to run script when startup)\nvagrant up --provision Step 2: Checking connection\nsshpass -p vagrant ssh vagrant@192.168.56.10 sshpass -p vagrant ssh vagrant@192.168.56.11 sshpass -p vagrant ssh vagrant@192.168.56.12 Create cluster kubernetes Step 1: Build environment by apt dependencies and config for all server\nansible-playbook -i hosts build-env.yml Step 2: Build master node\nansible-playbook -i hosts build-master.yml Step 3: Build worker node\nansible-playbook -i hosts build-worker.yml Step 4: Explore cluster\nsshpass -p vagrant ssh vagrant@192.168.56.10 kubectl get nodes kubectl get po -n kube-system NAME STATUS ROLES AGE VERSION master Ready control-plane,master 34m v1.23.0 worker-1 Ready \u0026lt;none\u0026gt; 5m36s v1.23.0 worker-2 Ready \u0026lt;none\u0026gt; 5m48s v1.23.0 NAME READY STATUS RESTARTS AGE coredns-64897985d-92fhv 1/1 Running 0 34m coredns-64897985d-kf6bt 1/1 Running 0 34m etcd-master 1/1 Running 2 35m kube-apiserver-master 1/1 Running 2 35m kube-controller-manager-master 1/1 Running 2 35m kube-flannel-ds-6pcq5 1/1 Running 0 6m22s kube-flannel-ds-wfq5l 1/1 Running 0 34m kube-flannel-ds-xzh2s 1/1 Running 0 6m10s kube-proxy-9vr5m 1/1 Running 0 34m kube-proxy-cbm87 1/1 Running 0 6m10s kube-proxy-nj2l5 1/1 Running 0 6m22s kube-scheduler-master 1/1 Running 2 35m Step 5: Deploy application\nAccess to master node: sshpass -p vagrant ssh vagrant@192.168.56.10 Create file demo-nginx.yaml: apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment spec: selector: matchLabels: app: nginx replicas: 2 template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: nginx-service spec: selector: app: nginx type: NodePort ports: - port: 80 targetPort: 80 nodePort: 32000 Create pods: kubectl apply -f demo-nginx.yaml Get pods status: kubectl get pods NAME READY STATUS RESTARTS AGE nginx-deployment-74d589986c-75xqx 1/1 Running 0 47s nginx-deployment-74d589986c-lrgq5 1/1 Running 0 47s Access application from external service: curl \u0026lt;workerIP\u0026gt;:\u0026lt;nodePort\u0026gt; curl http://192.168.56.11:32000 curl http://192.168.56.12:32000 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;Welcome to nginx!\u0026lt;/title\u0026gt; \u0026lt;style\u0026gt; html { color-scheme: light dark; } body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Welcome to nginx!\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;If you see this page, the nginx web server is successfully installed and working. Further configuration is required.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;For online documentation and support please refer to \u0026lt;a href=\u0026#34;http://nginx.org/\u0026#34;\u0026gt;nginx.org\u0026lt;/a\u0026gt;.\u0026lt;br/\u0026gt; Commercial support is available at \u0026lt;a href=\u0026#34;http://nginx.com/\u0026#34;\u0026gt;nginx.com\u0026lt;/a\u0026gt;.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;\u0026lt;em\u0026gt;Thank you for using nginx.\u0026lt;/em\u0026gt;\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Stacked etcd Topology Create kubernetes cluster using kubeadm and ansible from scratch Starting Virtual Machine Step 1: Create master and worker server (–provision flag to run script when startup)\nvagrant up --provision Step 2: Checking connection\nsshpass -p vagrant ssh vagrant@192.168.56.10 sshpass -p vagrant ssh vagrant@192.168.56.11 sshpass -p vagrant ssh vagrant@192.168.56.12 sshpass -p vagrant ssh vagrant@192.168.56.13 sshpass -p vagrant ssh vagrant@192.168.56.14 Create cluster kubernetes Step 1: Build environment by apt dependencies and config for master and worker node\nansible-playbook -i hosts build-env-cluster.yml Step 2: Build load balancer\nansible-playbook -i hosts build-load-balancer.yml Step 3: Build master and worker node\nansible-playbook -i hosts build-master-and-worker.yml Step 4: Explore cluster\nsshpass -p vagrant ssh vagrant@192.168.56.10 kubectl get nodes kubectl get po -n kube-system -o wide NAME STATUS ROLES AGE VERSION master-1 Ready control-plane,master 32m v1.23.0 master-2 Ready control-plane,master 31m v1.23.0 worker-1 Ready \u0026lt;none\u0026gt; 30m v1.23.0 worker-2 Ready \u0026lt;none\u0026gt; 30m v1.23.0 NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES coredns-64897985d-2nwxr 1/1 Running 0 32m 10.244.0.3 master-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; coredns-64897985d-gnh4h 1/1 Running 0 32m 10.244.0.2 master-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; etcd-master-1 1/1 Running 3 33m 192.168.56.10 master-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; etcd-master-2 1/1 Running 0 31m 192.168.56.11 master-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-apiserver-master-1 1/1 Running 3 33m 192.168.56.10 master-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-apiserver-master-2 1/1 Running 0 31m 192.168.56.11 master-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-controller-manager-master-1 1/1 Running 4 (31m ago) 33m 192.168.56.10 master-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-controller-manager-master-2 1/1 Running 0 31m 192.168.56.11 master-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-flannel-ds-2mjsv 1/1 Running 0 30m 192.168.56.12 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-flannel-ds-cmmf8 1/1 Running 0 32m 192.168.56.10 master-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-flannel-ds-kqrd6 1/1 Running 0 30m 192.168.56.13 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-flannel-ds-r4kcz 1/1 Running 0 31m 192.168.56.11 master-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-proxy-h4fh5 1/1 Running 0 32m 192.168.56.10 master-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-proxy-mdfm7 1/1 Running 0 30m 192.168.56.12 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-proxy-wsr69 1/1 Running 0 31m 192.168.56.11 master-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-proxy-zkhpf 1/1 Running 0 30m 192.168.56.13 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-scheduler-master-1 1/1 Running 4 (31m ago) 33m 192.168.56.10 master-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-scheduler-master-2 1/1 Running 0 31m 192.168.56.11 master-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; Step 5: Deploy application\nAccess to master node: sshpass -p vagrant ssh vagrant@192.168.56.10 Create file demo-nginx.yaml: apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment spec: selector: matchLabels: app: nginx replicas: 4 template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: nginx-service spec: selector: app: nginx type: NodePort ports: - port: 80 targetPort: 80 nodePort: 32000 Create pods: kubectl apply -f demo-nginx.yaml Get pods status: kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-deployment-8d545c96d-6r4vr 1/1 Running 0 41s 10.244.3.2 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; nginx-deployment-8d545c96d-jftnn 1/1 Running 0 41s 10.244.2.3 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; nginx-deployment-8d545c96d-vll4h 1/1 Running 0 41s 10.244.2.2 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; nginx-deployment-8d545c96d-x5m5g 1/1 Running 0 41s 10.244.3.3 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; Access application from external service: curl \u0026lt;workerIP\u0026gt;:\u0026lt;nodePort\u0026gt; curl http://192.168.56.12:32000 curl http://192.168.56.13:32000 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;Welcome to nginx!\u0026lt;/title\u0026gt; \u0026lt;style\u0026gt; html { color-scheme: light dark; } body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Welcome to nginx!\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;If you see this page, the nginx web server is successfully installed and working. Further configuration is required.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;For online documentation and support please refer to \u0026lt;a href=\u0026#34;http://nginx.org/\u0026#34;\u0026gt;nginx.org\u0026lt;/a\u0026gt;.\u0026lt;br/\u0026gt; Commercial support is available at \u0026lt;a href=\u0026#34;http://nginx.com/\u0026#34;\u0026gt;nginx.com\u0026lt;/a\u0026gt;.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;\u0026lt;em\u0026gt;Thank you for using nginx.\u0026lt;/em\u0026gt;\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; External etcd Topology Create kubernetes cluster using kubeadm and ansible from scratch Starting Virtual Machine Step 1: Create master and worker server (–provision flag to run script when startup)\nvagrant up --provision Step 2: Checking connection\nsshpass -p vagrant ssh vagrant@192.168.56.11 sshpass -p vagrant ssh vagrant@192.168.56.12 sshpass -p vagrant ssh vagrant@192.168.56.21 sshpass -p vagrant ssh vagrant@192.168.56.22 sshpass -p vagrant ssh vagrant@192.168.56.15 Generate the certificate Firstly, install cfssl:\nwget https://pkg.cfssl.org/R1.2/cfssl_linux-amd64 wget https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64 wget https://pkg.cfssl.org/R1.2/cfssl-certinfo_linux-amd64 cd Downloads chmod +x cfssl* sudo mv cfssl_linux-amd64 /usr/local/bin/cfssl sudo mv cfssljson_linux-amd64 /usr/local/bin/cfssljson sudo mv cfssl-certinfo_linux-amd64 /usr/local/bin/cfssl-certinfo cd certs chmod +x gen.sh ./gen.sh 2022/04/04 13:33:36 [INFO] generating a new CA key and certificate from CSR 2022/04/04 13:33:36 [INFO] generate received request 2022/04/04 13:33:36 [INFO] received CSR 2022/04/04 13:33:36 [INFO] generating key: rsa-2048 2022/04/04 13:33:37 [INFO] encoded CSR 2022/04/04 13:33:37 [INFO] signed certificate with serial number 168845797640225205009115083971177470265899005809 2022/04/04 13:33:37 [INFO] generate received request 2022/04/04 13:33:37 [INFO] received CSR 2022/04/04 13:33:37 [INFO] generating key: rsa-2048 2022/04/04 13:33:37 [INFO] encoded CSR 2022/04/04 13:33:37 [INFO] signed certificate with serial number 445423558377599319684907386638393716430750399638 2022/04/04 13:33:37 [WARNING] This certificate lacks a \u0026#34;hosts\u0026#34; field. This makes it unsuitable for websites. For more information see the Baseline Requirements for the Issuance and Management of Publicly-Trusted Certificates, v.1.1.6, from the CA/Browser Forum (https://cabforum.org); specifically, section 10.2.3 (\u0026#34;Information Requirements\u0026#34;). Now we will verify the ca certificate and private key were generated:\nls -la total 40 drwxr-xr-x 3 ph3 ph3 4096 Apr 4 13:33 . drwxr-xr-x 4 ph3 ph3 4096 Apr 4 13:29 .. -rw-r--r-- 1 ph3 ph3 997 Apr 4 13:33 ca.csr -rw------- 1 ph3 ph3 1679 Apr 4 13:33 ca-key.pem -rw-r--r-- 1 ph3 ph3 1350 Apr 4 13:33 ca.pem drwxr-xr-x 2 ph3 ph3 4096 Apr 4 12:38 config -rwxr-xr-x 1 ph3 ph3 235 Apr 4 13:06 gen.sh -rw-r--r-- 1 ph3 ph3 1249 Apr 4 13:33 server.csr -rw------- 1 ph3 ph3 1679 Apr 4 13:33 server-key.pem -rw-r--r-- 1 ph3 ph3 1610 Apr 4 13:33 server.pem Create cluster kubernetes Step 1: Build environment by apt dependencies and config for master and worker node\nansible-playbook -i hosts build-env-cluster.yml Step 2: Build load balancer\nansible-playbook -i hosts build-load-balancer.yml Step 3: Build external etcd cluster on master nodes\nansible-playbook -i hosts build-etcd.yml After finish, you can see the etcd cluster status:\nTASK [print etcd status cluster] *************************************************************************************************************************************** ok: [master-1] =\u0026gt; { \u0026#34;msg\u0026#34;: [ \u0026#34;+------------------+---------+----------+----------------------------+----------------------------+------------+\u0026#34;, \u0026#34;| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS | IS LEARNER |\u0026#34;, \u0026#34;+------------------+---------+----------+----------------------------+----------------------------+------------+\u0026#34;, \u0026#34;| 7de60f185c634ebb | started | master-2 | https://192.168.56.12:2380 | https://192.168.56.12:2379 | false |\u0026#34;, \u0026#34;| e81c9fc39b7ba9f8 | started | master-1 | https://192.168.56.11:2380 | https://192.168.56.11:2379 | false |\u0026#34;, \u0026#34;+------------------+---------+----------+----------------------------+----------------------------+------------+\u0026#34; ] } ok: [master-2] =\u0026gt; { \u0026#34;msg\u0026#34;: [ \u0026#34;+------------------+---------+----------+----------------------------+----------------------------+------------+\u0026#34;, \u0026#34;| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS | IS LEARNER |\u0026#34;, \u0026#34;+------------------+---------+----------+----------------------------+----------------------------+------------+\u0026#34;, \u0026#34;| 7de60f185c634ebb | started | master-2 | https://192.168.56.12:2380 | https://192.168.56.12:2379 | false |\u0026#34;, \u0026#34;| e81c9fc39b7ba9f8 | started | master-1 | https://192.168.56.11:2380 | https://192.168.56.11:2379 | false |\u0026#34;, \u0026#34;+------------------+---------+----------+----------------------------+----------------------------+------------+\u0026#34; ] } Step 4: Build master and worker node\nansible-playbook -i hosts build-master-and-worker.yml Step 5: Verify the cluster\nsshpass -p vagrant ssh vagrant@192.168.56.11 kubectl get nodes kubectl get po -n kube-system -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES coredns-64897985d-fppdp 0/1 Running 3 30m 10.244.0.3 master-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; coredns-64897985d-t4mxq 0/1 Running 3 30m 10.244.0.2 master-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-apiserver-master-1 1/1 Running 4 30m 192.168.56.11 master-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-apiserver-master-2 1/1 Running 0 3m11s 192.168.56.12 master-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-controller-manager-master-1 1/1 Running 4 30m 192.168.56.11 master-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-controller-manager-master-2 1/1 Running 1 25m 192.168.56.12 master-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-flannel-ds-4qtvn 1/1 Running 1 25m 192.168.56.12 master-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-flannel-ds-fxptk 1/1 Running 12 (3m37s ago) 28m 192.168.56.11 master-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-flannel-ds-g68rx 1/1 Running 3 23m 192.168.56.22 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-flannel-ds-hkwqd 1/1 Running 1 23m 192.168.56.21 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-proxy-7tcjf 1/1 Running 1 23m 192.168.56.21 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-proxy-j22bn 1/1 Running 1 25m 192.168.56.12 master-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-proxy-wq9hg 1/1 Running 1 23m 192.168.56.22 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-proxy-xsrqt 1/1 Running 3 30m 192.168.56.11 master-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-scheduler-master-1 1/1 Running 4 30m 192.168.56.11 master-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; kube-scheduler-master-2 1/1 Running 1 25m 192.168.56.12 master-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; Now we can see the etcd isn’t present in the cluster, because it’s external.\nStep 5: Deploy application\nAccess to master node: sshpass -p vagrant ssh vagrant@192.168.56.11 Create file demo-nginx.yaml: apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment spec: selector: matchLabels: app: nginx replicas: 4 template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: nginx-service spec: selector: app: nginx type: NodePort ports: - port: 80 targetPort: 80 nodePort: 32000 Create pods: kubectl apply -f demo-nginx.yaml Get pods status: kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-deployment-74d589986c-96lkf 1/1 Running 0 40s 10.244.3.3 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; nginx-deployment-74d589986c-f8cgh 1/1 Running 0 40s 10.244.2.2 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; nginx-deployment-74d589986c-pph6t 1/1 Running 0 40s 10.244.3.2 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; nginx-deployment-74d589986c-vzn68 1/1 Running 0 40s 10.244.2.3 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; Access application from external service: curl \u0026lt;workerIP\u0026gt;:\u0026lt;nodePort\u0026gt; curl http://192.168.56.21:32000 curl http://192.168.56.22:32000 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;Welcome to nginx!\u0026lt;/title\u0026gt; \u0026lt;style\u0026gt; html { color-scheme: light dark; } body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Welcome to nginx!\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;If you see this page, the nginx web server is successfully installed and working. Further configuration is required.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;For online documentation and support please refer to \u0026lt;a href=\u0026#34;http://nginx.org/\u0026#34;\u0026gt;nginx.org\u0026lt;/a\u0026gt;.\u0026lt;br/\u0026gt; Commercial support is available at \u0026lt;a href=\u0026#34;http://nginx.com/\u0026#34;\u0026gt;nginx.com\u0026lt;/a\u0026gt;.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;\u0026lt;em\u0026gt;Thank you for using nginx.\u0026lt;/em\u0026gt;\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Certificate Rotation Auto rotation of Kubernetes certificates by DaemonSet Run minikube, other-wise you can run a kubernetes cluster: minikube start Validate the cluster: kubectl get pods -A NAMESPACE NAME READY STATUS RESTARTS AGE kube-system coredns-6d4b75cb6d-n96gg 1/1 Running 0 18m kube-system etcd-minikube 1/1 Running 0 18m kube-system kube-apiserver-minikube 1/1 Running 0 18m kube-system kube-controller-manager-minikube 1/1 Running 0 18m kube-system kube-proxy-r7tkh 1/1 Running 0 18m kube-system kube-scheduler-minikube 1/1 Running 0 18m kube-system storage-provisioner 1/1 Running 1 (17m ago) 18m Create a symlink in the minikube container: Firstly, ssh to the minikube container:\nminikube ssh docker@minikube:~$ In the minikube container, we create a symlink. If you run a kubernetes cluster, you can skip this.\nsudo ln -s /var/lib/minikube/binaries/v1.24.3/kube* /usr/bin/ sudo mkdir -p /etc/kubernetes/pki sudo ln -s /var/lib/minikube/certs/* /etc/kubernetes/pki/ Exit the container:\n\u0026lt;Ctrl\u0026gt; + D Check certificate expiration: docker exec -it minikube kubeadm certs check-expiration [check-expiration] Reading configuration from the cluster... [check-expiration] FYI: You can look at this config file with \u0026#39;kubectl -n kube-system get cm kubeadm-config -o yaml\u0026#39; CERTIFICATE EXPIRES RESIDUAL TIME CERTIFICATE AUTHORITY EXTERNALLY MANAGED admin.conf Jan 13, 2024 17:04 UTC 364d ca no apiserver Jan 12, 2026 17:04 UTC 2y ca no apiserver-etcd-client Jan 13, 2024 17:04 UTC 364d etcd-ca no apiserver-kubelet-client Jan 13, 2024 17:04 UTC 364d ca no controller-manager.conf Jan 13, 2024 17:04 UTC 364d ca no etcd-healthcheck-client Jan 13, 2024 17:04 UTC 364d etcd-ca no etcd-peer Jan 13, 2024 17:04 UTC 364d etcd-ca no etcd-server Jan 13, 2024 17:04 UTC 364d etcd-ca no front-proxy-client Jan 13, 2024 17:04 UTC 364d front-proxy-ca no scheduler.conf Jan 13, 2024 17:04 UTC 364d ca no CERTIFICATE AUTHORITY EXPIRES RESIDUAL TIME EXTERNALLY MANAGED ca Jan 10, 2033 17:04 UTC 9y no etcd-ca Jan 10, 2033 17:04 UTC 9y no front-proxy-ca Jan 10, 2033 17:04 UTC 9y no We can see the expire date is Jan 13, 2024 17:04 UTC.\nCreate the DaemonSet: Now we will setup a DaemonSet to rotate the kubernetes certificates by applying the manifests:\nkubectl apply -f manifests/ daemonset.apps/kucero created clusterrole.rbac.authorization.k8s.io/kucero created clusterrolebinding.rbac.authorization.k8s.io/kucero created role.rbac.authorization.k8s.io/kucero created rolebinding.rbac.authorization.k8s.io/kucero created serviceaccount/kucero created Validate the DaemonSet:\nkubectl get ds -n kube-system kucero NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE kucero 1 1 1 1 1 \u0026lt;none\u0026gt; 3m56s Tracking: Every 5 minutes:\ndocker exec -it minikube kubeadm certs check-expiration [check-expiration] Reading configuration from the cluster... [check-expiration] FYI: You can look at this config file with \u0026#39;kubectl -n kube-system get cm kubeadm-config -o yaml\u0026#39; CERTIFICATE EXPIRES RESIDUAL TIME CERTIFICATE AUTHORITY EXTERNALLY MANAGED admin.conf Jan 13, 2024 17:45 UTC 364d ca no apiserver Jan 12, 2026 17:04 UTC 2y ca no apiserver-etcd-client Jan 13, 2024 17:45 UTC 364d etcd-ca no apiserver-kubelet-client Jan 13, 2024 17:45 UTC 364d ca no controller-manager.conf Jan 13, 2024 17:46 UTC 364d ca no etcd-healthcheck-client Jan 13, 2024 17:45 UTC 364d etcd-ca no etcd-peer Jan 13, 2024 17:46 UTC 364d etcd-ca no etcd-server Jan 13, 2024 17:45 UTC 364d etcd-ca no front-proxy-client Jan 13, 2024 17:46 UTC 364d front-proxy-ca no scheduler.conf Jan 13, 2024 17:45 UTC 364d ca no CERTIFICATE AUTHORITY EXPIRES RESIDUAL TIME EXTERNALLY MANAGED ca Jan 10, 2033 17:04 UTC 9y no etcd-ca Jan 10, 2033 17:04 UTC 9y no front-proxy-ca Jan 10, 2033 17:04 UTC 9y no The expire date now is Jan 13, 2024 17:45 UTC.\nEvery 5 minutes:\ndocker exec -it minikube kubeadm certs check-expiration [check-expiration] Reading configuration from the cluster... [check-expiration] FYI: You can look at this config file with \u0026#39;kubectl -n kube-system get cm kubeadm-config -o yaml\u0026#39; CERTIFICATE EXPIRES RESIDUAL TIME CERTIFICATE AUTHORITY EXTERNALLY MANAGED admin.conf Jan 13, 2024 17:52 UTC 364d ca no apiserver Jan 12, 2026 17:04 UTC 2y ca no apiserver-etcd-client Jan 13, 2024 17:52 UTC 364d etcd-ca no apiserver-kubelet-client Jan 13, 2024 17:52 UTC 364d ca no controller-manager.conf Jan 13, 2024 17:52 UTC 364d ca no etcd-healthcheck-client Jan 13, 2024 17:52 UTC 364d etcd-ca no etcd-peer Jan 13, 2024 17:52 UTC 364d etcd-ca no etcd-server Jan 13, 2024 17:52 UTC 364d etcd-ca no front-proxy-client Jan 13, 2024 17:52 UTC 364d front-proxy-ca no scheduler.conf Jan 13, 2024 17:52 UTC 364d ca no CERTIFICATE AUTHORITY EXPIRES RESIDUAL TIME EXTERNALLY MANAGED ca Jan 10, 2033 17:04 UTC 9y no etcd-ca Jan 10, 2033 17:04 UTC 9y no front-proxy-ca Jan 10, 2033 17:04 UTC 9y no The expire date now is Jan 13, 2024 17:52 UTC.\nThat’s working!\nContexts and Multiple Clusters Configure Access to multiple clusters Suppose we have a minikube cluster on local machine, we can get the content of config file from: $HOME/.kube/config\napiVersion: v1 clusters: - cluster: certificate-authority: /home/ph3/.minikube/ca.crt extensions: - extension: last-update: Sat, 23 Apr 2022 22:15:17 EDT provider: minikube.sigs.k8s.io version: v1.24.0 name: cluster_info server: https://192.168.49.2:8443 name: minikube contexts: - context: cluster: minikube extensions: - extension: last-update: Sat, 23 Apr 2022 22:15:17 EDT provider: minikube.sigs.k8s.io version: v1.24.0 name: context_info namespace: default user: minikube name: minikube current-context: minikube kind: Config preferences: {} users: - name: minikube user: client-certificate: /home/ph3/.minikube/profiles/minikube/client.crt client-key: /home/ph3/.minikube/profiles/minikube/client.key Show all contexts and current context:\n$ kubectl config get-contexts CURRENT NAME CLUSTER AUTHINFO NAMESPACE * minikube minikube minikube default $ kubectl config current-context minikube Suppose we have another kubernetes cluster with information about certificate-authority-data and server also client-certificate-data and client-key-data. If we want to access to this cluster from local machine, we need to change config file, look like following that:\napiVersion: v1 clusters: - cluster: certificate-authority: /home/ph3/.minikube/ca.crt extensions: - extension: last-update: Sat, 23 Apr 2022 22:15:17 EDT provider: minikube.sigs.k8s.io version: v1.24.0 name: cluster_info server: https://192.168.49.2:8443 name: minikube - cluster: certificate-authority-data: xxxxxx server: xxxxxx name: kubernetes contexts: - context: cluster: minikube extensions: - extension: last-update: Sat, 23 Apr 2022 22:15:17 EDT provider: minikube.sigs.k8s.io version: v1.24.0 name: context_info namespace: default user: minikube name: minikube - context: cluster: kubernetes namespace: default user: dev-admin name: dev-admin@kubernetes current-context: minikube kind: Config preferences: {} users: - name: minikube user: client-certificate: /home/ph3/.minikube/profiles/minikube/client.crt client-key: /home/ph3/.minikube/profiles/minikube/client.key - name: dev-admin user: client-certificate-data: xxxxxx client-key-data: xxxxxx Filling data from certificate-authority-data, server, client-certificate-data and client-key-data into xxxxxx.\nNow we listing contexts, then switching between contexts:\n$ kubectl config get-contexts CURRENT NAME CLUSTER AUTHINFO NAMESPACE dev-admin@kubernetes kubernetes dev-admin default * minikube minikube minikube default $ kubectl get nodes NAME STATUS ROLES AGE VERSION minikube Ready control-plane,master 87d v1.22.3 $ kubectl config use-context dev-admin@kubernetes Switched to context \u0026#34;dev-admin@kubernetes\u0026#34;. $ kubectl get nodes NAME STATUS ROLES AGE VERSION master-1 Ready control-plane,master 19d v1.23.0 master-2 Ready control-plane,master 19d v1.23.0 worker-1 Ready \u0026lt;none\u0026gt; 19d v1.23.0 worker-2 Ready \u0026lt;none\u0026gt; 19d v1.23.0 You can get full source code here: make-basic-cluster, make-stacked-etcd-cluster, make-external-etcd-cluster, certificate-rotation, contexts-multiple-clusters.\n","permalink":"https://hoangph3.github.io/posts/kubernetes_cluster_bootstrap/","summary":"Stand up Kubernetes clusters with kubeadm using stacked and external etcd topologies, plus certificate rotation and multi-cluster contexts.","title":"Bootstrapping Kubernetes Clusters"},{"content":"Node.js + MongoDB Demo demo app - developing with Kubernetes This demo app shows a simple user profile app set up using\nserver.js run web app mongo for data storage All components are kubernetes\nWith minikube To start the application Step 1: Moving docker environment to kubernetes\neval $(minikube docker-env) Step 2: Build python app image\ndocker build -t mongo-demo-k8s:1.0 . Step 3: Create deployment and service\nkubectl apply -f mongo-config.yaml kubectl apply -f mongo-secret.yaml kubectl apply -f mongo.yaml kubectl apply -f webapp.yaml Step 4: Get public IP of minikube\nminikube ip #192.168.49.2 Step 5: Access you python application UI from browser\ncurl http://192.168.49.2:30100 Python + MySQL Demo demo app - developing with Kubernetes This demo app shows a simple user profile app set up using\nmain.py run web app mysql for data storage All components are kubernetes\nWith minikube To start the application Step 1: Moving docker environment to kubernetes\neval $(minikube docker-env) Step 2: Build python app image\ndocker build -t python-docker-dev . Step 3: Create deployment and service\nkubectl apply -f mysql.yaml kubectl apply -f webapp.yaml Step 4: Get public IP of minikube\nminikube ip #192.168.49.2 Step 5: Access you python application UI from browser\ncurl http://192.168.49.2:30900 curl http://192.168.49.2:30900/initdb curl http://192.168.49.2:30900/widgets You can get full source code here: node-mongo, python-mysql.\n","permalink":"https://hoangph3.github.io/posts/kubernetes_demo_apps/","summary":"Two end-to-end example apps deployed on Kubernetes: a Node.js + MongoDB app and a Python + MySQL app.","title":"Deploying Demo Applications on Kubernetes"},{"content":"demo app - developing with Docker This demo app shows a simple user profile app set up using\nmain.py run web app mysql for data storage All components are docker-based\nWith Docker To start the application Step 1: Create docker network\ndocker network create mysqlnet Step 2: Create volume\ndocker volume create mysql docker volume create mysql_config Step 3: Start mysql database\ndocker run -v mysql:/var/lib/mysql \\ -v mysql_config:/etc/mysql -dp 3306:3306 \\ --network mysqlnet \\ --name mysqldb \\ -e MYSQL_ROOT_PASSWORD=p@ssw0rd1 \\ mysql Step 4: Check mysql is running\ndocker exec -it mysqldb bash mysql -u root -p Enter password: p@ssw0rd1 Step 5: Build image for python app\ndocker build -t python-docker-dev . Step 6: Start python app\ndocker run --network mysqlnet \\ --name rest-server \\ -dp 9000:9000 \\ python-docker-dev Step 7: Access you python application UI from browser\ncurl http://localhost:9000 Step 8: Check python app is connected to mysqldb\ncurl http://localhost:9000/initdb curl http://localhost:9000/widgets With Docker Compose To start the application Step 1: Start mysqldb and python app (make sure you have installed docker-compose)\ndocker-compose up -d Step 2: Access you python application UI from browser\ncurl http://localhost:9000 curl http://localhost:9000/initdb curl http://localhost:9000/widgets You can get full source code here!\n","permalink":"https://hoangph3.github.io/posts/docker/","summary":"Learn Docker basics: images, containers, volumes, and Docker Compose workflows.","title":"Docker Fundamentals Tutorial"},{"content":"ConfigMaps and Secrets Configure all key-value pairs as environment variables kubectl apply -f config-env-vars-envFrom.yaml configmap/myapp-config created secret/myapp-secret created deployment.apps/my-app created kubectl get pods NAME READY STATUS RESTARTS AGE my-app-6594549577-7s7ks 0/1 Completed 3 (32s ago) 60s kubectl logs -f my-app-6594549577-7s7ks KUBERNETES_PORT=tcp://10.96.0.1:443 KUBERNETES_SERVICE_PORT=443 HOSTNAME=my-app-6594549577-7s7ks SHLVL=1 username=admin HOME=/root KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin KUBERNETES_PORT_443_TCP_PORT=443 password=admin KUBERNETES_PORT_443_TCP_PROTO=tcp KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443 KUBERNETES_SERVICE_PORT_HTTPS=443 KUBERNETES_SERVICE_HOST=10.96.0.1 PWD=/ db_host=mysql-service Configure defined environment variables in the command and args of a container using the $(VAR_NAME) kubectl apply -f config-env-vars-valueFrom.yaml configmap/myapp-config created secret/myapp-secret created deployment.apps/my-app created kubectl get pods NAME READY STATUS RESTARTS AGE my-app-6df7cd5d47-dhd2c 0/1 Completed 0 6s kubectl logs -f my-app-6df7cd5d47-dhd2c admin admin mysql-service Configure as a Volume kubectl apply -f config-volumes.yaml configmap/mysql-config created secret/mysql-secret created deployment.apps/my-db created kubectl get pods NAME READY STATUS RESTARTS AGE my-db-569cdd7c6c-mrshr 0/1 ContainerCreating 0 3s kubectl logs -f my-db-569cdd7c6c-mrshr /mysql/db-config/mysql.conf [mysqld] port=3306 socket=/tmp/mysql.sock key_buffer_size=16M max_allowed_packet=128M /mysql/db-config/secure-flag ThiS_Is_FLagggggggggg_4U@@ /mysql/db-config/test.conf ThiS_iS_0nLy_f0R_T3st!^^ /mysql/db-secret/secret.file Sup3r_s3cure_F1agggggggggg!^^ Because we omit the items array entirely, every key in the ConfigMap and Secret becomes a file with the same name as the key. So we get 4 files, contain 3 files from ConfigMap and 1 file from Secret.\nConfigure as a Volume with items kubectl apply -f config-volumes-with-items.yaml kubectl get pods kubectl logs -f my-db-5f9585df5f-8fzlc configmap/mysql-config created secret/mysql-secret created deployment.apps/my-db created NAME READY STATUS RESTARTS AGE my-db-5f9585df5f-8fzlc 0/1 Completed 0 8s /mysql/db-config/flag.txt ThiS_Is_FLagggggggggg_4U@@ /mysql/db-config/test.conf ThiS_iS_0nLy_f0R_T3st!^^ /mysql/db-secret/flag.txt Sup3r_s3cure_F1agggggggggg!^^ We defined 2 arrays of keys from the ConfigMap (not contain mysql.conf) and 1 array from Secret to create as files, the filename was changed from key to path (default is key).\nConfigure Redis kubectl apply -f config-redis.yaml configmap/example-redis-config created deployment.apps/my-redis created service/my-redis-service created kubectl get svc -o wide kubectl get pods NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR kubernetes ClusterIP 10.96.0.1 \u0026lt;none\u0026gt; 443/TCP 65d \u0026lt;none\u0026gt; my-redis-service NodePort 10.105.172.53 \u0026lt;none\u0026gt; 6379:30100/TCP 35s app=my-redis NAME READY STATUS RESTARTS AGE my-redis-6496f6bbf8-nsgjk 1/1 Running 0 40s Access redis server to get config and data:\nkubectl exec -it my-redis-6496f6bbf8-nsgjk -- redis-cli 127.0.0.1:6379\u0026gt; CONFIG GET maxmemory 1) \u0026#34;maxmemory\u0026#34; 2) \u0026#34;2097152\u0026#34; 127.0.0.1:6379\u0026gt; CONFIG GET maxmemory-policy 1) \u0026#34;maxmemory-policy\u0026#34; 2) \u0026#34;allkeys-lru\u0026#34; 127.0.0.1:6379\u0026gt; keys * (empty array) Now we will create python script test_redis.py to communicate with redis server:\n# test_redis.py import redis r = redis.Redis(host=\u0026#34;192.168.49.2\u0026#34;, # host is url that kubernetes control plane is running. port=\u0026#34;30100\u0026#34;, db=0) r.rpush(\u0026#39;foo\u0026#39;, \u0026#39;bar\u0026#39;) r.rpush(\u0026#39;foo\u0026#39;, \u0026#39;bar2\u0026#39;) Note that host argument is the url that kubernetes control plane is running. In this case, we use minikube and the url can find by the command minikube ip or kubectl cluster-info:\nKubernetes control plane is running at https://192.168.49.2:8443 CoreDNS is running at https://192.168.49.2:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy To further debug and diagnose cluster problems, use \u0026#39;kubectl cluster-info dump\u0026#39;. Access redis server to get data after run python script:\npython3 test_redis.py kubectl exec -it my-redis-6496f6bbf8-nsgjk -- redis-cli 127.0.0.1:6379\u0026gt; keys * 1) \u0026#34;foo\u0026#34; 127.0.0.1:6379\u0026gt; lrange foo 0 -1 1) \u0026#34;bar\u0026#34; 2) \u0026#34;bar2\u0026#34; Pass credentials for the Docker registry with Secret First, creat a Secret holding the credentials for authenticating with a Docker registry:\nkubectl create secret docker-registry mydockerhubsecret --docker-username=hoangph3 --docker-password=mypassword --docker-email=hoangph3@example.com Let’s run pod with private image:\napiVersion: v1 kind: Pod metadata: name: private-pod spec: imagePullSecrets: - name: mydockerhubsecret containers: - image: hoangph3/python-docker:v1.0 name: myapp kubectl apply -f secret-private-image.yaml kubectl describe pod private-pod Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 63s default-scheduler Successfully assigned default/private-pod to minikube Normal Pulling 62s kubelet Pulling image \u0026#34;hoangph3/python-docker:v1.0\u0026#34; Normal Pulled 46s kubelet Successfully pulled image \u0026#34;hoangph3/python-docker:v1.0\u0026#34; in 16.430969904s Normal Created 2s (x4 over 45s) kubelet Created container myapp Normal Started 2s (x4 over 45s) kubelet Started container myapp Data Persistence Volume Share data between containers with emptyDir Step 1: Create deployment with emptyDir volume:\nkubectl apply -f emptydir-volume.yaml Step 2: Tracking logs in pods:\nkubectl get pods kubectl logs -f my-app-68f4bfd84c-79wvl log-sidecar NAME READY STATUS RESTARTS AGE my-app-68f4bfd84c-79wvl 2/2 Running 0 9s Thu Apr 7 14:44:10 UTC 2022 INFO some app data Thu Apr 7 14:44:15 UTC 2022 INFO some app data Thu Apr 7 14:44:20 UTC 2022 INFO some app data Thu Apr 7 14:44:25 UTC 2022 INFO some app data Thu Apr 7 14:44:30 UTC 2022 INFO some app data Thu Apr 7 14:44:35 UTC 2022 INFO some app data Thu Apr 7 14:44:40 UTC 2022 INFO some app data PersistentVolumeClaims and PersistentVolumes The PersistentVolumes is resource that communicate with Storage, and the PersistentVolumeClaims request resource from PersistentVolumes. In production environment, the administrator will create the cluster, install plugin, … while the developers will write yaml file to deploy application. So, the PersistentVolumes will created by the administrator, the developers only need to create PersistentVolumeClaims to use.\nSuppose you are administrator, you will create the PersistentVolumes:\nkubectl apply -f pv.yaml kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE data-pv 10Gi RWO Retain Available 59s Note that the PersistentVolumes is not belong to any namespace, this is cluster resource, same as node. But the Pod, Deployment, … is the namespace resource.\nNow, suppose you are developers, you need to create PersistentVolumeClaims to store persistent data. If exist any PersistentVolumes, the PersistentVolumeClaims you created will request storage from it.\nkubectl apply -f pvc.yaml kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE mysql-data-pvc Bound pvc-e5d8e277-3831-4a1b-b9c4-351df960f58a 5Gi RWO standard 7s The STATUS=Bound indicate that the mysql-data-pvc bounded pvc-e5d8e277-3831-4a1b-b9c4-351df960f58a volume, now let’s show the pv:\nkubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE data-pv 10Gi RWO Retain Available 119s pvc-e5d8e277-3831-4a1b-b9c4-351df960f58a 5Gi RWO Delete Bound default/mysql-data-pvc standard 31s The default/mysql-data-pvc pvc was claimed resource from pvc-e5d8e277-3831-4a1b-b9c4-351df960f58a pv.\nYou can get full source code here: configmap-secret, data-persistence.\n","permalink":"https://hoangph3.github.io/posts/kubernetes_config_storage/","summary":"Manage application configuration with ConfigMaps and Secrets, and persist data using Kubernetes volumes.","title":"Kubernetes Configuration and Storage"},{"content":"Custom Resources Configure the CustomResourceDefinitions To define a new resource type, all you need to do is post a CustomResourceDefinition object (CRD) to the Kubernetes API server. The CustomResourceDefinition object is the description of the custom resource type. Once the CRD is posted, users can then create instances of the custom resource by posting JSON or YAML manifests to the API server, the same as with any other Kubernetes resource.\nLet’s imagine you want to allow users of your Kubernetes cluster to run static websites as easily as possible, without having to deal with Pods, Services, and other Kubernetes resources. What you want to achieve is for users to create objects of type Website that contain nothing more than the website’s name and the source from which the website’s files (HTML, CSS, PNG, and others) should be obtained, eg., docker image, github, … When a user creates an instance of the Website resource, you want Kubernetes to spin up a new web server pod and expose it through a Service.\nTo create the Website resource, you want users to post manifests along the lines of the one shown in the following listing.\napiVersion: extensions.example.com/v1 kind: Website metadata: name: kubia spec: gitRepo: https://github.com/hoangph3/kubia-website-example.git If you try posting this resource to Kubernetes, you’ll receive an error because Kubernetes doesn’t know what a Website object is yet:\n$ kubectl apply -f imaginary-website.yaml error: unable to recognize \u0026#34;imaginary-website.yaml\u0026#34;: no matches for kind \u0026#34;Website\u0026#34; in version \u0026#34;extensions.example.com/v1\u0026#34; Now you will create the CRD from the website-crd.yaml file:\napiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: # name must match the spec fields below, and be in the form: \u0026lt;plural\u0026gt;.\u0026lt;group\u0026gt; name: websites.extensions.example.com spec: scope: Namespaced # either Namespaced or Cluster group: extensions.example.com # group name to use for REST API: /apis/\u0026lt;group\u0026gt;/\u0026lt;version\u0026gt; versions: - name: v1 served: true # Each version can be enabled/disabled by Served flag. storage: true # One and only one version must be marked as the storage version. schema: openAPIV3Schema: type: object properties: spec: type: object properties: gitRepo: type: string names: kind: Website # kind is normally the CamelCased singular type. Your resource manifests use this. singular: website # singular name to be used as an alias on the CLI and for display plural: websites # plural name to be used in the URL: /apis/\u0026lt;group\u0026gt;/\u0026lt;version\u0026gt;/\u0026lt;plural\u0026gt; shortNames: # shortNames allow shorter string to match your resource on the CLI - gitrp $ kubectl apply -f website-crd.yaml customresourcedefinition.apiextensions.k8s.io/websites.extensions.example.com created Create your Website object now:\n$ kubectl apply -f imaginary-website.yaml website.extensions.example.com/kubia created $ kubectl get websites NAME AGE kubia 35s $ kubectl describe websites.extensions.example.com kubia Name: kubia Namespace: default Labels: \u0026lt;none\u0026gt; Annotations: \u0026lt;none\u0026gt; API Version: extensions.example.com/v1 Kind: Website Metadata: Creation Timestamp: 2022-04-24T08:06:14Z Generation: 1 Managed Fields: API Version: extensions.example.com/v1 Fields Type: FieldsV1 fieldsV1: f:metadata: f:annotations: .: f:kubectl.kubernetes.io/last-applied-configuration: f:spec: .: f:gitRepo: Manager: kubectl-client-side-apply Operation: Update Time: 2022-04-24T08:06:14Z Resource Version: 505709 UID: e1721dc4-e80e-4628-be20-11366aa6bffa Spec: Git Repo: https://github.com/hoangph3/kubia-website-example.git Events: \u0026lt;none\u0026gt; To make your Website objects run a web server pod exposed through a Service, you’ll need to build and deploy a Website controller, which will watch the API server for the creation of Website objects and then create the Service and the web server Pod for each of them.\nFirstly, build website-controller image:\n$ cd website-controller \u0026amp;\u0026amp; ls deployment-template.json Dockerfile pkg service-template.json $ go mod init website-controller go: creating new go.mod: module website-controller go: to add module requirements and sums: go mod tidy $ go get github.com/hoangph3/website-controller/pkg/v1 go: downloading github.com/hoangph3/website-controller v0.0.0-20170607104431-912c847af8cc go: added github.com/hoangph3/website-controller v0.0.0-20170607104431-912c847af8cc $ CGO_ENABLED=0 GOOS=linux go build -o website-controller -a pkg/website-controller.go $ ls -la total 6564 drwxr-xr-x 3 ph3 ph3 4096 Apr 24 07:09 . drwxr-xr-x 4 ph3 ph3 4096 Apr 24 06:51 .. -rw-r--r-- 1 ph3 ph3 1786 Apr 24 05:51 deployment-template.json -rw-r--r-- 1 ph3 ph3 125 Apr 24 05:49 Dockerfile -rw-r--r-- 1 ph3 ph3 266 Jun 7 2017 .gitignore -rw-r--r-- 1 ph3 ph3 130 Apr 24 06:45 go.mod -rw-r--r-- 1 ph3 ph3 251 Apr 24 06:45 go.sum drwx------ 3 ph3 ph3 4096 Jun 7 2017 pkg -rw-r--r-- 1 ph3 ph3 342 Jun 7 2017 service-template.json -rwxr-xr-x 1 ph3 ph3 6680760 Apr 24 07:09 website-controller $ docker build -t website-controller . Sending build context to Docker daemon 6.696MB Step 1/5 : FROM scratch ---\u0026gt; Step 2/5 : ADD website-controller / ---\u0026gt; 6f3fd9ca8612 Step 3/5 : ADD deployment-template.json / ---\u0026gt; 77a642a5c4af Step 4/5 : ADD service-template.json / ---\u0026gt; 7886a1be5b6c Step 5/5 : CMD [\u0026#34;/website-controller\u0026#34;] ---\u0026gt; Running in e0677b637e27 Removing intermediate container e0677b637e27 ---\u0026gt; 757ada991a0c Successfully built 757ada991a0c Successfully tagged kube-website-controller:latest Then build ambassador kubectl-proxy image used for communication with the API server:\n$ cd kubectl-proxy \u0026amp;\u0026amp; ls Dockerfile kubectl-proxy.sh $ docker build -t kubectl-proxy . Sending build context to Docker daemon 3.072kB Step 1/5 : FROM python:3.8-slim-buster ---\u0026gt; 9f9436d44487 Step 2/5 : RUN apt -y update \u0026amp;\u0026amp; apt -y install curl telnet nano \u0026amp;\u0026amp; curl -L -O https://dl.k8s.io/v1.23.5/kubernetes-client-linux-amd64.tar.gz \u0026amp;\u0026amp; tar zvxf kubernetes-client-linux-amd64.tar.gz kubernetes/client/bin/kubectl \u0026amp;\u0026amp; mv kubernetes/client/bin/kubectl / \u0026amp;\u0026amp; rm -rf kubernetes \u0026amp;\u0026amp; rm -f kubernetes-client-linux-amd64.tar.gz ---\u0026gt; Using cache ---\u0026gt; 6e63691a26b0 Step 3/5 : ADD kubectl-proxy.sh /kubectl-proxy.sh ---\u0026gt; 8188274c8f2c Step 4/5 : RUN chmod +x kubectl-proxy.sh ---\u0026gt; Running in f18dacc8be53 Removing intermediate container f18dacc8be53 ---\u0026gt; 2619234d7639 Step 5/5 : ENTRYPOINT /kubectl-proxy.sh ---\u0026gt; Running in 5ab5b47a9706 Removing intermediate container 5ab5b47a9706 ---\u0026gt; fd2d8e870acf Successfully built fd2d8e870acf Successfully tagged kubectl-proxy:latest Create kubernetes website controller:\napiVersion: apps/v1 kind: Deployment metadata: name: website-controller labels: app: website-controller spec: replicas: 1 selector: matchLabels: app: website-controller template: metadata: name: website-controller labels: app: website-controller spec: serviceAccountName: website-controller containers: - name: main image: website-controller imagePullPolicy: Never - name: proxy image: kubectl-proxy imagePullPolicy: Never Note that you need create ServiceAccount website-controller first:\n$ kubectl create serviceaccount website-controller serviceaccount/website-controller created Then create the controller:\n$ kubectl apply -f website-controller.yaml deployment.apps/website-controller created Bind ClusterRole cluster-admin to ServiceAccount:\n$ kubectl create clusterrolebinding website-controller --clusterrole=cluster-admin --serviceaccount=default:website-controller clusterrolebinding.rbac.authorization.k8s.io/website-controller created With the controller now running, create the Website resource again:\n$ kubectl apply -f imaginary-website.yaml website.extensions.example.com/kubia created Now, let’s check the controller’s logs and lists all Deployments, Services and Pods:\n$ kubectl get pods NAME READY STATUS RESTARTS AGE kubia-website-7d94854d9f-clqg5 2/2 Running 0 5s website-controller-5dd4555d87-rwsz7 2/2 Running 1 (2m4s ago) 2m5s $ kubectl logs -f website-controller-5dd4555d87-4rl2x -c main 2022/04/25 01:45:17 website-controller started. 2022/04/25 01:45:30 Received watch event: ADDED: kubia: https://github.com/hoangph3/kubia-website-example.git 2022/04/25 01:45:30 Creating services with name kubia-website in namespace default 2022/04/25 01:45:30 response Status: 201 Created 2022/04/25 01:45:30 Creating deployments with name kubia-website in namespace default 2022/04/25 01:45:30 response Status: 201 Created $ kubectl get deploy,svc,po NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/kubia-website 0/1 1 0 4s deployment.apps/website-controller 1/1 1 1 19s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/kubernetes ClusterIP 10.96.0.1 \u0026lt;none\u0026gt; 443/TCP 88d service/kubia-website NodePort 10.104.14.151 \u0026lt;none\u0026gt; 80:32522/TCP 4s NAME READY STATUS RESTARTS AGE pod/kubia-website-7d94854d9f-jxzbh 0/2 ContainerCreating 0 4s pod/website-controller-5dd4555d87-rwsz7 2/2 Running 1 (18s ago) 19s $ minikube service list |---------------|------------------------------------|--------------|---------------------------| | NAMESPACE | NAME | TARGET PORT | URL | |---------------|------------------------------------|--------------|---------------------------| | default | kubernetes | No node port | | default | kubia-service | 80 | http://192.168.49.2:32522 | | ingress-nginx | ingress-nginx-controller | http/80 | http://192.168.49.2:31300 | | | | https/443 | http://192.168.49.2:30830 | | ingress-nginx | ingress-nginx-controller-admission | No node port | | kube-system | kube-dns | No node port | |---------------|------------------------------------|--------------|---------------------------| $ curl http://192.168.49.2:32522 \u0026lt;html\u0026gt; \u0026lt;body\u0026gt; Hello there. \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; If you want to delete this website:\n$ kubectl delete -f imaginary-website.yaml website.extensions.example.com \u0026#34;kubia\u0026#34; deleted $ kubectl get deploy,svc,po NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/website-controller 1/1 1 1 4m6s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/kubernetes ClusterIP 10.96.0.1 \u0026lt;none\u0026gt; 443/TCP 88d NAME READY STATUS RESTARTS AGE pod/website-controller-5dd4555d87-rwsz7 2/2 Running 1 (4m5s ago) 4m6s $ kubectl logs -f website-controller-5dd4555d87-rwsz7 -c main 2022/04/25 01:45:17 website-controller started. 2022/04/25 01:45:30 Received watch event: ADDED: kubia: https://github.com/hoangph3/kubia-website-example.git 2022/04/25 01:45:30 Creating services with name kubia-website in namespace default 2022/04/25 01:45:30 response Status: 201 Created 2022/04/25 01:45:30 Creating deployments with name kubia-website in namespace default 2022/04/25 01:45:30 response Status: 201 Created 2022/04/25 01:46:01 Received watch event: DELETED: kubia: https://github.com/hoangph3/kubia-website-example.git 2022/04/25 01:46:01 Deleting services with name kubia-website in namespace default 2022/04/25 01:46:01 response Status: 200 OK 2022/04/25 01:46:01 Deleting deployments with name kubia-website in namespace default 2022/04/25 01:46:01 response Status: 200 OK GPU Scheduling Note: Support 1 local machine with nvidia-gpu, and minikube on it Step 1: setup gpu (secureBoot must be off)\ninstall nvidia-driver (nvidia-driver-515), cuda (11.7), cudnn following the instructions: https://developer.nvidia.com/cuda-downloads Step 2: install minikube (v1.25.2)\n$ curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube_latest_amd64.deb $ sudo dpkg -i minikube_latest_amd64.deb $ minikube version minikube version: v1.25.2 commit: 362d5fdc0a3dbee389b3d3f1034e8023e72bd3a7 Step 3: install nvidia-docker\n$ wget http://mirror.cs.uchicago.edu/nvidia-docker/libnvidia-container/stable/ubuntu18.04/amd64/libnvidia-container1_1.9.0-1_amd64.deb (libnvidia-container1) $ wget http://mirror.cs.uchicago.edu/nvidia-docker/libnvidia-container/stable/ubuntu18.04/amd64/libnvidia-container-tools_1.9.0-1_amd64.deb (libnvidia-container-tools) $ sudo dpkg -i libnvidia-container1_1.9.0-1_amd64.deb libnvidia-container-tools_1.9.0-1_amd64.deb $ wget http://mirror.cs.uchicago.edu/nvidia-docker/libnvidia-container/stable/ubuntu18.04/amd64/nvidia-container-runtime_3.9.0-1_all.deb (nvidia-container-runtime) $ wget http://mirror.cs.uchicago.edu/nvidia-docker/libnvidia-container/stable/ubuntu18.04/amd64/nvidia-container-toolkit_1.9.0-1_amd64.deb (nvidia-container-toolkit) $ wget http://mirror.cs.uchicago.edu/nvidia-docker/libnvidia-container/stable/ubuntu18.04/amd64/nvidia-docker2_2.10.0-1_all.deb (nvidia-docker2) $ sudo dpkg -i nvidia-container-runtime_3.9.0-1_all.deb nvidia-container-toolkit_1.9.0-1_amd64.deb nvidia-docker2_2.10.0-1_all.deb Step 4: config daemon docker in /etc/docker/daemon.json\n{ \u0026#34;runtimes\u0026#34;: { \u0026#34;nvidia\u0026#34;: { \u0026#34;path\u0026#34;: \u0026#34;nvidia-container-runtime\u0026#34;, \u0026#34;runtimeArgs\u0026#34;: [] } }, \u0026#34;default-runtime\u0026#34;: \u0026#34;nvidia\u0026#34; } Step 5: download kube-system images from https://hub.docker.com/ (because the proxy server has no connection to https://k8s.gcr.io/)\nList the images of kubernetes cluster:\n$ kubeadm config images list W0618 11:26:15.229688 15772 version.go:103] could not fetch a Kubernetes version from the internet: unable to get URL \u0026#34;https://dl.k8s.io/release/stable-1.txt\u0026#34;: Get \u0026#34;https://dl.k8s.io/release/stable-1.txt\u0026#34;: x509: certificate signed by unknown authority W0618 11:26:15.229716 15772 version.go:104] falling back to the local client version: v1.24.2 k8s.gcr.io/kube-apiserver:v1.24.2 k8s.gcr.io/kube-controller-manager:v1.24.2 k8s.gcr.io/kube-scheduler:v1.24.2 k8s.gcr.io/kube-proxy:v1.24.2 k8s.gcr.io/pause:3.7 k8s.gcr.io/etcd:3.5.3-0 k8s.gcr.io/coredns/coredns:v1.8.6 Now we will pull images (stable version v1.23.3), also we need to pull the storage-provider image:\n# api-server $ docker pull k8simage/kube-apiserver:v1.23.3 # controller-manager $ docker pull k8simage/kube-controller-manager:v1.23.3 # scheduler $ docker pull k8simage/kube-scheduler:v1.23.3 # proxy $ docker pull k8simage/kube-proxy:v1.23.3 # pause $ docker pull k8simage/pause:3.6 # etcd $ docker pull k8simage/etcd:3.5.1-0 # coredns $ docker pull k8simage/coredns:v1.8.6 # storage-provisioner $ docker pull yedward/gcr.io.k8s-minikube.storage-provisioner:v1.8.1 Then re-tag image to ensure that the image’s name same as the image name will pulled when run minikube start:\n# api-server $ docker tag k8simage/kube-apiserver:v1.23.3 k8s.gcr.io/kube-apiserver:v1.23.3 # controller-manager $ docker tag k8simage/kube-controller-manager:v1.23.3 k8s.gcr.io/kube-controller-manager:v1.23.3 # scheduler $ docker tag k8simage/kube-scheduler:v1.23.3 k8s.gcr.io/kube-scheduler:v1.23.3 # proxy $ docker tag k8simage/kube-proxy:v1.23.3 k8s.gcr.io/kube-proxy:v1.23.3 # pause $ docker tag k8simage/pause:3.6 k8s.gcr.io/pause:3.6 # etcd $ docker tag k8simage/etcd:3.5.1-0 k8s.gcr.io/etcd:3.5.1-0 # coredns $ docker tag k8simage/coredns:v1.8.6 k8s.gcr.io/coredns/coredns:v1.8.6 # storage-provisioner $ docker tag yedward/gcr.io.k8s-minikube.storage-provisioner:v1.8.1 gcr.io/k8s-minikube/storage-provisioner:v5 Step 6: start minikube cluster\nInitialize the cluster as root privileges:\n$ sudo -i $ minikube start --driver=none --apiserver-ips 127.0.0.1 --apiserver-name localhost Change the permission to $USER:\n$ sudo mv /root/.kube /root/.minikube $HOME $ sudo chown -R $USER $HOME/.kube $HOME/.minikube Edit kubernetes config file, change certificate-authority, client-certificate, and client-key from root path to $USER path:\napiVersion: v1 clusters: - cluster: certificate-authority: /home/hoang/.minikube/ca.crt extensions: - extension: last-update: Fri, 17 Jun 2022 17:10:56 +07 provider: minikube.sigs.k8s.io version: v1.25.2 name: cluster_info server: https://localhost:8443 name: minikube contexts: - context: cluster: minikube extensions: - extension: last-update: Fri, 17 Jun 2022 17:10:56 +07 provider: minikube.sigs.k8s.io version: v1.25.2 name: context_info namespace: default user: minikube name: minikube current-context: minikube kind: Config preferences: {} users: - name: minikube user: client-certificate: /home/hoang/.minikube/profiles/minikube/client.crt client-key: /home/hoang/.minikube/profiles/minikube/client.key Step 7: install NVIDIA’s device plugin\nCreate the manifest yaml file nvidia-device-plugin.yml:\n# Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the \u0026#34;License\u0026#34;); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an \u0026#34;AS IS\u0026#34; BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. apiVersion: apps/v1 kind: DaemonSet metadata: name: nvidia-device-plugin-daemonset namespace: kube-system spec: selector: matchLabels: name: nvidia-device-plugin-ds updateStrategy: type: RollingUpdate template: metadata: labels: name: nvidia-device-plugin-ds spec: tolerations: - key: nvidia.com/gpu operator: Exists effect: NoSchedule # Mark this pod as a critical add-on; when enabled, the critical add-on # scheduler reserves resources for critical add-on pods so that they can # be rescheduled after a failure. # See https://kubernetes.io/docs/tasks/administer-cluster/guaranteed-scheduling-critical-addon-pods/ priorityClassName: \u0026#34;system-node-critical\u0026#34; containers: - image: nvidia/k8s-device-plugin name: nvidia-device-plugin-ctr env: - name: FAIL_ON_INIT_ERROR value: \u0026#34;false\u0026#34; securityContext: allowPrivilegeEscalation: false capabilities: drop: [\u0026#34;ALL\u0026#34;] volumeMounts: - name: device-plugin mountPath: /var/lib/kubelet/device-plugins volumes: - name: device-plugin hostPath: path: /var/lib/kubelet/device-plugins Create the daemonset:\n$ kubectl create -f nvidia-device-plugin.yml Step 8: testing\nCreate the manifest yaml file gpu-demo.yaml:\napiVersion: apps/v1 kind: Deployment metadata: name: gpu-demo spec: selector: matchLabels: app: gpu replicas: 2 template: metadata: labels: app: gpu spec: containers: - name: gpu-demo image: nvidia/cuda:10.1-cudnn7-runtime-ubuntu18.04 command: [\u0026#34;/bin/sh\u0026#34;, \u0026#34;-c\u0026#34;] args: [\u0026#34;nvidia-smi \u0026amp;\u0026amp; tail -f /dev/null\u0026#34;] ports: - containerPort: 80 Create the deployment:\n$ kubectl apply -f gpu-demo.yaml deployment.apps/gpu-demo created Tracking pods:\n$ kubectl get pods NAME READY STATUS RESTARTS AGE gpu-demo-78b68dbfb6-wdxns 1/1 Running 0 58s gpu-demo-78b68dbfb6-wkk25 1/1 Running 0 58s $ kubectl logs -f gpu-demo-78b68dbfb6-wdxns Fri Jun 17 11:45:07 2022 +-----------------------------------------------------------------------------+ | NVIDIA-SMI 515.43.04 Driver Version: 515.43.04 CUDA Version: 11.7 | |-------------------------------+----------------------+----------------------+ | GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |===============================+======================+======================| | 0 NVIDIA GeForce ... On | 00000000:01:00.0 On | N/A | | 30% 38C P8 35W / 350W | 544MiB / 24576MiB | 12% Default | | | | N/A | +-------------------------------+----------------------+----------------------+ +-----------------------------------------------------------------------------+ | Processes: | | GPU GI CI PID Type Process name GPU Memory | | ID ID Usage | |=============================================================================| +-----------------------------------------------------------------------------+ You can get full source code here: custom-resources, gpus.\n","permalink":"https://hoangph3.github.io/posts/kubernetes_custom_resources_gpus/","summary":"Extend the Kubernetes API with Custom Resource Definitions and schedule GPU-backed workloads.","title":"Kubernetes Custom Resources and GPU Scheduling"},{"content":"Kubernetes Services External access by configure Ingress Firstly, install nginx ingress:\nminikube addons enable ingress kubectl get all -n ingress-nginx NAME READY STATUS RESTARTS AGE pod/ingress-nginx-admission-create--1-6rs7v 0/1 Completed 0 14m pod/ingress-nginx-admission-patch--1-qf2zd 0/1 Completed 1 14m pod/ingress-nginx-controller-5f66978484-wtpph 1/1 Running 0 14m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/ingress-nginx-controller NodePort 10.104.234.97 \u0026lt;none\u0026gt; 80:31300/TCP,443:30830/TCP 14m service/ingress-nginx-controller-admission ClusterIP 10.109.145.221 \u0026lt;none\u0026gt; 443/TCP 14m NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/ingress-nginx-controller 1/1 1 1 14m NAME DESIRED CURRENT READY AGE replicaset.apps/ingress-nginx-controller-5f66978484 1 1 1 14m NAME COMPLETIONS DURATION AGE job.batch/ingress-nginx-admission-create 1/1 4s 14m job.batch/ingress-nginx-admission-patch 1/1 5s 14m Create kubia application:\napiVersion: apps/v1 kind: Deployment metadata: name: kubia-deployment labels: app: kubia spec: replicas: 1 selector: matchLabels: app: kubia template: metadata: labels: app: kubia spec: containers: - name: kubia image: luksa/kubia:latest ports: - containerPort: 8080 protocol: TCP --- apiVersion: v1 kind: Service metadata: name: kubia-internal-service spec: # type default is ClusterIP selector: app: kubia ports: - protocol: TCP port: 8080 targetPort: 8080 kubectl apply -f kubia.yaml Create ingress for kubia-internal-service:\napiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: kubia-ingress spec: rules: - host: kubia.example.com http: paths: - pathType: Prefix path: / backend: service: name: kubia-internal-service port: number: 8080 kubectl apply -f ingress.yaml Now we can list of ingress:\nkubectl get ingress NAME CLASS HOSTS ADDRESS PORTS AGE kubia-ingress nginx kubia.example.com localhost 80 94s The ADDRESS is localhost, this is the url that kubernetes control plane is running. In this case is minikube ip: 192.168.49.2.\nEdit /etc/hosts file:\n127.0.0.1 localhost 127.0.1.1 jump-windows # The following lines are desirable for IPv6 capable hosts ::1 localhost ip6-localhost ip6-loopback ff02::1 ip6-allnodes ff02::2 ip6-allrouters # k8s 192.168.49.2 kubia.example.com Go to your browser or use curl, we can access service through ingress:\ncurl kubia.example.com You\u0026#39;ve hit kubia-deployment-7b895464d7-h8448 Configure TLS for Ingress Firstly, we create the RSA private key:\nopenssl genrsa -out tls.key 2048 Next we will use the RSA private key to generate the certificate:\nopenssl req -new -x509 -key tls.key -days 360 -subj \u0026#34;/CN=kubia\u0026#34; -out tls.crt Perform base64 encode for tls.key and tls.crt, then fill to yaml file:\ncat tls.crt | base64 | tr -d \u0026#34;\\n\u0026#34; cat tls.key | base64 | tr -d \u0026#34;\\n\u0026#34; Create tls-secret.yaml save secret TLS:\napiVersion: v1 kind: Secret metadata: name: secret-tls type: kubernetes.io/tls data: # the data is abbreviated in this example tls.crt: | LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURBVENDQWVtZ0F3SUJBZ0lVUWw2eitJOUY4MmgwQThjbzUxVmJ5K2Vjay9nd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0VERU9NQXdHQTFVRUF3d0ZhM1ZpYVdFd0hoY05Nakl3TkRFd01UWXlOek0wV2hjTk1qTXdOREExTVRZeQpOek0wV2pBUU1RNHdEQVlEVlFRRERBVnJkV0pwWVRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDCkFRb0NnZ0VCQU1DNURMa3JXMHhJWkVEUFE4N1RYdlMxU3M3UkJKWk9wZGlTcnlaSTFHb0s4cndZRTZQZ2pwM1MKaUNlQ3FHbWRWNGV1aGhmbjk5WGlEQVFYSjRlS1R5TDlndTdvV2M5ZFg4d25KR0tXSjVnRE8zTzFnUWNjcGRhSwppSWRyUTlyMXBPTkIrSjZnR0t6NUtucXpZK2wvL0NSSDU2M0FuZ1h2TWlvODVWTWNOd3ZOV09nVjBpNlRjSk1kCmE4aWk2YXhLQWcwMjlaWjFRRFFPTVhYR3ZNeFFzRkhUM045ZUthbDFKYlZRL2ZFUy9hOHNNbXFYN01ncE0wdkUKV2xLSzF1TjlzeWYyamtKQzlhd0Fac3hveDQxbjd0WENPdTJ5alVqTlQvWGNyU0hPbUtScXRqL215T096N1Myawp0VE5QckVBVWk1U3FNbjBTbG43UXVjVEdtUVIrdDNrQ0F3RUFBYU5UTUZFd0hRWURWUjBPQkJZRUZMZTYvbERqCmZSNk8yOXptTHlLc3hZQmxmb0FMTUI4R0ExVWRJd1FZTUJhQUZMZTYvbERqZlI2TzI5em1MeUtzeFlCbGZvQUwKTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQUgyTDh6S0dLUmZrdkMxNgplbW9ESE1jT0MxOEpiWmIzNG5yRDB0MWlleHp4MHJLNTFVRU5TZlJYK2E4Y3Vma2xFREROc0hUNnRHV2xEYmFECkhPeXRUZDNKcXg5bFJNMG1LK1pXZk1ITGNXQVF2bXZWcUNGa3NDcUhHc3ZTb1Q3WVY0OHBqcytpL1VsZnJPcE8KcmlNYk5tbTdHNkdqLzA4VkluTDdIVUZaQnNLdEovNkJ5WXJrR01LUHoyNjB5aHQ2TDJxaUx1NlRIbUN3eGV1YwpNd1J3bWN1ajlzQmFtL3JQOWJoTHJYcThuSUxCSUlEZmlIWE5zYXBnL1I0WGVlcDZaaG8yUDJlSmlHRE1udHRDCkVyVUtnT29IOWdHZmlrR2FiWHNQK0c2dkFFZk1FQXR3L09wVmZHTHNSZnZ1U1RQT0JTVXdkaThvcldUTDFvOVAKMVFjSVRVTT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= tls.key: | LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBd0xrTXVTdGJURWhrUU05RHp0TmU5TFZLenRFRWxrNmwySkt2SmtqVWFncnl2QmdUCm8rQ09uZEtJSjRLb2FaMVhoNjZHRitmMzFlSU1CQmNuaDRwUEl2MkM3dWhaejExZnpDY2tZcFlubUFNN2M3V0IKQnh5bDFvcUloMnREMnZXazQwSDRucUFZclBrcWVyTmo2WC84SkVmbnJjQ2VCZTh5S2p6bFV4dzNDODFZNkJYUwpMcE53a3gxcnlLTHByRW9DRFRiMWxuVkFOQTR4ZGNhOHpGQ3dVZFBjMzE0cHFYVWx0VkQ5OFJMOXJ5d3lhcGZzCnlDa3pTOFJhVW9yVzQzMnpKL2FPUWtMMXJBQm16R2pIaldmdTFjSTY3YktOU00xUDlkeXRJYzZZcEdxMlArYkkKNDdQdExhUzFNMCtzUUJTTGxLb3lmUktXZnRDNXhNYVpCSDYzZVFJREFRQUJBb0lCQURzdkdPY3NsMmIvdkRuaQo3TEh4VzNITzB1QmNkQW9zc09XbmRqNU5rMTNWYXVHMGl5T0NiSW12QTcwT2RPV3FPaDBpelc4OS8zQWhjUXM0CmlSMG9ybERTaFlrVXRhL212dXFWQXFsNzcwRFJqVXBsYlBCZ0xkV0t5WTY4dENQajEvVXFaMDFmWVBTTnVDdmkKTjBhWDFUalhGQ0RaekMyS1hWOTNQLzJiNXBPcXZMRHRiWDhDRkYrcmloYnhGOEk5ZkhmdXBTYzFwQXNYWkNBQQoxRnNXZG83Nmk2YVBuL3ZuRU9kRG1KK09ZN3p5K01hM3E3alF3ajhFMmRkUFdDQkp1UE9yNzEzRGt1TXJUc3RTCjFMRVptWlIzckFJcytxbDI4U0ZVbkdiWHZHZjhSNDBkWnhsNkpzR09hS3d1VjNwck9zN2hUbW9leUhSYlk1V0cKNkV5TXp6a0NnWUVBKzFlSjdoTVR6eFhDVTAxTU5sMjFmYjU1cEpaNm55ZmFPMWI1bUhUR295dndrWEN0NFRUcwovb1plN0tzVUtJZFVmWDZ1OCs0Nms0VzNkZjBwVDZWR2ZHODdwWlBNVVJaMk53SmUzc3FMMnBBNHRzZXQ2N3Q0ClVlaGswdlJYRXRQQXV3SVBlbXhHMXI0Q0FMNFZsMFJOK29UQ1B4YmJrRlRVZ1FsdzUrS29TWU1DZ1lFQXhFdG0KVml4eVluUUZROW0wZk4vWndUZTZVMzZ2S0s3WXF3NHRVelAyejRwQ1RkWEhsREgrWEhtMDR1aFd3dVg0V1hHMwpvNHV5NTFBZmpjS1VJcFVyckgxa3RMWkM4T3RZYzlVY0dUTUZEYzhmNzI0R2dNRXU1c3dYWTJHS1RvbkJOcm8wCktLYmhlTEFpeW9HMGM0OEppS3BtYTYrRVdvSDFNM1EvOS9hWjlsTUNnWUVBMURCeklhcTVibnJRTThOdU0vZW8KNFIrTlVvWTN2MlhGdDVNVjVMK3hjdEFGcU1PWUNDakdhNXJGU01pbG5CR2tJczV3cFQ3WjlQRk9rUzNKVXBRVgpqYmZhZzA3amp4R0hlNmxrcm5JUTM5UWlEUzFHaDEwZGx3aTdGZDF5SlZMZnd3RmFUK0JaYmJHN3Z5UzYxWm0wCnUycVpFdW9aTXlCcXh3VlJiSExONEVFQ2dZRUFuVmhuTnNvNEFrMUg3eVJ5ZGVxbHpTalRsWndsNGJHT0FrZkIKODBEakpXZUpVSVQ5andBb0NZNlJmWldKL242Qy9ZZVhFV1NveXB4Q1BzcnJIWEYvYWF1MTd0bHVmVm5aTkRodQpacENzQzI2dEJhcW5VY3dJd1g1MWZQY3grMVNXNlR5SEZOTDRSMXJBK0p6UnZoTzVLN0NUbXR3OWRxTlhucUFmCnFxOGtxUHNDZ1lFQWcxQng5TksyVVVkZTgyaDZmZmFlWXROVDdXa1RXM2dvandFV2p3WXhzZVBVQ0Q1MWloVUEKYVQ0QVhvaWxsRys1Wnd3Q3Z4V0NYNE1kWTl5SDlVMng5SGhjS1QreCszZnp3NjNPN0g1ampjcHRXYVJhdlZadApsdUlEM1crVnpaL3ZvYnV2WUdScVc5NUhLbThRN2FuU283SzB2TkswUXZzYXJySjZ4c01rcW1FPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= kubectl apply -f tls-secret.yaml Now create Ingress with TLS:\napiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: tls-kubia-ingress spec: tls: - hosts: - kubia.example.com secretName: secret-tls rules: - host: kubia.example.com http: paths: - path: / pathType: Prefix backend: service: name: kubia-internal-service port: number: 8080 kubectl apply -f tls-ingress.yaml kubectl get ingress NAME CLASS HOSTS ADDRESS PORTS AGE tls-kubia-ingress nginx kubia.example.com localhost 80, 443 17s We can see the PORTS is 80, 443. Let’s use HTTPS to access your service through the Ingress:\ncurl -k -v https://kubia.example.com/kubia * Trying 192.168.49.2:443... * Connected to kubia.example.com (192.168.49.2) port 443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 * ALPN, server accepted to use h2 * Server certificate: * subject: O=Acme Co; CN=Kubernetes Ingress Controller Fake Certificate * start date: Apr 10 14:38:48 2022 GMT * expire date: Apr 10 14:38:48 2023 GMT * issuer: O=Acme Co; CN=Kubernetes Ingress Controller Fake Certificate * SSL certificate verify result: self signed certificate (18), continuing anyway. * Using HTTP2, server supports multiplexing * Connection state changed (HTTP/2 confirmed) * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0 * Using Stream ID: 1 (easy handle 0x560b8745fb10) \u0026gt; GET /kubia HTTP/2 \u0026gt; Host: kubia.example.com \u0026gt; user-agent: curl/7.81.0 \u0026gt; accept: */* \u0026gt; * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): * old SSL session ID is stale, removing * Connection state changed (MAX_CONCURRENT_STREAMS == 128)! \u0026lt; HTTP/2 200 \u0026lt; date: Sun, 10 Apr 2022 16:51:41 GMT \u0026lt; You\u0026#39;ve hit kubia-deployment-7b895464d7-h8448 * Connection #0 to host kubia.example.com left intact Configure headless service apiVersion: v1 kind: Service metadata: name: kubia-headless spec: selector: app: kubia clusterIP: None ports: - port: 80 targetPort: 8080 A headless service is a service with a service IP but instead of load-balancing it will return the IPs of our associated Pods. This allows us to interact directly with the Pods instead of a proxy.\nLet’s list of service:\nkubectl get service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 \u0026lt;none\u0026gt; 443/TCP 73d kubia-headless ClusterIP None \u0026lt;none\u0026gt; 80/TCP 33m kubia-internal-service ClusterIP 10.96.220.32 \u0026lt;none\u0026gt; 8080/TCP 3h1m Because headless service is connected to Pod’s IPs without proxy. If we exec to the pod, we can interact with headless service:\nkubectl get pods NAME READY STATUS RESTARTS AGE kubia-deployment-7b895464d7-h8448 1/1 Running 0 3h5m kubectl exec -it kubia-deployment-7b895464d7-h8448 -- bash root@kubia-deployment-7b895464d7-h8448:/# curl kubia-headless:8080 You\u0026#39;ve hit kubia-deployment-7b895464d7-h8448 root@kubia-deployment-7b895464d7-h8448:/# curl kubia-internal-service:8080 Istio Service Mesh Configure Istio for Kubernetes cluster Firstly, starting your minikube cluster:\n$ minikube start --memory=8192 --cpus=4 ... Done! kubectl is now configured to use \u0026#34;minikube\u0026#34; cluster and \u0026#34;default\u0026#34; namespace by default To build service mesh for kubernetes cluster, we can use istio, let’s download istio from https://github.com/istio/istio/releases/ and extracting:\n$ wget https://github.com/istio/istio/releases/download/1.12.7/istio-1.12.7-linux-amd64.tar.gz $ tar zvxf istio-1.12.7-linux-amd64.tar.gz Checking the binary istio:\n$ cd istio-1.12.7 \u0026amp;\u0026amp; ls bin LICENSE manifests manifest.yaml README.md samples tools $ export PATH=$PWD/bin:$PATH $ istioctl Istio configuration command line utility for service operators to debug and diagnose their Istio mesh. Usage: istioctl [command] Available Commands: ... Flags: ... Additional help topics: istioctl options Displays istioctl global options Use \u0026#34;istioctl [command] --help\u0026#34; for more information about a command. If you have not installed istio:\n$ kubectl get ns NAME STATUS AGE default Active 8m47s kube-node-lease Active 8m50s kube-public Active 8m50s kube-system Active 8m50s $ kubectl get pods No resources found in default namespace. Let’s install istio service mesh:\n$ istioctl install This will install the Istio 1.12.7 default profile with [\u0026#34;Istio core\u0026#34; \u0026#34;Istiod\u0026#34; \u0026#34;Ingress gateways\u0026#34;] components into the cluster. Proceed? (y/N) y ✔ Istio core installed ✔ Istiod installed- Processing resources for Ingress gateways. Waiting for Deployment/istio-system/istio-ingressgateway ✔ Ingress gateways installed ✔ Installation complete Making this installation the default for injection and validation. Now listing pods and namespace in kubernetes cluster:\n$ kubectl get ns NAME STATUS AGE default Active 10m istio-system Active 62s kube-node-lease Active 10m kube-public Active 10m kube-system Active 10m $ kubectl get pods -n istio-system NAME READY STATUS RESTARTS AGE istio-ingressgateway-5744ff657c-cb6z2 1/1 Running 0 42s istiod-76f7bb65df-tcfvj 1/1 Running 0 66s $ kubectl get svc -A NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE default kubernetes ClusterIP 10.96.0.1 \u0026lt;none\u0026gt; 443/TCP 10m istio-system istio-ingressgateway LoadBalancer 10.111.182.40 \u0026lt;pending\u0026gt; 15021:31549/TCP,80:30496/TCP,443:32456/TCP 61s istio-system istiod ClusterIP 10.103.110.234 \u0026lt;none\u0026gt; 15010/TCP,15012/TCP,443/TCP,15014/TCP 86s kube-system kube-dns ClusterIP 10.96.0.10 \u0026lt;none\u0026gt; 53/UDP,53/TCP,9153/TCP 10m We can get a list of the ports available with the istio-ingressgateway service using:\n$ kubectl get svc istio-ingressgateway -n istio-system NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE istio-ingressgateway LoadBalancer 10.111.182.40 \u0026lt;pending\u0026gt; 15021:31549/TCP,80:30496/TCP,443:32456/TCP 10m $ kubectl describe svc istio-ingressgateway -n istio-system Name: istio-ingressgateway Namespace: istio-system Labels: app=istio-ingressgateway install.operator.istio.io/owning-resource=unknown install.operator.istio.io/owning-resource-namespace=istio-system istio=ingressgateway istio.io/rev=default operator.istio.io/component=IngressGateways operator.istio.io/managed=Reconcile operator.istio.io/version=1.12.7 release=istio Annotations: \u0026lt;none\u0026gt; Selector: app=istio-ingressgateway,istio=ingressgateway Type: LoadBalancer IP Family Policy: SingleStack IP Families: IPv4 IP: 10.111.182.40 IPs: 10.111.182.40 Port: status-port 15021/TCP TargetPort: 15021/TCP NodePort: status-port 31549/TCP Endpoints: 172.17.0.4:15021 Port: http2 80/TCP TargetPort: 8080/TCP NodePort: http2 30496/TCP Endpoints: 172.17.0.4:8080 Port: https 443/TCP TargetPort: 8443/TCP NodePort: https 32456/TCP Endpoints: 172.17.0.4:8443 Session Affinity: None External Traffic Policy: Cluster Events: \u0026lt;none\u0026gt; The output here shows that the istio-ingressgateway service is forwarding requests from port 80 to port 30496 (http2).\nThe load balancer listener is set to listen on HTTP port 80, which is the port for the NGINX web server application used in the virtual service in this example.\nStep 1: Create the namespace my-namespace and enable automatic proxy sidecar injection.\n$ kubectl create ns my-namespace namespace/my-namespace created $ kubectl label ns my-namespace istio-injection=enabled namespace/my-namespace labeled $ kubectl get ns --show-labels NAME STATUS AGE LABELS default Active 60m kubernetes.io/metadata.name=default istio-system Active 51m kubernetes.io/metadata.name=istio-system kube-node-lease Active 60m kubernetes.io/metadata.name=kube-node-lease kube-public Active 60m kubernetes.io/metadata.name=kube-public kube-system Active 60m kubernetes.io/metadata.name=kube-system my-namespace Active 30m istio-injection=enabled,kubernetes.io/metadata.name=my-namespace Step 2: Create the NGINX deployment and NGINX service by create the manifest file nginx.yaml.\napiVersion: apps/v1 kind: Deployment metadata: labels: app: webserver name: my-nginx namespace: my-namespace spec: replicas: 3 selector: matchLabels: app: webserver template: metadata: labels: app: webserver spec: containers: - image: nginx name: my-nginx ports: - containerPort: 80 # matched targetPort --- apiVersion: v1 kind: Service metadata: labels: app: my-nginx name: webserver namespace: my-namespace spec: ports: - name: http port: 80 protocol: TCP targetPort: 80 # matched containerPort selector: app: webserver type: ClusterIP $ kubectl apply -f nginx.yaml deployment.apps/my-nginx created service/webserver created $ kubectl get deploy,svc,po -n my-namespace NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/my-nginx 3/3 3 3 19s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/webserver ClusterIP 10.96.221.204 \u0026lt;none\u0026gt; 80/TCP 19s NAME READY STATUS RESTARTS AGE pod/my-nginx-856fb4777f-7s67q 2/2 Running 0 19s pod/my-nginx-856fb4777f-8bhj2 2/2 Running 0 19s pod/my-nginx-856fb4777f-d77fs 2/2 Running 0 19s We can see 2/2 in READY column, means that the pods have deployed with sidecar proxy. To validate this, we describe one of the pods:\n$ kubectl describe pods my-nginx-856fb4777f-7s67q -n my-namespace Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 46s default-scheduler Successfully assigned my-namespace/my-nginx-856fb4777f-7s67q to minikube Normal Pulled 45s kubelet Container image \u0026#34;docker.io/istio/proxyv2:1.12.7\u0026#34; already present on machine Normal Created 45s kubelet Created container istio-init Normal Started 45s kubelet Started container istio-init Normal Pulling 45s kubelet Pulling image \u0026#34;nginx\u0026#34; Normal Pulled 41s kubelet Successfully pulled image \u0026#34;nginx\u0026#34; in 3.441922626s Normal Created 41s kubelet Created container my-nginx Normal Started 41s kubelet Started container my-nginx Normal Pulled 41s kubelet Container image \u0026#34;docker.io/istio/proxyv2:1.12.7\u0026#34; already present on machine Normal Created 41s kubelet Created container istio-proxy Normal Started 41s kubelet Started container istio-proxy Warning Unhealthy 38s (x3 over 40s) kubelet Readiness probe failed: Get \u0026#34;http://172.17.0.5:15021/healthz/ready\u0026#34;: dial tcp 172.17.0.5:15021: connect: connection refused Because my environment does not provide an external load balancer for the ingress gateway, the connection refused to 172.17.0.5:15021. But don’t worry about it, we can access the gateway using the service’s node port (30496).\nStep 3: Create an ingress gateway for the NGINX service by create the manifest file nginx-gateway.yaml.\napiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: my-nginx-gateway namespace: my-namespace spec: selector: istio: ingressgateway # get from labels when describe istio-ingressgateway service servers: - port: number: 80 name: http protocol: HTTP hosts: - \u0026#34;mynginx.example.com\u0026#34; $ kubectl apply -f nginx-gateway.yaml gateway.networking.istio.io/my-nginx-gateway created $ kubectl get gateways.networking.istio.io -n my-namespace NAME AGE my-nginx-gateway 14m Step 4: Create a virtual service for the ingress gateway by create the manifest file nginx-virtualservice.yaml.\napiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: my-nginx-virtualservice namespace: my-namespace spec: hosts: - \u0026#34;mynginx.example.com\u0026#34; gateways: - my-nginx-gateway http: - match: - uri: prefix: / route: - destination: port: number: 80 host: webserver # matched .metadata.name in Service $ kubectl apply -f nginx-virtualservice.yaml virtualservice.networking.istio.io/my-nginx-virtualservice created $ kubectl get virtualservices.networking.istio.io -n my-namespace NAME GATEWAYS HOSTS AGE my-nginx-virtualservice [\u0026#34;my-nginx-gateway\u0026#34;] [\u0026#34;mynginx.example.com\u0026#34;] 17s To confirm the ingress gateway is serving the application to the load balancer, use:\n$ minikube ip 192.168.49.2 $ kubectl describe svc istio-ingressgateway -n istio-system | grep http2 Port: http2 80/TCP NodePort: http2 30496/TCP $ curl -I -HHost:mynginx.example.com 192.168.49.2:30496 HTTP/1.1 200 OK server: istio-envoy date: Sun, 22 May 2022 13:07:54 GMT content-type: text/html content-length: 615 last-modified: Tue, 25 Jan 2022 15:03:52 GMT etag: \u0026#34;61f01158-267\u0026#34; accept-ranges: bytes x-envoy-upstream-service-time: 17 To monitoring and data visualization, we can use kiali - an observability console for Istio with service mesh configuration and validation capabilities.\nIstio provides a basic sample installation to quickly get Kiali up and running:\n$ kubectl apply -f prometheus.yaml serviceaccount/prometheus created configmap/prometheus created clusterrole.rbac.authorization.k8s.io/prometheus created clusterrolebinding.rbac.authorization.k8s.io/prometheus created service/prometheus created deployment.apps/prometheus created $ kubectl apply -f kiali.yaml serviceaccount/kiali created configmap/kiali created clusterrole.rbac.authorization.k8s.io/kiali-viewer created clusterrole.rbac.authorization.k8s.io/kiali created clusterrolebinding.rbac.authorization.k8s.io/kiali created role.rbac.authorization.k8s.io/kiali-controlplane created rolebinding.rbac.authorization.k8s.io/kiali-controlplane created service/kiali created deployment.apps/kiali created $ kubectl get svc -n istio-system NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE istio-ingressgateway LoadBalancer 10.111.182.40 \u0026lt;pending\u0026gt; 15021:31549/TCP,80:30496/TCP,443:32456/TCP 4h34m istiod ClusterIP 10.103.110.234 \u0026lt;none\u0026gt; 15010/TCP,15012/TCP,443/TCP,15014/TCP 4h34m kiali ClusterIP 10.100.112.235 \u0026lt;none\u0026gt; 20001/TCP,9090/TCP 8s prometheus ClusterIP 10.110.80.171 \u0026lt;none\u0026gt; 9090/TCP 54s Instead expose kiali service, we can forward port from kiali service to localhost by command line:\n$ kubectl port-forward svc/kiali -n istio-system 20001 Forwarding from 127.0.0.1:20001 -\u0026gt; 20001 Forwarding from [::1]:20001 -\u0026gt; 20001 Then, access Kiali by visiting http://127.0.0.1:20001 in your preferred web browser.\nYou can get full source code here: service, istio-service-mesh.\n","permalink":"https://hoangph3.github.io/posts/kubernetes_networking_service_mesh/","summary":"Expose applications with Kubernetes Services and add traffic management with the Istio service mesh.","title":"Kubernetes Networking and Service Mesh"},{"content":"Pod Security Policies Configure the node’s network for a pod A pod with hostNetwork: true uses the node’s network interfaces instead of its own.\n# pod-host-network.yaml apiVersion: v1 kind: Pod metadata: name: pod-host-network spec: hostNetwork: true containers: - name: main image: busybox command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;] args: - echo \u0026#34;$(date) Hello Kubernetes !\u0026#34;; sleep 999999; kubectl apply -f pod-host-network.yaml $ kubectl exec -it pod-host-network -- sh / # ifconfig docker0 Link encap:Ethernet HWaddr 02:42:80:85:1A:48 inet addr:172.17.0.1 Bcast:172.17.255.255 Mask:255.255.0.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:14359 errors:0 dropped:0 overruns:0 frame:0 TX packets:15146 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:1433351 (1.3 MiB) TX bytes:5055820 (4.8 MiB) eth0 Link encap:Ethernet HWaddr 02:42:C0:A8:31:02 inet addr:192.168.49.2 Bcast:192.168.49.255 Mask:255.255.255.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:3718 errors:0 dropped:0 overruns:0 frame:0 TX packets:3598 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:1441107 (1.3 MiB) TX bytes:1062006 (1.0 MiB) lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 UP LOOPBACK RUNNING MTU:65536 Metric:1 RX packets:486808 errors:0 dropped:0 overruns:0 frame:0 TX packets:486808 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:96821077 (92.3 MiB) TX bytes:96821077 (92.3 MiB) veth0953ee8 Link encap:Ethernet HWaddr F2:DD:B9:2F:C6:7B UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:2 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:0 (0.0 B) TX bytes:84 (84.0 B) veth8447375 Link encap:Ethernet HWaddr 42:4F:33:37:87:C4 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:2 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:0 (0.0 B) TX bytes:84 (84.0 B) ... We can see the eth0 ip is the control plane ip (use: $ kubectl cluster-info).\nWithout hostNetwork: true:\n$ kubectl exec -it pod-host-network -- sh / # ifconfig eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:07 inet addr:172.17.0.7 Bcast:172.17.255.255 Mask:255.255.0.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 UP LOOPBACK RUNNING MTU:65536 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) Configure hostPort for a pod If a host port is used, only a single pod instance can be scheduled to a node, because the port is already bound.\napiVersion: v1 kind: Pod metadata: name: pod-host-port spec: containers: - name: main image: busybox command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;] args: - echo \u0026#34;$(date) Hello Kubernetes !\u0026#34;; sleep 999999; ports: - containerPort: 8080 hostPort: 9000 protocol: TCP kubectl apply -f pod-host-port.yaml Configure hostPID, hostIPC for a pod apiVersion: v1 kind: Pod metadata: name: pod-host-pid-ipc spec: hostPID: true hostIPC: true containers: - name: main image: busybox command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;] args: - echo \u0026#34;$(date) Hello Kubernetes !\u0026#34;; sleep 999999; kubectl apply -f pod-host-pid-ipc.yaml By setting the hostIPC: true, processes in the pod’s containers can also communicate with all the other processes running on the node, through Inter-Process Communication.\n$ kubectl exec -it pod-host-pid-ipc -- sh / # ps aux PID USER TIME COMMAND 1 root 0:01 {systemd} /sbin/init 96 root 0:00 /lib/systemd/systemd-journald 108 108 0:00 /usr/bin/dbus-daemon --system --address=systemd: --nofork - 114 root 0:05 /usr/bin/containerd 121 root 0:00 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups 213 root 2:45 /usr/bin/dockerd -H tcp://0.0.0.0:2376 -H unix:///var/run/d 805 root 5:53 /var/lib/minikube/binaries/v1.22.3/kubelet --bootstrap-kube 1439 root 0:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 90ed40 1440 root 0:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id ea5af7 1444 root 0:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 1604e3 1446 root 0:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 6d4476 1518 65535 0:00 /pause 1520 65535 0:00 /pause 1528 65535 0:00 /pause 1529 65535 0:00 /pause ... Running a container as a specific user Make the container run as user guest with chown: 405.\napiVersion: v1 kind: Pod metadata: name: pod-as-guest spec: containers: - name: main image: busybox command: [\u0026#34;/bin/sleep\u0026#34;, \u0026#34;999999\u0026#34;] securityContext: runAsUser: 405 kubectl apply -f pod-as-guest.yaml Now we will run the id command in this new pod:\n$ kubectl exec -it pod-as-guest -- sh / $ id uid=405 gid=0(root) / $ Preventing a container from running as root apiVersion: v1 kind: Pod metadata: name: pod-as-non-root spec: containers: - name: main image: busybox command: [\u0026#34;/bin/sleep\u0026#34;, \u0026#34;999999\u0026#34;] securityContext: runAsNonRoot: true kubectl apply -f pod-as-non-root.yaml If you deploy this pod, it gets scheduled, but is not allowed to run:\nkubectl get pods kubectl describe pods pod-as-non-root kubectl get pods NAME READY STATUS RESTARTS AGE pod-as-non-root 0/1 CreateContainerConfigError 0 4m24s Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 4m47s default-scheduler Successfully assigned default/pod-as-non-root to minikube Normal Pulled 3m18s kubelet Successfully pulled image \u0026#34;busybox\u0026#34; in 3.064278141s Warning Failed 3m1s (x8 over 4m43s) kubelet Error: container has runAsNonRoot and image will run as root (pod: \u0026#34;pod-as-non-root_default(fd120b40-998e-4421-b054-ed7284df9b00)\u0026#34;, container: main) Normal Pulled 3m1s kubelet Successfully pulled image \u0026#34;busybox\u0026#34; in 3.002184008s Normal Pulling 2m47s (x9 over 4m46s) kubelet Pulling image \u0026#34;busybox\u0026#34; Running pods in privileged mode apiVersion: v1 kind: Pod metadata: name: pod-privileged spec: containers: - name: main image: busybox command: [\u0026#34;/bin/sleep\u0026#34;, \u0026#34;999999\u0026#34;] securityContext: privileged: true kubectl apply -f pod-privileged.yaml We can list the device files your privileged pod. In fact, the privileged container sees all the host node’s devices. This means it can use any device freely.\n$ kubectl exec -it pod-privileged -- sh / # ls /dev autofs kmsg rtc0 tty13 tty30 tty48 tty8 vcs6 vcsu7 bsg kvm sda tty14 tty31 tty49 tty9 vcs7 vcsu8 btrfs-control loop-control sda1 tty15 tty32 tty5 ttyS0 vcs8 vfio bus mapper sda2 tty16 tty33 tty50 ttyS1 vcsa vga_arbiter core media0 sg0 tty17 tty34 tty51 ttyS2 vcsa1 vhci cpu mei0 shm tty18 tty35 tty52 ttyS3 vcsa2 vhost-net cpu_dma_latency mem snapshot tty19 tty36 tty53 uhid vcsa3 vhost-vsock cuse mqueue snd tty2 tty37 tty54 uinput vcsa4 video0 dri net stderr tty20 tty38 tty55 urandom vcsa5 video1 drm_dp_aux0 null stdin tty21 tty39 tty56 vboxdrv vcsa6 watchdog drm_dp_aux1 nvram stdout tty22 tty4 tty57 vboxdrvu vcsa7 watchdog0 drm_dp_aux2 port termination-log tty23 tty40 tty58 vboxnetctl vcsa8 watchdog1 fb0 ppp tpm0 tty24 tty41 tty59 vboxusb vcsu zero fd psaux tty tty25 tty42 tty6 vcs vcsu1 full ptmx tty0 tty26 tty43 tty60 vcs1 vcsu2 fuse ptp0 tty1 tty27 tty44 tty61 vcs2 vcsu3 hidraw0 pts tty10 tty28 tty45 tty62 vcs3 vcsu4 hpet random tty11 tty29 tty46 tty63 vcs4 vcsu5 input rfkill tty12 tty3 tty47 tty7 vcs5 vcsu6 Dropping capabilities from a container apiVersion: v1 kind: Pod metadata: name: pod-drop-chown-capability spec: containers: - name: main image: busybox command: [\u0026#34;/bin/sleep\u0026#34;, \u0026#34;999999\u0026#34;] securityContext: capabilities: drop: - CHOWN kubectl apply -f pod-drop-chown-capability.yaml By dropping the CHOWN capability, you’re not allowed to change the owner of the /tmp directory in this pod:\n$ kubectl exec -it pod-drop-chown-capability -- sh / # chown 1000:1000 /tmp/ chown: /tmp/: Operation not permitted Preventing processes from writing to the container’s filesystem apiVersion: v1 kind: Pod metadata: name: pod-with-readonly-filesystem spec: containers: - name: main image: busybox command: [\u0026#34;/bin/sleep\u0026#34;, \u0026#34;999999\u0026#34;] securityContext: readOnlyRootFilesystem: true volumeMounts: - name: my-volume mountPath: /volume readOnly: false volumes: - name: my-volume emptyDir: kubectl apply -f pod-with-readonly-filesystem.yaml When you deploy this pod, the container is running as root, which has write permissions to the / directory, but trying to write a file there fails:\n$ kubectl exec -it pod-with-readonly-filesystem -- sh / # touch a.txt touch: a.txt: Read-only file system Sharing volumes when containers run as different users apiVersion: v1 kind: Pod metadata: name: pod-with-shared-volume spec: containers: - name: first image: busybox command: [\u0026#34;/bin/sleep\u0026#34;, \u0026#34;999999\u0026#34;] securityContext: runAsUser: 1111 volumeMounts: - name: shared-volume mountPath: /volume readOnly: false - name: second image: busybox command: [\u0026#34;/bin/sleep\u0026#34;, \u0026#34;999999\u0026#34;] securityContext: runAsUser: 2222 volumeMounts: - name: shared-volume mountPath: /volume readOnly: false volumes: - name: shared-volume emptyDir: The first container runs as user ID 1111, the second container runs as user ID 2222, and both containers use the same volume.\nkubectl apply -f pod-with-shared-volume.yaml $ kubectl exec -it pod-with-shared-volume -c first -- sh / $ ls -la | grep volume drwxrwxrwx 2 root root 4096 Apr 20 16:37 volume / $ echo foo \u0026gt; volume/foo / $ ls -la volume/ total 12 drwxrwxrwx 2 root root 4096 Apr 20 16:37 . drwxr-xr-x 1 root root 4096 Apr 20 16:32 .. -rw-r--r-- 1 1111 root 4 Apr 20 16:39 foo As you can see, the group ID is always root, the container runs as user ID that we set in .spec.securityContext.runAsUser.\nWhat happen when we define the fsGroup and supplementalGroups in the security context at the pod level?\napiVersion: v1 kind: Pod metadata: name: pod-with-shared-volume-fsgroup spec: securityContext: fsGroup: 555 supplementalGroups: [666, 777] containers: - name: first image: busybox command: [\u0026#34;/bin/sleep\u0026#34;, \u0026#34;999999\u0026#34;] securityContext: runAsUser: 1111 volumeMounts: - name: shared-volume mountPath: /volume readOnly: false - name: second image: busybox command: [\u0026#34;/bin/sleep\u0026#34;, \u0026#34;999999\u0026#34;] securityContext: runAsUser: 2222 volumeMounts: - name: shared-volume mountPath: /volume readOnly: false volumes: - name: shared-volume emptyDir: kubectl apply -f pod-with-shared-volume-fsgroup.yaml $ kubectl exec -it pod-with-shared-volume-fsgroup -c first -- sh / $ id uid=1111 gid=0(root) groups=555,666,777 / $ ls -la | grep volume drwxrwsrwx 2 root 555 4096 Apr 20 16:42 volume / $ echo bar \u0026gt; volume/bar / $ ls -la volume/ total 12 drwxrwsrwx 2 root 555 4096 Apr 20 16:43 . drwxr-xr-x 1 root root 4096 Apr 20 16:42 .. -rw-r--r-- 1 1111 555 4 Apr 20 16:43 bar / $ echo foo \u0026gt; tmp/foo / $ ls -la tmp/ total 12 drwxrwxrwt 1 root root 4096 Apr 20 16:43 . drwxr-xr-x 1 root root 4096 Apr 20 16:42 .. -rw-r--r-- 1 1111 root 4 Apr 20 16:43 foo In the pod definition, we set fsGroup to 555, so the mounted volume will be owned by group ID 555. And because we create a file in the mounted volume’s directory, the file is owned by user ID 1111 and by group ID 555.\nEnabling network isolation in a namespace By default, pods in a given namespace can be accessed by anyone. Now we can use NetworkPolicy to prevent all clients from connecting to any pod in your namespace.\napiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny spec: podSelector: Note that: Empty pod selector matches all pods in the same namespace.\nTo let clients connect to the pods in the namespace, you must now explicitly say who can connect to the pods.\napiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: postgres-netpolicy spec: podSelector: matchLabels: app: database ingress: - from: - podSelector: matchLabels: app: webserver ports: - port: 5432 The example NetworkPolicy allows pods with the app=webserver label to connect to pods with the app=database label, only on port 5432. Other pods can’t connect to the database pods, and no one (not even the webserver pods) can connect to anything other than port 5432 of the database pods.\nA NetworkPolicy only allowing pods in namespaces matching a namespaceSelector to access a specific pod.\napiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: shoppingcart-netpolicy spec: podSelector: matchLabels: app: shopping-cart ingress: - from: - namespaceSelector: matchLabels: tenant: manning ports: - port: 80 You can also specify an IP block in CIDR notation.\napiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: ipblock-netpolicy spec: podSelector: matchLabels: app: shopping-cart ingress: - from: - ipBlock: cidr: 192.168.1.0/24 You can also limit their outbound traffic through egress rules.\napiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: egress-net-policy spec: podSelector: matchLabels: app: webserver egress: - to: - podSelector: matchLabels: app: database ports: - port: 5432 Users and Permissions Authentication and Authorization in Kubernetes In this tutorial, we will create two namespaces “my-app-dev” and “my-app-prod” and two users “dev” and “test” with different roles to those namespaces:\nmy-app-dev: dev: Edit my-app-prod: dev: View test: Edit The default Roles defined in Kubernetes are:\n- view: read-only access, excludes secrets. - edit: above + ability to edit most resources, excludes roles and role bindings. - admin: above + ability to manage roles and role bindings at a namespace level. - cluster-admin: everything. To manage normal users, we will use X.509 client certificate:\n1. Create a user\u0026#39;s private key and a certificate signing request. 2. Get it certified by a CA (Kubernetes CA) to have the user\u0026#39;s certificate. Suppose you are the admin (like root privileges), now you will create the normal user dev:123456:\nsudo useradd -m dev sudo passwd dev New password: 123456 Retype new password: 123456 passwd: password updated successfully Go into dev’s home directory as root privileges:\nsudo su cd /home/dev \u0026amp;\u0026amp; ls -la ┌──(root💀jump-windows)-[/home/dev] └─# cd /home/dev \u0026amp;\u0026amp; ls -la total 60 drwxr-xr-x 4 dev dev 4096 Apr 11 13:09 . drwxr-xr-x 4 root root 4096 Apr 11 13:09 .. -rw-r--r-- 1 dev dev 220 Feb 24 2021 .bash_logout -rw-r--r-- 1 dev dev 5551 Apr 2 00:54 .bashrc -rw-r--r-- 1 dev dev 3526 Feb 24 2021 .bashrc.original drwxr-xr-x 5 dev dev 4096 Jan 24 05:06 .config -rw-r--r-- 1 dev dev 11759 May 19 2021 .face lrwxrwxrwx 1 dev dev 5 Mar 4 11:38 .face.icon -\u0026gt; .face drwxr-xr-x 3 dev dev 4096 Jan 24 05:06 .java -rw-r--r-- 1 dev dev 807 Feb 24 2021 .profile -rw-r--r-- 1 dev dev 10875 Feb 3 22:45 .zshrc Create a private key:\nopenssl genrsa -out dev.key 2048 Create a certificate signing request (CSR):\nopenssl req -new -key dev.key -subj \u0026#34;/CN=dev\u0026#34; -out dev.csr Sign the CSR with the Kubernetes CA:\nIf minikube:\nopenssl x509 -req -in dev.csr -CA /home/ph3/.minikube/ca.crt -CAkey /home/ph3/.minikube/ca.key -CAcreateserial -out dev.crt -days 500 If kubernetes cluster:\nopenssl x509 -req -in dev.csr -CA /etc/kubernetes/pki/ca.crt -CAkey /etc/kubernetes/pki/ca.key -CAcreateserial -out dev.crt -days 500 List files in dev’s home directory:\n┌──(root💀jump-windows)-[/home/dev] └─# ls -la total 72 drwxr-xr-x 4 dev dev 4096 Apr 11 13:17 . drwxr-xr-x 4 root root 4096 Apr 11 13:09 .. -rw-r--r-- 1 dev dev 220 Feb 24 2021 .bash_logout -rw-r--r-- 1 dev dev 5551 Apr 2 00:54 .bashrc -rw-r--r-- 1 dev dev 3526 Feb 24 2021 .bashrc.original drwxr-xr-x 5 dev dev 4096 Jan 24 05:06 .config -rw-r--r-- 1 root root 985 Apr 11 13:17 dev.crt -rw-r--r-- 1 root root 883 Apr 11 13:13 dev.csr -rw------- 1 root root 1675 Apr 11 13:12 dev.key -rw-r--r-- 1 dev dev 11759 May 19 2021 .face lrwxrwxrwx 1 dev dev 5 Mar 4 11:38 .face.icon -\u0026gt; .face drwxr-xr-x 3 dev dev 4096 Jan 24 05:06 .java -rw-r--r-- 1 dev dev 807 Feb 24 2021 .profile -rw-r--r-- 1 dev dev 10875 Feb 3 22:45 .zshrc We have new three files: dev.crt, dev.csr, dev.key but under root privileges. We need to grant all the created files and directories to the “dev” user:\nchown -R dev: /home/dev/ ┌──(root💀jump-windows)-[/home/dev] └─# ls -la total 72 drwxr-xr-x 4 dev dev 4096 Apr 11 13:17 . drwxr-xr-x 4 root root 4096 Apr 11 13:09 .. -rw-r--r-- 1 dev dev 220 Feb 24 2021 .bash_logout -rw-r--r-- 1 dev dev 5551 Apr 2 00:54 .bashrc -rw-r--r-- 1 dev dev 3526 Feb 24 2021 .bashrc.original drwxr-xr-x 5 dev dev 4096 Jan 24 05:06 .config -rw-r--r-- 1 dev dev 985 Apr 11 13:17 dev.crt -rw-r--r-- 1 dev dev 883 Apr 11 13:13 dev.csr -rw------- 1 dev dev 1675 Apr 11 13:12 dev.key -rw-r--r-- 1 dev dev 11759 May 19 2021 .face lrwxrwxrwx 1 dev dev 5 Mar 4 11:38 .face.icon -\u0026gt; .face drwxr-xr-x 3 dev dev 4096 Jan 24 05:06 .java -rw-r--r-- 1 dev dev 807 Feb 24 2021 .profile -rw-r--r-- 1 dev dev 10875 Feb 3 22:45 .zshrc Now we will login to “dev” user to create user and context in kubernetes:\nsudo -u dev bash Create the user inside kubernetes as “dev” privileges:\nkubectl config set-credentials dev --client-certificate=/home/dev/dev.crt --client-key=/home/dev/dev.key ┌──(dev㉿jump-windows)-[~] └─$ kubectl config set-credentials dev --client-certificate=/home/dev/dev.crt --client-key=/home/dev/dev.key User \u0026#34;dev\u0026#34; set. Create a context for the user:\nIf minikube:\nkubectl config set-context dev-context --cluster=minikube --user=dev If kubernetes cluster:\nkubectl config set-context dev-context --cluster=kubernetes --user=dev After above steps, the /home/dev/.kube/config file is created:\napiVersion: v1 clusters: null contexts: - context: cluster: minikube user: dev name: dev-context current-context: \u0026#34;\u0026#34; kind: Config preferences: {} users: - name: dev user: client-certificate: /home/dev/dev.crt client-key: /home/dev/dev.key Edit user config file:\napiVersion: v1 clusters: - cluster: certificate-authority: /home/ph3/.minikube/ca.crt server: https://192.168.49.2:8443 name: minikube contexts: - context: cluster: minikube user: dev name: dev-context current-context: dev-context kind: Config preferences: {} users: - name: dev user: client-certificate: /home/dev/dev.crt client-key: /home/dev/dev.key Note that the certificate-authority and server variables have to be as in the cluster admin config.\nNow we have a user “dev” created. We will do the same for user “test”.\nAs administrator, we can create the two namespaces:\nkubectl create namespace my-app-dev kubectl create namespace my-app-prod As we have not defined any authorization to the users, they should get forbidden access to all cluster resources.\n┌──(dev㉿jump-windows)-[~] └─$ kubectl get nodes Error from server (Forbidden): nodes is forbidden: User \u0026#34;dev\u0026#34; cannot list resource \u0026#34;nodes\u0026#34; in API group \u0026#34;\u0026#34; at the cluster scope ┌──(dev㉿jump-windows)-[~] └─$ kubectl get pods Error from server (Forbidden): pods is forbidden: User \u0026#34;dev\u0026#34; cannot list resource \u0026#34;pods\u0026#34; in API group \u0026#34;\u0026#34; in the namespace \u0026#34;default\u0026#34; ┌──(dev㉿jump-windows)-[~] └─$ kubectl get pods -n my-app-dev Error from server (Forbidden): pods is forbidden: User \u0026#34;dev\u0026#34; cannot list resource \u0026#34;pods\u0026#34; in API group \u0026#34;\u0026#34; in the namespace \u0026#34;my-app-dev\u0026#34; ┌──(dev㉿jump-windows)-[~] └─$ kubectl get pods -n my-app-prod Error from server (Forbidden): pods is forbidden: User \u0026#34;dev\u0026#34; cannot list resource \u0026#34;pods\u0026#34; in API group \u0026#34;\u0026#34; in the namespace \u0026#34;my-app-prod\u0026#34; ┌──(test㉿jump-windows)-[~] └─$ kubectl get nodes Error from server (Forbidden): nodes is forbidden: User \u0026#34;test\u0026#34; cannot list resource \u0026#34;nodes\u0026#34; in API group \u0026#34;\u0026#34; at the cluster scope ┌──(test㉿jump-windows)-[~] └─$ kubectl get pods Error from server (Forbidden): pods is forbidden: User \u0026#34;test\u0026#34; cannot list resource \u0026#34;pods\u0026#34; in API group \u0026#34;\u0026#34; in the namespace \u0026#34;default\u0026#34; ┌──(test㉿jump-windows)-[~] └─$ kubectl get pods -n my-app-dev Error from server (Forbidden): pods is forbidden: User \u0026#34;test\u0026#34; cannot list resource \u0026#34;pods\u0026#34; in API group \u0026#34;\u0026#34; in the namespace \u0026#34;my-app-dev\u0026#34; ┌──(test㉿jump-windows)-[~] └─$ kubectl get pods -n my-app-prod Error from server (Forbidden): pods is forbidden: User \u0026#34;test\u0026#34; cannot list resource \u0026#34;pods\u0026#34; in API group \u0026#34;\u0026#34; in the namespace \u0026#34;my-app-prod\u0026#34; As administrator, we will create a Role/ClusterRole. A Role/ClusterRole are just a list of verbs (actions) permitted on specific resources and namespaces. Here is role.yaml file:\napiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: list-deployments namespace: my-app-dev rules: - apiGroups: [ apps ] resources: [ deployments ] verbs: [ get, list ] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: list-deployments rules: - apiGroups: [ apps ] resources: [ deployments ] verbs: [ get, list ] kubectl apply -f role.yaml We can list ClusterRole:\nkubectl get clusterrole NAME CREATED AT admin 2022-01-27T01:18:51Z cluster-admin 2022-01-27T01:18:51Z edit 2022-01-27T01:18:51Z ingress-nginx 2022-04-10T14:38:31Z ingress-nginx-admission 2022-04-10T14:38:31Z kubeadm:get-nodes 2022-01-27T01:18:53Z list-deployments 2022-04-11T18:18:04Z system:aggregate-to-admin 2022-01-27T01:18:51Z system:aggregate-to-edit 2022-01-27T01:18:51Z system:aggregate-to-view 2022-01-27T01:18:51Z system:auth-delegator 2022-01-27T01:18:51Z system:basic-user 2022-01-27T01:18:51Z ... ... system:volume-scheduler 2022-01-27T01:18:51Z view 2022-01-27T01:18:51Z We are now going to bind Role/ClusterRole to users by role-binding.yaml as below:\ndev: edit on namespace \u0026#34;my-app-dev\u0026#34; view on namespace \u0026#34;my-app-prod\u0026#34; test: edit on namespace \u0026#34;my-app-prod\u0026#34; apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: dev namespace: my-app-dev subjects: - kind: User name: dev apiGroup: rbac.authorization.k8s.io roleRef: kind: ClusterRole name: edit apiGroup: rbac.authorization.k8s.io --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: dev namespace: my-app-prod subjects: - kind: User name: dev apiGroup: rbac.authorization.k8s.io roleRef: kind: ClusterRole name: view apiGroup: rbac.authorization.k8s.io --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: test namespace: my-app-prod subjects: - kind: User name: test apiGroup: rbac.authorization.k8s.io roleRef: kind: ClusterRole name: edit apiGroup: rbac.authorization.k8s.io kubectl apply -f role-binding.yaml Let’s check if our users have the right permissions.\nUser: test (edit on \u0026#34;my-app-prod\u0026#34; namespace) - can create deployments, list pods on \u0026#34;my-app-prod\u0026#34; - can\u0026#39;t create or list pods, deployments on \u0026#34;my-app-dev\u0026#34; ┌──(test㉿jump-windows)-[~] └─$ kubectl run nginx --image=nginx -n my-app-prod pod/nginx created ┌──(test㉿jump-windows)-[~] └─$ kubectl get pods -n my-app-prod NAME READY STATUS RESTARTS AGE nginx 1/1 Running 0 15s ┌──(test㉿jump-windows)-[~] └─$ kubectl run nginx --image=nginx -n my-app-dev Error from server (Forbidden): pods is forbidden: User \u0026#34;test\u0026#34; cannot create resource \u0026#34;pods\u0026#34; in API group \u0026#34;\u0026#34; in the namespace \u0026#34;my-app-dev\u0026#34; ┌──(test㉿jump-windows)-[~] └─$ kubectl get pods -n my-app-dev Error from server (Forbidden): pods is forbidden: User \u0026#34;test\u0026#34; cannot list resource \u0026#34;pods\u0026#34; in API group \u0026#34;\u0026#34; in the namespace \u0026#34;my-app-dev\u0026#34; User: dev (view on \u0026#34;my-app-prod\u0026#34; namespace edit on \u0026#34;my-app-dev\u0026#34; namespace) - can list pods, deployments on \u0026#34;my-app-prod\u0026#34; but can\u0026#39;t create - can list, create, delete pods, deployments on \u0026#34;my-app-dev\u0026#34; ┌──(dev㉿jump-windows)-[~] └─$ kubectl get pods -n my-app-prod NAME READY STATUS RESTARTS AGE nginx 1/1 Running 0 7m14s ┌──(dev㉿jump-windows)-[~] └─$ kubectl run nginx --image=nginx -n my-app-prod Error from server (Forbidden): pods is forbidden: User \u0026#34;dev\u0026#34; cannot create resource \u0026#34;pods\u0026#34; in API group \u0026#34;\u0026#34; in the namespace \u0026#34;my-app-prod\u0026#34; ┌──(dev㉿jump-windows)-[~] └─$ kubectl run nginx --image=nginx -n my-app-dev pod/nginx created ┌──(dev㉿jump-windows)-[~] └─$ kubectl get pods -n my-app-dev NAME READY STATUS RESTARTS AGE nginx 1/1 Running 0 11s ┌──(dev㉿jump-windows)-[~] └─$ kubectl delete pods nginx -n my-app-dev pod \u0026#34;nginx\u0026#34; deleted ┌──(dev㉿jump-windows)-[~] └─$ kubectl get pods -n my-app-dev No resources found in my-app-dev namespace. Configure ServiceAccount to pull private image Create service account with imagePullSecrets by sa-image-pull-secrets.yaml:\napiVersion: v1 kind: ServiceAccount metadata: name: sa-docker-registry imagePullSecrets: - name: mydockerhubsecret Now, you can inspect the ServiceAccount with the describe command:\nkubectl describe sa sa-docker-registry Name: sa-docker-registry Namespace: default Labels: \u0026lt;none\u0026gt; Annotations: \u0026lt;none\u0026gt; Image pull secrets: mydockerhubsecret Mountable secrets: sa-docker-registry-token-bcbmg Tokens: sa-docker-registry-token-bcbmg Events: \u0026lt;none\u0026gt; You can see that a custom token Secret has been created and associated with the ServiceAccount.\nLet’s inspect the secret sa-docker-registry-token-bcbmg:\nkubectl describe secret sa-docker-registry-token-bcbmg Name: sa-docker-registry-token-bcbmg Namespace: default Labels: \u0026lt;none\u0026gt; Annotations: kubernetes.io/service-account.name: sa-docker-registry kubernetes.io/service-account.uid: 643bb848-6f9f-4f4e-a087-0ce61fec527f Type: kubernetes.io/service-account-token Data ==== ca.crt: 1111 bytes namespace: 7 bytes token: eyJhbGciOiJSUzI1NiIs... To assign a ServiceAccount to a pod, we will set the name of the ServiceAccount in the spec.serviceAccountName field in the pod definition:\nThis is busybox-sa.yaml without service account:\napiVersion: v1 kind: Pod metadata: name: busybox spec: containers: - name: busybox image: hoangph3/busybox command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;] args: - echo \u0026#34;$(date) Hello Kubernetes !\u0026#34;; sleep 9999999; kubectl apply -f busybox-sa.yaml kubectl get pods kubectl describe pod busybox NAME READY STATUS RESTARTS AGE busybox 0/1 ErrImagePull 0 7s Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 16s default-scheduler Successfully assigned default/busybox to minikube Normal Pulling 15s kubelet Pulling image \u0026#34;hoangph3/busybox\u0026#34; Warning Failed 11s kubelet Failed to pull image \u0026#34;hoangph3/busybox\u0026#34;: rpc error: code = Unknown desc = Error response from daemon: pull access denied for hoangph3/busybox, repository does not exist or may require \u0026#39;docker login\u0026#39;: denied: requested access to the resource is denied Warning Failed 11s kubelet Error: ErrImagePull Normal BackOff 11s kubelet Back-off pulling image \u0026#34;hoangph3/busybox\u0026#34; Warning Failed 11s kubelet Error: ImagePullBackOff This is busybox-sa.yaml with service account:\napiVersion: v1 kind: Pod metadata: name: busybox spec: serviceAccountName: sa-docker-registry containers: - name: busybox image: hoangph3/busybox command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;] args: - echo \u0026#34;$(date) Hello Kubernetes !\u0026#34;; sleep 9999999; kubectl apply -f busybox-sa.yaml kubectl get pods kubectl describe pod busybox kubectl logs -f busybox NAME READY STATUS RESTARTS AGE busybox 1/1 Running 0 20s Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 15s default-scheduler Successfully assigned default/busybox to minikube Normal Pulling 14s kubelet Pulling image \u0026#34;hoangph3/busybox\u0026#34; Normal Pulled 10s kubelet Successfully pulled image \u0026#34;hoangph3/busybox\u0026#34; in 3.227788488s Normal Created 10s kubelet Created container busybox Normal Started 10s kubelet Started container busybox Wed Apr 13 16:31:10 UTC 2022 Hello Kubernetes ! The custom ServiceAccount’s token is mounted into the container, you can confirm by exec command:\n$ kubectl exec -it busybox -- sh / # cd /var/run/secrets/kubernetes.io/serviceaccount/ /var/run/secrets/kubernetes.io/serviceaccount # ls ca.crt namespace token /var/run/secrets/kubernetes.io/serviceaccount # cat token eyJhbGciOiJSUzI1NiIs... Connect to cluster with ServiceAccount Token When we create the service account, we will get the CA certificate, namespace, and token. We will use this information to connect to cluster.\nFirst build a docker image with Dockerfile:\nFROM python:3.8-slim-buster RUN apt -y update \u0026amp;\u0026amp; apt -y install curl telnet nano \u0026amp;\u0026amp; curl -L -O https://dl.k8s.io/v1.23.5/kubernetes-client-linux-amd64.tar.gz \u0026amp;\u0026amp; tar zvxf kubernetes-client-linux-amd64.tar.gz kubernetes/client/bin/kubectl \u0026amp;\u0026amp; mv kubernetes/client/bin/kubectl / \u0026amp;\u0026amp; rm -rf kubernetes \u0026amp;\u0026amp; rm -f kubernetes-client-linux-amd64.tar.gz CMD [\u0026#34;sleep\u0026#34;, \u0026#34;9999999\u0026#34;] Build image:\ndocker build -t hoangph3/kubectl-client . $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE hoangph3/kubectl-client latest 535bed2a335c 6 seconds ago 185MB hoangph3/kubectl-proxy latest 9a536cb8b202 36 minutes ago 184MB redis latest bba24acba395 2 weeks ago 113MB nginx latest 12766a6745ee 2 weeks ago 142MB curlimages/curl latest 375c62ad3696 2 weeks ago 8.3MB Now, we are going to create one pod in namespace foo and the other one in namespace bar:\n$ kubectl create ns foo namespace/foo created $ kubectl run test --image=hoangph3/kubectl-client --image-pull-policy=Never -n foo pod/test created $ kubectl create ns bar namespace/bar created $ kubectl run test --image=hoangph3/kubectl-client --image-pull-policy=Never -n bar pod/test created Now we will use kubectl exec to run a shell inside each of the two pods (one in each terminal):\nkubectl exec -it test -n foo -- bash root@test:/# env KUBERNETES_SERVICE_PORT_HTTPS=443 KUBERNETES_SERVICE_PORT=443 HOSTNAME=test PYTHON_VERSION=3.8.12 PWD=/ PYTHON_SETUPTOOLS_VERSION=57.5.0 HOME=/root LANG=C.UTF-8 KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443 GPG_KEY=E3FF2839C048B25C084DEBE9B26995E310250568 TERM=xterm SHLVL=1 KUBERNETES_PORT_443_TCP_PROTO=tcp PYTHON_PIP_VERSION=21.2.4 KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1 PYTHON_GET_PIP_SHA256=7c5239cea323cadae36083079a5ee6b2b3d56f25762a0c060d2867b89e5e06c5 KUBERNETES_SERVICE_HOST=10.96.0.1 KUBERNETES_PORT=tcp://10.96.0.1:443 KUBERNETES_PORT_443_TCP_PORT=443 PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/2caf84b14febcda8077e59e9b8a6ef9a680aa392/public/get-pip.py PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin _=/usr/bin/env root@test:/# /kubectl get svc Error from server (Forbidden): services is forbidden: User \u0026#34;system:serviceaccount:foo:default\u0026#34; cannot list resource \u0026#34;services\u0026#34; in API group \u0026#34;\u0026#34; in the namespace \u0026#34;foo\u0026#34; Because the default permissions for a ServiceAccount don’t allow it to list or modify any resources. Now, let’s learn how to allow the ServiceAccount to do that. First, you’ll need to create a Role resource.\nkubectl create role service-reader --verb=get --verb=list --resource=services -n foo role.rbac.authorization.k8s.io/service-reader created kubectl create role service-reader --verb=get --verb=list --resource=services -n bar role.rbac.authorization.k8s.io/service-reader created A Role defines what actions can be performed, but it doesn’t specify who can perform them. To do that, you must bind the Role to a subject, which can be a user, a Service-Account, or a group (of users or ServiceAccounts).\nBinding Roles to subjects is achieved by creating a RoleBinding resource. To bind the Role to the default ServiceAccount, run the following command:\nkubectl create rolebinding test --role=service-reader --serviceaccount=foo:default -n foo rolebinding.rbac.authorization.k8s.io/test created Note that, if you want to bind a Role to a user instead of a ServiceAccount, use the --user argument to specify the username. To bind it to a group, use --group.\nBecause this RoleBinding binds the Role to the ServiceAccount the pod in namespace foo is running under, you can now list Services from within that pod.\nkubectl exec -it test -n foo -- bash root@test:/# /kubectl get svc No resources found in foo namespace. Because we only create the RoleBinding for the foo namespace, the bar namespace is not. This leads the pod in namespace bar can’t list the Services in its own namespace, and obviouslyalso not those in the foo namespace.\nkubectl exec -it test -n bar -- bash root@test:/# /kubectl get svc Error from server (Forbidden): services is forbidden: User \u0026#34;system:serviceaccount:bar:default\u0026#34; cannot list resource \u0026#34;services\u0026#34; in API group \u0026#34;\u0026#34; in the namespace \u0026#34;bar\u0026#34; root@test:/# /kubectl get svc -n foo Error from server (Forbidden): services is forbidden: User \u0026#34;system:serviceaccount:bar:default\u0026#34; cannot list resource \u0026#34;services\u0026#34; in API group \u0026#34;\u0026#34; in the namespace \u0026#34;foo\u0026#34; But you can edit your RoleBinding in the foo namespace and add the other pod’s ServiceAccount, even though it’s in a different namespace. Run the following command:\nkubectl edit rolebinding test -n foo # Please edit the object below. Lines beginning with a \u0026#39;#\u0026#39; will be ignored, # and an empty file will abort the edit. If an error occurs while saving this file will be # reopened with the relevant failures. # apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: creationTimestamp: \u0026#34;2022-04-16T06:36:58Z\u0026#34; name: test namespace: foo resourceVersion: \u0026#34;376922\u0026#34; uid: b35fb7a1-82d0-4114-a4a6-2602ec00394f roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: service-reader subjects: - kind: ServiceAccount name: default namespace: foo Then add the following lines to the list of subjects, look like that:\n... subjects: - kind: ServiceAccount name: default namespace: foo - kind: ServiceAccount name: default namespace: bar Now you can also list Services in the foo namespace from inside the pod running in the bar namespace:\nkubectl exec -it test -n bar -- bash root@test:/# /kubectl get svc Error from server (Forbidden): services is forbidden: User \u0026#34;system:serviceaccount:bar:default\u0026#34; cannot list resource \u0026#34;services\u0026#34; in API group \u0026#34;\u0026#34; in the namespace \u0026#34;bar\u0026#34; root@test:/# /kubectl get svc -n foo No resources found in foo namespace. ClusterRoles and ClusterRoleBindings A ClusterRole can be used to allow access to cluster-level resources. Let’s look at how to allow your pod to list PersistentVolumes in your cluster. First, we will create a ClusterRole called pv-reader:\nkubectl create clusterrole pv-reader --verb=get,list --resource=persistentvolumes clusterrole.rbac.authorization.k8s.io/pv-reader created We will exec to the pod in foo namespace and list PersistentVolumes:\nkubectl exec -it test -n foo -- bash root@test:/# /kubectl get pv Error from server (Forbidden): persistentvolumes is forbidden: User \u0026#34;system:serviceaccount:foo:default\u0026#34; cannot list resource \u0026#34;persistentvolumes\u0026#34; in API group \u0026#34;\u0026#34; at the cluster scope Default ServiceAccount is unable to get and list PersistentVolumes, you must bind the ClusterRole to a ServiceAccount by run the following command:\nkubectl create clusterrolebinding pv-test --clusterrole=pv-reader --serviceaccount=foo:default clusterrolebinding.rbac.authorization.k8s.io/pv-test created Let’s see if you can list PersistentVolumes now:\nkubectl exec -it test -n foo -- bash root@test:/# /kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pvc-5865deec-56a1-4743-9e9f-e638b9e959fa 1Mi RWO Delete Bound default/data-kubia-1 standard 5d pvc-d74affe3-ded5-46f7-889d-c0abf4c12e0a 1Mi RWO Delete Bound default/data-kubia-0 standard 5d Note that if you create a ClusterRoleBinding and reference the ClusterRole in it, the subjects listed in the binding can view the specified resources across all namespaces. If, on the other hand, you create a RoleBinding, the subjects listed in the binding can only view resources in the namespace of the RoleBinding. We will confirm now.\nFirst, trying to list pods across all namespaces and foo namespace:\nkubectl exec -it test -n foo -- bash root@test:/# /kubectl get pods --all-namespaces Error from server (Forbidden): pods is forbidden: User \u0026#34;system:serviceaccount:foo:default\u0026#34; cannot list resource \u0026#34;pods\u0026#34; in API group \u0026#34;\u0026#34; at the cluster scope root@test:/# /kubectl get pods -n foo Error from server (Forbidden): pods is forbidden: User \u0026#34;system:serviceaccount:foo:default\u0026#34; cannot list resource \u0026#34;pods\u0026#34; in API group \u0026#34;\u0026#34; in the namespace \u0026#34;foo\u0026#34; Now, let’s see what happens when you create a ClusterRoleBinding (not RoleBinding) and bind it to the pod’s ServiceAccount:\nkubectl create clusterrolebinding view-test --clusterrole=view --serviceaccount=foo:default clusterrolebinding.rbac.authorization.k8s.io/view-test created kubectl exec -it test -n foo -- bash root@test:/# /kubectl get pods --all-namespaces NAMESPACE NAME READY STATUS RESTARTS AGE bar test 1/1 Running 3 (6h21m ago) 39h foo test 1/1 Running 3 (6h21m ago) 39h ingress-nginx ingress-nginx-admission-create--1-6rs7v 0/1 Completed 0 5d17h ingress-nginx ingress-nginx-admission-patch--1-qf2zd 0/1 Completed 1 5d17h ingress-nginx ingress-nginx-controller-5f66978484-wtpph 1/1 Running 7 (6h21m ago) 5d17h kube-system coredns-78fcd69978-kfxrb 1/1 Running 29 (8h ago) 79d kube-system etcd-minikube 1/1 Running 29 (8h ago) 79d kube-system kube-apiserver-minikube 1/1 Running 29 (8h ago) 79d kube-system kube-controller-manager-minikube 1/1 Running 30 (8h ago) 79d kube-system kube-proxy-wjfqr 1/1 Running 29 (6h21m ago) 79d kube-system kube-scheduler-minikube 1/1 Running 29 (6h21m ago) 79d kube-system storage-provisioner 1/1 Running 59 (6h19m ago) 79d my-app-prod nginx 1/1 Running 6 (6h21m ago) 4d13h root@test:/# /kubectl get pods -n foo NAME READY STATUS RESTARTS AGE test 1/1 Running 3 (6h22m ago) 39h root@test:/# /kubectl get pods -n bar NAME READY STATUS RESTARTS AGE test 1/1 Running 3 (6h22m ago) 39h As expected, the pod can get a list of all the pods in the cluster. Let’s describe the view-test ClusterRole:\nkubectl get clusterrolebindings.rbac.authorization.k8s.io view-test -o yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: creationTimestamp: \u0026#34;2022-04-16T08:30:33Z\u0026#34; name: view-test resourceVersion: \u0026#34;382606\u0026#34; uid: 08b8d16a-8bc7-4b95-8ba3-c28650b08f9f roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: view subjects: - kind: ServiceAccount name: default namespace: foo To summarize, combining a ClusterRoleBinding with a ClusterRole referring to namespaced resources allows the pod to access namespaced resources in any namespace.\nNow we will use RoleBinding instead ClusterRoleBinding:\nkubectl delete clusterrolebindings.rbac.authorization.k8s.io view-test clusterrolebinding.rbac.authorization.k8s.io \u0026#34;view-test\u0026#34; deleted kubectl create rolebinding view-test --clusterrole=view --serviceaccount=foo:default -n foo rolebinding.rbac.authorization.k8s.io/view-test created kubectl exec -it test -n foo -- bash root@test:/# /kubectl get pods --all-namespaces Error from server (Forbidden): pods is forbidden: User \u0026#34;system:serviceaccount:foo:default\u0026#34; cannot list resource \u0026#34;pods\u0026#34; in API group \u0026#34;\u0026#34; at the cluster scope root@test:/# /kubectl get pods -n bar Error from server (Forbidden): pods is forbidden: User \u0026#34;system:serviceaccount:foo:default\u0026#34; cannot list resource \u0026#34;pods\u0026#34; in API group \u0026#34;\u0026#34; in the namespace \u0026#34;bar\u0026#34; root@test:/# /kubectl get pods -n foo NAME READY STATUS RESTARTS AGE test 1/1 Running 3 (6h33m ago) 39h It’s not too surprising, your pod can list pods in the foo namespace, but not in any other specific namespace or across all namespaces.\nYou can get full source code here: security-policy, users-and-permissions.\n","permalink":"https://hoangph3.github.io/posts/kubernetes_security_access/","summary":"Harden a Kubernetes cluster with pod security policies and fine-grained RBAC for users and permissions.","title":"Kubernetes Security and Access Control"},{"content":"Kubernetes Jobs Running a single completable task by Job # job.yaml apiVersion: batch/v1 kind: Job metadata: name: my-job spec: template: metadata: labels: app: my-job spec: containers: - name: my-job image: busybox command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;] args: - echo \u0026#34;$(date) Job starting\u0026#34;; sleep 30; echo \u0026#34;$(date) Finished succesfully\u0026#34;; restartPolicy: OnFailure The YAML defines a Job that will run the busybox image, which invokes a process that runs for exactly 30 seconds and then exits. In spec section, the restartPolicy is set OnFailure, because Job can’t use the default restart policy (which is Always). This setting is what prevents the container from being restarted when it finishes.\nAfter we run command kubectl apply -f job.yaml to create job, we can list pods by kubectl get pods:\nNAME READY STATUS RESTARTS AGE my-job--1-4n4rk 1/1 Running 0 8s After 30 seconds have passed, the job has completed and no restart:\nNAME READY STATUS RESTARTS AGE my-job--1-4n4rk 0/1 Completed 0 39s We can get log of job:\nkubectl logs -f my-job--1-4n4rk Sat Apr 9 15:34:32 UTC 2022 Job starting Sat Apr 9 15:35:02 UTC 2022 Finished succesfully To running job pods sequentially or parallel, we can configure the .spec.completions and the .spec.parallelism:\n# batch-job.yaml apiVersion: batch/v1 kind: Job metadata: name: my-job spec: completions: 5 # this job must ensure 5 pods complete successfully parallelism: 2 # up to 2 pods can run in parallel template: metadata: labels: app: my-job spec: containers: - name: my-job image: busybox command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;] args: - echo \u0026#34;$(date) Job starting\u0026#34;; sleep 30; echo \u0026#34;$(date) Finished succesfully\u0026#34;; restartPolicy: OnFailure With completions: 5 indicate that this job must ensure 5 pods complete successfully, and up to 2 pods can run in parallel with parallelism: 2.\nkubectl get pods kubectl get jobs NAME READY STATUS RESTARTS AGE my-job--1-grq6l 1/1 Running 0 9s my-job--1-w85p2 1/1 Running 0 9s NAME COMPLETIONS DURATION AGE my-job 0/5 17s 17s When the job completed:\nkubectl get pods kubectl get jobs NAME READY STATUS RESTARTS AGE my-job--1-cv5js 0/1 Completed 0 77s my-job--1-grq6l 0/1 Completed 0 112s my-job--1-khnbl 0/1 Completed 0 73s my-job--1-w85p2 0/1 Completed 0 112s my-job--1-x95mk 0/1 Completed 0 41s NAME COMPLETIONS DURATION AGE my-job 5/5 107s 115s Scheduling Jobs to run periodically Let’s create a CronJob:\n# cron-job.yaml apiVersion: batch/v1 kind: CronJob metadata: name: cronjob-every-minute spec: schedule: \u0026#34;* * * * *\u0026#34; jobTemplate: spec: template: metadata: labels: app: cronjob-every-minute spec: containers: - name: cronjob-every-minute image: busybox command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;] args: - echo \u0026#34;$(date) Job starting\u0026#34;; sleep 30; echo \u0026#34;$(date) Finished succesfully\u0026#34;; restartPolicy: OnFailure kubectl apply -f cron-job.yaml kubectl get cronjobs.batch NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE cronjob-every-minute * * * * * False 1 8s 44s After 3 minutes, let’s list of pods:\nkubectl get pods NAME READY STATUS RESTARTS AGE cronjob-every-minute-27492030--1-qdtgl 0/1 Completed 0 2m38s cronjob-every-minute-27492031--1-wl9jr 0/1 Completed 0 98s cronjob-every-minute-27492032--1-rb6p6 0/1 Completed 0 38s Print log of pods, we can see the job is scheduled every minute:\nkubectl logs -f cronjob-every-minute-27492030--1-qdtgl kubectl logs -f cronjob-every-minute-27492031--1-wl9jr kubectl logs -f cronjob-every-minute-27492032--1-rb6p6 Sat Apr 9 16:30:04 UTC 2022 Job starting Sat Apr 9 16:30:34 UTC 2022 Finished succesfully Sat Apr 9 16:31:04 UTC 2022 Job starting Sat Apr 9 16:31:34 UTC 2022 Finished succesfully Sat Apr 9 16:32:04 UTC 2022 Job starting Sat Apr 9 16:32:34 UTC 2022 Finished succesfully The .spec.successfulJobsHistoryLimit and .spec.failedJobsHistoryLimit fields are optional. These fields specify how many completed and failed jobs should be kept. By default, they are set to 3 and 1 respectively.\nAccess to https://crontab.guru/ to get more info about cronjob schedule expressions.\nStatefulSets StatefulSet Application The StatefulSet look like the ReplicaSet, but it’s creates both Pods and PersistentVolumeClaims. And a StatefulSet maintains a sticky identity for each of their Pods. StatefulSet Pods have a unique identity that is comprised of an ordinal, a stable network identity, and stable storage.\nFor a StatefulSet with N replicas, each Pod in the StatefulSet will be assigned an integer ordinal, from 0 up through N-1, that is unique over the Set, each Pod is attached to a PersistentVolumeClaim.\nCreate the PersistentVolume Because the PersistentVolumeClaim will request resource from PersistentVolume, so we need create PersistentVolume first. Note that we must create more if we plan on scaling the StatefulSet up more than that.\nIn this tutorial, we will need 3 PersistentVolumes, because we will be scaling the StatefulSet up to 3 replicas.\nkubectl apply -f persistent-volumes-hostpath.yaml kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pv-a 1Mi RWO Retain Available 7s pv-b 1Mi RWO Retain Available 7s pv-c 1Mi RWO Retain Available 7s Create StatefulSet with Headless Service:\nkubectl apply -f kubia-statefulset.yaml kubectl get pods NAME READY STATUS RESTARTS AGE kubia-0 1/1 Running 0 104s kubia-1 1/1 Running 0 56s Forward port to test connection from localhost:\nkubectl port-forward kubia-0 8080:8080 curl localhost:8080 Forwarding from 127.0.0.1:8080 -\u0026gt; 8080 Forwarding from [::1]:8080 -\u0026gt; 8080 You\u0026#39;ve hit kubia-0 Data stored on this pod: No data posted yet Send HTTP POST request to the application:\ncurl -X POST -d \u0026#34;Hello kubia-0\u0026#34; localhost:8080 Data stored on pod kubia-0 Send HTTP GET request to the application:\ncurl localhost:8080 You\u0026#39;ve hit kubia-0 Data stored on this pod: Hello kubia-0 Now let’s interact with another pod:\nkubectl port-forward kubia-0 8081:8080 curl localhost:8081 You\u0026#39;ve hit kubia-1 Data stored on this pod: No data posted yet As expected, each node has its own state. But is that state persisted?\nWe are going to delete the kubia-0 pod and wait for it to be rescheduled. Then we will see if it’s still serving the same data as before.\nkubectl delete pods kubia-0 kubectl get pods NAME READY STATUS RESTARTS AGE kubia-0 1/1 Terminating 0 20m kubia-1 1/1 Running 0 20m The pod is rescheduled:\nkubectl get pods NAME READY STATUS RESTARTS AGE kubia-0 0/1 ContainerCreating 0 5s kubia-1 1/1 Running 0 21m NAME READY STATUS RESTARTS AGE kubia-0 1/1 Running 0 8s kubia-1 1/1 Running 0 21m Now we will use API server to communicate the kubia-0, it’s another option like forward port:\nFirst, run proxy:\nkubectl proxy Send HTTP GET request to the URL with template: \u0026lt;apiServerHost\u0026gt;:\u0026lt;port\u0026gt;/api/v1/namespaces/default/pods/kubia-0/proxy/\u0026lt;path\u0026gt;\ncurl 127.0.0.1:8001/api/v1/namespaces/default/pods/kubia-0/proxy/ You\u0026#39;ve hit kubia-0 Data stored on this pod: Hello kubia-0 It’s data persistence!\nTaints and Tolerations Configure Taints and Tolerations We use a taint to prevent pods from being scheduled to the master node or worker node, unless those pods tolerate this taint. The pods that tolerate it are usually system pods.\nSuppose you have a cluster with 2 master nodes and 2 worker nodes:\nvagrant@master-1:~$ kubectl get nodes NAME STATUS ROLES AGE VERSION master-1 Ready control-plane,master 16d v1.23.0 master-2 Ready control-plane,master 16d v1.23.0 worker-1 Ready \u0026lt;none\u0026gt; 16d v1.23.0 worker-2 Ready \u0026lt;none\u0026gt; 16d v1.23.0 In kubernetes cluster, default you can only deploy your pods to the worker nodes (not master nodes), unless the kube-system pods. Because the master nodes have taints, and the kube-system pods tolerates the master nodes’s taints.\nLet’s get taints from master nodes:\nvagrant@master-1:~$ kubectl describe node master-1 Name: master-1 Roles: control-plane,master Labels: beta.kubernetes.io/arch=amd64 beta.kubernetes.io/os=linux kubernetes.io/arch=amd64 kubernetes.io/hostname=master-1 kubernetes.io/os=linux node-role.kubernetes.io/control-plane= node-role.kubernetes.io/master= node.kubernetes.io/exclude-from-external-load-balancers= Annotations: flannel.alpha.coreos.com/backend-data: {\u0026#34;VNI\u0026#34;:1,\u0026#34;VtepMAC\u0026#34;:\u0026#34;42:7b:34:b8:33:95\u0026#34;} flannel.alpha.coreos.com/backend-type: vxlan flannel.alpha.coreos.com/kube-subnet-manager: true flannel.alpha.coreos.com/public-ip: 10.0.2.15 kubeadm.alpha.kubernetes.io/cri-socket: /var/run/dockershim.sock node.alpha.kubernetes.io/ttl: 0 volumes.kubernetes.io/controller-managed-attach-detach: true CreationTimestamp: Tue, 05 Apr 2022 07:00:15 +0000 Taints: node-role.kubernetes.io/master:NoSchedule ... The format of Taints is \u0026lt;key\u0026gt;=\u0026lt;value\u0026gt;:\u0026lt;effect\u0026gt;. Only the kube-system pods with Tolerations: node-role.kubernetes.io/master=:NoSchedule can be scheduled on the master nodes.\nLet’s describe the kube-system pods:\nvagrant@master-1:~$ kubectl get po -n kube-system NAME READY STATUS RESTARTS AGE coredns-64897985d-fppdp 1/1 Running 5 (9m13s ago) 16d coredns-64897985d-t4mxq 1/1 Running 5 (9m13s ago) 16d kube-apiserver-master-1 1/1 Running 6 (9m13s ago) 16d kube-apiserver-master-2 1/1 Running 2 (8m23s ago) 16d kube-controller-manager-master-1 1/1 Running 8 (5m7s ago) 16d kube-controller-manager-master-2 1/1 Running 4 (8m23s ago) 16d kube-flannel-ds-4qtvn 1/1 Running 4 (8m23s ago) 16d kube-flannel-ds-fxptk 1/1 Running 14 (9m13s ago) 16d kube-flannel-ds-g68rx 1/1 Running 5 (6m36s ago) 16d kube-flannel-ds-hkwqd 1/1 Running 4 (4m59s ago) 16d kube-proxy-7tcjf 1/1 Running 3 (7m22s ago) 16d kube-proxy-j22bn 1/1 Running 3 (8m23s ago) 16d kube-proxy-wq9hg 1/1 Running 3 (6m36s ago) 16d kube-proxy-xsrqt 1/1 Running 5 (9m13s ago) 16d kube-scheduler-master-1 1/1 Running 8 (5m10s ago) 16d kube-scheduler-master-2 1/1 Running 3 (8m23s ago) 16d vagrant@master-1:~$ kubectl describe pod -n kube-system | grep Tolerations Tolerations: CriticalAddonsOnly op=Exists Tolerations: CriticalAddonsOnly op=Exists Tolerations: :NoExecute op=Exists Tolerations: :NoExecute op=Exists Tolerations: :NoExecute op=Exists Tolerations: :NoExecute op=Exists Tolerations: :NoSchedule op=Exists Tolerations: :NoSchedule op=Exists Tolerations: :NoSchedule op=Exists Tolerations: :NoSchedule op=Exists Tolerations: op=Exists Tolerations: op=Exists Tolerations: op=Exists Tolerations: op=Exists Tolerations: :NoExecute op=Exists Tolerations: :NoExecute op=Exists Each taint has an effect associated with it. Three possible effects exist:\nNoSchedule: pods won’t be scheduled to the node if they don’t tolerate the taint. PreferNoSchedule is a soft version of NoSchedule, meaning the scheduler will try to avoid scheduling the pod to the node, but will schedule it to the node if it can’t schedule it somewhere else. NoExecute, unlike NoSchedule and PreferNoSchedule that only affect scheduling, also affects pods already running on the node. If you add a NoExecute taint to a node, pods that are already running on that node and don’t tolerate the NoExecute taint will be evicted from the node. Add Taints to Nodes Imagine having a single Kubernetes cluster where you run both production and development workloads. It’s of the utmost importance that development pods never run on the production nodes. This can be achieved by adding a taint to your production nodes.\nvagrant@master-1:~$ kubectl get nodes NAME STATUS ROLES AGE VERSION master-1 Ready control-plane,master 16d v1.23.0 master-2 Ready control-plane,master 16d v1.23.0 worker-1 Ready \u0026lt;none\u0026gt; 16d v1.23.0 worker-2 Ready \u0026lt;none\u0026gt; 16d v1.23.0 vagrant@master-1:~$ kubectl taint node worker-1 node-type=production:NoSchedule node/worker-1 tainted This adds a taint with key node-type, value production and the NoSchedule effect. If you now deploy multiple replicas of a regular pod, you’ll see none of them are scheduled to the node you tainted, as shown in the following listing.\nvagrant@master-1:~$ kubectl create deploy test --image busybox --replicas 5 -- sleep 99999 deployment.apps/test created vagrant@master-1:~$ kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES test-5c4f786f47-59hf5 1/1 Running 0 102s 10.244.3.8 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; test-5c4f786f47-h6ttk 1/1 Running 0 102s 10.244.3.9 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; test-5c4f786f47-jr92r 1/1 Running 0 102s 10.244.3.12 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; test-5c4f786f47-qvt4r 1/1 Running 0 102s 10.244.3.10 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; test-5c4f786f47-r2r7z 1/1 Running 0 102s 10.244.3.11 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; To deploy production pods to the production nodes, they need to tolerate the taint you added to the nodes, look like pod-with-toleration.yaml file:\napiVersion: apps/v1 kind: Deployment metadata: name: pod-with-toleration labels: app: nginx spec: selector: matchLabels: app: nginx replicas: 5 template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:latest tolerations: - key: node-type operator: Equal value: production effect: NoSchedule vagrant@master-1:~$ kubectl apply -f pod-with-tolerations.yaml deployment.apps/pod-with-toleration created vagrant@master-1:~$ kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES pod-with-toleration-6c884d5b5b-6bjs4 1/1 Running 0 37s 10.244.3.13 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; pod-with-toleration-6c884d5b5b-8zx8q 1/1 Running 0 37s 10.244.2.9 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; pod-with-toleration-6c884d5b5b-c6jkl 1/1 Running 0 37s 10.244.2.10 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; pod-with-toleration-6c884d5b5b-gb6rn 1/1 Running 0 37s 10.244.2.8 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; pod-with-toleration-6c884d5b5b-hhcvf 1/1 Running 0 37s 10.244.3.14 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; You can also use a toleration to specify how long Kubernetes should wait before rescheduling a pod to another node if the node that the pod is running on becomes unready or unreachable. Let’s see the tolerations of one of your pods:\nvagrant@master-1:~$ kubectl get pod pod-with-toleration-6c884d5b5b-hhcvf -o yaml apiVersion: v1 kind: Pod ... spec: ... tolerations: - effect: NoSchedule key: node-type operator: Equal value: production - effect: NoExecute key: node.kubernetes.io/not-ready operator: Exists tolerationSeconds: 300 - effect: NoExecute key: node.kubernetes.io/unreachable operator: Exists tolerationSeconds: 300 ... These tolerations say that this pod tolerates a node being notReady or unreachable for 300 seconds. The Kubernetes Control Plane, when it detects that a node is no longer ready or no longer reachable, will wait for 300 seconds before it deletes the pod and reschedules it to another node.\nFinally, remove taints from node:\nvagrant@master-1:~$ kubectl taint node worker-1 node-type=production:NoSchedule- node/worker-1 untainted Node and Pod Affinity Node Affinity The nodeAffinity allows you to tell Kubernetes to schedule pods only to specific subsets of nodes. Each pod can define its own nodeAffinity rules. These allow you to specify either hard requirements or preferences. By specifying a preference, you tell Kubernetes which nodes you prefer for a specific pod, and Kubernetes will try to schedule the pod to one of those nodes. If that’s not possible, it will choose one of the other nodes (the nodeSelector is not).\nList the nodes in your cluster, along with their labels:\nvagrant@master-1:~$ kubectl get nodes --show-labels NAME STATUS ROLES AGE VERSION LABELS master-1 Ready control-plane,master 17d v1.23.0 ...,kubernetes.io/hostname=master-1,... master-2 Ready control-plane,master 17d v1.23.0 ...,kubernetes.io/hostname=master-2,... worker-1 Ready \u0026lt;none\u0026gt; 17d v1.23.0 ...,kubernetes.io/hostname=worker-1,kubernetes.io/os=linux worker-2 Ready \u0026lt;none\u0026gt; 17d v1.23.0 ...,kubernetes.io/hostname=worker-2,kubernetes.io/os=linux Chose one of your nodes, and add a label to it:\nvagrant@master-1:~$ kubectl label nodes worker-1 device=gpu node/worker-1 labeled vagrant@master-1:~$ kubectl label nodes worker-2 device=cpu node/worker-2 labeled vagrant@master-1:~$ kubectl get nodes --show-labels NAME STATUS ROLES AGE VERSION LABELS master-1 Ready control-plane,master 17d v1.23.0 ...,kubernetes.io/hostname=master-1,... master-2 Ready control-plane,master 17d v1.23.0 ...,kubernetes.io/hostname=master-2,... worker-1 Ready \u0026lt;none\u0026gt; 17d v1.23.0 ...,device=gpu,kubernetes.io/hostname=worker-1,kubernetes.io/os=linux worker-2 Ready \u0026lt;none\u0026gt; 17d v1.23.0 ...,device=cpu,kubernetes.io/hostname=worker-2,kubernetes.io/os=linux Schedule a Pod using required node affinity with pod-required-node-affinity.yaml:\napiVersion: apps/v1 kind: Deployment metadata: name: pod-required-node-affinity spec: selector: matchLabels: app: nginx replicas: 5 template: metadata: labels: app: nginx spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: device operator: In values: - gpu containers: - name: nginx image: nginx The requiredDuringSchedulingIgnoredDuringExecution means that the pod will get scheduled only on a node that has a device=gpu label.\nvagrant@master-1:~$ kubectl apply -f pod-required-node-affinity.yaml deployment.apps/pod-required-node-affinity created vagrant@master-1:~$ kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES pod-required-node-affinity-6fbcb8c97c-cvrwv 1/1 Running 0 2m24s 10.244.2.14 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; pod-required-node-affinity-6fbcb8c97c-hwhg5 1/1 Running 0 2m24s 10.244.2.17 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; pod-required-node-affinity-6fbcb8c97c-kcrc6 1/1 Running 0 2m24s 10.244.2.16 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; pod-required-node-affinity-6fbcb8c97c-m82d4 1/1 Running 0 2m24s 10.244.2.18 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; pod-required-node-affinity-6fbcb8c97c-qstx5 1/1 Running 0 2m24s 10.244.2.15 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; Schedule a Pod using preferred node affinity with pod-preferred-node-affinity.yaml:\napiVersion: apps/v1 kind: Deployment metadata: name: pod-preferred-node-affinity spec: selector: matchLabels: app: nginx replicas: 5 template: metadata: labels: app: nginx spec: affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 80 preference: matchExpressions: - key: device operator: In values: - gpu - weight: 20 preference: matchExpressions: - key: device operator: In values: - cpu containers: - name: nginx image: nginx The preferredDuringSchedulingIgnoredDuringExecution means the first preference rule (device=gpu) is important by setting its weight to 80, whereas the second one is much less important (weight is set to 20 with device=cpu).\nvagrant@master-1:~$ kubectl apply -f pod-preferred-node-affinity.yaml deployment.apps/pod-preferred-node-affinity created vagrant@master-1:~$ kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES pod-preferred-node-affinity-66f89c6b4d-59src 1/1 Running 0 36s 10.244.3.27 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; pod-preferred-node-affinity-66f89c6b4d-8dlwd 1/1 Running 0 36s 10.244.2.20 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; pod-preferred-node-affinity-66f89c6b4d-br4s4 1/1 Running 0 36s 10.244.2.21 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; pod-preferred-node-affinity-66f89c6b4d-s5sr9 1/1 Running 0 36s 10.244.2.19 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; pod-preferred-node-affinity-66f89c6b4d-zn4b2 1/1 Running 0 36s 10.244.2.22 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; Pod Affinity Inter-pod affinity and anti-affinity allow you to configure that a set of workloads should be co-located in the same defined topology, eg., the same node.\nImagine having a web application and an in-memory cache like redis pod. Having those pods deployed near to each other reduces latency and improves the performance of the app. In Kubernetes, you could use inter-pod affinity and anti-affinity to co-locate the web servers with the cache as much as possible by podAffinity.\napiVersion: apps/v1 kind: Deployment metadata: name: redis-cache spec: selector: matchLabels: app: store replicas: 2 template: metadata: labels: app: store spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - store topologyKey: \u0026#34;kubernetes.io/hostname\u0026#34; containers: - name: redis-server image: redis:3.2-alpine In the above example Deployment for the redis cache, the replicas get the label app=store. The podAntiAffinity rule tells the scheduler to avoid placing multiple replicas with the app=store label on a single node. This creates each cache in a separate node. In this case, we have 2 replicas corresponding to 2 worker nodes.\nvagrant@master-1:~$ kubectl apply -f redis-pod-anti-affinity.yaml deployment.apps/redis-cache created vagrant@master-1:~$ kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES redis-cache-6b7b79d589-mfkdh 1/1 Running 0 75s 10.244.2.23 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; redis-cache-6b7b79d589-vzmlf 1/1 Running 0 74s 10.244.3.28 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; apiVersion: apps/v1 kind: Deployment metadata: name: web-server spec: selector: matchLabels: app: web-store replicas: 2 template: metadata: labels: app: web-store spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - web-store topologyKey: \u0026#34;kubernetes.io/hostname\u0026#34; podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app operator: In values: - store topologyKey: \u0026#34;kubernetes.io/hostname\u0026#34; containers: - name: web-app image: nginx:1.16-alpine The above Deployment for the web servers creates replicas with the label app=web-store. The podAffinity rule tells the scheduler to place each replica on a node that has a Pod with the label app=store. The podAntiAffinity rule tells the scheduler to avoid placing multiple app=web-store servers on a single node.\nvagrant@master-1:~$ kubectl apply -f webapp-pod-affinity.yaml deployment.apps/web-server created vagrant@master-1:~$ kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES redis-cache-6b7b79d589-mfkdh 1/1 Running 0 6m21s 10.244.2.23 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; redis-cache-6b7b79d589-vzmlf 1/1 Running 0 6m20s 10.244.3.28 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; web-server-f6798875f-b5nn2 1/1 Running 0 17s 10.244.2.24 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; web-server-f6798875f-s625n 1/1 Running 0 16s 10.244.3.29 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; Suppose we need scaling the replicas up to 3, we will get the Pending status:\nvagrant@master-1:~$ kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES redis-cache-6b7b79d589-gvcdh 1/1 Running 0 2m54s 10.244.3.32 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; redis-cache-6b7b79d589-hl44v 0/1 Pending 0 2m24s \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; redis-cache-6b7b79d589-pfxlc 1/1 Running 0 2m54s 10.244.2.27 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; web-server-f6798875f-2j65x 1/1 Running 0 2m45s 10.244.3.33 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; web-server-f6798875f-dp6pj 0/1 Pending 0 2m14s \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; web-server-f6798875f-k5595 1/1 Running 0 2m45s 10.244.2.28 worker-1 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; Let’s describe the Pending pod:\nvagrant@master-1:~$ kubectl describe pod web-server-f6798875f-dp6pj ... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 8m51s default-scheduler 0/4 nodes are available: 2 node(s) didn\u0026#39;t match pod anti-affinity rules, 2 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn\u0026#39;t tolerate. ... My cluster have 4 nodes with 2 master nodes and 2 worker nodes. With podAntiAffinity, the scheduler will create each pod in a separate node, we have 3 pods with replicas: 3 (2 pods on 2 worker nodes, 1 pod on 1 master node). Because the master node have taint, the pod can’t be scheduled on it.\nNode Name nodeName is a more direct form of node selection than nodeAffinity or nodeSelector. nodeName is a field in the Pod spec. If the nodeName field is not empty, the scheduler ignores the Pod and the kubelet on the named node tries to place the Pod on that node.\nThe nodeName have some limitations such as: the Pod will not run if the named node does not exist, the Pod will fail if the named node does not have the resources to accommodate the Pod, and node names in cloud environments are not always stable.\napiVersion: v1 kind: Pod metadata: name: nginx spec: containers: - name: nginx image: nginx nodeName: worker-2 vagrant@master-1:~$ kubectl apply -f pod-with-node-name.yaml pod/nginx created vagrant@master-1:~$ kubectl get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx 1/1 Running 0 14s 10.244.3.42 worker-2 \u0026lt;none\u0026gt; \u0026lt;none\u0026gt; Pod Health Probes Configure Liveness, Readiness and Startup Probes Configure livenessProbe by execute command # exec-liveness.yaml apiVersion: v1 kind: Pod metadata: labels: test: liveness name: liveness-exec spec: containers: - name: liveness image: busybox args: - /bin/sh - -c - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600 livenessProbe: exec: # the kubelet executes the command to perform a probe command: - cat - /tmp/healthy initialDelaySeconds: 5 # perform a liveness probe every 5 seconds periodSeconds: 5 # wait 5 seconds before performing the first probe In livenessProbe section, the kubelet executes the command cat /tmp/healthy to perform a probe.\ninitialDelaySeconds: perform a liveness probe every 5 seconds.\nperiodSeconds: wait 5 seconds before performing the first probe.\nIf the command succeeds (returns 0), the kubelet considers the container to be alive and healthy. Otherwise, the kubelet kills the container and restarts it (returns a non-zero value).\nLet’s create the pod:\nkubectl apply -f exec-liveness.yaml Now we can describe the pod:\nkubectl describe pod liveness-exec Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 21s default-scheduler Successfully assigned default/liveness-exec to minikube Normal Pulling 20s kubelet Pulling image \u0026#34;busybox\u0026#34; Normal Pulled 17s kubelet Successfully pulled image \u0026#34;busybox\u0026#34; in 3.800450597s Normal Created 16s kubelet Created container liveness Normal Started 16s kubelet Started container livenesss Wait for half a minute, then describe the pod:\nEvents: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 66s default-scheduler Successfully assigned default/liveness-exec to minikube Normal Pulling 65s kubelet Pulling image \u0026#34;busybox\u0026#34; Normal Pulled 62s kubelet Successfully pulled image \u0026#34;busybox\u0026#34; in 3.800450597s Normal Created 61s kubelet Created container liveness Normal Started 61s kubelet Started container liveness Warning Unhealthy 21s (x3 over 31s) kubelet Liveness probe failed: cat: can\u0026#39;t open \u0026#39;/tmp/healthy\u0026#39;: No such file or directory Normal Killing 21s kubelet Container liveness failed liveness probe, will be restarted As above, the pod is unhealthy and the kubelet will kill the container and restart it:\nEvents: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 83s default-scheduler Successfully assigned default/liveness-exec to minikube Normal Pulled 79s kubelet Successfully pulled image \u0026#34;busybox\u0026#34; in 3.800450597s Warning Unhealthy 38s (x3 over 48s) kubelet Liveness probe failed: cat: can\u0026#39;t open \u0026#39;/tmp/healthy\u0026#39;: No such file or directory Normal Killing 38s kubelet Container liveness failed liveness probe, will be restarted Normal Pulling 8s (x2 over 82s) kubelet Pulling image \u0026#34;busybox\u0026#34; Normal Created 4s (x2 over 78s) kubelet Created container liveness Normal Started 4s (x2 over 78s) kubelet Started container liveness Normal Pulled 4s kubelet Successfully pulled image \u0026#34;busybox\u0026#34; in 3.417375064s Configure livenessProbe by HTTP GET request # http-liveness.yaml apiVersion: v1 kind: Pod metadata: labels: test: liveness name: liveness-http spec: containers: - name: liveness image: k8s.gcr.io/liveness args: - /server livenessProbe: httpGet: # the kubelet sends HTTP GET request path: /healthz port: 8080 # listening on port 8080 httpHeaders: - name: Custom-Header value: Awesome initialDelaySeconds: 3 periodSeconds: 3 To perform a probe, the kubelet sends an HTTP GET request to the server that is running in the container and listening on port 8080. If the handler for the server’s /healthz path returns a success code ([200;400)), the kubelet considers the container to be alive and healthy. If the handler returns a failure code, the kubelet kills the container and restarts it.\nkubectl apply -f http-liveness.yaml kubectl get pods NAME READY STATUS RESTARTS AGE liveness-http 1/1 Running 2 (2s ago) 41s After a minute, let’s describe the pod:\nkubectl describe pods liveness-http Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 45s default-scheduler Successfully assigned default/liveness-http to minikube Normal Pulled 43s kubelet Successfully pulled image \u0026#34;k8s.gcr.io/liveness\u0026#34; in 1.461349146s Normal Pulled 23s kubelet Successfully pulled image \u0026#34;k8s.gcr.io/liveness\u0026#34; in 1.51144169s Warning Unhealthy 6s (x6 over 30s) kubelet Liveness probe failed: HTTP probe failed with statuscode: 500 Normal Killing 6s (x2 over 24s) kubelet Container liveness failed liveness probe, will be restarted Normal Pulling 6s (x3 over 44s) kubelet Pulling image \u0026#34;k8s.gcr.io/liveness\u0026#34; Normal Created 5s (x3 over 43s) kubelet Created container liveness Normal Started 5s (x3 over 43s) kubelet Started container liveness Normal Pulled 5s kubelet Successfully pulled image \u0026#34;k8s.gcr.io/liveness\u0026#34; in 1.421177595s Configure livenessProbe, readinessProbe by TCP socket In Kubernetes, you can use readinessProbe to ensure that traffic does not reach a container that is not ready for it. For example, an application might need to load large data or configuration files during startup, or depend on external services after startup. In such cases, you don’t want to kill the application, but you don’t want to send it requests either. Note that readinessProbe runs on the container during its whole lifecycle.\napiVersion: v1 kind: Pod metadata: name: myapp-health-probes spec: containers: - image: nginx name: myapp-container ports: - containerPort: 80 readinessProbe: tcpSocket: port: 80 initialDelaySeconds: 10 periodSeconds: 5 livenessProbe: tcpSocket: port: 80 initialDelaySeconds: 5 periodSeconds: 15 Let’s create pod:\nkubectl apply -f tcp-liveness-readiness.yaml kubectl describe pod myapp-health-probes Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 8m30s default-scheduler Successfully assigned default/myapp-health-probes to minikube Normal Pulling 8m29s kubelet Pulling image \u0026#34;nginx\u0026#34; Normal Pulled 8m13s kubelet Successfully pulled image \u0026#34;nginx\u0026#34; in 16.010486815s Normal Created 8m13s kubelet Created container myapp-container Normal Started 8m13s kubelet Started container myapp-container Configure startupProbe Additionally, you can protect slow starting containers with startupProbe. Because sometimes, you have to deal with legacy applications that might require an additional startup time on their first initialization.\nThe trick is to set up a startupProbe with the same command, HTTP or TCP check, with a failureThreshold * periodSeconds long enough to cover the worse case startup time.\nports: - name: liveness-port containerPort: 8080 hostPort: 8080 livenessProbe: httpGet: path: /healthz port: liveness-port failureThreshold: 1 periodSeconds: 10 startupProbe: httpGet: path: /healthz port: liveness-port failureThreshold: 30 periodSeconds: 10 As the startupProbe section, the application will have a maximum of 30 * 10 = 300s to finish its startup.\nOnce the startupProbe has succeeded once, the livenessProbe takes over to provide a fast response to container deadlocks. If the startupProbe never succeeds, the container is killed after 300s and subject to the pod’s restartPolicy.\nManaging Resource Requests and Limits Manage pod resource by LimitRange apiVersion: v1 kind: LimitRange metadata: name: example spec: limits: - type: Pod min: cpu: 50m memory: 5Mi max: cpu: 1 memory: 1Gi - type: Container defaultRequest: cpu: 100m memory: 10Mi default: cpu: 200m memory: 100Mi min: cpu: 50m memory: 5Mi max: cpu: 1 memory: 1Gi maxLimitRequestRatio: cpu: 4 memory: 10 - type: PersistentVolumeClaim min: storage: 1Gi max: storage: 10Gi kubectl apply -f limit-range.yaml Now we try creating a pod that requests more CPU than allowed by the LimitRange:\napiVersion: v1 kind: Pod metadata: name: big-pod spec: containers: - image: busybox args: [\u0026#34;sleep\u0026#34;, \u0026#34;9999999\u0026#34;] name: main resources: requests: cpu: 2 We will get error:\n$ kubectl apply -f pod-with-big-resource.yaml The Pod \u0026#34;big-pod\u0026#34; is invalid: spec.containers[0].resources.requests: Invalid value: \u0026#34;2\u0026#34;: must be less than or equal to cpu limit Manage namespace resource by ResourceQuota Step 1: Create demo namespace\nkubectl create namespace deployment-demo Step 2: Use Resource Quotas\nCreate resource-quota.yaml file:\napiVersion: v1 kind: ResourceQuota metadata: name: mem-cpu-quota namespace: deployment-demo spec: hard: requests.cpu: \u0026#34;1\u0026#34; requests.memory: 2Gi limits.cpu: \u0026#34;2\u0026#34; limits.memory: 4Gi Apply to create the resource quota for deployment-demo namespace.\nkubectl apply -f resource-quota.yaml Now, let’s describe the deployment-demo namespace\nkubectl describe namespace deployment-demo Name: deployment-demo Labels: kubernetes.io/metadata.name=deployment-demo Annotations: \u0026lt;none\u0026gt; Status: Active Resource Quotas Name: mem-cpu-quota Resource Used Hard -------- --- --- limits.cpu 0 2 limits.memory 0 4Gi requests.cpu 0 1 requests.memory 0 2Gi No LimitRange resource. Step 3: Create the nginx deployment\nFollowing is my-deployment.yaml:\napiVersion: apps/v1 kind: Deployment metadata: name: my-app namespace: deployment-demo labels: app: my-app spec: replicas: 1 selector: matchLabels: app: my-app template: metadata: labels: app: my-app spec: containers: - name: my-app image: nginx:1.20 resources: requests: memory: \u0026#34;64Mi\u0026#34; cpu: \u0026#34;250m\u0026#34; limits: memory: \u0026#34;128Mi\u0026#34; cpu: \u0026#34;500m\u0026#34; ports: - containerPort: 80 - name: logging-sidecar image: busybox:1.28 command: [\u0026#39;sh\u0026#39;, \u0026#39;-c\u0026#39;, \u0026#34;while true; do echo sync logs; sleep 20; done\u0026#34;] resources: requests: memory: \u0026#34;32Mi\u0026#34; cpu: \u0026#34;125m\u0026#34; limits: memory: \u0026#34;64Mi\u0026#34; cpu: \u0026#34;250m\u0026#34; Apply to create the deployment:\nkubectl apply -f my-deployment.yaml Now, let’s describe my deployment in deployment-demo namespace:\nkubectl describe -n deployment-demo deployments.apps my-app Name: my-app Namespace: deployment-demo CreationTimestamp: Tue, 05 Apr 2022 23:17:28 -0400 Labels: app=my-app Annotations: deployment.kubernetes.io/revision: 1 Selector: app=my-app Replicas: 1 desired | 1 updated | 1 total | 1 available | 0 unavailable StrategyType: RollingUpdate MinReadySeconds: 0 RollingUpdateStrategy: 25% max unavailable, 25% max surge Pod Template: Labels: app=my-app Containers: my-app: Image: nginx:1.20 Port: 80/TCP Host Port: 0/TCP Limits: cpu: 500m memory: 128Mi Requests: cpu: 250m memory: 64Mi Environment: \u0026lt;none\u0026gt; Mounts: \u0026lt;none\u0026gt; logging-sidecar: Image: busybox:1.28 Port: \u0026lt;none\u0026gt; Host Port: \u0026lt;none\u0026gt; Command: sh -c while true; do echo sync logs; sleep 20; done Limits: cpu: 250m memory: 64Mi Requests: cpu: 125m memory: 32Mi Environment: \u0026lt;none\u0026gt; Mounts: \u0026lt;none\u0026gt; Volumes: \u0026lt;none\u0026gt; Conditions: Type Status Reason ---- ------ ------ Available True MinimumReplicasAvailable Progressing True NewReplicaSetAvailable OldReplicaSets: \u0026lt;none\u0026gt; NewReplicaSet: my-app-57d67fffc4 (1/1 replicas created) Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal ScalingReplicaSet 36s deployment-controller Scaled up replica set my-app-57d67fffc4 to 1 Step 4: Create the service and expose the deployment\nkubectl expose deployment my-app -n deployment-demo --type=NodePort --name=my-service kubectl get svc -n deployment-demo kubectl get svc -n deployment-demo NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE my-service NodePort 10.97.225.53 \u0026lt;none\u0026gt; 80:30991/TCP 20s Now, use the external IP address (http://\u0026lt;minikube-ip\u0026gt;:\u0026lt;port\u0026gt;) to access the nginx application:\ncurl http://192.168.49.2:30991 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;Welcome to nginx!\u0026lt;/title\u0026gt; \u0026lt;style\u0026gt; body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } \u0026lt;/style\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;Welcome to nginx!\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;If you see this page, the nginx web server is successfully installed and working. Further configuration is required.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;For online documentation and support please refer to \u0026lt;a href=\u0026#34;http://nginx.org/\u0026#34;\u0026gt;nginx.org\u0026lt;/a\u0026gt;.\u0026lt;br/\u0026gt; Commercial support is available at \u0026lt;a href=\u0026#34;http://nginx.com/\u0026#34;\u0026gt;nginx.com\u0026lt;/a\u0026gt;.\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;\u0026lt;em\u0026gt;Thank you for using nginx.\u0026lt;/em\u0026gt;\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; The ResourceQuota have many .spec.hard properties other:\napiVersion: v1 kind: ResourceQuota metadata: name: object-storage-quota spec: hard: pods: 10 replicationcontrollers: 5 secrets: 10 configmaps: 10 persistentvolumeclaims: 5 services: 5 services.loadbalancers: 1 services.nodeports: 2 ssd.storageclass.storage.k8s.io/persistentvolumeclaims: 2 requests.storage: 500Gi ssd.storageclass.storage.k8s.io/requests.storage: 300Gi standard.storageclass.storage.k8s.io/requests.storage: 1Ti You can get full source code here: job, statefulsets, taints-and-tolerations, node-pod-affinity, pod-healthy-probe, manage-resource.\n","permalink":"https://hoangph3.github.io/posts/kubernetes_workloads_scheduling/","summary":"Control how Kubernetes schedules and manages workloads: Jobs, StatefulSets, taints/tolerations, affinity rules, health probes and resource limits.","title":"Kubernetes Workloads and Scheduling"},{"content":"Custom Conda environments in MLServer It’s not unusual that model runtimes require extra dependencies that are not direct dependencies of MLServer. This is the case when we want to use custom runtimes.\nIn these cases, since these dependencies (or dependency versions) are not known in advance by MLServer, they won’t be included in the default seldonio/mlserver Docker image. To cover these cases, the seldonio/mlserver Docker image allows you to load custom environments before starting the server itself.\nThis example will walk you through how to create and save an custom environment, so that it can be loaded in MLServer without any extra change to the seldonio/mlserver Docker image.\nDefine our environment For this example, we will create a custom environment to serve a model trained with Faiss. The first step will be define this environment, using a environment.yml.\nNote that these environments can also be created on the fly as we go, and then serialised later.\n%%writefile environment.yml name: faiss channels: - conda-forge dependencies: - python == 3.8 - faiss-cpu - pip Train model in our custom environment The first step will be to create and activate an environment which reflects what’s outlined in our environment.yml file.\nNOTE: If you are running this from a Jupyter Notebook, you will need to restart your Jupyter instance so that it runs from this environment.\nbash create_env.sh Serialise our custom environment Lastly, we will need to serialise our environment in the format expected by MLServer. To do that, we will use a tool called conda-pack.\nThis tool, will save a portable version of our environment as a .tar.gz file, also known as tarball.\nbash export_env.sh Serving Now that we have defined our environment (and we’ve got a sample artifact trained in that environment), we can move to serving our model.\nTo do that, we will first need to select the right runtime through a model-settings.json config file.\n%%writefile model-settings.json { \u0026#34;name\u0026#34;: \u0026#34;mnist\u0026#34;, \u0026#34;implementation\u0026#34;: \u0026#34;serve_model.Mnist\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;environment_tarball\u0026#34;: \u0026#34;/mnt/models/faiss.tar.gz\u0026#34; } } We can then spin up our model, using our custom environment, leveraging MLServer’s Docker image. Keep in mind that you will need Docker installed in your machine to run this example.\nOur Docker command will need to take into account the following points:\nMount the example’s folder as a volume so that it can be accessed from within the container. Let MLServer know that our custom environment’s tarball can be found as old-sklearn.tar.gz. Expose port 9080 so that we can send requests from the outside. From the command line, this can be done using Docker’s CLI as:\nbash run_docker.sh 2023-07-29 07:32:12,619 [mlserver] INFO - Extracting environment tarball from /mnt/models/faiss.tar.gz... 2023-07-29 07:32:16,004 [mlserver.parallel] DEBUG - Starting response processing loop... 2023-07-29 07:32:17.343874: I tensorflow/tsl/cuda/cudart_stub.cc:28] Could not find cuda drivers on your machine, GPU will not be used. 2023-07-29 07:32:17.395480: I tensorflow/tsl/cuda/cudart_stub.cc:28] Could not find cuda drivers on your machine, GPU will not be used. 2023-07-29 07:32:17.396004: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations. To enable the following instructions: AVX2 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags. 2023-07-29 07:32:18.412235: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT Model: \u0026#34;sequential\u0026#34; _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= reshape (Reshape) (None, 28, 28, 1) 0 conv2d (Conv2D) (None, 26, 26, 32) 320 flatten (Flatten) (None, 21632) 0 dense (Dense) (None, 128) 2769024 dense_1 (Dense) (None, 10) 1290 ================================================================= Total params: 2770634 (10.57 MB) Trainable params: 2770634 (10.57 MB) Non-trainable params: 0 (0.00 Byte) _________________________________________________________________ Load faiss: 1.7.4 2023-07-29 07:32:19,779 [mlserver] INFO - Loaded model \u0026#39;mnist\u0026#39; succesfully. 2023-07-29 07:32:19,781 [mlserver] INFO - Loaded model \u0026#39;mnist\u0026#39; succesfully. Note that we need to keep the server running in the background while we send requests. Therefore, it’s best to run this command on a separate terminal session.\nSend test inference request We now have our model being served by mlserver. To make sure that everything is working as expected, let’s send a request from our test set.\nFor that, we can use the Python types that mlserver provides out of box, or we can build our request manually.\npython3 test.py You can get full source code here!\n","permalink":"https://hoangph3.github.io/posts/ml_serving_custom_env/","summary":"Build a custom serving environment to run TensorFlow models with extra dependencies.","title":"Serving TensorFlow Models with a Custom Environment"},{"content":"Content Type Decoding MLServer extends the V2 inference protocol by adding support for a content_type annotation. This annotation can be provided either through the model metadata parameters, or through the input parameters. By leveraging the content_type annotation, we can provide the necessary information to MLServer so that it can decode the input payload from the “wire” V2 protocol to something meaningful to the model / user (e.g. a NumPy array).\nThis example will walk you through some examples which illustrate how this works, and how it can be extended.\nEcho Inference Runtime To start with, we will write a dummy runtime which just prints the input, the decoded input and returns it. This will serve as a testbed to showcase how the content_type support works.\nLater on, we will extend this runtime by adding custom codecs that will decode our V2 payload to custom types.\n%%writefile runtime.py import json from mlserver import MLModel from mlserver.types import InferenceRequest, InferenceResponse, ResponseOutput from mlserver.codecs import DecodedParameterName _to_exclude = { \u0026#34;parameters\u0026#34;: {DecodedParameterName, \u0026#34;headers\u0026#34;}, \u0026#39;inputs\u0026#39;: {\u0026#34;__all__\u0026#34;: {\u0026#34;parameters\u0026#34;: {DecodedParameterName, \u0026#34;headers\u0026#34;}}} } class EchoRuntime(MLModel): async def predict(self, payload: InferenceRequest) -\u0026gt; InferenceResponse: outputs = [] for request_input in payload.inputs: decoded_input = self.decode(request_input) print(f\u0026#34;------ Encoded Input ({request_input.name}) ------\u0026#34;) as_dict = request_input.dict(exclude=_to_exclude) # type: ignore print(json.dumps(as_dict, indent=2)) print(f\u0026#34;------ Decoded input ({request_input.name}) ------\u0026#34;) print(decoded_input) outputs.append( ResponseOutput( name=request_input.name, datatype=request_input.datatype, shape=request_input.shape, data=request_input.data ) ) return InferenceResponse(model_name=self.name, outputs=outputs) As you can see above, this runtime will decode the incoming payloads by calling the self.decode() helper method. This method will check what’s the right content type for each input in the following order:\nIs there any content type defined in the inputs[].parameters.content_type field within the request payload? Is there any content type defined in the inputs[].parameters.content_type field within the model metadata? Is there any default content type that should be assumed? Model Settings In order to enable this runtime, we will also create a model-settings.json file. This file should be present (or accessible from) in the folder where we run mlserver start ..\n%%writefile model-settings.json { \u0026#34;name\u0026#34;: \u0026#34;content-type-example\u0026#34;, \u0026#34;implementation\u0026#34;: \u0026#34;runtime.EchoRuntime\u0026#34; } Request Inputs Our initial step will be to decide the content type based on the incoming inputs[].parameters field. For this, we will start our MLServer in the background (e.g. running mlserver start .)\nimport requests payload = { \u0026#34;inputs\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;parameters-np\u0026#34;, \u0026#34;datatype\u0026#34;: \u0026#34;INT32\u0026#34;, \u0026#34;shape\u0026#34;: [2, 2], \u0026#34;data\u0026#34;: [1, 2, 3, 4], \u0026#34;parameters\u0026#34;: { \u0026#34;content_type\u0026#34;: \u0026#34;np\u0026#34; } }, { \u0026#34;name\u0026#34;: \u0026#34;parameters-str\u0026#34;, \u0026#34;datatype\u0026#34;: \u0026#34;BYTES\u0026#34;, \u0026#34;shape\u0026#34;: [1], \u0026#34;data\u0026#34;: \u0026#34;hello world 😁\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;content_type\u0026#34;: \u0026#34;str\u0026#34; } } ] } response = requests.post( \u0026#34;http://localhost:8080/v2/models/content-type-example/infer\u0026#34;, json=payload ) Codecs As you’ve probably already noticed, writing request payloads compliant with both the V2 Inference Protocol requires a certain knowledge about both the V2 spec and the structure expected by each content type. To account for this and simplify usage, the MLServer package exposes a set of utilities which will help you interact with your models via the V2 protocol.\nThese helpers are mainly shaped as “codecs”. That is, abstractions which know how to “encode” and “decode” arbitrary Python datatypes to and from the V2 Inference Protocol.\nGenerally, we recommend using the existing set of codecs to generate your V2 payloads. This will ensure that requests and responses follow the right structure, and should provide a more seamless experience.\nFollowing with our previous example, the same code could be rewritten using codecs as:\nimport requests import numpy as np from mlserver.types import InferenceRequest, InferenceResponse from mlserver.codecs import NumpyCodec, StringCodec parameters_np = np.array([[1, 2], [3, 4]]) parameters_str = [\u0026#34;hello world 😁\u0026#34;] payload = InferenceRequest( inputs=[ NumpyCodec.encode_input(\u0026#34;parameters-np\u0026#34;, parameters_np), # The `use_bytes=False` flag will ensure that the encoded payload is JSON-compatible StringCodec.encode_input(\u0026#34;parameters-str\u0026#34;, parameters_str, use_bytes=False), ] ) response = requests.post( \u0026#34;http://localhost:8080/v2/models/content-type-example/infer\u0026#34;, json=payload.dict() ) response_payload = InferenceResponse.parse_raw(response.text) print(NumpyCodec.decode_output(response_payload.outputs[0])) print(StringCodec.decode_output(response_payload.outputs[1])) Note that the rewritten snippet now makes use of the built-in InferenceRequest class, which represents a V2 inference request. On top of that, it also uses the NumpyCodec and StringCodec implementations, which know how to encode a Numpy array and a list of strings into V2-compatible request inputs.\nModel Metadata Our next step will be to define the expected content type through the model metadata. This can be done by extending the model-settings.json file, and adding a section on inputs.\n%%writefile model-settings.json { \u0026#34;name\u0026#34;: \u0026#34;content-type-example\u0026#34;, \u0026#34;implementation\u0026#34;: \u0026#34;runtime.EchoRuntime\u0026#34;, \u0026#34;inputs\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;metadata-np\u0026#34;, \u0026#34;datatype\u0026#34;: \u0026#34;INT32\u0026#34;, \u0026#34;shape\u0026#34;: [2, 2], \u0026#34;parameters\u0026#34;: { \u0026#34;content_type\u0026#34;: \u0026#34;np\u0026#34; } }, { \u0026#34;name\u0026#34;: \u0026#34;metadata-str\u0026#34;, \u0026#34;datatype\u0026#34;: \u0026#34;BYTES\u0026#34;, \u0026#34;shape\u0026#34;: [11], \u0026#34;parameters\u0026#34;: { \u0026#34;content_type\u0026#34;: \u0026#34;str\u0026#34; } } ] } After adding this metadata, we will re-start MLServer (e.g. mlserver start .) and we will send a new request without any explicit parameters.\nimport requests payload = { \u0026#34;inputs\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;metadata-np\u0026#34;, \u0026#34;datatype\u0026#34;: \u0026#34;INT32\u0026#34;, \u0026#34;shape\u0026#34;: [2, 2], \u0026#34;data\u0026#34;: [1, 2, 3, 4], }, { \u0026#34;name\u0026#34;: \u0026#34;metadata-str\u0026#34;, \u0026#34;datatype\u0026#34;: \u0026#34;BYTES\u0026#34;, \u0026#34;shape\u0026#34;: [11], \u0026#34;data\u0026#34;: \u0026#34;hello world 😁\u0026#34;, } ] } response = requests.post( \u0026#34;http://localhost:8080/v2/models/content-type-example/infer\u0026#34;, json=payload ) As you should be able to see in the server logs, MLServer will cross-reference the input names against the model metadata to find the right content type.\nCustom Codecs There may be cases where a custom inference runtime may need to encode / decode to custom datatypes. As an example, we can think of computer vision models which may only operate with pillow image objects.\nIn these scenarios, it’s possible to extend the Codec interface to write our custom encoding logic. A Codec, is simply an object which defines a decode() and encode() methods. To illustrate how this would work, we will extend our custom runtime to add a custom PillowCodec.\n%%writefile runtime.py import io import json from PIL import Image from mlserver import MLModel from mlserver.types import ( InferenceRequest, InferenceResponse, RequestInput, ResponseOutput, ) from mlserver.codecs import NumpyCodec, register_input_codec, DecodedParameterName from mlserver.codecs.utils import InputOrOutput _to_exclude = { \u0026#34;parameters\u0026#34;: {DecodedParameterName}, \u0026#34;inputs\u0026#34;: {\u0026#34;__all__\u0026#34;: {\u0026#34;parameters\u0026#34;: {DecodedParameterName}}}, } @register_input_codec class PillowCodec(NumpyCodec): ContentType = \u0026#34;img\u0026#34; DefaultMode = \u0026#34;L\u0026#34; @classmethod def can_encode(cls, payload: Image) -\u0026gt; bool: return isinstance(payload, Image) @classmethod def _decode(cls, input_or_output: InputOrOutput) -\u0026gt; Image: if input_or_output.datatype != \u0026#34;BYTES\u0026#34;: # If not bytes, assume it\u0026#39;s an array image_array = super().decode_input(input_or_output) # type: ignore return Image.fromarray(image_array, mode=cls.DefaultMode) encoded = input_or_output.data.__root__ if isinstance(encoded, str): encoded = encoded.encode() return Image.frombytes( mode=cls.DefaultMode, size=input_or_output.shape, data=encoded ) @classmethod def encode_output(cls, name: str, payload: Image) -\u0026gt; ResponseOutput: # type: ignore byte_array = io.BytesIO() payload.save(byte_array, mode=cls.DefaultMode) return ResponseOutput( name=name, shape=payload.size, datatype=\u0026#34;BYTES\u0026#34;, data=byte_array.getvalue() ) @classmethod def decode_output(cls, response_output: ResponseOutput) -\u0026gt; Image: return cls._decode(response_output) @classmethod def encode_input(cls, name: str, payload: Image) -\u0026gt; RequestInput: # type: ignore output = cls.encode_output(name, payload) return RequestInput( name=output.name, shape=output.shape, datatype=output.datatype, data=output.data, ) @classmethod def decode_input(cls, request_input: RequestInput) -\u0026gt; Image: return cls._decode(request_input) class EchoRuntime(MLModel): async def predict(self, payload: InferenceRequest) -\u0026gt; InferenceResponse: outputs = [] for request_input in payload.inputs: decoded_input = self.decode(request_input) print(f\u0026#34;------ Encoded Input ({request_input.name}) ------\u0026#34;) as_dict = request_input.dict(exclude=_to_exclude) # type: ignore print(json.dumps(as_dict, indent=2)) print(f\u0026#34;------ Decoded input ({request_input.name}) ------\u0026#34;) print(decoded_input) outputs.append( ResponseOutput( name=request_input.name, datatype=request_input.datatype, shape=request_input.shape, data=request_input.data, ) ) return InferenceResponse(model_name=self.name, outputs=outputs) We should now be able to restart our instance of MLServer (i.e. with the mlserver start . command), to send a few test requests.\nimport requests payload = { \u0026#34;inputs\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;image-int32\u0026#34;, \u0026#34;datatype\u0026#34;: \u0026#34;INT32\u0026#34;, \u0026#34;shape\u0026#34;: [8, 8], \u0026#34;data\u0026#34;: [ 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0 ], \u0026#34;parameters\u0026#34;: { \u0026#34;content_type\u0026#34;: \u0026#34;img\u0026#34; } }, { \u0026#34;name\u0026#34;: \u0026#34;image-bytes\u0026#34;, \u0026#34;datatype\u0026#34;: \u0026#34;BYTES\u0026#34;, \u0026#34;shape\u0026#34;: [8, 8], \u0026#34;data\u0026#34;: ( \u0026#34;10101010\u0026#34; \u0026#34;10101010\u0026#34; \u0026#34;10101010\u0026#34; \u0026#34;10101010\u0026#34; \u0026#34;10101010\u0026#34; \u0026#34;10101010\u0026#34; \u0026#34;10101010\u0026#34; \u0026#34;10101010\u0026#34; ), \u0026#34;parameters\u0026#34;: { \u0026#34;content_type\u0026#34;: \u0026#34;img\u0026#34; } } ] } response = requests.post( \u0026#34;http://localhost:8080/v2/models/content-type-example/infer\u0026#34;, json=payload ) As you should be able to see in the MLServer logs, the server is now able to decode the payload into a Pillow image. This example also illustrates how Codec objects can be compatible with multiple datatype values (e.g. tensor and BYTES in this case).\nRequest Codecs So far, we’ve seen how you can specify codecs so that they get applied at the input level. However, it is also possible to use request-wide codecs that aggregate multiple inputs to decode the payload. This is usually relevant for cases where the models expect a multi-column input type, like a Pandas DataFrame.\nTo illustrate this, we will first tweak our EchoRuntime so that it prints the decoded contents at the request level.\n%%writefile runtime.py import json from mlserver import MLModel from mlserver.types import InferenceRequest, InferenceResponse, ResponseOutput from mlserver.codecs import DecodedParameterName _to_exclude = { \u0026#34;parameters\u0026#34;: {DecodedParameterName}, \u0026#39;inputs\u0026#39;: {\u0026#34;__all__\u0026#34;: {\u0026#34;parameters\u0026#34;: {DecodedParameterName}}} } class EchoRuntime(MLModel): async def predict(self, payload: InferenceRequest) -\u0026gt; InferenceResponse: print(\u0026#34;------ Encoded Input (request) ------\u0026#34;) as_dict = payload.dict(exclude=_to_exclude) # type: ignore print(json.dumps(as_dict, indent=2)) print(\u0026#34;------ Decoded input (request) ------\u0026#34;) decoded_request = None if payload.parameters: decoded_request = getattr(payload.parameters, DecodedParameterName) print(decoded_request) outputs = [] for request_input in payload.inputs: outputs.append( ResponseOutput( name=request_input.name, datatype=request_input.datatype, shape=request_input.shape, data=request_input.data ) ) return InferenceResponse(model_name=self.name, outputs=outputs) We should now be able to restart our instance of MLServer (i.e. with the mlserver start . command), to send a few test requests.\nimport requests payload = { \u0026#34;inputs\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;parameters-np\u0026#34;, \u0026#34;datatype\u0026#34;: \u0026#34;INT32\u0026#34;, \u0026#34;shape\u0026#34;: [2, 2], \u0026#34;data\u0026#34;: [1, 2, 3, 4], \u0026#34;parameters\u0026#34;: { \u0026#34;content_type\u0026#34;: \u0026#34;np\u0026#34; } }, { \u0026#34;name\u0026#34;: \u0026#34;parameters-str\u0026#34;, \u0026#34;datatype\u0026#34;: \u0026#34;BYTES\u0026#34;, \u0026#34;shape\u0026#34;: [2, 11], \u0026#34;data\u0026#34;: [\u0026#34;hello world 😁\u0026#34;, \u0026#34;bye bye 😁\u0026#34;], \u0026#34;parameters\u0026#34;: { \u0026#34;content_type\u0026#34;: \u0026#34;str\u0026#34; } } ], \u0026#34;parameters\u0026#34;: { \u0026#34;content_type\u0026#34;: \u0026#34;pd\u0026#34; } } response = requests.post( \u0026#34;http://localhost:8080/v2/models/content-type-example/infer\u0026#34;, json=payload ) You can get full source code here!\n","permalink":"https://hoangph3.github.io/posts/ml_serving_content_type/","summary":"Explore how TensorFlow Serving handles different request content types for inference.","title":"TensorFlow Serving: Handling Content Types"},{"content":"Feature Store with Spark Streaming, Kafka, DataLake Feature Store provides a centralized location to store and document features that will be used in machine learning models and can be shared across projects.\nA Feature Store solution might address one or a combination of these problems:\nFeature management: A feature store can help teams share and discover features, as well as manage roles and sharing settings for each feature (feature catalog). Feature computation: A feature store can help with both performing feature computation and storing the results of this computation (data warehouse). Feature consistency: A key selling point of modern feature stores is that they unify the logic for both batch features and streaming features, ensuring the consistency between features during training and features during inference. Proposal Feature Store proposed architecture The solution currently implemented contains only the following components:\nProducer: applications producing entities to the kafka. Registry: repository of the schemas created on the pipeline stages. Kafka: streaming platform used to enable spark jobs to transform the entities produced by applications into features used by ML models. a high-performance data pipeline. Transformations: spark jobs to transform the entities produced by applications into features used by ML models. (User defined functions). Sinks: application that syncs the offline feature to the online store. Redis: in-memory data structure store, used as an online storage layer (caching). Deploy: make start Produce logs to kafka: make produce Sink jobs: make sink Access the UI: Kafdrop ui Transformations spark ui Mongodb ui Redis ui Clean: make clean You can get full source code here: feature-store, proposal.\n","permalink":"https://hoangph3.github.io/posts/feature_store/","summary":"Design and proposal for a feature store to serve consistent features for training and serving.","title":"Building a Feature Store for Machine Learning"},{"content":"Data Mining - Deep dive into Big Data Airflow + Spark cluster docker-airflow-spark Docker with Airflow + Postgres + Spark cluster + JDK (spark-submit support) + Jupyter Notebooks\n📦 The Containers airflow-webserver: Airflow webserver and scheduler, with spark-submit support. image: hoangph3/airflow:2.6.3-extend (custom, Spark version 3.4.1) Based on apache/airflow:2.6.3-python3.8, and puckel/docker-airflow port: 8080 postgres: Postgres database, used by Airflow. image: postgres:14.0 port: 5432 spark-master: Spark Master. image: hoangph3/spark:3.4.1 port: 8081 spark-worker[-N]: Spark workers (default number: 1). Modify docker-compose.yml file to add more. image: hoangph3/spark:3.4.1 🛠 Setup Build airflow and spark Docker $ make build_airflow build_spark Launch containers $ docker-compose up -d Check accesses Airflow: http://localhost:8080 Spark Master: http://localhost:8181 👣 Additional steps Create test database in postgres: Jump into postgres container: docker exec -it airflow-postgres psql -U airflow. In container, execute the following command line to create database: Copy data into postgres: docker cp data/ airflow-postgres:/data Run python3 psql_client.py to insert data into orders table. Add airflow variables Go to Airflow UI \u0026gt; Admin \u0026gt; Variables Upload variables json file. Edit connection from Airflow to Spark Go to Airflow UI \u0026gt; Admin \u0026gt; Edit connections Edit spark_default entry: Connection Type: Spark Host: spark://spark-master Port: 7077 Edit postgres_localhost entry: Connection Type: Postgres Host: postgres Schema: test Login: airflow Password: airflow Port: 5432 Edit minio_conn entry:{ \u0026quot;host\u0026quot;: \u0026quot;http://airflow-minio:9000\u0026quot; } Connection Type: Amazon Web Services AWS Access Key ID: superadmin AWS Secret Access Key: secretpassword Extra: Test spark-submit from Airflow Go to the Airflow UI and run the tutorial_spark_submit_operator DAG :)\nTest postgres operator from Airflow Go to the Airflow UI and run the tutorial_postgres_hooks DAG :)\nTest ml pipeline from Airflow Go to the Airflow UI and run the tutorial_ml_simple_pipeline DAG :)\nKafka cluster Deploying kafka cluster kubernetes Deploy kafka broker: NAMESPACE=\u0026#34;kafka\u0026#34; # create and select a new namespace $ kubectl create ns $NAMESPACE $ kubectl config set-context --current --namespace=\u0026#34;$NAMESPACE\u0026#34; # deploy the Strimzi operator $ kubectl create -f strimzi-cluster-operator-0.31.1.yaml # deploy the Kafka cluster with external accessing $ kubectl apply -f kafka-ephemeral.yaml kafka.kafka.strimzi.io/ephemeral-cluster created $ kubectl get pods NAME READY STATUS RESTARTS AGE ephemeral-cluster-entity-operator-776554c699-h522h 3/3 Running 0 29s ephemeral-cluster-kafka-0 1/1 Running 0 57s ephemeral-cluster-zookeeper-0 1/1 Running 0 82s strimzi-cluster-operator-54cb64cfdd-kbndn 1/1 Running 0 2m36s # list service kafka $ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE ephemeral-cluster-kafka-bootstrap ClusterIP 10.102.159.187 \u0026lt;none\u0026gt; 9091/TCP 73s ephemeral-cluster-kafka-brokers ClusterIP None \u0026lt;none\u0026gt; 9090/TCP,9091/TCP 73s ephemeral-cluster-kafka-external-0 NodePort 10.111.197.45 \u0026lt;none\u0026gt; 9092:32000/TCP 73s ephemeral-cluster-kafka-external-bootstrap NodePort 10.103.113.162 \u0026lt;none\u0026gt; 9092:32100/TCP 73s ephemeral-cluster-zookeeper-client ClusterIP 10.99.29.241 \u0026lt;none\u0026gt; 2181/TCP 99s ephemeral-cluster-zookeeper-nodes ClusterIP None \u0026lt;none\u0026gt; 2181/TCP,2888/TCP,3888/TCP 99s # switch to default namespace $ kubectl config set-context --current --namespace=default Create kafka topics: $ kubectl apply -f kafka-topics.yaml kafkatopic.kafka.strimzi.io/test-topic created Get the address of kafka broker: $ kubectl get svc -n kafka ephemeral-cluster-kafka-external-bootstrap -o jsonpath -o=jsonpath=\u0026#39;{.spec.clusterIP}:{.spec.ports[0].port}\u0026#39; 10.103.113.162:9092 Testing: # Send data python3 kafka-client.py --command produce 100%|██████████| 100/100 [00:00\u0026lt;00:00, 18429.21it/s] # Read data python3 kafka-client.py --command consume {\u0026#39;test-topic\u0026#39;, \u0026#39;__strimzi-topic-operator-kstreams-topic-store-changelog\u0026#39;, \u0026#39;__strimzi_store_topic\u0026#39;} {\u0026#39;data\u0026#39;: 95} {\u0026#39;data\u0026#39;: 96} {\u0026#39;data\u0026#39;: 97} ... Spark cluster Spark Cluster with Docker A simple spark standalone cluster for your development environment.\nContainer Exposed ports spark-master 9090 spark-worker-1 9091 spark-worker-2 9092 my-postgres 5432 Installation 1. Build the image make build 2. Run the spark cluster make start 3. Validate your cluster Accessing the spark UI:\nSpark Master Spark Worker 1 Spark Worker 2 Resource Allocation The default CPU cores allocation for each spark worker is 1 core. The default RAM for each spark worker is 1024MB. The default RAM allocation for spark executors is 256MB. The default RAM allocation for spark driver is 128MB. If you wish to modify this allocations just edit the env/spark-worker.sh file. Run sample jobs Connect to one of the workers or the master:\ndocker exec -it spark-worker-1 bash Then execute:\n/opt/spark/bin/spark-submit --master spark://spark-master:7077 \\ --jars /opt/spark/examples/postgresql-42.5.1.jar \\ --driver-memory 1G \\ --executor-memory 1G \\ /opt/spark/examples/main.py Verify the database:\npsql -h localhost -U admin -d my_db Password for user admin: ****** my_db=# \\dt List of relations Schema | Name | Type | Owner --------+-----------+-------+------- public | starbucks | table | admin (1 row) my_db=# select * from starbucks; name | size | price | sale_price -----------------------+--------+-------+-------------------- White Chocolate Mocha | Tall | 3.75 | 3.375 White Chocolate Mocha | Grande | 4.45 | 4.005 White Chocolate Mocha | Venti | 4.75 | 4.275 Cinnamon Dolce Latte | Tall | 3.65 | 3.285 Cinnamon Dolce Latte | Grande | 4.25 | 3.825 Cinnamon Dolce Latte | Venti | 4.65 | 4.1850000000000005 Caramel Macchiato | Tall | 3.75 | 3.375 Caramel Macchiato | Grande | 4.45 | 4.005 Caramel Macchiato | Venti | 4.75 | 4.275 (9 rows) You can get full source code here: data-pipeline, airflow-spark, kafka-cluster, spark-cluster.\n","permalink":"https://hoangph3.github.io/posts/data_pipeline/","summary":"Build a big-data pipeline stack combining Airflow, Spark and Kafka clusters with Docker.","title":"Data Mining - Deep Dive into Big Data Pipelines"},{"content":"Serving a custom model with JSON serialization The mlserver package comes with inference runtime implementations for scikit-learn and xgboost models. However, some times we may also need to roll out our own inference server, with custom logic to perform inference. To support this scenario, MLServer makes it really easy to create your own extensions, which can then be containerised and deployed in a production environment.\nOverview In this example, we create a simple Hello World JSON model that parses and modifies a JSON data chunk. This is often useful as a means to quickly bootstrap existing models that utilize JSON based model inputs.\nServing The next step will be to serve our model using mlserver. For that, we will first implement an extension which serve as the runtime to perform inference using our custom Hello World JSON model.\nCustom inference runtime This is a trivial model to demonstrate how to conceptually work with JSON inputs / outputs. In this example:\nParse the JSON input from the client Create a JSON response echoing back the client request as well as a server generated message %%writefile jsonmodels.py import json from typing import Dict, Any from mlserver import MLModel, types from mlserver.codecs import StringCodec class JsonHelloWorldModel(MLModel): async def load(self) -\u0026gt; bool: # Perform additional custom initialization here. print(\u0026#34;Initialize model\u0026#34;) # Set readiness flag for model return await super().load() async def predict(self, payload: types.InferenceRequest) -\u0026gt; types.InferenceResponse: request = self._extract_json(payload) response = { \u0026#34;request\u0026#34;: request, \u0026#34;server_response\u0026#34;: \u0026#34;Got your request. Hello from the server.\u0026#34;, } response_bytes = json.dumps(response).encode(\u0026#34;UTF-8\u0026#34;) return types.InferenceResponse( id=payload.id, model_name=self.name, model_version=self.version, outputs=[ types.ResponseOutput( name=\u0026#34;echo_response\u0026#34;, shape=[len(response_bytes)], datatype=\u0026#34;BYTES\u0026#34;, data=[response_bytes], parameters=types.Parameters(content_type=\u0026#34;str\u0026#34;), ) ], ) def _extract_json(self, payload: types.InferenceRequest) -\u0026gt; Dict[str, Any]: inputs = {} for inp in payload.inputs: inputs[inp.name] = json.loads( \u0026#34;\u0026#34;.join(self.decode(inp, default_codec=StringCodec)) ) return inputs Settings files The next step will be to create 2 configuration files:\nsettings.json: holds the configuration of our server (e.g. ports, log level, etc.). model-settings.json: holds the configuration of our model (e.g. input type, runtime to use, etc.). settings.json %%writefile settings.json { \u0026#34;debug\u0026#34;: \u0026#34;true\u0026#34; } model-settings.json %%writefile model-settings.json { \u0026#34;name\u0026#34;: \u0026#34;json-hello-world\u0026#34;, \u0026#34;implementation\u0026#34;: \u0026#34;jsonmodels.JsonHelloWorldModel\u0026#34; } Start serving our model Now that we have our config in-place, we can start the server by running mlserver start .. This needs to either be ran from the same directory where our config files are or pointing to the folder where they are.\nmlserver start . Since this command will start the server and block the terminal, waiting for requests, this will need to be ran in the background on a separate terminal.\nSend test inference request (REST) We now have our model being served by mlserver. To make sure that everything is working as expected, let’s send a request from our test set.\nFor that, we can use the Python types that mlserver provides out of box, or we can build our request manually.\nimport requests import json from mlserver.types import InferenceResponse from mlserver.codecs.string import StringRequestCodec from pprint import PrettyPrinter pp = PrettyPrinter(indent=1) inputs = {\u0026#34;name\u0026#34;: \u0026#34;Foo Bar\u0026#34;, \u0026#34;message\u0026#34;: \u0026#34;Hello from Client (REST)!\u0026#34;} # NOTE: this uses characters rather than encoded bytes. It is recommended that you use the `mlserver` types to assist in the correct encoding. inputs_string = json.dumps(inputs) inference_request = { \u0026#34;inputs\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;echo_request\u0026#34;, \u0026#34;shape\u0026#34;: [len(inputs_string)], \u0026#34;datatype\u0026#34;: \u0026#34;BYTES\u0026#34;, \u0026#34;data\u0026#34;: [inputs_string], } ] } endpoint = \u0026#34;http://localhost:8080/v2/models/json-hello-world/infer\u0026#34; response = requests.post(endpoint, json=inference_request) print(f\u0026#34;full response:\\n\u0026#34;) print(response) # retrive text output as dictionary inference_response = InferenceResponse.parse_raw(response.text) raw_json = StringRequestCodec.decode_response(inference_response) output = json.loads(raw_json[0]) print(f\u0026#34;\\ndata part:\\n\u0026#34;) pp.pprint(output) Send test inference request (gRPC) Utilizing string data with the gRPC interface can be a bit tricky. To ensure we are correctly handling inputs and outputs we will be handled correctly.\nFor simplicity in this case, we leverage the Python types that mlserver provides out of the box. Alternatively, the gRPC stubs can be generated regenerated from the V2 specification directly for use by non-Python as well as Python clients.\nimport requests import json import grpc from mlserver.codecs.string import StringRequestCodec import mlserver.grpc.converters as converters import mlserver.grpc.dataplane_pb2_grpc as dataplane import mlserver.types as types from pprint import PrettyPrinter pp = PrettyPrinter(indent=1) model_name = \u0026#34;json-hello-world\u0026#34; inputs = {\u0026#34;name\u0026#34;: \u0026#34;Foo Bar\u0026#34;, \u0026#34;message\u0026#34;: \u0026#34;Hello from Client (gRPC)!\u0026#34;} inputs_bytes = json.dumps(inputs).encode(\u0026#34;UTF-8\u0026#34;) inference_request = types.InferenceRequest( inputs=[ types.RequestInput( name=\u0026#34;echo_request\u0026#34;, shape=[len(inputs_bytes)], datatype=\u0026#34;BYTES\u0026#34;, data=[inputs_bytes], parameters=types.Parameters(content_type=\u0026#34;str\u0026#34;), ) ] ) inference_request_g = converters.ModelInferRequestConverter.from_types( inference_request, model_name=model_name, model_version=None ) grpc_channel = grpc.insecure_channel(\u0026#34;localhost:8081\u0026#34;) grpc_stub = dataplane.GRPCInferenceServiceStub(grpc_channel) response = grpc_stub.ModelInfer(inference_request_g) print(f\u0026#34;full response:\\n\u0026#34;) print(response) # retrive text output as dictionary inference_response = converters.ModelInferResponseConverter.to_types(response) raw_json = StringRequestCodec.decode_response(inference_response) output = json.loads(raw_json[0]) print(f\u0026#34;\\ndata part:\\n\u0026#34;) pp.pprint(output) You can get full source code here!\n","permalink":"https://hoangph3.github.io/posts/ml_serving_json_model/","summary":"Send and parse JSON prediction requests against a TensorFlow Serving model.","title":"Serving TensorFlow Models with JSON Requests"},{"content":"Serving a custom model The mlserver package comes with inference runtime implementations for scikit-learn and xgboost models. However, some times we may also need to roll out our own inference server, with custom logic to perform inference. To support this scenario, MLServer makes it really easy to create your own extensions, which can then be containerised and deployed in a production environment.\nOverview In this example, we will train a tensorflow model. Out of the box, mlserver doesn’t provide an inference runtime for tensorflow. However, through this example we will see how easy is to develop our own.\nTraining Prepare data Download MNIST dataset, then extracting into data directory by command:\ngzip -d train-images-idx3-ubyte.gz gzip -d train-labels-idx1-ubyte.gz Training our model python3 train.py Epoch 4/5 100/100 [==============================] - 6s 63ms/step - loss: 1.7229 - accuracy: 0.7255 Epoch 5/5 100/100 [==============================] - 8s 78ms/step - loss: 1.4595 - accuracy: 0.7819 INFO:root:Saving the trained model to: ./models/latest WARNING:absl:Found untraced functions such as _jit_compiled_convolution_op while saving (showing 1 of 1). These functions will not be directly callable after loading. INFO:tensorflow:Assets written to: ./models/latest/assets INFO:tensorflow:Assets written to: ./models/latest/assets Serving our model mlserver start models 2023-07-26 00:31:04,926 [mlserver.grpc] INFO - gRPC server running on http://0.0.0.0:9081 INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:9080 (Press CTRL+C to quit) INFO: Uvicorn running on http://0.0.0.0:9082 (Press CTRL+C to quit) 2023-07-26 00:31:05.188780: I tensorflow/tsl/cuda/cudart_stub.cc:28] Could not find cuda drivers on your machine, GPU will not be used. 2023-07-26 00:31:05.239173: I tensorflow/tsl/cuda/cudart_stub.cc:28] Could not find cuda drivers on your machine, GPU will not be used. 2023-07-26 00:31:05.239651: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations. To enable the following instructions: AVX2 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags. 2023-07-26 00:31:06.079638: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT Model: \u0026#34;sequential\u0026#34; _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= reshape (Reshape) (None, 28, 28, 1) 0 conv2d (Conv2D) (None, 26, 26, 32) 320 flatten (Flatten) (None, 21632) 0 dense (Dense) (None, 128) 2769024 dense_1 (Dense) (None, 10) 1290 ================================================================= Total params: 2770634 (10.57 MB) Trainable params: 2770634 (10.57 MB) Non-trainable params: 0 (0.00 Byte) _________________________________________________________________ 2023-07-26 00:31:07,452 [mlserver] INFO - Loaded model \u0026#39;mnist\u0026#39; succesfully. INFO:mlserver:Loaded model \u0026#39;mnist\u0026#39; succesfully. 2023-07-26 00:31:07,453 [mlserver] INFO - Loaded model \u0026#39;mnist\u0026#39; succesfully. Testing python3 test.py {\u0026#39;model_name\u0026#39;: \u0026#39;mnist\u0026#39;, \u0026#39;id\u0026#39;: \u0026#39;f4ce4cfd-04fa-406f-a360-676ef0cfd925\u0026#39;, \u0026#39;parameters\u0026#39;: {}, \u0026#39;outputs\u0026#39;: [{\u0026#39;name\u0026#39;: \u0026#39;output-0\u0026#39;, \u0026#39;shape\u0026#39;: [16, 1], \u0026#39;datatype\u0026#39;: \u0026#39;INT64\u0026#39;, \u0026#39;parameters\u0026#39;: {\u0026#39;content_type\u0026#39;: \u0026#39;np\u0026#39;}, \u0026#39;data\u0026#39;: [3, 0, 4, 1, 4, 2, 1, 3, 1, 4, 3, 2, 3, 6, 1, 7]}]} You can get full source code here!\n","permalink":"https://hoangph3.github.io/posts/ml_serving_custom_model/","summary":"Package and serve a custom TensorFlow model signature with TensorFlow Serving.","title":"Serving Custom TensorFlow Models"},{"content":"Multi-Model Serving MLServer has been built with Multi-Model Serving (MMS) in mind. This means that, within a single instance of MLServer, you can serve multiple models under different paths. This also includes multiple versions of the same model.\nThis notebook shows an example of how you can leverage MMS with MLServer.\nRequirements Install packages dependencies:\npython3 -m pip install mlserver mlserver-sklearn mlserver-xgboost Training We will first start by training 2 different models:\nName Framework Source Trained Model Path mnist_sklearn scikit-learn MNIST example from the scikit-learn documentation ./models/mnist_sklearn/model.joblib mushroom_xgboost xgboost Mushrooms example from the xgboost Getting Started guide ./models/mushroom_xgboost/model.json Training our model python3 trainer/mnist_sklearn.py python3 trainer/mushroom_xgboost.py Serving The next step will be serving both our models within the same MLServer instance. For that, we will just need to create a model-settings.json file local to each of our models and a server-wide settings.json. That is,\nsettings.json: holds the configuration of our server (e.g. ports, log level, etc.). models/mnist_sklearn/model-settings.json: holds the configuration specific to our mnist_sklearn model (e.g. input type, runtime to use, etc.). models/mushroom_xgboost/model-settings.json: holds the configuration specific to our mushroom_xgboost model (e.g. input type, runtime to use, etc.). settings.json %%writefile settings.json { \u0026#34;debug\u0026#34;: \u0026#34;true\u0026#34;, \u0026#34;http_port\u0026#34;: 9080, \u0026#34;grpc_port\u0026#34;: 9081, \u0026#34;metrics_port\u0026#34;: 9082 } models/mnist-svm/model-settings.json %%writefile models/mnist_sklearn/model-settings.json { \u0026#34;name\u0026#34;: \u0026#34;mnist_sklearn\u0026#34;, \u0026#34;implementation\u0026#34;: \u0026#34;mlserver_sklearn.SKLearnModel\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;version\u0026#34;: \u0026#34;v1.0.0\u0026#34; } } models/mushroom-xgboost/model-settings.json %%writefile models/mushroom_xgboost/model-settings.json { \u0026#34;name\u0026#34;: \u0026#34;mushroom_xgboost\u0026#34;, \u0026#34;implementation\u0026#34;: \u0026#34;mlserver_xgboost.XGBoostModel\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;uri\u0026#34;: \u0026#34;./model.json\u0026#34;, \u0026#34;version\u0026#34;: \u0026#34;v2.0.0\u0026#34; } } Start serving our model Now that we have our config in-place, we can start the server. This needs to either be ran from the same directory where our config files are or pointing to the folder where they are.\nmlserver start . 2023-07-25 23:05:15,731 [mlserver.parallel] DEBUG - Starting response processing loop... 2023-07-25 23:05:15,732 [mlserver.rest] INFO - HTTP server running on http://0.0.0.0:9080 INFO: Started server process [24741] INFO: Waiting for application startup. 2023-07-25 23:05:15,755 [mlserver.metrics] INFO - Metrics server running on http://0.0.0.0:9082 2023-07-25 23:05:15,755 [mlserver.metrics] INFO - Prometheus scraping endpoint can be accessed on http://0.0.0.0:9082/metrics INFO: Started server process [24741] INFO: Waiting for application startup. INFO: Application startup complete. 2023-07-25 23:05:16,447 [mlserver.grpc] INFO - gRPC server running on http://0.0.0.0:9081 INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:9080 (Press CTRL+C to quit) INFO: Uvicorn running on http://0.0.0.0:9082 (Press CTRL+C to quit) /home/hoang/.local/lib/python3.8/site-packages/xgboost/sklearn.py:782: UserWarning: Loading a native XGBoost model with Scikit-Learn interface. warnings.warn(\u0026#34;Loading a native XGBoost model with Scikit-Learn interface.\u0026#34;) 2023-07-25 23:05:17,216 [mlserver] INFO - Loaded model \u0026#39;mushroom_xgboost\u0026#39; succesfully. 2023-07-25 23:05:17,219 [mlserver] INFO - Loaded model \u0026#39;mushroom_xgboost\u0026#39; succesfully. /home/hoang/.local/lib/python3.8/site-packages/sklearn/base.py:347: InconsistentVersionWarning: Trying to unpickle estimator SVC from version 1.0.2 when using version 1.3.0. This might lead to breaking code or invalid results. Use at your own risk. For more info please refer to: https://scikit-learn.org/stable/model_persistence.html#security-maintainability-limitations warnings.warn( 2023-07-25 23:05:17,258 [mlserver] INFO - Loaded model \u0026#39;mnist_sklearn\u0026#39; succesfully. 2023-07-25 23:05:17,259 [mlserver] INFO - Loaded model \u0026#39;mnist_sklearn\u0026#39; succesfully. Since this command will start the server and block the terminal, waiting for requests, this will need to be ran in the background on a separate terminal.\nTesting By this point, we should have both our models getting served by MLServer. To make sure that everything is working as expected, let’s send a request from each test set.\nFor that, we can use the Python types that the mlserver package provides out of box, or we can build our request manually.\nTesting our model python3 test.py --model mnist_sklearn --version v1.0.0 {\u0026#39;model_name\u0026#39;: \u0026#39;mnist_sklearn\u0026#39;, \u0026#39;model_version\u0026#39;: \u0026#39;v1.0.0\u0026#39;, \u0026#39;id\u0026#39;: \u0026#39;43139df6-3ca7-4ec5-9de3-e7cfe891537b\u0026#39;, \u0026#39;parameters\u0026#39;: {}, \u0026#39;outputs\u0026#39;: [{\u0026#39;name\u0026#39;: \u0026#39;predict\u0026#39;, \u0026#39;shape\u0026#39;: [1, 1], \u0026#39;datatype\u0026#39;: \u0026#39;INT64\u0026#39;, \u0026#39;parameters\u0026#39;: {\u0026#39;content_type\u0026#39;: \u0026#39;np\u0026#39;}, \u0026#39;data\u0026#39;: [4]}]} python3 test.py --model mushroom_xgboost --version v2.0.0 {\u0026#39;model_name\u0026#39;: \u0026#39;mushroom_xgboost\u0026#39;, \u0026#39;model_version\u0026#39;: \u0026#39;v2.0.0\u0026#39;, \u0026#39;id\u0026#39;: \u0026#39;78635bc6-b14c-4010-9358-88cf5eef76c1\u0026#39;, \u0026#39;parameters\u0026#39;: {}, \u0026#39;outputs\u0026#39;: [{\u0026#39;name\u0026#39;: \u0026#39;predict\u0026#39;, \u0026#39;shape\u0026#39;: [1, 1], \u0026#39;datatype\u0026#39;: \u0026#39;FP32\u0026#39;, \u0026#39;parameters\u0026#39;: {\u0026#39;content_type\u0026#39;: \u0026#39;np\u0026#39;}, \u0026#39;data\u0026#39;: [0.28583016991615295]}]} Prometheus MLServer exposes metrics that can be scraped by Prometheus.\nSwagger MLServer includes an autogenerated Swagger UI which can be used to interact dynamically with the Open Inference Protocol.\nList available models Now that we’ve got our inference server up and running, and serving 2 different models, we can start using the Model Repository API. To get us started, we will first list all available models in the repository.\ncurl --header \u0026#34;Content-Type: application/json\u0026#34; --request POST --data \u0026#39;{}\u0026#39; http://localhost:9080/v2/repository/index | json_pp [ { \u0026#34;state\u0026#34; : \u0026#34;READY\u0026#34;, \u0026#34;name\u0026#34; : \u0026#34;mushroom_xgboost\u0026#34;, \u0026#34;reason\u0026#34; : \u0026#34;\u0026#34;, \u0026#34;version\u0026#34; : \u0026#34;v2.0.0\u0026#34; }, { \u0026#34;state\u0026#34; : \u0026#34;READY\u0026#34;, \u0026#34;name\u0026#34; : \u0026#34;mnist_sklearn\u0026#34;, \u0026#34;version\u0026#34; : \u0026#34;v1.0.0\u0026#34;, \u0026#34;reason\u0026#34; : \u0026#34;\u0026#34; } ] As we can, the repository lists 2 models (i.e. mushroom_xgboost and mnist_sklearn). Note that the state for both is set to READY. This means that both models are loaded, and thus ready for inference.\nUnloading our mushroom_xgboost model We will now try to unload one of the 2 models, mushroom_xgboost. This will unload the model from the inference server but will keep it available on our model repository.\ncurl --header \u0026#34;Content-Type: application/json\u0026#34; --request POST http://localhost:9080/v2/repository/models/mushroom_xgboost/unload If we now try to list the models available in our repository, we will see that the mushroom_xgboost model is flagged as UNAVAILABLE. This means that it’s present in the repository but it’s not loaded for inference.\ncurl --header \u0026#34;Content-Type: application/json\u0026#34; --request POST --data \u0026#39;{}\u0026#39; http://localhost:9080/v2/repository/index | json_pp [ { \u0026#34;version\u0026#34; : \u0026#34;v2.0.0\u0026#34;, \u0026#34;state\u0026#34; : \u0026#34;UNAVAILABLE\u0026#34;, \u0026#34;name\u0026#34; : \u0026#34;mushroom_xgboost\u0026#34;, \u0026#34;reason\u0026#34; : \u0026#34;\u0026#34; }, { \u0026#34;reason\u0026#34; : \u0026#34;\u0026#34;, \u0026#34;state\u0026#34; : \u0026#34;READY\u0026#34;, \u0026#34;version\u0026#34; : \u0026#34;v1.0.0\u0026#34;, \u0026#34;name\u0026#34; : \u0026#34;mnist_sklearn\u0026#34; } ] Loading our mushroom_xgboost model back We will now load our model back into our inference server.\ncurl --header \u0026#34;Content-Type: application/json\u0026#34; --request POST http://localhost:9080/v2/repository/models/mushroom_xgboost/load If we now try to list the models again, we will see that our mushroom_xgboost is back again, ready for inference.\ncurl --header \u0026#34;Content-Type: application/json\u0026#34; --request POST --data \u0026#39;{}\u0026#39; http://localhost:9080/v2/repository/index | json_pp [ { \u0026#34;state\u0026#34; : \u0026#34;READY\u0026#34;, \u0026#34;name\u0026#34; : \u0026#34;mushroom_xgboost\u0026#34;, \u0026#34;version\u0026#34; : \u0026#34;v2.0.0\u0026#34;, \u0026#34;reason\u0026#34; : \u0026#34;\u0026#34; }, { \u0026#34;reason\u0026#34; : \u0026#34;\u0026#34;, \u0026#34;state\u0026#34; : \u0026#34;READY\u0026#34;, \u0026#34;name\u0026#34; : \u0026#34;mnist_sklearn\u0026#34;, \u0026#34;version\u0026#34; : \u0026#34;v1.0.0\u0026#34; } ] You can get full source code here!\n","permalink":"https://hoangph3.github.io/posts/ml_serving_multi_model/","summary":"Serve multiple TensorFlow models simultaneously and monitor them with Prometheus and Swagger.","title":"Serving Multiple TensorFlow Models with Monitoring"},{"content":"Seldon Kafka Integration In this example we will run SeldonDeployments for a RandomForest Sklearn model which take their inputs from a Kafka topic and push their outputs to a Kafka topic. We will experiment with both REST and gRPC Seldon graphs.\nFirstly, deploy a randomforest model by applying the seldon manifest shown below: $ kubectl apply -f mnist.yml seldondeployment.machinelearning.seldon.io/mnist-classifier created $ kubectl get pods -n seldon-model NAME READY STATUS RESTARTS AGE mnist-classifier-default-0-classifier-5cdbd58c5b-9rbh5 3/3 Running 0 76s There are three pods, consist of classifier, istio-proxy and seldon-container-engine.\nMake a prediction after the Seldon Deployment is available via the ingress gateway: $ export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath=\u0026#39;{.status.loadBalancer.ingress[0].ip}\u0026#39;) $ echo $INGRESS_HOST 10.106.254.233 $ curl -X POST http://$INGRESS_HOST/seldon/seldon-model/mnist-classifier/api/v1.0/predictions \\ -H \u0026#39;Content-Type: application/json\u0026#39; -d @payload.json --noproxy $INGRESS_HOST | json_pp {\u0026#34;data\u0026#34;:{\u0026#34;names\u0026#34;:[\u0026#34;t:0\u0026#34;,\u0026#34;t:1\u0026#34;,\u0026#34;t:2\u0026#34;,\u0026#34;t:3\u0026#34;,\u0026#34;t:4\u0026#34;,\u0026#34;t:5\u0026#34;,\u0026#34;t:6\u0026#34;,\u0026#34;t:7\u0026#34;,\u0026#34;t:8\u0026#34;,\u0026#34;t:9\u0026#34;],\u0026#34;ndarray\u0026#34;:[[0.0,0.1,0.0,0.0,0.4,0.2,0.1,0.2,0.0,0.0]]},\u0026#34;meta\u0026#34;:{\u0026#34;requestPath\u0026#34;:{\u0026#34;classifier\u0026#34;:\u0026#34;hoangph3/sklearn_mnist_classifier:v0.0.1\u0026#34;}}} Deploy kafka broker: $ docker-compose up -d [+] Running 5/5 ⠿ Network kafka-integration_default Created 0.1s ⠿ Volume \u0026#34;kafka-integration_kafka_data\u0026#34; Created 0.0s ⠿ Volume \u0026#34;kafka-integration_zookeeper_data\u0026#34; Created 0.0s ⠿ Container zookeeper Started 1.0s ⠿ Container kafka Started 1.9s Deploy another mnist model with kafka: Note that the seldon model can’t connect to the kafka broker because the iptables of istio proxy. We should remove proxy first.\n# Show labels for seldon-model namespace $ kubectl get ns seldon-model --show-labels NAME STATUS AGE LABELS seldon-model Active 7d13h istio-injection=enabled,kubernetes.io/metadata.name=seldon-model # Remove istio-injection label $ kubectl label ns seldon-model istio-injection- namespace/seldon-model unlabeled # Recheck $ kubectl get ns seldon-model --show-labels NAME STATUS AGE LABELS seldon-model Active 7d13h kubernetes.io/metadata.name=seldon-model Now we will deploy a model can read real time data:\n$ kubectl apply -f mnist_kafka.yml $ kubectl get pods -n seldon-model NAME READY STATUS RESTARTS AGE mnist-kafka-default-0-classifier-f69f8589c-z46zm 2/2 Running 0 65s Send real time data for stream processing, then check the data processed: # The bootstrap_servers is 192.168.0.5:9092 $ python3 producer.py --proto rest 50%|████████████████████ | 30020/60000 [00:32\u0026lt;00:28, 1051.43it/s] $ python3 consumer.py --proto rest {\u0026#39;mnist-rest-input\u0026#39;, \u0026#39;mnist-grpc-output\u0026#39;, \u0026#39;mnist-rest-output\u0026#39;, \u0026#39;mnist-grpc-input\u0026#39;} 2345it [00:16, 145.44it/s] $ kubectl logs -f -n seldon-model mnist-kafka-default-0-classifier-f69f8589c-z46zm seldon-container-engine {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1669906765.922243,\u0026#34;logger\u0026#34;:\u0026#34;entrypoint.KafkaServer\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;Processed\u0026#34;,\u0026#34;messages\u0026#34;:21000} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1669906766.4445083,\u0026#34;logger\u0026#34;:\u0026#34;entrypoint.KafkaServer\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;Ignored\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;OffsetsCommitted (\u0026lt;nil\u0026gt;, [mnist-rest-input[0]@21051])\u0026#34;} {\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;ts\u0026#34;:1669906771.4442313,\u0026#34;logger\u0026#34;:\u0026#34;entrypoint.KafkaServer\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;Ignored\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;OffsetsCommitted (\u0026lt;nil\u0026gt;, [mnist-rest-input[0]@21616])\u0026#34;} Test model with gRPC: Create grpc model:\n$ kubectl apply -f mnist_kafka_grpc.yml Send real time data:\n$ python3 producer.py --proto grpc Get response message:\n$ python3 consumer.py --proto grpc You can get full source code here!\n","permalink":"https://hoangph3.github.io/posts/seldon_core_kafka_integration/","summary":"Stream predictions in and out of Seldon Core model deployments using Kafka.","title":"Integrating Seldon Core with Kafka"},{"content":"Serving your Models in a Pipeline Setup environments Install and setup Istio $ wget https://github.com/istio/istio/releases/download/1.12.7/istio-1.12.7-linux-amd64.tar.gz $ tar zvxf istio-1.12.7-linux-amd64.tar.gz $ cd istio-1.12.7 \u0026amp;\u0026amp; ls bin LICENSE manifests manifest.yaml README.md samples tools $ export PATH=$PWD/bin:$PATH $ istioctl install This will install the Istio 1.12.7 default profile with [\u0026#34;Istio core\u0026#34; \u0026#34;Istiod\u0026#34; \u0026#34;Ingress gateways\u0026#34;] components into the cluster. Proceed? (y/N) y ✔ Istio core installed ✔ Istiod installed- Processing resources for Ingress gateways. Waiting for Deployment/istio-system/istio-ingressgateway ✔ Ingress gateways installed ✔ Installation complete Making this installation the default for injection and validation. Create namespace seldon-model and setup an istio gateway $ kubectl create namespace seldon-model namespace/seldon-model created $ kubectl label namespace seldon-model istio-injection=enabled namespace/seldon-model labeled $ kubectl get ns seldon-model --show-labels NAME STATUS AGE LABELS seldon-model Active 28s istio-injection=enabled,kubernetes.io/metadata.name=seldon-model $ kubectl apply -f seldon-gateway.yaml gateway.networking.istio.io/seldon-gateway created Create a seldon-system namespace and install Seldon Core. $ kubectl create namespace seldon-system namespace/seldon-system created $ kubectl config set-context --current --namespace=seldon-system Context \u0026#34;minikube\u0026#34; modified. $ kubectl get pods No resources found in seldon-system namespace. $ git clone https://github.com/SeldonIO/seldon-core $ cd seldon-core $ git checkout v1.14.1 $ git branch * (HEAD detached at v1.14.1) master $ cd helm-charts/ # Generate the manifests file to deploy seldon-core # -\u0026gt; Skipping this step if you haven\u0026#39;t installed helm $ helm template --output-dir ./yamls seldon-core-operator/ wrote ./yamls/seldon-core-operator/templates/serviceaccount_seldon-manager.yaml wrote ./yamls/seldon-core-operator/templates/webhook.yaml wrote ./yamls/seldon-core-operator/templates/configmap_seldon-config.yaml wrote ./yamls/seldon-core-operator/templates/customresourcedefinition_v1_seldondeployments.machinelearning.seldon.io.yaml wrote ./yamls/seldon-core-operator/templates/clusterrole_seldon-manager-role.yaml wrote ./yamls/seldon-core-operator/templates/clusterrole_seldon-manager-sas-role.yaml wrote ./yamls/seldon-core-operator/templates/clusterrolebinding_seldon-manager-rolebinding.yaml wrote ./yamls/seldon-core-operator/templates/clusterrolebinding_seldon-manager-sas-rolebinding.yaml wrote ./yamls/seldon-core-operator/templates/role_seldon-leader-election-role.yaml wrote ./yamls/seldon-core-operator/templates/rolebinding_seldon-leader-election-rolebinding.yaml wrote ./yamls/seldon-core-operator/templates/service_seldon-webhook-service.yaml wrote ./yamls/seldon-core-operator/templates/deployment_seldon-controller-manager.yaml wrote ./yamls/seldon-core-operator/templates/webhook.yaml # Update ISTIO_ENABLED=true $ nano yamls/seldon-core-operator/templates/deployment_seldon-controller-manager.yaml # Create the manifests $ kubectl create -f yamls/seldon-core-operator/templates/ clusterrole.rbac.authorization.k8s.io/seldon-manager-role-seldon-system created clusterrole.rbac.authorization.k8s.io/seldon-manager-sas-role-seldon-system created clusterrolebinding.rbac.authorization.k8s.io/seldon-manager-rolebinding-seldon-system created clusterrolebinding.rbac.authorization.k8s.io/seldon-manager-sas-rolebinding-seldon-system created configmap/seldon-config created customresourcedefinition.apiextensions.k8s.io/seldondeployments.machinelearning.seldon.io created deployment.apps/seldon-controller-manager created role.rbac.authorization.k8s.io/seldon-leader-election-role created rolebinding.rbac.authorization.k8s.io/seldon-leader-election-rolebinding created service/seldon-webhook-service created serviceaccount/seldon-manager created secret/seldon-webhook-server-cert created validatingwebhookconfiguration.admissionregistration.k8s.io/seldon-validating-webhook-configuration-seldon-system created Verify # set default namespace $ kubectl config set-context --current --namespace=default Context \u0026#34;minikube\u0026#34; modified. $ kubectl get pods -n istio-system NAME READY STATUS RESTARTS AGE istio-ingressgateway-859d74978f-w7wqm 1/1 Running 0 20m istiod-64699c7b75-rb9fb 1/1 Running 0 20m $ kubectl get pods -n seldon-system NAME READY STATUS RESTARTS AGE seldon-controller-manager-59d8b884b4-pgnnj 1/1 Running 0 9s Build inference pipeline Build image for each component $ python3 build_component.py Validate $ docker images | grep seldon | grep hoang hoangph3/seldon-sentiment-analysis v0.0.1 db580a87ede5 39 seconds ago 242MB hoangph3/seldon-text-tagging v0.0.1 8ed0623d8aa1 43 seconds ago 613MB hoangph3/seldon-summarize-text v0.0.1 cda1d68390bf 47 seconds ago 441MB Run inference pipeline from manifest --- apiVersion: machinelearning.seldon.io/v1alpha2 kind: SeldonDeployment metadata: labels: app: seldon name: seldon-pipeline namespace: seldon-model spec: annotations: project_name: seldon-pipeline deployment_version: v0.0.1 seldon.io/rest-read-timeout: \u0026#39;100000\u0026#39; seldon.io/rest-connection-timeout: \u0026#39;100000\u0026#39; seldon.io/grpc-read-timeout: \u0026#39;100000\u0026#39; name: seldon-pipeline oauth_key: oauth-key oauth_secret: oauth-secret predictors: - componentSpecs: - spec: containers: - name: sentiment-analysis image: hoangph3/seldon-sentiment-analysis:v0.0.1 imagePullPolicy: IfNotPresent - name: text-tagging image: hoangph3/seldon-text-tagging:v0.0.1 imagePullPolicy: IfNotPresent securityContext: allowPrivilegeEscalation: false runAsUser: 0 - name: summarize-text image: hoangph3/seldon-summarize-text:v0.0.1 imagePullPolicy: IfNotPresent securityContext: allowPrivilegeEscalation: false runAsUser: 0 terminationGracePeriodSeconds: 20 graph: children: - name: text-tagging endpoint: type: REST type: MODEL children: - name: summarize-text endpoint: type: REST type: MODEL children: [] name: sentiment-analysis endpoint: type: REST type: MODEL name: example replicas: 1 annotations: predictor_version: v1 $ kubectl apply -f deploy-model.yml seldondeployment.machinelearning.seldon.io/seldon-pipeline created $ kubectl get pods -n seldon-model NAME READY STATUS RESTARTS AGE seldon-c4888704f0b4934a7cbd2df09d86d3da-7df5dd4f99-jl887 5/5 Running 0 119s Note that in this case, we have three pods corresponding to three components (sentiment-analysis, text-tagging, summarize-text), one pod is seldon-container-engine, and the last one is istio-proxy.\nTesting Firstly, start an external load balancer: $ minikube tunnel Status:\tmachine: minikube pid: 15766 route: 10.96.0.0/12 -\u0026gt; 192.168.0.5 minikube: Running services: [istio-ingressgateway] errors: minikube: no errors router: no errors loadbalancer emulator: no errors Make a prediction after the Seldon Deployment is available via the ingress gateway created. $ export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath=\u0026#39;{.status.loadBalancer.ingress[0].ip}\u0026#39;) $ echo $INGRESS_HOST 10.106.254.233 $ curl -X POST -d @payload.json -H \u0026#39;Content-Type: application/json\u0026#39; \\ http://$INGRESS_HOST/seldon/seldon-model/seldon-pipeline/api/v1.0/predictions | json_pp { \u0026#34;data\u0026#34;: { \u0026#34;names\u0026#34;: [], \u0026#34;ndarray\u0026#34;: [ \u0026#34;In an attempt to build an AI-ready workforce, Microsoft announced Intelligent Cloud Hub which has been launched to empower the next generation of students with AI-ready skills. Envisioned as a three-year collaborative program, Intelligent Cloud Hub will support around 100 institutions with AI infrastructure, course content and curriculum, developer support, development tools and give students access to cloud and AI services. As part of the program, the Redmond giant which wants to expand its reach and is planning to build a strong developer ecosystem in India with the program will set up the core AI infrastructure and IoT Hub for the selected campuses. The company will provide AI development tools and Azure AI services such as Microsoft Cognitive Services, Bot Services and Azure Machine Learning. According to Manish Prakash, Country General Manager-PS, Health and Education, Microsoft India, said, With AI being the defining technology of our time, it is transforming lives and industry and the jobs of tomorrow will require a different skillset. This will require more collaborations and training and working with AI. That’s why it has become more critical than ever for educational institutions to integrate new cloud and AI technologies. The program is an attempt to ramp up the institutional set-up and build capabilities among the educators to educate the workforce of tomorrow. The program aims to build up the cognitive skills and in-depth understanding of developing intelligent cloud connected solutions for applications across industry. Earlier in April this year, the company announced Microsoft Professional Program In AI as a learning track open to the public. The program was developed to provide job ready skills to programmers who wanted to hone their skills in AI and data science with a series of online courses which featured hands-on labs and expert instructors as well. This program also included developer-focused AI school that provided a bunch of assets to help build AI skills\u0026#34; ] }, \u0026#34;meta\u0026#34;: { \u0026#34;requestPath\u0026#34;: { \u0026#34;sentiment-analysis\u0026#34;: \u0026#34;hoangph3/seldon-sentiment-analysis:v0.0.1\u0026#34;, \u0026#34;summarize-text\u0026#34;: \u0026#34;hoangph3/seldon-summarize-text:v0.0.1\u0026#34;, \u0026#34;text-tagging\u0026#34;: \u0026#34;hoangph3/seldon-text-tagging:v0.0.1\u0026#34; }, \u0026#34;tags\u0026#34;: { \u0026#34;input_text\u0026#34;: \u0026#34;In an attempt to build an AI-ready workforce, Microsoft announced Intelligent Cloud Hub which has been launched to empower the next generation of students with AI-ready skills. Envisioned as a three-year collaborative program, Intelligent Cloud Hub will support around 100 institutions with AI infrastructure, course content and curriculum, developer support, development tools and give students access to cloud and AI services. As part of the program, the Redmond giant which wants to expand its reach and is planning to build a strong developer ecosystem in India with the program will set up the core AI infrastructure and IoT Hub for the selected campuses. The company will provide AI development tools and Azure AI services such as Microsoft Cognitive Services, Bot Services and Azure Machine Learning. According to Manish Prakash, Country General Manager-PS, Health and Education, Microsoft India, said, With AI being the defining technology of our time, it is transforming lives and industry and the jobs of tomorrow will require a different skillset. This will require more collaborations and training and working with AI. That’s why it has become more critical than ever for educational institutions to integrate new cloud and AI technologies. The program is an attempt to ramp up the institutional set-up and build capabilities among the educators to educate the workforce of tomorrow. The program aims to build up the cognitive skills and in-depth understanding of developing intelligent cloud connected solutions for applications across industry. Earlier in April this year, the company announced Microsoft Professional Program In AI as a learning track open to the public. The program was developed to provide job ready skills to programmers who wanted to hone their skills in AI and data science with a series of online courses which featured hands-on labs and expert instructors as well. This program also included developer-focused AI school that provided a bunch of assets to help build AI skills\u0026#34;, \u0026#34;sentiment_analysis_passed\u0026#34;: true, \u0026#34;sentiment_analysis_result\u0026#34;: { \u0026#34;compound\u0026#34;: 0.9769, \u0026#34;neg\u0026#34;: 0.008, \u0026#34;neu\u0026#34;: 0.891, \u0026#34;pos\u0026#34;: 0.101 }, \u0026#34;summarize_text_passed\u0026#34;: true, \u0026#34;summarize_text_result\u0026#34;: \u0026#34;[\u0026#39; As part of the program, the Redmond giant which wants to expand its reach and is planning to build a strong developer ecosystem in India with the program will set up the core AI infrastructure and IoT Hub for the selected campuses\u0026#39;, \u0026#39; The program was developed to provide job ready skills to programmers who wanted to hone their skills in AI and data science with a series of online courses which featured hands-on labs and expert instructors as well\u0026#39;]\u0026#34;, \u0026#34;tags\u0026#34;: \u0026#34;[\u0026#39;#collaboration\u0026#39;, \u0026#39;#skillset\u0026#39;, \u0026#39;#different\u0026#39;, \u0026#39;#life\u0026#39;, \u0026#39;#transforming\u0026#39;]\u0026#34;, \u0026#34;text_tagging_passed\u0026#34;: true } } } You can get full source code here!\n","permalink":"https://hoangph3.github.io/posts/seldon_core_inference_graph/","summary":"Compose multi-step inference graphs (transformers, combiners, routers) with Seldon Core.","title":"Building Inference Graphs with Seldon Core"},{"content":"Train Various Models on MNIST using kubeflow and seldon-core Install kubeflow training-operator $ kubectl apply -k \u0026#34;github.com/kubeflow/training-operator/manifests/overlays/standalone?ref=v1.5.0\u0026#34; Build training and serving components $ python3 build_component.py Create a persistent volume to save model file apiVersion: v1 kind: PersistentVolumeClaim metadata: name: \u0026#34;nfs-1\u0026#34; namespace: seldon-model spec: accessModes: - ReadWriteOnce resources: requests: storage: 4Gi $ kubectl apply -f pvc.yml persistentvolumeclaim/nfs-1 created Training tensorflow mnist model by the manifest file: $ kubectl apply -f tf_mnist_training.yml tfjob.kubeflow.org/tf-mnist-training created $ kubectl get pods -n seldon-model tf-mnist-training-worker-0 NAME READY STATUS RESTARTS AGE tf-mnist-training-worker-0 2/2 Running 0 43s $ kubectl logs -f -n seldon-model tf-mnist-training-worker-0 Model: \u0026#34;sequential\u0026#34; _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense (Dense) (None, 128) 100480 _________________________________________________________________ dense_1 (Dense) (None, 64) 8256 _________________________________________________________________ dense_2 (Dense) (None, 10) 650 ================================================================= Total params: 109,386 Trainable params: 109,386 Non-trainable params: 0 _________________________________________________________________ Epoch 1/20 938/938 [==============================] - 8s 4ms/step - loss: 2.2312 - accuracy: 0.1851 - val_loss: 1.8747 - val_accuracy: 0.5213 2022-10-28 14:48:01.857621: W tensorflow/core/framework/cpu_allocator_impl.cc:80] Allocation of 47040000 exceeds 10% of free system memory. Epoch 2/20 938/938 [==============================] - 3s 3ms/step - loss: 1.7639 - accuracy: 0.5803 - val_loss: 1.3615 - val_accuracy: 0.7254 2022-10-28 14:48:04.905285: W tensorflow/core/framework/cpu_allocator_impl.cc:80] Allocation of 47040000 exceeds 10% of free system memory. Epoch 3/20 938/938 [==============================] - 4s 4ms/step - loss: 1.2764 - accuracy: 0.7416 - val_loss: 0.9752 - val_accuracy: 0.7975 Epoch 4/20 938/938 [==============================] - 4s 4ms/step - loss: 0.9448 - accuracy: 0.7993 - val_loss: 0.7602 - val_accuracy: 0.8338 Epoch 5/20 938/938 [==============================] - 3s 3ms/step - loss: 0.7512 - accuracy: 0.8294 - val_loss: 0.6379 - val_accuracy: 0.8522 Epoch 6/20 938/938 [==============================] - 3s 3ms/step - loss: 0.6378 - accuracy: 0.8487 - val_loss: 0.5612 - val_accuracy: 0.8637 Epoch 7/20 938/938 [==============================] - 4s 4ms/step - loss: 0.5654 - accuracy: 0.8591 - val_loss: 0.5081 - val_accuracy: 0.8730 Epoch 8/20 938/938 [==============================] - 3s 3ms/step - loss: 0.5128 - accuracy: 0.8704 - val_loss: 0.4698 - val_accuracy: 0.8798 Epoch 9/20 938/938 [==============================] - 3s 4ms/step - loss: 0.4876 - accuracy: 0.8723 - val_loss: 0.4410 - val_accuracy: 0.8855 Epoch 10/20 938/938 [==============================] - 4s 4ms/step - loss: 0.4569 - accuracy: 0.8768 - val_loss: 0.4181 - val_accuracy: 0.8905 Epoch 11/20 938/938 [==============================] - 3s 3ms/step - loss: 0.4340 - accuracy: 0.8811 - val_loss: 0.4004 - val_accuracy: 0.8940 Epoch 12/20 938/938 [==============================] - 3s 4ms/step - loss: 0.4115 - accuracy: 0.8867 - val_loss: 0.3850 - val_accuracy: 0.8962 Epoch 13/20 938/938 [==============================] - 4s 4ms/step - loss: 0.3953 - accuracy: 0.8902 - val_loss: 0.3724 - val_accuracy: 0.8987 Epoch 14/20 938/938 [==============================] - 3s 3ms/step - loss: 0.3857 - accuracy: 0.8935 - val_loss: 0.3619 - val_accuracy: 0.9010 Epoch 15/20 938/938 [==============================] - 3s 3ms/step - loss: 0.3741 - accuracy: 0.8951 - val_loss: 0.3525 - val_accuracy: 0.9037 Epoch 16/20 938/938 [==============================] - 3s 3ms/step - loss: 0.3680 - accuracy: 0.8965 - val_loss: 0.3442 - val_accuracy: 0.9049 Epoch 17/20 938/938 [==============================] - 3s 3ms/step - loss: 0.3569 - accuracy: 0.8998 - val_loss: 0.3371 - val_accuracy: 0.9069 Epoch 18/20 938/938 [==============================] - 3s 3ms/step - loss: 0.3552 - accuracy: 0.9004 - val_loss: 0.3306 - val_accuracy: 0.9078 Epoch 19/20 938/938 [==============================] - 3s 3ms/step - loss: 0.3435 - accuracy: 0.9037 - val_loss: 0.3244 - val_accuracy: 0.9085 Epoch 20/20 938/938 [==============================] - 3s 3ms/step - loss: 0.3415 - accuracy: 0.9042 - val_loss: 0.3188 - val_accuracy: 0.9108 INFO:root:Saving the trained model to: /models/saved_model_dir 2022-10-28 14:49:04.714874: W tensorflow/python/util/util.cc:348] Sets are not currently considered sequences, but this may change in the future, so consider avoiding using them. INFO:tensorflow:Assets written to: /models/saved_model_dir/assets INFO:tensorflow:Assets written to: /models/saved_model_dir/assets Training sklearn mnist model by the manifest file: $ kubectl apply -f components_spec/sk_mnist_training.yml job.batch/sk-mnist-training created $ kubectl get pods -n seldon-model NAME READY STATUS RESTARTS AGE sk-mnist-training-5xpkj 2/2 Running 0 21s $ kubectl logs -f -n seldon-model sk-mnist-training-knrzv Train report RandomForestClassifier(n_estimators=15): precision recall f1-score support 0 1.00 1.00 1.00 5923 1 1.00 1.00 1.00 6742 2 1.00 1.00 1.00 5958 3 1.00 1.00 1.00 6131 4 1.00 1.00 1.00 5842 5 1.00 1.00 1.00 5421 6 1.00 1.00 1.00 5918 7 1.00 1.00 1.00 6265 8 1.00 1.00 1.00 5851 9 1.00 1.00 1.00 5949 accuracy 1.00 60000 macro avg 1.00 1.00 1.00 60000 weighted avg 1.00 1.00 1.00 60000 Train confusion matrix: [[5923 0 0 0 0 0 0 0 0 0] [ 0 6741 1 0 0 0 0 0 0 0] [ 1 0 5955 0 0 0 0 1 1 0] [ 0 0 1 6129 0 0 0 0 1 0] [ 0 0 0 0 5841 0 0 0 0 1] [ 0 0 0 1 0 5420 0 0 0 0] [ 1 0 0 0 0 0 5917 0 0 0] [ 0 0 1 0 0 0 0 6264 0 0] [ 0 0 0 0 0 0 0 0 5851 0] [ 1 0 0 1 1 1 0 1 0 5944]] Test report RandomForestClassifier(n_estimators=15): precision recall f1-score support 0 0.97 0.99 0.98 980 1 0.98 0.99 0.99 1135 2 0.94 0.95 0.95 1032 3 0.95 0.95 0.95 1010 4 0.96 0.96 0.96 982 5 0.95 0.93 0.94 892 6 0.97 0.97 0.97 958 7 0.96 0.94 0.95 1028 8 0.93 0.94 0.94 974 9 0.96 0.93 0.94 1009 accuracy 0.96 10000 macro avg 0.96 0.96 0.96 10000 weighted avg 0.96 0.96 0.96 10000 Test confusion matrix: [[ 969 0 0 2 0 3 2 1 3 0] [ 0 1123 4 2 0 2 1 0 2 1] [ 7 0 983 5 4 1 5 11 16 0] [ 0 0 14 956 0 15 1 12 10 2] [ 3 0 2 0 938 0 9 2 7 21] [ 6 0 5 17 4 833 7 2 15 3] [ 8 3 4 0 3 6 928 0 6 0] [ 2 10 23 5 5 1 0 971 3 8] [ 2 0 11 13 3 10 6 3 917 9] [ 7 7 1 10 19 10 0 5 7 943]] Save model to /models/sk_mnist.pkl Deploying Various MNIST Models on Kubernetes Link here!\nYou can get full source code here!\n","permalink":"https://hoangph3.github.io/posts/seldon_core_workflows/","summary":"Automate model deployment workflows on top of Seldon Core.","title":"Orchestrating ML Workflows with Seldon Core"},{"content":"Setup environments Link here!\nTraining a model $ python3 train.py * optimization finished, #iter = 8 obj = -4.874554, rho = -0.126317 nSV = 11, nBSV = 8 * optimization finished, #iter = 17 obj = -2.130718, rho = 0.064464 nSV = 8, nBSV = 2 * optimization finished, #iter = 37 obj = -34.058808, rho = 0.107043 nSV = 47, nBSV = 45 Total nSV = 60 Write a class wrapper that exposes the logic of your model import pickle from sklearn import svm class IrisClassifier: def __init__(self): self._model: svm.SVC = pickle.load(open(\u0026#34;model.pkl\u0026#34;, \u0026#34;rb\u0026#34;)) def predict(self, X, features_names=None, meta=None): output = self._model.predict(X) return output Build image $ docker build -t hoangph3/sklearn_iris_classifier:v0.0.1 . Deploy the model Create a namespace to run your model in: $ kubectl create namespace seldon-model namespace/seldon created Create the manifest file deploy-model.yaml apiVersion: machinelearning.seldon.io/v1alpha2 kind: SeldonDeployment metadata: name: iris-model namespace: seldon-model spec: name: iris predictors: - componentSpecs: - spec: containers: - name: classifier image: hoangph3/sklearn_iris_classifier:v0.0.1 graph: name: classifier name: default replicas: 1 Deploy it to our Seldon Core Kubernetes Cluster: $ kubectl apply -f deploy-model.yaml seldondeployment.machinelearning.seldon.io/iris-model created $ kubectl get pods -n seldon-model NAME READY STATUS RESTARTS AGE iris-model-default-0-classifier-64d68cbd8d-tj2r5 3/3 Running 0 38s Testing $ export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath=\u0026#39;{.status.loadBalancer.ingress[0].ip}\u0026#39;) $ echo $INGRESS_HOST 10.106.254.233 $ curl -X POST -d @payload.json -H \u0026#39;Content-Type: application/json\u0026#39; \\ http://$INGRESS_HOST/seldon/seldon-model/iris-model/api/v1.0/predictions | json_pp { \u0026#34;data\u0026#34; : { \u0026#34;ndarray\u0026#34; : [ 2 ], \u0026#34;names\u0026#34; : [] }, \u0026#34;meta\u0026#34; : { \u0026#34;requestPath\u0026#34; : { \u0026#34;classifier\u0026#34; : \u0026#34;hoangph3/sklearn_iris_classifier:v0.0.1\u0026#34; } } } Authentication and Authorization Authentication and Authorization for Seldon Core Requests This is an example of setting up auth for seldon core model deployments in an istio enabled kubernetes cluster. Here we discuss the following topics:\nAuthentication at Seldon Deployment Component Level Authorization based on user id token claims Authentication at the Ingress Level You can get full source code here: first-model, auth.\n","permalink":"https://hoangph3.github.io/posts/seldon_core_first_model/","summary":"Deploy a first machine learning model on Kubernetes using Seldon Core, including authentication basics.","title":"Deploying Your First Model with Seldon Core"},{"content":"Building Kubeflow Components Pipeline components are self-contained sets of code that perform one step in your ML workflow, such as preprocessing data or training a model. To create a component, you must build the component’s implementation and define the component specification. This document describes the concepts required to build components, and demonstrates how to get started building components.\n1. Install the Kubeflow Pipelines SDK: $ export PIPELINE_VERSION=1.8.5 $ pip3 install kfp==$PIPELINE_VERSION $ kubectl apply -k \u0026#34;github.com/kubeflow/pipelines/manifests/kustomize/cluster-scoped-resources?ref=$PIPELINE_VERSION\u0026amp;timeout=300\u0026#34; $ kubectl wait --for condition=established --timeout=60s crd/applications.app.k8s.io $ kubectl apply -k \u0026#34;github.com/kubeflow/pipelines/manifests/kustomize/env/platform-agnostic-pns?ref=$PIPELINE_VERSION\u0026#34; $ kubectl get pods -n kubeflow NAME READY STATUS RESTARTS AGE cache-deployer-deployment-679cb5c746-4lrsj 1/1 Running 0 36s cache-server-864c559d7f-ql5cb 1/1 Running 0 36s metadata-envoy-deployment-7c8fc4dc6c-w9qvz 1/1 Running 0 35s metadata-grpc-deployment-5c8599b99c-h6qn9 1/1 Running 2 (29s ago) 35s metadata-writer-664d5b498d-zc9zg 1/1 Running 0 35s minio-6d6d45469f-59p4p 1/1 Running 0 35s ml-pipeline-7b4b88c975-4cnth 1/1 Running 0 35s ml-pipeline-persistenceagent-77bdd854b8-mszrn 1/1 Running 0 35s ml-pipeline-scheduledworkflow-7bbb6c9dc9-nxdsj 1/1 Running 0 35s ml-pipeline-ui-b77595fcf-lmj8j 1/1 Running 0 35s ml-pipeline-viewer-crd-7c87784fcf-2rpx9 1/1 Running 0 35s ml-pipeline-visualizationserver-754c5dd4dd-ltn7f 1/1 Running 0 34s mysql-55778745b6-lddcp 1/1 Running 0 34s workflow-controller-b7f95d6c6-d4lt2 1/1 Running 0 34s When everything is ready, you can run the following command to access the ml-pipeline-ui service.\n$ kubectl get svc -n kubeflow ml-pipeline-ui NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE ml-pipeline-ui ClusterIP 10.99.209.141 \u0026lt;none\u0026gt; 80/TCP 76s $ kubectl port-forward -n kubeflow svc/ml-pipeline-ui 8080:80 Forwarding from 127.0.0.1:8080 -\u0026gt; 3000 Forwarding from [::1]:8080 -\u0026gt; 3000 You can then open your browser and go to http://localhost:8080 to see the user interface.\n2. Building components and pipeline $ python3 build_component.py $ python3 build_pipeline.py 4. Run pipeline Upload pipeline Create new experiment Run pipeline in the experiment You can get full source code here!\n","permalink":"https://hoangph3.github.io/posts/kubeflow_pipeline_components/","summary":"Create custom and reusable components for Kubeflow Pipelines.","title":"Building Kubeflow Pipeline Components"},{"content":"Distributed training with TensorFlow on Kubernetes Suppose we have already with minikube cluster look like that below:\n$ minikube profile list |----------|-----------|---------|-------------|------|---------|---------|-------|--------| | Profile | VM Driver | Runtime | IP | Port | Version | Status | Nodes | Active | |----------|-----------|---------|-------------|------|---------|---------|-------|--------| | minikube | none | docker | 192.168.0.5 | 8443 | v1.24.3 | Running | 1 | * | |----------|-----------|---------|-------------|------|---------|---------|-------|--------| If you aren’t ready yet, following the instruction: https://github.com/hoangph3/devops-tutorial/tree/main/kubernetes/components/gpus\nCreate the kubeflow namespace:\n$ kubectl create namespace kubeflow namespace/kubeflow created Install kubeflow training-operator:\n$ kubectl apply -k \u0026#34;github.com/kubeflow/training-operator/manifests/overlays/standalone?ref=v1.5.0\u0026#34; Verify installation:\n$ kubectl get pods -n kubeflow NAME READY STATUS RESTARTS AGE training-operator-748d5cdc48-gffhk 1/1 Running 0 80s Build a training container:\n$ docker build -t mnist-distributed-training:v0.0.1 . Submit a training job:\n$ kubectl apply -f tfjob.yaml tfjob.kubeflow.org/multi-worker created Monitor the job:\n$ kubectl describe tfjobs.kubeflow.org multi-worker ... Normal SuccessfulCreatePod 2m24s tfjob-controller Created pod: multi-worker-worker-0 Normal SuccessfulCreatePod 2m24s tfjob-controller Created pod: multi-worker-worker-1 Normal SuccessfulCreatePod 2m24s tfjob-controller Created pod: multi-worker-worker-2 $ kubectl get pods NAME READY STATUS RESTARTS AGE multi-worker-worker-0 1/1 Running 0 12s multi-worker-worker-1 1/1 Running 0 12s multi-worker-worker-2 1/1 Running 0 12s $ kubectl logs -f multi-worker-worker-0 Epoch 1/10 100/100 [==============================] - 19s 84ms/step - loss: 2.3002 - accuracy: 0.1213 Epoch 2/10 100/100 [==============================] - 8s 82ms/step - loss: 2.2512 - accuracy: 0.2613 Epoch 3/10 100/100 [==============================] - 8s 83ms/step - loss: 2.1932 - accuracy: 0.4032 Epoch 4/10 100/100 [==============================] - 8s 83ms/step - loss: 2.1053 - accuracy: 0.5168 Epoch 5/10 100/100 [==============================] - 9s 95ms/step - loss: 1.9883 - accuracy: 0.5827 Epoch 6/10 100/100 [==============================] - 9s 90ms/step - loss: 1.8277 - accuracy: 0.6753 Epoch 7/10 100/100 [==============================] - 9s 90ms/step - loss: 1.6206 - accuracy: 0.7193 Epoch 8/10 100/100 [==============================] - 9s 93ms/step - loss: 1.4133 - accuracy: 0.7405 Epoch 9/10 100/100 [==============================] - 9s 85ms/step - loss: 1.1952 - accuracy: 0.7876 Epoch 10/10 100/100 [==============================] - 12s 121ms/step - loss: 1.0255 - accuracy: 0.7938 $ kubectl logs -f multi-worker-worker-1 Epoch 1/10 100/100 [==============================] - 19s 87ms/step - loss: 2.2840 - accuracy: 0.1466 Epoch 2/10 100/100 [==============================] - 9s 85ms/step - loss: 2.1953 - accuracy: 0.3773 Epoch 3/10 100/100 [==============================] - 9s 90ms/step - loss: 2.0776 - accuracy: 0.5458 Epoch 4/10 100/100 [==============================] - 9s 89ms/step - loss: 1.9279 - accuracy: 0.6505 Epoch 5/10 100/100 [==============================] - 9s 92ms/step - loss: 1.7265 - accuracy: 0.7306 Epoch 6/10 100/100 [==============================] - 9s 86ms/step - loss: 1.5023 - accuracy: 0.7515 Epoch 7/10 100/100 [==============================] - 9s 89ms/step - loss: 1.2802 - accuracy: 0.7727 Epoch 8/10 100/100 [==============================] - 9s 93ms/step - loss: 1.0960 - accuracy: 0.8052 Epoch 9/10 100/100 [==============================] - 8s 83ms/step - loss: 0.9224 - accuracy: 0.8259 Epoch 10/10 100/100 [==============================] - 6s 61ms/step - loss: 0.8297 - accuracy: 0.8219 $ kubectl logs -f multi-worker-worker-2 Epoch 1/10 100/100 [==============================] - 20s 85ms/step - loss: 2.3063 - accuracy: 0.0720 Epoch 2/10 100/100 [==============================] - 8s 83ms/step - loss: 2.2620 - accuracy: 0.1988 Epoch 3/10 100/100 [==============================] - 9s 89ms/step - loss: 2.2118 - accuracy: 0.2999 Epoch 4/10 100/100 [==============================] - 8s 84ms/step - loss: 2.1418 - accuracy: 0.4118 Epoch 5/10 100/100 [==============================] - 9s 89ms/step - loss: 2.0414 - accuracy: 0.5416 Epoch 6/10 100/100 [==============================] - 9s 95ms/step - loss: 1.9031 - accuracy: 0.6236 Epoch 7/10 100/100 [==============================] - 9s 93ms/step - loss: 1.7247 - accuracy: 0.7008 Epoch 8/10 100/100 [==============================] - 9s 95ms/step - loss: 1.5122 - accuracy: 0.7417 Epoch 9/10 100/100 [==============================] - 8s 84ms/step - loss: 1.2802 - accuracy: 0.7831 Epoch 10/10 100/100 [==============================] - 7s 67ms/step - loss: 1.1046 - accuracy: 0.7928 Check saved model directory:\n$ ls -la /home/hoang/Downloads/mnist/saved_model_dir total 112 drwxrwxr-x 4 hoang hoang 4096 Thg 9 27 00:55 . drwxrwxr-x 4 hoang hoang 4096 Thg 9 27 00:41 .. drwxr-xr-x 2 root root 4096 Thg 9 27 00:49 assets -rw-r--r-- 1 root root 96091 Thg 9 27 00:55 saved_model.pb drwxr-xr-x 2 root root 4096 Thg 9 27 00:55 variables You can get full source code here!\n","permalink":"https://hoangph3.github.io/posts/distributed_training_tensorflow/","summary":"Train TensorFlow models across multiple workers using distributed training strategies.","title":"Distributed Training with TensorFlow"},{"content":"Autoscaling Tensorflow model with Kubernetes Serving deep learning models can be especially challenging. The models are often large, requiring gigabytes of memory. They are also very compute intensive - a small number of concurrent requests can fully utilize a CPU or GPU. Automatic horizontal scaling is one of the primary strategies used in architecting scalable and reliable model serving infrastructures for deep learning models.\nFirstly, we need to deploy a dummy tensorflow model. In this case we use Resnet101 pretrained model.\nCreate the tf-serving namespace:\n$ kubectl create ns tf-serving namespace/tf-serving created Create the ConfigMap from the configmap-resnet101.yaml manifest file:\napiVersion: v1 kind: ConfigMap metadata: name: resnet101-configs namespace: tf-serving data: MODEL_NAME: image_classifier MODEL_PATH: /models/resnet101 $ kubectl apply -f configmap-resnet101.yaml configmap/resnet101-configs created Create the ResNet101 deployment:\napiVersion: apps/v1 kind: Deployment metadata: name: image-classifier-resnet101 namespace: tf-serving labels: app: image-classifier version: resnet101 spec: replicas: 1 selector: matchLabels: app: image-classifier version: resnet101 template: metadata: labels: app: image-classifier version: resnet101 spec: containers: - name: tf-serving image: \u0026#34;tensorflow/serving:2.5.1\u0026#34; args: - \u0026#34;--model_name=$(MODEL_NAME)\u0026#34; - \u0026#34;--model_base_path=$(MODEL_PATH)\u0026#34; envFrom: - configMapRef: name: resnet101-configs imagePullPolicy: IfNotPresent readinessProbe: tcpSocket: port: 8500 initialDelaySeconds: 10 periodSeconds: 5 failureThreshold: 10 ports: - name: http containerPort: 8501 protocol: TCP - name: grpc containerPort: 8500 protocol: TCP resources: requests: cpu: \u0026#34;0.5\u0026#34; memory: 1Gi volumeMounts: - name: model mountPath: /models/resnet101 volumes: - name: model hostPath: path: /home/hoang/Downloads/resnet101 $ kubectl apply -f deployment-resnet101.yaml deployment.apps/image-classifier-resnet101 created $ kubectl get deployments.apps -n tf-serving NAME READY UP-TO-DATE AVAILABLE AGE image-classifier-resnet101 1/1 1 1 6m27s Exposing the deployment to service:\napiVersion: v1 kind: Service metadata: name: image-classifier namespace: tf-serving labels: app: image-classifier spec: type: LoadBalancer ports: - port: 8500 protocol: TCP name: tf-serving-grpc - port: 8501 protocol: TCP name: tf-serving-http selector: app: image-classifier $ kubectl apply -f service.yaml service/image-classifier created $ kubectl get svc -n tf-serving NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE image-classifier LoadBalancer 10.101.25.178 \u0026lt;pending\u0026gt; 8500:31261/TCP,8501:31055/TCP 18s $ minikube tunnel $ kubectl get svc -n tf-serving NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE image-classifier LoadBalancer 10.101.25.178 10.101.25.178 8500:31261/TCP,8501:31055/TCP 43s The final step is to add Horizontal Pod Autoscaler (HPA). The command below configures HPA to start a new replica of TensorFlow Serving whenever the mean CPU utilization across all already running replicas reaches 60%. HPA will attempt to create up to 4 replicas and scale down to 1 replica.\n$ kubectl autoscale deployment image-classifier-resnet101 -n tf-serving \\ --cpu-percent=60 \\ --min=1 \\ --max=4 horizontalpodautoscaler.autoscaling/image-classifier-resnet101 autoscaled $ kubectl get hpa -n tf-serving NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE image-classifier-resnet101 Deployment/image-classifier-resnet101 \u0026lt;unknown\u0026gt;/60% 1 4 1 25s We get \u0026lt;unknown\u0026gt;/60% value in the TARGETS. Is there anything wrong? Let’s describe the horizontalpodautoscaler.autoscaling.\n$ kubectl describe horizontalpodautoscalers.autoscaling -n tf-serving image-classifier-resnet101 ... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedGetResourceMetric 13s horizontal-pod-autoscaler failed to get cpu utilization: missing request for cpu Warning FailedComputeMetricsReplicas 13s horizontal-pod-autoscaler invalid metrics (1 invalid out of 1), first error is: failed to get cpu utilization: missing request for cpu $ kubectl top nodes error: Metrics API not available We must install the metrics-server, note that we need to add --kubelet-insecure-tls under spec.template.spec.containers.args to disable certificate validation.\n$ kubectl apply -f metrics-server.yaml serviceaccount/metrics-server created clusterrole.rbac.authorization.k8s.io/system:aggregated-metrics-reader created clusterrole.rbac.authorization.k8s.io/system:metrics-server created rolebinding.rbac.authorization.k8s.io/metrics-server-auth-reader created clusterrolebinding.rbac.authorization.k8s.io/metrics-server:system:auth-delegator created clusterrolebinding.rbac.authorization.k8s.io/system:metrics-server created service/metrics-server created deployment.apps/metrics-server created apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io created $ kubectl get pods -n kube-system metrics-server-df6668697-nvg6c NAME READY STATUS RESTARTS AGE metrics-server-df6668697-nvg6c 1/1 Running 0 20m $ kubectl top nodes NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% jump-windows 1143m 28% 6777Mi 68% Now re-create the hpa and describe it:\n$ kubectl get hpa -n tf-serving NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE image-classifier-resnet101 Deployment/image-classifier-resnet101 0%/60% 1 4 1 17m $ kubectl describe hpa -n tf-serving image-classifier-resnet101 ... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal SuccessfulRescale 10m horizontal-pod-autoscaler New size: 1; reason: All metrics below target Testing the model with sample request body locust/request-body.json:\n$ EXTERNAL_IP=10.101.25.178 $ curl -d @locust/request-body.json -X POST http://${EXTERNAL_IP}:8501/v1/models/image_classifier/versions/1:predict { \u0026#34;predictions\u0026#34;: [[ ... ] ] } We are now ready to load test the ResNet101, we will use an open source load testing tool Locust to generate prediction requests.\nInstall locust:\n$ pip3 install locust $ locust -V locust 1.4.1 $ locust Could not find any locustfile! Ensure file ends in \u0026#39;.py\u0026#39; and see --help for available options. The locust folder contains the Locust script that generates prediction requests against the ResNet101 model. The script uses the same request body you used previously to verify the TensorFlow Serving deployment. The script is configured to progressively increase the number of simulated users that send prediction requests to the ResNet101 model. After reaching the maximum number of configured users, the script stops generating the load. The number of users is adjusted every 60s.\nTo start the test, execute the command:\n$ cd locust $ locust -f tasks.py --host http://${EXTERNAL_IP}:8501 ... [2022-09-30 21:07:01,259] jump-windows/INFO/locust.main: Starting web interface at http://0.0.0.0:8089 (accepting connections from all network interfaces) [2022-09-30 21:07:01,266] jump-windows/INFO/locust.main: Starting Locust 1.4.1 Open your favorite browser, and access to the address: http://0.0.0.0:8089, then Start swarming.\nWithin a minute or so, you should see the higher CPU load; for example:\n$ kubectl get hpa -n tf-serving image-classifier-resnet101 -w NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE image-classifier-resnet101 Deployment/image-classifier-resnet101 426%/60% 1 4 1 24m image-classifier-resnet101 Deployment/image-classifier-resnet101 215%/60% 1 4 4 24m Here, CPU consumption has increased to 215% of the request. As a result, the Deployment was resized to 4 replicas:\n$ kubectl get deployment -n tf-serving image-classifier-resnet101 NAME READY UP-TO-DATE AVAILABLE AGE image-classifier-resnet101 4/4 4 4 29m Stop sending the load by typing \u0026lt;Ctrl\u0026gt; + C in the locust terminal screen.\nThen verify the result state (after a minute or so):\n$ kubectl get hpa -n tf-serving image-classifier-resnet101 -w NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE image-classifier-resnet101 Deployment/image-classifier-resnet101 0%/60% 1 4 4 27m image-classifier-resnet101 Deployment/image-classifier-resnet101 0%/60% 1 4 1 32m and the Deployment also shows that it has scaled down:\n$ kubectl get deploy -n tf-serving image-classifier-resnet101 NAME READY UP-TO-DATE AVAILABLE AGE image-classifier-resnet101 1/1 1 1 36m Once CPU utilization dropped to 0, the HPA automatically scaled the number of replicas back down to 1. Autoscaling the replicas may take a few minutes.\nYou can get full source code here!\n","permalink":"https://hoangph3.github.io/posts/autoscale_tensorflow_serving/","summary":"Set up horizontal pod autoscaling for a TensorFlow Serving deployment on Kubernetes.","title":"Autoscaling TensorFlow Serving on Kubernetes"},{"content":"A mix of production ML engineering, DevOps tutorials, and side projects in computer vision and IoT.\nLLM Applications A working reference for the practical side of LLM engineering: fine-tuning, retrieval-augmented generation, model serving, and worked tutorials.\nLLMPython GitHub Claude Skills A personal library of Claude Code skills — reusable slash-command workflows any project can install, starting with a skill that drafts formatted .docx documents from a template.\nAI AgentClaude CodeAutomation GitHub MLOps Labs Hands-on labs for production machine learning: model serving with TensorFlow Serving and Seldon Core, feature stores, Kubeflow pipeline components, canary releases, and autoscaling.\nKubernetesTensorFlowMLOps Read the series GitHub DevOps Tutorial Ansible playbooks, Docker workflows, and Kubernetes cluster operations: bootstrapping, workload scheduling, security, networking, custom resources, and HashiCorp Nomad orchestration.\nDevOpsKubernetesAnsible Read the series GitHub Object-Oriented Programming SOLID principles and the classic creational, structural, and behavioral design patterns, implemented and explained with runnable Python examples.\nOOPPythonDesign Patterns Read the series GitHub Picking Robot A YOLO-based computer vision pipeline for a warehouse picking robot: object detection and grasp-point estimation feeding a robotic arm's pick logic.\nComputer VisionRoboticsYOLO GitHub Behavioral Biometrics Authenticating users from how they type and move the mouse, not what they know: siamese/contrastive networks trained on keystroke and mouse-dynamics datasets.\nBiometricsSecurityDeep Learning Keystroke Mouse Dicom 3D Generation A web tool that reconstructs and segments 3D bone models from DICOM CT scans, with adjustable smoothing per axis and a browser-based upload UI.\nComputer Vision3D ReconstructionPython GitHub Parking Slot IoT A simulated smart-parking system: sensor producers, a stream consumer that tracks per-slot state in a database, and a live front-end dashboard.\nIoTStreamingPython GitHub Web Security A hands-on progression through web application security topics, from beginner fundamentals to advanced exploitation and defense.\nSecurityJavaScript GitHub ","permalink":"https://hoangph3.github.io/projects/","summary":"A selection of projects and write-up series.","title":"Projects"}]