paint-brush
कुमा मेशेस हेड-ऑन - वह सब कुछ जो आपको जानना चाहिएद्वारा@jesperancinha
707 रीडिंग
707 रीडिंग

कुमा मेशेस हेड-ऑन - वह सब कुछ जो आपको जानना चाहिए

द्वारा João Esperancinha27m2024/04/13
Read on Terminal Reader

बहुत लंबा; पढ़ने के लिए

कुमा को जल्दी से सीखना शुरू करने के लिए, हमें सबसे महत्वपूर्ण चीजों में से एक क्लस्टर की आवश्यकता है। फिर, हमें कुबेरनेट्स (उर्फ k8s) में हमारे पॉड्स की स्थिति का पता लगाने के लिए एक कमांड की भी आवश्यकता है।
featured image - कुमा मेशेस हेड-ऑन - वह सब कुछ जो आपको जानना चाहिए
João Esperancinha HackerNoon profile picture
0-item
1-item

कुमा मेशेस हेड-ऑन - एक शुरुआती गाइड

Kuma जल्दी से सीखना शुरू करने के लिए, सबसे महत्वपूर्ण चीजों में से एक जो हमें चाहिए वह है क्लस्टर। फिर, हमें कुबेरनेट्स (उर्फ k8s ) में हमारे पॉड्स की स्थिति का पता लगाने के लिए एक कमांड की भी आवश्यकता है, हमें Kuma इंस्टॉल करने में भी सक्षम होना चाहिए, और अंत में, हमें कुछ Kuma कमांड जारी करने में भी सक्षम होना चाहिए।


यह कहने का एक लंबा तरीका है कि हमें Kuma के लिए सब कुछ तैयार करने के लिए 4 आवश्यक कमांड स्थापित करने की आवश्यकता है। ये कमांड हैं:

  • kind - इसे Docker में Kubernetes के नाम से भी जाना जाता है। यह एक ऐसा कमांड है जो सिर्फ़ kubectl के साथ सामान बनाने के भार का लाभ उठाता है।


  • kubectl - शायद इस सूची में सबसे अधिक अपेक्षित है, यदि आप पहले से ही k8s के साथ काम करने के आदी हैं। इस तरह हम अपने k8s क्लस्टर को कमांड जारी कर सकते हैं।


  • helm - हेल्म हमें कुछ बहुत ही उपयोगी स्क्रिप्ट निष्पादित करने की अनुमति देता है जो अन्य के अलावा, Kuma नियंत्रण विमान की स्थापना की अनुमति देता है।


  • kumactl - हम इस गाइड में इस कमांड का बहुत बार उपयोग नहीं करेंगे, लेकिन इसका उपयोग कैसे करें, इसके बारे में जानकारी होना महत्वपूर्ण है।


यह गाइड आपको बताएगी कि Ubuntu में यह कैसे करना है। यह सब Ubuntu सिस्टम में परीक्षण किया गया है। यदि आप Mac-OS या Windows या आपके पास मौजूद किसी अन्य ऑपरेटिंग सिस्टम में इसे इंस्टॉल करने के तरीके के बारे में गाइड में रुचि रखते हैं, तो कृपया मुझे मेरे YouTube चैनल JESPROTECH समुदाय पर एक शाउट-आउट दें।


I. कमांड स्थापित करना


चित्र का वर्णन


Docker में प्रकार ( k8s )

काइंड को स्थापित करने के लिए, हमें ये आदेश जारी करने होंगे:

 [ $(uname -m) = x86_64 ] && curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64 chmod +x ./kind sudo mv ./kind /usr/local/bin/kind


यह ध्यान रखना महत्वपूर्ण है कि कमांड kind आपके /usr/local/bin/kind में स्थापित किया जाएगा। यह प्रत्येक सिस्टम के अनुसार अलग-अलग हो सकता है, यहाँ तक कि Linux वितरण में भी।


प्रमाणपत्र और GPG कुंजियाँ स्थापित करना

helm और kubectl दोनों कमांड को कुछ GPG कुंजियों की उपस्थिति के साथ इंस्टॉल किया जाना चाहिए। इस तरह हम उन्हें अपने Linux apt वितरण के स्थानीय रिपॉजिटरी में जोड़ सकते हैं:

 sudo apt-get install -y apt-transport-https ca-certificates curl gpg curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.29/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list sudo apt-get update



कुबेक्टल

पिछला चरण पूरा हो जाने के बाद Kubectl की स्थापना बहुत आसान है:

 sudo apt-get install -y kubelet kubeadm kubectl


kubelet , kubeadm और kubectl कमांड अनिवार्य नहीं हैं, लेकिन उन्हें इंस्टॉल करना एक अच्छा विचार है।


संचालन, पतवार

जैसा कि आप पहले ही अनुमान लगा चुके होंगे, helm अब स्थापित करना भी बहुत आसान है:

 sudo apt-get install -y helm

कुमा

Kuma स्थापना थोड़ी बोझिल हो सकती है क्योंकि इसमें एक मैनुअल चरण शामिल है, लेकिन सबसे पहले, हमें अपनी निर्भरताएँ डाउनलोड करनी होंगी:

 cd ~ || exit; curl -L https://kuma.io/installer.sh | VERSION=2.6.1 sh -


यह आदेश जारी करने से पहले सुनिश्चित करें कि आप अपने HOME फ़ोल्डर में हैं। यह महत्वपूर्ण है कि Kuma ऐसी जगह पर स्थापित किया जाए जहाँ यह आसानी से सुलभ हो और आसानी से देखा जा सके, उदाहरण के लिए, अगर हम इसे हटाने का फैसला करते हैं।


एक बार जब हम यह कर लें, तो हमारे PATH में bin फ़ोल्डर को जोड़ना भी बहुत महत्वपूर्ण है:

 export PATH=~/kuma-2.6.1/bin:$PATH;


इस लाइन को अपने स्टार्ट-अप स्क्रिप्ट के अंत में या बीच में कहीं भी जोड़ने से यह प्रक्रिया आसान हो जाएगी। आपकी स्टार्टअप स्क्रिप्ट इनमें से कोई भी हो सकती है .bashrc , .zshrc , .profile और संभवतः कोई दूसरा रूप ले सकती है।


k9s

k9s इंस्टॉल करना भी अन्य एप्लिकेशन से काफी अलग है। इस मामले में, हम या तो pacman या Linux के लिए brew उपयोग कर सकते हैं। मैंने अधिकतर Mac-OS के लिए brew उपयोग किया है और Linux में इसकी शायद ही कभी आवश्यकता पड़ी हो, लेकिन इस मामले में, इसकी बहुत आवश्यकता है, और इसलिए ऐसा करने के लिए सबसे पहले, हमें इस तरह से Brew को इंस्टॉल करना होगा:

 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"


एक बार ब्रू इंस्टॉलेशन पूरा हो जाने के बाद, हमें बस k9s (" kanines ") इंस्टॉल करना है:

 brew install derailed/k9s/k9s


एक बात जो ध्यान में रखना महत्वपूर्ण है, और आप शायद इस बात को तब नोटिस करेंगे जब आप पहली बार k9s इंस्टॉल और चलाना शुरू करेंगे, वह यह है कि यदि कोई क्लस्टर जिसकी निगरानी k9s कर रहा है उसे हटा दिया जाता है और/या जोड़ दिया जाता है, तो k9s क्रैश हो जाएगा।


II. क्लस्टर बनाना

 kind create cluster --name=wlsm-mesh-zone kubectl cluster-info --context kind-wlsm-mesh-zone


पहला कमांड wlsm-mesh-zone नाम का एक क्लस्टर बनाता है। यह सिर्फ़ एक क्लस्टर है जिसका इस्तेमाल हम Kuma को इंस्टॉल करने के लिए करेंगे। दूसरा कमांड क्लस्टर की स्थिति जाँचने के लिए इस्तेमाल किया जाता है।


III. स्थानीय डॉकर रजिस्ट्री बनाना

जैसा कि मैंने पहले बताया, हम एक डॉकर रजिस्ट्री काफी आसानी से बना सकते हैं। इसे बनाना जितना आसान लग सकता है, इसे बनाने के लिए स्क्रिप्ट काफी मुश्किल है। इसलिए, सबसे अच्छी बात यह है कि उनकी वेबसाइट पर पहले से उपलब्ध स्क्रिप्ट को कॉपी करके पेस्ट कर दें। यहाँ, हम इस स्क्रिप्ट को डाउनलोड कर सकते हैं:

 #!/bin/sh # Original Source # https://creativecommons.org/licenses/by/4.0/ # https://kind.sigs.k8s.io/docs/user/local-registry/ set -o errexit # 1. Create registry container unless it already exists reg_name='kind-registry' reg_port='5001' if [ "$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" != 'true' ]; then docker run \ -d --restart=always -p "127.0.0.1:${reg_port}:5000" --network bridge --name "${reg_name}" \ registry:2 fi # 2. Create kind cluster with containerd registry config dir enabled # TODO: kind will eventually enable this by default and this patch will # be unnecessary. # # See: # https://github.com/kubernetes-sigs/kind/issues/2875 # https://github.com/containerd/containerd/blob/main/docs/cri/config.md#registry-configuration # See: https://github.com/containerd/containerd/blob/main/docs/hosts.md cat <<EOF | kind create cluster --config=- kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 containerdConfigPatches: - |- [plugins."io.containerd.grpc.v1.cri".registry] config_path = "/etc/containerd/certs.d" EOF # 3. Add the registry config to the nodes # # This is necessary because localhost resolves to loopback addresses that are # network-namespace local. # In other words: localhost in the container is not localhost on the host. # # We want a consistent name that works from both ends, so we tell containerd to # alias localhost:${reg_port} to the registry container when pulling images REGISTRY_DIR="/etc/containerd/certs.d/localhost:${reg_port}" for node in $(kind get nodes); do docker exec "${node}" mkdir -p "${REGISTRY_DIR}" cat <<EOF | docker exec -i "${node}" cp /dev/stdin "${REGISTRY_DIR}/hosts.toml" [host."http://${reg_name}:5000"] EOF done # 4. Connect the registry to the cluster network if not already connected # This allows kind to bootstrap the network but ensures they're on the same network if [ "$(docker inspect -f='{{json .NetworkSettings.Networks.kind}}' "${reg_name}")" = 'null' ]; then docker network connect "kind" "${reg_name}" fi # 5. Document the local registry # https://github.com/kubernetes/enhancements/tree/master/keps/sig-cluster-lifecycle/generic/1755-communicating-a-local-registry cat <<EOF | kubectl apply -f - apiVersion: v1 kind: ConfigMap metadata: name: local-registry-hosting namespace: kube-public data: localRegistryHosting.v1: | host: "localhost:${reg_port}" help: "https://kind.sigs.k8s.io/docs/user/local-registry/" EOF



यह स्क्रिप्टप्रोजेक्ट के रूट फ़ोल्डर में पाई जा सकती है। और स्थानीय डॉकर रजिस्ट्री को स्थापित करने के लिए, हमें केवल इस बैश स्क्रिप्ट को चलाने की आवश्यकता है।


IV. कोड कैसे बनाया गया है

इस ब्लॉग पोस्ट के उदाहरण के लिए मैंने जो कोड प्रदान किया है, उसके बारे में बहुत कुछ कहा जा सकता है। हालाँकि, इस मामले में, आइए केवल कुछ प्रमुख पहलुओं पर ध्यान केंद्रित करें। आइएश्रोता सेवा से कलेक्टर और फिरडेटाबेस तक शुरू करें। जब हम स्थानीय रूप से सेवाएँ चलाते हैं या कंटेनर को चालू करने के लिए docker-compose कॉन्फ़िगरेशन का उपयोग करते हैं, तो आमतौर पर, हम DNS-एट्रिब्यूटेड नामों का उपयोग करते हैं जो स्वचालित रूप से कंटेनर नाम या उस नाम के रूप में असाइन हो जाते हैं जिसे हम hostname के साथ कॉन्फ़िगर करते हैं।


k8s के साथ, नियमों का एक सेट भी है जो होस्ट नामों को पूरे क्लस्टर में उपलब्ध कराता है। आइए श्रोता और कलेक्टर उदाहरणों पर एक नज़र डालें:


श्रोता उदाहरण

श्रोता Spring framework उपयोग करके Java में विकसित एक एप्लिकेशन है। इस तरह से बनाए गए सभी एप्लिकेशन की तरह, इसमें भी एक application.properties फ़ाइल है:

 spring.application.name=wlsm-listener-service server.port=8080 spring.main.web-application-type=reactive spring.webflux.base-path=/app/v1/listener wslm.url.collector=http://localhost:8081/api/v1/collector


इन सभी गुणों में, इस समय ध्यान देने योग्य सबसे महत्वपूर्ण गुण wslm.url.collector गुण है। default कॉन्फ़िगरेशन के साथ, हम किसी भी कंटेनरीकृत वातावरण का उपयोग किए बिना स्थानीय रूप से इस सेवा को चला सकते हैं। हालाँकि, k8s क्लस्टर में, हमें collector तक पहुँचने में सक्षम होना चाहिए, और इसके लिए, हमारे पास परिभाषा फ़ाइल application-prod.properties के साथ एक prod प्रोफ़ाइल है:

 wslm.url.collector=http://wlsm-collector-deployment.wlsm-namespace.svc.cluster.local:8081/api/v1/collector


यह प्रॉपर्टी होस्ट wlsm-collector-deployment.wlsm-namespace.svc.cluster.local तक पहुँचने का प्रयास करती है। यह फ़ाइल इस कॉन्फ़िगरेशन का अनुसरण करती है:

<Service Name>.<Namespace>.svc.cluster.local

हमारे पास 5 डॉट-सेपरेटेड एलिमेंट हैं। अंतिम तीन स्थिर हैं, और पहले दो उस मशीन पर निर्भर करते हैं जिस तक हम पहुँचने की कोशिश कर रहे हैं। बाईं ओर, हम सेवा का नाम उसके बाद नामस्थान रखते हैं। यह समझना महत्वपूर्ण है कि क्लस्टर के भीतर कंटेनर एक दूसरे से कैसे जुड़े हुए हैं।


कोड का वह हिस्सा जिस पर नज़र डालना दिलचस्प है, वह है कंट्रोलर और सर्विस। कंट्रोलर इस तरह दिखता है:

 @RestController @RequestMapping public class ListenerController { private final ListenerService listenerService; ListenerController(ListenerService listenerService) { this.listenerService = listenerService; } @GetMapping("info") public String info() { return "Listener Service V1"; } @PostMapping("create") public Mono<AnimalLocationDto> sendAnimalLocation( @RequestBody AnimalLocationDto animalLocationDto) { return listenerService.persist(animalLocationDto); } }


और यह सेवा इस प्रकार दिखती है:

 @Service public class ListenerService { @Value("${wslm.url.collector:http://localhost:8080}") private String collectorUrl; private final WebClient client = WebClient.create(collectorUrl); HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(); List<AnimalLocationDto> cache = hazelcastInstance.getList("data"); public Mono<AnimalLocationDto> persist(AnimalLocationDto animalLocationDto) { cache.add(animalLocationDto); return client.post() .uri(collectorUrl.concat("/animals")) .contentType(MediaType.APPLICATION_JSON) .bodyValue(animalLocationDto) .retrieve() .bodyToMono(AnimalLocationDto.class); } }


जैसा कि आपने पहले ही देखा होगा, यह पहला एप्लिकेशन, इस रिपॉजिटरी में Spring Framework उपयोग करके कार्यान्वित किए गए सभी एप्लिकेशन की तरह, प्रतिक्रियाशील है, और वे सभी tomcat के बजाय netty उपयोग करते हैं। फिलहाल, हम इस कोड में hazelcast उपयोग को अनदेखा कर सकते हैं। इसका उपयोग इस परियोजना के बाद के संस्करणों के लिए किया जाएगा।


कलेक्टर उदाहरण

इस बिंदु पर कलेक्टर श्रोता की तरह ही काम करता है। अभी इसका एकमात्र काम श्रोता से डेटाबेस तक डेटा रिले करना है और ऐसा करने के लिए, कलेक्टर को केवल यह जानना होगा कि डेटाबेस कहाँ है। आइए application.properties file of this project पर भी यही विश्लेषण करें:

 spring.application.name=wlsm-collector-service server.port=8081 spring.main.web-application-type=reactive spring.webflux.base-path=/api/v1/collector spring.r2dbc.url=r2dbc:postgresql://localhost:5432/wlsm spring.r2dbc.username=admin spring.r2dbc.password=admin spring.data.r2dbc.repositories.naming-strategy=org.springframework.data.relational.core.mapping.BasicRelationalPersistentEntityNamingStrategy spring.data.r2dbc.repositories.naming-strategy.table=org.springframework.data.relational.core.mapping.SnakeCaseNamingStrategy spring.data.r2dbc.repositories.naming-strategy.column=org.springframework.data.relational.core.mapping.SnakeCaseNamingStrategy


ये गुण सेवा को चालू करने के लिए न्यूनतम आवश्यक हैं। हालाँकि, यह केवल इसे स्थानीय रूप से चलाने में सक्षम होने के लिए है। और इस सेवा के लिए, हमारे पास prod प्रोफ़ाइल फ़ाइल भी है, और हम इसे यहाँ application-prod.properties में देख सकते हैं:

 spring.r2dbc.url=r2dbc:postgresql://wlsm-database-deployment.wlsm-namespace.svc.cluster.local:5432/wlsm


इस मामले में डेटाबेस कनेक्शन डेटाबेस के होस्ट को संदर्भित करता है:

wlsm-database-deployment.wlsm-namespace.svc.cluster.local


जो फिर से उसी विश्लेषण का अनुसरण करता है जैसा कि हमने पहले देखा है। बाईं ओर, हम सेवा का नाम देखते हैं, उसके बाद नामस्थान है जिसे अंत में svc.cluster.local के साथ जोड़ा गया है।


और इस सेवा के लिए, हम एक नियंत्रक और एक सेवा का भी उपयोग करते हैं। नियंत्रक इस तरह दिखता है:

 @RestController @RequestMapping class CollectorController( val collectorService: CollectorService ) { @PostMapping("animals") suspend fun listenAnimalLocation(@RequestBody animalLocationDto: AnimalLocationDto): AnimalLocationDto = run { collectorService.persist(animalLocationDto) animalLocationDto } }


और यह सेवा इस प्रकार दिखती है:

 @Service class CollectorService( val applicationEventPublisher: ApplicationEventPublisher ) { fun persist(animalLocationDto: AnimalLocationDto) = applicationEventPublisher.publishEvent(AnimalLocationEvent(animalLocationDto)) }


यह सेवा एक इवेंट प्रकाशक का उपयोग करती है जिसे applicationEventPublisher कहा जाता है, जो एक इवेंट स्ट्रीमिंग आर्किटेक्चर का अनुसरण करता है जिसे बाद में इस इवेंट श्रोता में नियंत्रित किया जाता है, जिसे हम आसानी से देख सकते हैं कि यह प्रतिक्रियाशील आर्किटेक्चर कार्यान्वयन प्रतिमानों में रखने के लिए r2dbc उपयोग करता है:

 @Service class EventHandlerService( val animalLocationDao: AnimalLocationDao ) { @EventListener fun processEvent(animalLocationEvent: AnimalLocationEvent){ println(animalLocationEvent) runBlocking(Dispatchers.IO) { animalLocationDao.save(animalLocationEvent.animalLocationDto.toEntity()) } } }



V. स्क्रिप्ट तैनात करें

k8s के साथ तैनाती करना आम तौर पर एक बहुत ही सरल कार्य है। हालाँकि, हमारी सेवाओं के लिए आवश्यक कॉन्फ़िगरेशन पर एक नज़र डालना भी महत्वपूर्ण है। उदाहरण के लिए, आइए श्रोता कार्यान्वयन पर एक नज़र डालें:

 apiVersion: v1 kind: Namespace metadata: name: wlsm-namespace labels: kuma.io/sidecar-injection: enabled --- apiVersion: apps/v1 kind: Deployment metadata: name: wlsm-listener namespace: wlsm-namespace spec: replicas: 1 selector: matchLabels: app: wlsm-listener template: metadata: labels: app: wlsm-listener spec: containers: - name: wlsm-listener-service image: localhost:5001/wlsm-listener-service:latest imagePullPolicy: Always ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: wlsm-listener-deployment namespace: wlsm-namespace spec: selector: app: wlsm-listener ports: - protocol: TCP appProtocol: http port: 8080


इस कॉन्फ़िगरेशन में तीन ब्लॉक हैं। पहला ब्लॉक नेमस्पेस ब्लॉक है। नेमस्पेस कॉन्फ़िगरेशन कुमा को एनवॉय साइडकार्स को इंजेक्ट करने में सक्षम बनाने के लिए महत्वपूर्ण है, जिसकी उसे नीतियों को लागू करने के लिए आवश्यकता है। परिभाषित नेमस्पेस के बिना, kuma ऐसा करने में सक्षम नहीं होगा। कुमा को कॉन्फ़िगर करते समय हमें जिस दूसरी चीज़ पर ध्यान देने की ज़रूरत है, वह यह है कि नेमस्पेस में उचित लेबल होना चाहिए जिसे कुमा पहचान सके:

kuma.io/sidecar-injection: enabled.


कुमा को काम करने के लिए सही लेबल के साथ नामस्थान परिभाषा महत्वपूर्ण है। दूसरे ब्लॉक में, हम परिनियोजन की परिभाषा पाते हैं। इस तरह हम परिभाषित करते हैं कि हमारे पॉड की परिनियोजन हमारे कुबेरनेट्स क्लस्टर में कैसी दिखेगी। यहाँ जिस चीज़ पर ध्यान देना ज़रूरी है, वह है image , imagePullPolicy और containerPort । इमेज उस डॉकर इमेज का पूरा टैग है जिसका हम उपयोग कर रहे हैं।


kind के साथ बनाए गए हमारे डॉकर रजिस्ट्री के लिए कॉन्फ़िगर किया गया पोर्ट 5001 है, और यह हमारी छवि के लिए टैग में शामिल है। यह एक टैग के रूप में काम करता है, लेकिन हमारे डॉकर रजिस्ट्री के कनेक्शन के रूप में भी काम करता है। इस तरह, हम छवियों को खींच सकते हैं और अपने Kubernetes वातावरण में चलाने के लिए अपना कंटेनर बना सकते हैं।


लेकिन, ज़ाहिर है, छवियों का उपयोग करने में सक्षम होने के लिए, हमें उन्हें बनाने की ज़रूरत है, और इसके लिए, आइए देखें कि listener उदाहरण और database उदाहरण में यह कैसे किया जाता है। listener के लिए डॉकर छवि इस तरह परिभाषित की गई है:

 FROM eclipse-temurin:21-jdk-alpine WORKDIR /root ENV LANG=C.UTF-8 COPY entrypoint.sh /root COPY build/libs/wlsm-listener-service.jar /root/wlsm-listener-service.jar ENTRYPOINT ["/root/entrypoint.sh"]


यह सब eclipse-temurin:21-jdk-alpine नामक बेस इमेज से शुरू होता है। इसके बाद, हम प्रोजेक्ट बनाकर बनाए गए jar को कॉपी करते हैं और फिर उसकी एक कॉपी अपनी इमेज में बनाते हैं। इससे पहले, हम entrypoint.sh को कंटेनर में भी कॉपी करते हैं और उसका उपयोग करने के लिए ENTRYPOINT परिभाषित करते हैं। entrypoint बस jar को इस तरह कॉल करता है:

 #!/usr/bin/env sh java -jar -Dspring.profiles.active=prod wlsm-listener-service.jar


database सेवा काफी अलग है क्योंकि यह कुछ स्क्रिप्ट का उपयोग करती है जो ओपनसोर्स हैं और ऑनलाइन उपलब्ध हैं:

 FROM postgres:15 COPY . /docker-entrypoint-initdb.d COPY ./multiple /docker-entrypoint-initdb.d/multiple ENV POSTGRES_USER=admin ENV POSTGRES_PASSWORD=admin ENV POSTGRES_MULTIPLE_DATABASES=wlsm EXPOSE 5432


यह स्क्रिप्ट docker init डायरेक्टरी में निम्न फ़ाइल और फ़ोल्डर की एक प्रतिलिपि बनाती है: create-multiple-postgresql-databases.sh और multiple । अंत में, हम अपने डेटाबेस और उपयोगकर्ता नाम/पासवर्ड संयोजनों को परिभाषित करने के लिए उन स्क्रिप्ट में उपयोग किए जाने वाले चर को परिभाषित करते हैं।


डेटाबेस निम्नलिखित स्कीमा का उपयोग करके बनाया गया है:

 CREATE TABLE families( id uuid DEFAULT gen_random_uuid(), name VARCHAR(100), PRIMARY KEY(id) ); CREATE TABLE genuses( id uuid DEFAULT gen_random_uuid(), name VARCHAR(100), PRIMARY KEY(id) ); CREATE TABLE species( id uuid DEFAULT gen_random_uuid(), common_name VARCHAR(100), family uuid, genus uuid, PRIMARY KEY(id), CONSTRAINT fk_species FOREIGN KEY(family) REFERENCES families(id), CONSTRAINT fk_genus FOREIGN KEY(genus) REFERENCES genuses(id) ); CREATE TABLE animal ( id uuid DEFAULT gen_random_uuid(), name VARCHAR(100), species_id uuid, PRIMARY KEY(id), CONSTRAINT fk_species FOREIGN KEY(species_id) REFERENCES species(id) ); CREATE TABLE animal_location ( id uuid DEFAULT gen_random_uuid(), animal_id uuid, latitude BIGINT, longitude BIGINT, PRIMARY KEY(id), CONSTRAINT fk_animal FOREIGN KEY(animal_id) REFERENCES animal(id) );


और, डेटा उदाहरण के तौर पर, हम piquinho नाम से एक जानवर को पंजीकृत करेंगे। पिक्विन्हो बस एक यात्रा करने वाले अल्बाट्रॉस का नाम है जो दुनिया भर में यात्रा कर रहा है, जिसमें एक सेंसर लगा हुआ है, और हम उस डेटा को पढ़ रहे हैं जो सेंसर हमें भेज रहा है। दो टेबल हैं जो प्रजातियों को परिभाषित करती हैं। यानी प्रजाति और जीनस जो प्रजातियों को परिभाषित करते हैं। ये टेबल families और genuses हैं।


तालिका species उस प्रजाति को परिभाषित करती है जिससे पशु संबंधित है। अंत में, हम तालिका में उसी नाम का एक animal परिभाषित करते हैं जहाँ प्रजाति और पशु का नाम पंजीकृत हो जाता है। डेटाबेस इस तरह दिखता है:
चित्र का वर्णन

चित्र बनाने, निर्माण करने और अपना प्रोजेक्ट शुरू करने के लिए, हम निम्नलिखित कमांड चला सकते हैं जो Makefile में उपलब्ध हैं:

 make make create-and-push-images make k8s-apply-deployment


पहला मेक सिर्फ़ एक gradle build कमांड है। दूसरे कमांड में इस वेरिएबल का इस्तेमाल किया गया:

 MODULE_TAGS := aggregator \ collector \ listener \ management \ database


चलाने के लिए:

 docker images "*/*wlsm*" --format '{{.Repository}}' | xargs -I {} docker rmi {} @for tag in $(MODULE_TAGS); do \ export CURRENT=$(shell pwd); \ echo "Building Image $$image..."; \ cd "wlsm-"$$tag"-service"; \ docker build . --tag localhost:5001/"wlsm-"$$tag"-service"; \ docker push localhost:5001/"wlsm-"$$tag"-service"; \ cd $$CURRENT; \ done


यह बस हर मॉड्यूल से होकर गुजरता है और एक मानक जेनेरिक कमांड का उपयोग करता है जो छवियों को बनाने और उन्हें पोर्ट 5001 पर स्थानीय रजिस्ट्री में पुश करने के लिए MODULE_TAGS में दिए गए मान के अनुसार बदलता है। उसी रणनीति का पालन करते हुए, हम अपने पॉड्स को तैनात करने के लिए तीसरे कमांड का उपयोग कर सकते हैं। यह तीसरा कमांड एक अलग लूप का उपयोग करता है जो इस तरह दिखता है:

 @for tag in $(MODULE_TAGS); do \ export CURRENT=$(shell pwd); \ echo "Applying File $$tag..."; \ cd "wlsm-"$$tag"-service"; \ kubectl apply -f $$tag-deployment.yaml --force; \ cd $$CURRENT; \ done


इस मामले में, यह प्रत्येक परिनियोजन स्क्रिप्ट को प्रत्येक सेवा पर लागू करता है। यदि हम kubectl get pods --all-namespaces कमांड चलाते हैं, तो हमें यह आउटपुट मिलना चाहिए:

 NAMESPACE NAME READY STATUS RESTARTS AGE kube-system coredns-76f75df574-dmt5m 1/1 Running 0 5m21s kube-system coredns-76f75df574-jtrfr 1/1 Running 0 5m21s kube-system etcd-kind-control-plane 1/1 Running 0 5m38s kube-system kindnet-7frts 1/1 Running 0 5m21s kube-system kube-apiserver-kind-control-plane 1/1 Running 0 5m36s kube-system kube-controller-manager-kind-control-plane 1/1 Running 0 5m36s kube-system kube-proxy-njzvl 1/1 Running 0 5m21s kube-system kube-scheduler-kind-control-plane 1/1 Running 0 5m36s kuma-system kuma-control-plane-5f47fdb4c6-7sqmp 1/1 Running 0 17s local-path-storage local-path-provisioner-7577fdbbfb-5qnxr 1/1 Running 0 5m21s wlsm-namespace wlsm-aggregator-64fc4599b-hg9qw 1/1 Running 0 4m23s wlsm-namespace wlsm-collector-5d44b54dbc-swf84 1/1 Running 0 4m23s wlsm-namespace wlsm-database-666d794c87-pslzp 1/1 Running 0 4m22s wlsm-namespace wlsm-listener-7bfbcf799-f44f5 1/1 Running 0 4m23s wlsm-namespace wlsm-management-748cf7b48f-8cjh9 1/1 Running 0 4m23s


इस बिंदु पर हमें जो देखना चाहिए वह है kuma-control-plane , kube-controller-manager और हमारे अपने कस्टम wlsm-namespace में चलने वाली सभी सेवाओं की उपस्थिति। हमारा क्लस्टर बाहर से अलग-थलग है, और विभिन्न पोर्ट तक पहुँचने में सक्षम होने के लिए, हमें हर उस पॉड के लिए port-forwarding बनाने की ज़रूरत है जिसे हम एक्सेस करना चाहते हैं। इसके लिए, हम अलग-अलग टैब में ये कमांड जारी कर सकते हैं:

हम k9s को देखकर भी इस पर एक नज़र डाल सकते हैं:

चित्र का वर्णन

 kubectl port-forward svc/wlsm-collector-deployment -n wlsm-namespace 8081:8081 kubectl port-forward svc/wlsm-listener-deployment -n wlsm-namespace 8080:8080 kubectl port-forward svc/wlsm-database-deployment -n wlsm-namespace 5432:5432 kubectl port-forward svc/kuma-control-plane -n kuma-system 5681:5681


VI. एप्लिकेशन चलाना

एप्लिकेशन को चलाने के लिए, हमें सभी पोर्ट खोलने चाहिए, और जब वे सभी खुल जाएं, तो हमें अपनी स्क्रीन पर कुछ इस तरह दिखना चाहिए:

चित्र का वर्णन

हम localhost और पोर्ट 5432 का उपयोग करके डेटाबेस से कनेक्ट कर सकते हैं। कनेक्शन स्ट्रिंग यह है: jdbc:postgresql://localhost:5432/wlsm । और इसे एक्सेस करने के लिए हम admin / admin के यूजरनेम/पासवर्ड संयोजन का उपयोग करते हैं।


किसी भी परीक्षण को करने से पहले हमें जो पहली चीज़ करनी है, वह है Piquinho की आईडी जानना, और हम ऐसा Intellij डेटाबेस टूल का उपयोग करके कर सकते हैं:

चित्र का वर्णन

प्रोजेक्ट के रूट फ़ोल्डर में, test-requests.http नामक एक फ़ाइल है। यह हमारे खुले पोर्ट के विरुद्ध REST अनुरोध बनाने के लिए एक स्क्रैच फ़ाइल है:

 ### GET http://localhost:8080/app/v1/listener/info ### POST http://localhost:8080/app/v1/listener/create Content-Type: application/json { "animalId": "2ffc17b7-1956-4105-845f-b10a766789da", "latitude": 52505252, "longitude": 2869152 } ### POST http://localhost:8081/api/v1/collector/animals Content-Type: application/json { "animalId": "2ffc17b7-1956-4105-845f-b10a766789da", "latitude": 52505252, "longitude": 2869152 }


इस फ़ाइल का उपयोग करने में सक्षम होने के लिए, हमें केवल इस उदाहरण में ID को 2ffc17b7-1956-4105-845f-b10a766789da से d5ad0824-71c0-4786-a04a-ac2b9a032da4 में बदलने की आवश्यकता है। इस मामले में, हम कलेक्टर या श्रोता से अनुरोध कर सकते हैं। दोनों अनुरोध काम करने चाहिए, और हमें बाद में प्रत्येक अनुरोध के लिए इस तरह की प्रतिक्रिया देखनी चाहिए:

 { "animalId": "d5ad0824-71c0-4786-a04a-ac2b9a032da4", "latitude": 52505252, "longitude": 2869152 } Response file saved. > 2024-04-12T001024.200.json Response code: 200 (OK); Time: 7460ms (7 s 460 ms); Content length: 91 bytes (91 B)


क्योंकि दोनों पोर्ट खुले हैं और वे, इस बिंदु पर, एक ही पेलोड प्रकार साझा करते हैं, हम श्रोता और कलेक्टर को समान अनुरोध कर सकते हैं। उन दो अनुरोधों को करने के बाद, हमें तालिका animal_locations में परिणाम मिलना चाहिए:

चित्र का वर्णन

तो, यह केवल इस बात की पुष्टि करता है कि क्लस्टर सही ढंग से चल रहा है, और अब, हम अपने कुमा मेश के साथ नीतियों का परीक्षण करने के लिए तैयार हैं।

VII. मेशट्रैफ़िकपरमिशन - भाग I

MeshTrafficPermission उन सुविधाओं में से एक है जिसे हम कुमा में चुन सकते हैं, और यह संभवतः सबसे अधिक उपयोग की जाने वाली सुविधा है।


लेकिन सबसे पहले, आइए कुमा कंट्रोल प्लेन को एक्सप्लोर करने के लिए कुछ समय निकालें। सभी फ़ॉरवर्डिंग चालू होने के बाद, हम बस localhost:5681/gui पर जा सकते हैं और अपने कुमा मेश को विज़ुअलाइज़ कर सकते हैं। मुख्य पृष्ठ पर, हमें कुछ इस तरह दिखना चाहिए:

चित्र का वर्णन

फिलहाल देखने के लिए कुछ खास नहीं है, लेकिन आइए अब MeshTrafficPermission लागू करें:

 echo "apiVersion: kuma.io/v1alpha1 kind: MeshTrafficPermission metadata: namespace: kuma-system name: mtp spec: targetRef: kind: Mesh from: - targetRef: kind: Mesh default: action: Allow" | kubectl apply -f -


एक बार जब हम इसे लागू करते हैं, तो हमें इस तरह की प्रतिक्रिया मिलनी चाहिए: meshtrafficpermission.kuma.io/mtp created .

VIII. जाल

जब हमारे क्लस्टर के सेटअप की बात आती है तो मेश को लागू करने से बहुत ज़्यादा बदलाव नहीं होता है। यह हमें ट्रैफ़िक रूटिंग नीतियाँ सेट करने की अनुमति देता है।


ऐसी कई चीजें हैं जिनमें से हम चुन सकते हैं, लेकिन सबसे स्पष्ट चीजों में से एक जिसे हम चुन सकते हैं वह है mTLS. इसे अन्यथा पारस्परिक TLS के रूप में संदर्भित किया जाता है, जिसका, बहुत ही संक्षिप्त शब्दों में, अर्थ है कि प्रमाणपत्रों को पारस्परिक रूप से स्वीकार किया जाता है और पार्टियों के बीच पहचान स्थापित करने और एन्क्रिप्टेड डेटा ट्रैफ़िक स्थापित करने के लिए मान्य किया जाता है।


इस सरल Mesh कॉन्फ़िगरेशन का उपयोग करके यह कार्य हमारे लिए स्वचालित रूप से किया जा सकता है:

 echo "apiVersion: kuma.io/v1alpha1 kind: Mesh metadata: name: default spec: mtls: enabledBackend: ca-1 backends: - name: ca-1 type: builtin" | kubectl apply -f -


इस नीति को लागू करने के बाद हमें इस तरह की चेतावनी मिल सकती है:

Warning: resource meshes/default is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.

फिलहाल हम इस चेतावनी को नजरअंदाज कर सकते हैं।

IX मेशट्रैफ़िकपरमिशन - भाग II

अब, मज़ेदार हिस्सा आता है, और पहली चीज़ जो हम करने जा रहे हैं वह है सभी पॉड्स के बीच सभी ट्रैफ़िक को अक्षम करना:

 echo " apiVersion: kuma.io/v1alpha1 kind: MeshTrafficPermission metadata: namespace: wlsm-namespace name: mtp spec: targetRef: kind: Mesh from: - targetRef: kind: Mesh default: action: Deny" | kubectl apply -f -


और जब हमें पुष्टिकरण संदेश meshtrafficpermission.kuma.io/mtp configured प्राप्त हो जाता है, तो यदि हम किसी भी पोर्ट-फॉरवर्डिंग का उपयोग करके कोई अनुरोध करने का प्रयास करते हैं, तो हमें मिलेगा:

 HTTP/1.1 500 Internal Server Error Content-Type: application/json Content-Length: 133 { "timestamp": "2024-04-12T07:09:26.718+00:00", "path": "/create", "status": 500, "error": "Internal Server Error", "requestId": "720749ce-56" } Response file saved. > 2024-04-12T090926.500.json Response code: 500 (Internal Server Error); Time: 10ms (10 ms); Content length: 133 bytes (133 B)


इसका मतलब है कि पॉड्स के बीच सभी ट्रैफ़िक को नकार दिया जा रहा है। अब हमारे पास एक आंतरिक प्रणाली है जो हमारे संगठन के भीतर संभावित बुरे लोगों से सुरक्षित है, लेकिन हमने अब सभी पॉड्स के बीच ट्रैफ़िक को भी ब्लॉक कर दिया है। तो, mTLS एक बढ़िया चीज़ है, लेकिन सभी ट्रैफ़िक को ब्लॉक करना बिल्कुल भी बढ़िया नहीं है।


इसे सही बनाने का तरीका बस उस DENY all नियम में अपवाद बनाना है, और ऐसा करने के लिए, हमें एक ऐसी नीति की आवश्यकता है जो श्रोता और कलेक्टर तथा कलेक्टर और डेटाबेस के बीच ट्रैफ़िक की अनुमति दे। आइए कलेक्टर और डेटाबेस के बीच ट्रैफ़िक से शुरू करें:

 echo " apiVersion: kuma.io/v1alpha1 kind: MeshTrafficPermission metadata: namespace: kuma-system name: wlsm-database spec: targetRef: kind: MeshService name: wlsm-database-deployment_wlsm-namespace_svc_5432 from: - targetRef: kind: MeshService name: wlsm-collector-deployment_wlsm-namespace_svc_8081 default: action: Allow" | kubectl apply -f -


इस मामले में, हम जो कर रहे हैं वह डेटा ट्रैफ़िक को targetRef कलेक्टर से targetRef डेटाबेस तक प्रवाहित करने की अनुमति देना है। यदि आप यह नहीं जानते हैं, तो शायद यह ध्यान रखना महत्वपूर्ण है कि कुमा name व्याख्या कैसे करता है, जिसका उपयोग hostname निर्माण की तरह ही कार्यात्मक उद्देश्यों के लिए भी किया जाता है।


इन name को बनाने का सामान्य तरीका इस प्रकार है:

<service name>_<namespace>_svc_<service port>


इस मामले में, विभाजक एक अंडरस्कोर है, और इस तरह से नाम बनाने से Kuma पता चलता है कि वास्तव में क्या अनुमति है। इस मामले में, यदि हम इस नीति को लागू करते हैं, तो हम यह प्रतिक्रिया प्राप्त करने के बाद कलेक्टर को अनुरोध भेज पाएंगे: meshtrafficpermission.kuma.io/wlsm-database created .


और उन्हें बनाते समय, प्रतिक्रिया अब 200 होनी चाहिए जो पुष्टि करती है कि स्थान रिकॉर्ड कलेक्टर को भेज दिया गया है:

 POST http://localhost:8081/api/v1/collector/animals HTTP/1.1 200 OK Content-Type: application/json Content-Length: 91 { "animalId": "a3a1bc1c-f284-4876-a84f-f75184b6998f", "latitude": 52505252, "longitude": 2869152 } Response file saved. > 2024-04-12T091754.200.json Response code: 200 (OK); Time: 1732ms (1 s 732 ms); Content length: 91 bytes (91 B)


हालाँकि, हमने अभी भी श्रोता और संग्राहक के बीच ट्रैफ़िक के लिए अपवादों को परिभाषित नहीं किया है, इसलिए इस तरह से अनुरोध करने पर परिणाम यह होगा:

 HTTP/1.1 500 Internal Server Error Content-Type: application/json Content-Length: 133 { "timestamp": "2024-04-12T07:18:54.149+00:00", "path": "/create", "status": 500, "error": "Internal Server Error", "requestId": "e8973d33-62" } Response file saved. > 2024-04-12T091854-1.500.json Response code: 500 (Internal Server Error); Time: 10ms (10 ms); Content length: 133 bytes (133 B)


और यह निश्चित रूप से अपेक्षित है। आइए अब इस डेटा ट्रैफ़िक के लिए एक और नीति लागू करें:

 echo " apiVersion: kuma.io/v1alpha1 kind: MeshTrafficPermission metadata: namespace: kuma-system name: wlsm-collector spec: targetRef: kind: MeshService name: wlsm-collector-deployment_wlsm-namespace_svc_8081 from: - targetRef: kind: MeshService name: wlsm-listener-deployment_wlsm-namespace_svc_8080 default: action: Allow" | kubectl apply -f -


अब श्रोता से संग्राहक तक अनुरोध निष्पादित करना संभव हो गया है:

 POST http://localhost:8080/app/v1/listener/create HTTP/1.1 200 OK Content-Type: application/json Content-Length: 91 { "animalId": "a3a1bc1c-f284-4876-a84f-f75184b6998f", "latitude": 52505252, "longitude": 2869152 } Response file saved. > 2024-04-12T092039-2.200.json Response code: 200 (OK); Time: 14ms (14 ms); Content length: 91 bytes (91 B)

X - मेशफॉल्टइंजेक्शन

अंत में और सिर्फ एक उदाहरण के रूप में एक और सुविधा प्रदान करने के लिए, हम MeshFaultInjection नामक एक अन्य सुविधा का भी उपयोग कर सकते हैं, जो Kuma के साथ परीक्षण करते समय बहुत उपयोगी हो सकती है। हम अपने जाल के भीतर संभावित समस्याओं का अनुकरण कर सकते हैं और जाँच कर सकते हैं कि क्या त्रुटि प्रबंधन सही तरीके से किया जा रहा है।


हम अन्य चीजों की भी जांच कर सकते हैं, जैसे कि हमने जो सर्किट ब्रेकर कॉन्फ़िगर किए हैं, वे दोषपूर्ण कनेक्शन या उच्च दर के अनुरोधों पर कैसे प्रतिक्रिया करेंगे।


तो, चलिए इसे आज़माते हैं। MeshFaultInjection लागू करने का एक तरीका इस प्रकार है:

 echo " apiVersion: kuma.io/v1alpha1 kind: MeshFaultInjection metadata: name: default namespace: kuma-system labels: kuma.io/mesh: default spec: targetRef: kind: MeshService name: wlsm-collector-deployment_wlsm-namespace_svc_8081 from: - targetRef: kind: MeshService name: wlsm-listener-deployment_wlsm-namespace_svc_8080 default: http: - abort: httpStatus: 500 percentage: 50" | kubectl apply -f -


इस नीति के साथ, हम कह रहे हैं कि श्रोता से आउटबाउंड और कलेक्टर के लिए इनबाउंड ट्रैफ़िक में सफलता की 50% संभावना होगी। अनुरोध के परिणाम अप्रत्याशित हैं, इसलिए इस नीति को लागू करने के बाद, हम श्रोता एंडपॉइंट पर त्रुटियों या सफल अनुरोधों की अपेक्षा कर सकते हैं।

 POST http://localhost:8080/app/v1/listener/create HTTP/1.1 500 Internal Server Error Content-Type: application/json Content-Length: 133 { "timestamp": "2024-04-12T07:28:00.008+00:00", "path": "/create", "status": 500, "error": "Internal Server Error", "requestId": "2206f29e-78" } Response file saved. > 2024-04-12T092800.500.json Response code: 500 (Internal Server Error); Time: 8ms (8 ms); Content length: 133 bytes (133 B)


 POST http://localhost:8080/app/v1/listener/create HTTP/1.1 200 OK Content-Type: application/json Content-Length: 91 { "animalId": "a3a1bc1c-f284-4876-a84f-f75184b6998f", "latitude": 52505252, "longitude": 2869152 } Response file saved. > 2024-04-12T092819.200.json Response code: 200 (OK); Time: 13ms (13 ms); Content length: 91 bytes (91 B)


अंत में, रुचि के लिए, हम देख सकते हैं कि हमारी animal_location तालिका अब कैसी दिखती है:

चित्र का वर्णन

XI - निष्कर्ष

मुझे उम्मीद है कि आप अब तक इस लेख का अनुसरण करने में सक्षम रहे होंगे और आप अपनी मशीन पर एक क्लस्टर चलाने में सक्षम रहे होंगे। वैसे भी इस लेख को पढ़ने और कुमा के बारे में थोड़ा और समझने और सीखने के लिए अपना थोड़ा समय देने के लिए धन्यवाद। मैं व्यक्तिगत रूप से इसके लिए एक बढ़िया उपयोग और Kuma के लिए एक शानदार भविष्य देखता हूँ क्योंकि यह हमारे नेटवर्क और हमारे पर्यावरण को कॉन्फ़िगर करना और उस पर अधिक बारीक नियंत्रण रखना संभव बनाता है।


इसका एंटरप्राइज़ संस्करण, Kong-Mesh , काफी पूर्ण लगता है। कुमा ओपन सोर्स है। और यह परीक्षण के लिए और एंटरप्राइज़ के लिए भी बहुत बढ़िया लगता है। मुझे मेश का विषय बहुत दिलचस्प लगता है, और मुझे लगता है कि Kuma यह जानने का एक शानदार तरीका प्रदान करता है कि मेश कैसे काम करते हैं और यह महसूस करने के लिए कि हम अपने नेटवर्क के भीतर डेटा प्रवाह को बेहतर तरीके से कैसे नियंत्रित कर सकते हैं।


यदि हम अपनी सेवाओं की स्थिति देखना चाहते हैं, तो हम इस localhost स्थान में अपने Kuma कंट्रोल प्लेन पर जा सकते हैं: http://localhost:5681/gui/meshes/default/services?page=1&size=50 :

चित्र का वर्णन

कुमा कंट्रोल प्लेन में, हम इंस्टॉल की गई नीतियों पर भी नज़र डाल सकते हैं, अपने पॉड्स की स्थिति की जाँच कर सकते हैं, बैकग्राउंड में क्या चल रहा है, इसकी निगरानी कर सकते हैं और आम तौर पर, हमारे मेश में क्या हो रहा है और इसे कैसे कॉन्फ़िगर किया गया है, इसका अवलोकन कर सकते हैं। मैं आपको आमंत्रित करता हूँ कि आप बस एप्लिकेशन को देखें और देखें कि क्या आप हमारे द्वारा इंस्टॉल की गई नीतियों की स्थिति की जाँच कर सकते हैं। कुमा कंट्रोल प्लेन, जिसे GUI भी कहा जाता है, को हमारे मेश को समझने और उसका अनुसरण करने में आसान बनाने के लिए सटीक रूप से बनाया गया है।

XII - संसाधन

मैंने इसके बारे में अपने JESPROTECH यूट्यूब चैनल पर एक वीडियो भी बनाया है: