<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>weeix</title>
        <link>https://www.weeix.com/</link>
        <description>Recent content on weeix</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>th</language>
        <lastBuildDate>Thu, 17 Jul 2025 21:16:00 +0700</lastBuildDate><atom:link href="https://www.weeix.com/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>เชื่อม K3s เข้ากับ OpenStack Block Storage (Cinder) เพื่อสร้าง Persistent Volume</title>
        <link>https://www.weeix.com/posts/k3s-openstack-cinder-persistent-volume/</link>
        <pubDate>Thu, 17 Jul 2025 21:16:00 +0700</pubDate>
        
        <guid>https://www.weeix.com/posts/k3s-openstack-cinder-persistent-volume/</guid>
        <description>&lt;img src="https://www.weeix.com/posts/k3s-openstack-cinder-persistent-volume/images/k3s-openstack-csi.png" alt="Featured image of post เชื่อม K3s เข้ากับ OpenStack Block Storage (Cinder) เพื่อสร้าง Persistent Volume" /&gt;&lt;p&gt;จากบล็อกก่อนหน้านี้ที่ผู้เขียนได้แนะนำการเชื่อม K3s เข้ากับ OpenStack Load Balancer ไปแล้ว ในบทความนี้จะมาลงลึกกันต่อกับการเชื่อม K3s เข้ากับ Block Storage (Cinder) ของ OpenStack เพื่อให้สามารถสร้างและจัดการพื้นที่จัดเก็บข้อมูล (Persistent Volume) ได้โดยอัตโนมัติ&lt;/p&gt;
&lt;h2 id=&#34;ขอกำหนดเบองตน-prerequisites&#34;&gt;ข้อกำหนดเบื้องต้น (Prerequisites)
&lt;/h2&gt;&lt;p&gt;ก่อนจะเริ่มดำเนินการ ผู้อ่านควรเตรียมสิ่งต่อไปนี้ให้พร้อม:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;มี K3s Cluster ที่ทำงานอยู่และสามารถเข้าถึงได้&lt;/li&gt;
&lt;li&gt;มีสิทธิ์การเข้าถึงโปรเจกต์บน OpenStack พร้อม Username และ Password&lt;/li&gt;
&lt;li&gt;ติดตั้ง kubectl บนเครื่องที่ใช้สั่งการ เพื่อควบคุม Kubernetes Cluster&lt;/li&gt;
&lt;li&gt;(แนะนำ) ติดตั้ง openstack-client เพื่อความสะดวกในการตรวจสอบค่าคอนฟิกต่าง ๆ&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;คำเตอน-อกครง&#34;&gt;คำเตือน (อีกครั้ง)
&lt;/h2&gt;&lt;p&gt;ก่อนจะไปต่อ ผู้เขียนขอย้ำอีกครั้งว่า โดยทั่วไปแล้วการใช้งาน Kubernetes ควรเลือกใช้บริการ Managed Kubernetes จาก Cloud Provider จะดีที่สุด เพราะสะดวกและมีความเสถียรสูงกว่าการติดตั้งและตั้งค่าคลัสเตอร์ด้วยตัวเอง ยกเว้นกรณีต่อไปนี้&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cloud Provider ที่ใช้อยู่ไม่มีบริการ Managed Kubernetes&lt;/li&gt;
&lt;li&gt;มีทีมงานที่เชี่ยวชาญด้าน Kubernetes โดยเฉพาะ&lt;/li&gt;
&lt;li&gt;ใช้สำหรับสร้างระบบทดสอบ (Testing) หรือระบบที่ไม่ค่อยมีความสำคัญ (Non-critical)&lt;/li&gt;
&lt;li&gt;ต้องการติดตั้งเพื่อศึกษาหาความรู้&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;เปาหมาย&#34;&gt;เป้าหมาย
&lt;/h2&gt;&lt;p&gt;เป้าหมายของบทความนี้คือ ทำให้ K3s สามารถจัดการ Block Storage ของ OpenStack (ที่มีชื่อบริการว่า Cinder) ได้โดยตรง ผลลัพธ์ที่ต้องการคือ เมื่อผู้ใช้สร้าง PersistentVolumeClaim (PVC) ใน K3s ระบบจะต้องไปสร้าง Block Storage Volume บน OpenStack ให้โดยอัตโนมัติ และเมื่อมีการขยายขนาดของ PVC ระบบก็จะต้องไปขยายขนาดของ Block Storage ด้วยเช่นกัน&lt;/p&gt;
&lt;h2 id=&#34;ทำไมตองเชอม-kubernetes-กบ-block-storage-ของ-cloud&#34;&gt;ทำไมต้องเชื่อม Kubernetes กับ Block Storage ของ Cloud?
&lt;/h2&gt;&lt;p&gt;หากไม่ทำการเชื่อมต่อนี้ เวลาที่ต้องการใช้งาน Persistent Volume บน K3s เราจะถูกจำกัดให้ใช้ได้แค่ local volume หรือ Local Path Provisioner ซึ่งเป็น Storage Provisioner พื้นฐานที่ติดตั้งมาพร้อมกับ K3s อยู่แล้ว&lt;/p&gt;
&lt;p&gt;ปัญหาหลักของ Local Path Provisioner คือข้อมูลจะถูกเก็บไว้บนพื้นที่ของโนด (Node) ที่ Pod ถูกสร้างขึ้นมาเท่านั้น ซึ่งหมายความว่าข้อมูลจะผูกติดอยู่กับโนดนั้น ๆ หากโนดดังกล่าวเกิดล่มหรือมีปัญหา ข้อมูลที่เก็บอยู่ก็จะไม่สามารถใช้งานได้จนกว่าจะกู้โนดนั้นกลับมาได้สำเร็จ วิธีนี้จึงทำให้เกิด Single Point of Failure (SPoF)&lt;/p&gt;
&lt;p&gt;จริง ๆ ก็มีทางแก้ปัญหานี้โดยไม่ทำให้เกิด SPoF เช่น การติดตั้ง Software-Defined Storage อย่าง Rook-Ceph เพื่อสร้าง Storage Cluster ของตัวเองขึ้นมา แต่วิธีนี้ก็มาพร้อมกับความซับซ้อนในการติดตั้งและดูแลรักษา แถมยังเปลืองค่าใช้จ่ายด้าน Block Storage เพิ่มขึ้นอีก ถึงแม้จะใช้เทคนิค erasure code เพื่อลดการใช้พื้นที่แล้วก็ตาม&lt;/p&gt;
&lt;p&gt;นอกจากนี้ยังอาจเป็นการเพิ่ม Overhead ถึงสองต่อ เพราะ Cloud Provider ที่ใช้ OpenStack เป็นพื้นฐานส่วนใหญ่มักจะใช้ Ceph เป็น Backend Storage อยู่แล้ว การที่เราติดตั้ง Rook-Ceph บนนั้นอีกชั้นจึงไม่ต่างอะไรกับการใช้ &amp;ldquo;Ceph ซ้อน Ceph&amp;rdquo; ซึ่งไม่ใช่วิธีที่มีประสิทธิภาพนัก&lt;/p&gt;
&lt;p&gt;ดังนั้น การเชื่อม Kubernetes เข้ากับ Block Storage ของ OpenStack โดยตรงจึงเป็นทางออกที่เรียบง่ายและมีประสิทธิภาพสูงสุดสำหรับกรณีนี้&lt;/p&gt;
&lt;h2 id=&#34;ขนตอนการตดตงและตงคา&#34;&gt;ขั้นตอนการติดตั้งและตั้งค่า
&lt;/h2&gt;&lt;p&gt;เพื่อทำให้ K3s คุยกับ Cinder ได้ เราจะติดตั้ง Cinder CSI Driver โดยผู้เขียนจะอ้างอิงวิธีการติดตั้งจาก&lt;a class=&#34;link&#34; href=&#34;https://github.com/kubernetes/cloud-provider-openstack/blob/master/docs/cinder-csi-plugin/using-cinder-csi-plugin.md&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;เอกสารทางการของ cloud-provider-openstack&lt;/a&gt; ซึ่งมีขั้นตอนดังต่อไปนี้&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;หมายเหตุ:&lt;/strong&gt; หากมี Secret &lt;code&gt;cloud-config&lt;/code&gt; ที่ทำไว้ตอนเชื่อม K3s เข้ากับ OpenStack Load Balancer อยู่แล้ว ให้ข้ามไป &lt;strong&gt;ขั้นตอนที่ 3&lt;/strong&gt; ได้เลย&lt;/p&gt;
&lt;h3 id=&#34;ขนตอนท-1-สรางไฟลตงคา-cloudconf&#34;&gt;&lt;strong&gt;ขั้นตอนที่ 1: สร้างไฟล์ตั้งค่า cloud.conf&lt;/strong&gt;
&lt;/h3&gt;&lt;p&gt;สร้างไฟล์ &lt;code&gt;cloud.conf&lt;/code&gt; ที่มีข้อมูลสำหรับเชื่อมต่อ OpenStack ตามตัวอย่างด้านล่าง&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ini&#34; data-lang=&#34;ini&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;[Global]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;auth-url&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;https://keystone.example.com/v3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;username&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;your_username&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;password&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;your_password&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;region&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;TH-BKK&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;tenant-id&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;9f6dbf311397409a92cbbc761c7f8865&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;domain-id&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;c6b00adf4ed04fc5a958121fadb0e401&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;โดยระบุค่าต่าง ๆ ให้ถูกต้อง ดังนี้&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;auth-url&lt;/code&gt;: URL ของ Keystone API v3 (ดูได้จากไฟล์ OpenStack RC หรือหน้า Dashboard)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;username&lt;/code&gt; และ &lt;code&gt;password&lt;/code&gt;: ชื่อผู้ใช้และรหัสผ่านสำหรับเข้าสู่ระบบคลาวด์&lt;/li&gt;
&lt;li&gt;&lt;code&gt;region&lt;/code&gt;: ชื่อ Region ของ OpenStack ที่ต้องการใช้งาน&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tenant-id&lt;/code&gt; หรือ &lt;code&gt;tenant-name&lt;/code&gt;: ชื่อหรือ ID ของโปรเจกต์&lt;/li&gt;
&lt;li&gt;&lt;code&gt;domain-id&lt;/code&gt; หรือ &lt;code&gt;domain-name&lt;/code&gt;: ชื่อหรือ ID ของโดเมนที่ผู้ให้บริการคลาวด์กำหนด&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;ขนตอนท-2-สราง-secret-ใน-kubernetes&#34;&gt;ขั้นตอนที่ 2: สร้าง Secret ใน Kubernetes
&lt;/h3&gt;&lt;p&gt;เมื่อได้ไฟล์ &lt;code&gt;cloud.conf&lt;/code&gt; มาแล้ว เราจะนำไฟล์นี้ไปสร้างเป็น Secret ใน Kubernetes เพื่อให้ Cinder CSI Driver นำไปใช้งานได้&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl create secret generic cloud-config --from-file&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;cloud.conf -n kube-system
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;ขนตอนท-3-ตดตง-cinder-csi-driver-manifests&#34;&gt;ขั้นตอนที่ 3: ติดตั้ง Cinder CSI Driver Manifests
&lt;/h3&gt;&lt;p&gt;เลือก Branch/Tag ของ &lt;code&gt;cloud-provider-openstack&lt;/code&gt; ให้ตรงกับเวอร์ชัน Kubernetes ที่ใช้ ตัวอย่างนี้สำหรับ Kubernetes v1.32.x จากนั้นใช้คำสั่ง &lt;code&gt;kubectl apply&lt;/code&gt; กับไฟล์ manifest ทั้งหมด ยกเว้น &lt;code&gt;csi-secret-cinderplugin.yaml&lt;/code&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl apply -f https://raw.githubusercontent.com/kubernetes/cloud-provider-openstack/refs/tags/v1.32.0/manifests/cinder-csi-plugin/cinder-csi-controllerplugin-rbac.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl apply -f https://raw.githubusercontent.com/kubernetes/cloud-provider-openstack/refs/tags/v1.32.0/manifests/cinder-csi-plugin/cinder-csi-controllerplugin.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl apply -f https://raw.githubusercontent.com/kubernetes/cloud-provider-openstack/refs/tags/v1.32.0/manifests/cinder-csi-plugin/cinder-csi-nodeplugin-rbac.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl apply -f https://raw.githubusercontent.com/kubernetes/cloud-provider-openstack/refs/tags/v1.32.0/manifests/cinder-csi-plugin/cinder-csi-nodeplugin.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl apply -f https://raw.githubusercontent.com/kubernetes/cloud-provider-openstack/refs/tags/v1.32.0/manifests/cinder-csi-plugin/csi-cinder-driver.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;ขนตอนท-4-สราง-storageclass&#34;&gt;ขั้นตอนที่ 4: สร้าง StorageClass
&lt;/h3&gt;&lt;p&gt;ขั้นตอนสุดท้ายคือการสร้าง &lt;code&gt;StorageClass&lt;/code&gt; เพื่อให้ K3s รู้ว่าจะต้องใช้ Provisioner ตัวไหนในการสร้าง Volume และตั้งค่าให้สามารถขยายขนาดได้ในภายหลัง&lt;/p&gt;
&lt;p&gt;สร้างไฟล์ &lt;code&gt;sc.yaml&lt;/code&gt; ด้วยเนื้อหาดังนี้:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;apiVersion&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;storage.k8s.io/v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;kind&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;StorageClass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;cinder-sc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;provisioner&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;cinder.csi.openstack.org&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;allowVolumeExpansion&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;จากนั้นสั่ง apply:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl apply -f sc.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;เพียงเท่านี้ K3s Cluster ของเราก็พร้อมที่จะสร้าง Persistent Volume ผ่าน OpenStack Cinder แล้ว&lt;/p&gt;
&lt;h3 id=&#34;ขนตอนท-5-ทดสอบการทำงาน&#34;&gt;ขั้นตอนที่ 5: ทดสอบการทำงาน
&lt;/h3&gt;&lt;p&gt;เราจะทดสอบโดยการสร้าง Pod &lt;code&gt;busybox&lt;/code&gt; และเขียนไฟล์ลงไปใน Volume เพื่อพิสูจน์ว่าข้อมูลยังคงอยู่แม้ Pod จะถูกสร้างขึ้นมาใหม่&lt;/p&gt;
&lt;p&gt;สร้างไฟล์ &lt;code&gt;pvc-busybox.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;apiVersion&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;kind&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;PersistentVolumeClaim&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;busybox-pvc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;accessModes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;ReadWriteOnce&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;resources&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;requests&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;storage&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;1Gi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;storageClassName&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;cinder-sc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;สร้างไฟล์ &lt;code&gt;pod-busybox.yaml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;apiVersion&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;kind&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;Pod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;busybox-pod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;containers&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;busybox&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;image&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;busybox&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;command&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;sleep&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;3600&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;volumeMounts&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        - &lt;span style=&#34;color:#f92672&#34;&gt;mountPath&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;/data&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;busybox-storage&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;busybox-storage&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;persistentVolumeClaim&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#f92672&#34;&gt;claimName&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;busybox-pvc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;apply ทั้ง 2 ไฟล์:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl apply -f pvc-busybox.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl apply -f pod-busybox.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;ถ้าเข้าไปดูใน Dashboard ในตอนนี้ จะเห็นว่ามี Volume ขนาด 1Gi ถูกสร้างขึ้นมา&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;รอจน Pod อยู่ในสถานะ &lt;code&gt;Running&lt;/code&gt; จากนั้นเขียนไฟล์ &lt;code&gt;hello.txt&lt;/code&gt; ลงไปใน Volume:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl exec -it busybox-pod -- sh -c &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;echo &amp;#34;Hello from Persistent Volume&amp;#34; &amp;gt; /data/hello.txt&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ตรวจสอบว่าไฟล์ถูกสร้างสำเร็จ:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl exec busybox-pod -- cat /data/hello.txt
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# ผลลัพธ์ที่คาดหวัง: Hello from Persistent Volume&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;จำลองสถานการณ์ Pod ล่ม โดยลบ Pod ทิ้ง:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl delete pod busybox-pod
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;จากนั้นจึง apply ไฟล์ &lt;code&gt;pod-busybox.yaml&lt;/code&gt; เพื่อสร้าง Pod ขึ้นใหม่:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl apply -f pod-busybox.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ตรวจสอบว่าข้อมูลยังคงอยู่ โดยรอจน Pod ใหม่อยู่ในสถานะ &lt;code&gt;Running&lt;/code&gt; แล้วเข้าไปอ่านไฟล์เดิม:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl exec busybox-pod -- cat /data/hello.txt
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# ผลลัพธ์ที่คาดหวัง: Hello from Persistent Volume&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ถ้าเห็นข้อความเดิมหมายความว่าการเชื่อมต่อทำงานได้อย่างสมบูรณ์!&lt;/p&gt;
&lt;h3 id=&#34;ขนตอนท-6-ทดสอบการขยายขนาด-volume&#34;&gt;ขั้นตอนที่ 6: ทดสอบการขยายขนาด Volume
&lt;/h3&gt;&lt;p&gt;เนื่องจากเราตั้งค่า &lt;code&gt;allowVolumeExpansion: true&lt;/code&gt; ไว้ใน StorageClass เราจึงสามารถขยายขนาด Volume ได้โดยตรงผ่าน Kubernetes&lt;/p&gt;
&lt;p&gt;เริ่มจากการแก้ไขไฟล์ pvc-busybox.yaml โดยเปลี่ยนขนาด storage จาก 1Gi เป็น 2Gi:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;apiVersion&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;kind&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;PersistentVolumeClaim&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;busybox-pvc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;accessModes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;ReadWriteOnce&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;resources&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;requests&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#f92672&#34;&gt;storage&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;2Gi&lt;/span&gt; &lt;span style=&#34;color:#75715e&#34;&gt;# &amp;lt;-- เปลี่ยนจาก 1Gi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;storageClassName&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;cinder-sc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;จากนั้น apply การเปลี่ยนแปลง:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl apply -f pvc-busybox.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;หากเข้าไปดูใน Dashboard จะเห็นว่า Volume ถูกขยายเป็น 2Gi แล้ว&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;ลองตรวจสอบขนาดใหม่จากภายใน Pod:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl exec -it busybox-pod -- df -h /data
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;จะเห็นว่าขนาดของ Filesystem ที่ Mount อยู่ที่ &lt;code&gt;/data&lt;/code&gt; ได้เพิ่มขึ้นเป็นประมาณ 2GB แล้ว&lt;/p&gt;
&lt;h2 id=&#34;สรป&#34;&gt;สรุป
&lt;/h2&gt;&lt;p&gt;การเชื่อมต่อ K3s เข้ากับ OpenStack Block Storage (Cinder) โดยตรงผ่าน CSI Driver เป็นวิธีที่ช่วยให้เราสามารถจัดการ Persistent Volume ได้อย่างมีประสิทธิภาพ ทนทานต่อความผิดพลาด และเป็นอัตโนมัติ ทำให้การบริหารจัดการ Storage สำหรับแอปพลิเคชันบน Kubernetes ของเราเป็นไปอย่างราบรื่นและเหมาะสมกับสถาปัตยกรรมแบบคลาวด์อย่างแท้จริง&lt;/p&gt;
</description>
        </item>
        <item>
        <title>เชื่อม K3s กับ OpenStack Load Balancer: คู่มือฉบับลุยจริง เจ็บจริง</title>
        <link>https://www.weeix.com/posts/k3s-openstack-loadbalancer-guide/</link>
        <pubDate>Tue, 15 Jul 2025 21:49:00 +0700</pubDate>
        
        <guid>https://www.weeix.com/posts/k3s-openstack-loadbalancer-guide/</guid>
        <description>&lt;img src="https://www.weeix.com/posts/k3s-openstack-loadbalancer-guide/images/k3s-openstack-ccm.png" alt="Featured image of post เชื่อม K3s กับ OpenStack Load Balancer: คู่มือฉบับลุยจริง เจ็บจริง" /&gt;&lt;p&gt;บทความนี้จะมาแชร์ประสบการณ์การเชื่อมต่อ K3s ซึ่งเป็น Kubernetes distribution ขนาดเล็ก เข้ากับ Load Balancer ของ Public Cloud ที่พัฒนาต่อยอดมาจาก OpenStack&lt;/p&gt;
&lt;p&gt;เรื่องของเรื่องคือผู้เขียนไปได้ไปเครดิตฟรีจาก Public Cloud เจ้าหนึ่งมาใช้งาน แล้วก็เกิดความคิดที่อยากจะลองติดตั้งคลัสเตอร์ Kubernetes ดู โดยที่เลือก K3s เป็นเพราะติดตั้งง่ายดี ใช้คำสั่งแค่บรรทัดเดียวก็พร้อมใช้งานแล้ว&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;curl -sfL https://get.k3s.io | sh -
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;ง่ายขนาดนี้ก็ไปลุยกันเลย แต่ก่อนหน้านั้น&amp;hellip;&lt;/p&gt;
&lt;h2 id=&#34;คำเตอน-กอนจะลงมอทำตาม&#34;&gt;คำเตือน! ก่อนจะลงมือทำตาม
&lt;/h2&gt;&lt;p&gt;การติดตั้งคลัสเตอร์ Kubernetes ด้วยตัวเองบน Cloud (หรือจะบนเครื่องแม่ข่ายก็เหมือนกัน) มีความซับซ้อนและต้องดูแลรักษาในระยะยาว ผู้เขียนจึงอยากแนะนำว่าควรใช้บริการ Managed Kubernetes (เช่น EKS, AKS, GKE) ของ Cloud Provider นั้น ๆ จะดีที่สุด เพราะสะดวกและมีเสถียรภาพสูงกว่ามาก&lt;/p&gt;
&lt;p&gt;อย่างไรก็ตาม การติดตั้งด้วยตัวเองก็ยังเป็นทางเลือกที่น่าสนใจในกรณีต่อไปนี้:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cloud Provider ที่คุณใช้ ไม่มี บริการ Managed Kubernetes&lt;/li&gt;
&lt;li&gt;องค์กรของคุณมีทีมงานที่เชี่ยวชาญด้าน Kubernetes และพร้อมที่จะดูแลรักษาระบบเอง&lt;/li&gt;
&lt;li&gt;คุณต้องการสร้างคลัสเตอร์สำหรับ ระบบทดสอบ (Testing/Development) หรือระบบที่ไม่สำคัญมากนัก&lt;/li&gt;
&lt;li&gt;คุณต้องการติดตั้งเพื่อศึกษาหาความรู้ และทำความเข้าใจการทำงานของ Kubernetes ให้ลึกซึ้งยิ่งขึ้น&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;หากเข้าข่ายข้อใดข้อหนึ่งข้างต้น&amp;hellip; ไปต่อกันเลย!&lt;/p&gt;
&lt;h2 id=&#34;เปาหมายของเราคออะไร&#34;&gt;เป้าหมายของเราคืออะไร?
&lt;/h2&gt;&lt;p&gt;เป้าหมายหลักของภารกิจนี้คือการทำให้คลัสเตอร์ Kubernetes ของเราสามารถ &amp;ldquo;คุย&amp;rdquo; กับระบบของ OpenStack เพื่อจัดการ Load Balancer ได้โดยอัตโนมัติ พูดง่าย ๆ คือ เมื่อเราสร้าง Service ใน Kubernetes โดยระบุชนิดเป็น type: LoadBalancer เราต้องการให้เกิดสิ่งต่อไปนี้:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;มีการสร้าง Load Balancer ขึ้นใน OpenStack โดยอัตโนมัติ&lt;/li&gt;
&lt;li&gt;Load Balancer นั้นถูกตั้งค่าให้กระจาย Traffic ไปยังโนดทั้งหมดในคลัสเตอร์ของเราอย่างถูกต้อง&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;ทำไมตองใช-load-balancer-ของ-cloud-provider&#34;&gt;ทำไมต้องใช้ Load Balancer ของ Cloud Provider?
&lt;/h2&gt;&lt;p&gt;ถ้าไม่ทำแบบนี้ได้ไหม? ได้สิ ยังมีอีกหลายวิธี เช่น:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ใช้ ServiceLB ที่มากับ K3s:&lt;/strong&gt; โดยเมื่อสร้าง Service แล้ว ServiceLB จะเปิดพอร์ตที่เราเลือกบนทุกโนดในคลัสเตอร์ ทำให้เราสามารถใช้เทคนิค DNS Round Robin สร้าง A Record ชี้มายังไอพีของทุกโนดได้ แต่กรณีที่มีโนดล่ม อาจจะทำให้ Service ของเราเข้าได้บ้าง ไม่ได้บ้าง แม้ว่าผู้ให้บริการ DNS บางรายจะมีเทคนิคในการนำไอพีที่ใช้งานไม่ได้ออกจากรอบหมุนเวียนของ Round Robin แต่กว่าจะส่งผลก็ต้องรอให้ DNS Cache หมดอายุก่อนอยู่ดี&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;สร้าง VM ติดตั้ง HAProxy/Nginx เอง:&lt;/strong&gt; เราสามารถสร้าง VM แยกขึ้นมาแล้วติดตั้ง Reverse Proxy เช่น HAProxy หรือ Nginx แล้วตั้งค่าให้ชี้ Traffic ไปยัง NodePort ของ Service ด้วยตัวเอง วิธีนี้ทนทานขึ้น แต่ก็ตามมาด้วยความยุ่งยากในการบริหารจัดการที่ต้องทำด้วยมือทั้งหมด&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;จะเห็นว่าการใช้ Load Balancer ของ Cloud Provider โดยตรงผ่าน Cloud Controller Manager นั้นเป็นวิธีที่ทนทานและเป็นอัตโนมัติที่สุด เรียกได้ว่าเป็นมาตรฐานของ Kubernetes ที่ทำงานบน Cloud เลยก็ว่าได้&lt;/p&gt;
&lt;h2 id=&#34;ขนตอนการตดตงและตงคา&#34;&gt;ขั้นตอนการติดตั้งและตั้งค่า
&lt;/h2&gt;&lt;p&gt;มาถึงส่วนที่ทุกคนรอคอย มาดูขั้นตอนกันเลย&lt;/p&gt;
&lt;h3 id=&#34;ขนตอนท-1-ปรบแตง-k3s-ใหพรอมคยกบภายนอก&#34;&gt;ขั้นตอนที่ 1: ปรับแต่ง k3s ให้พร้อมคุยกับภายนอก
&lt;/h3&gt;&lt;p&gt;สิ่งแรกที่ต้องทำคือบอกให้ K3s รู้ว่าเราจะใช้ Cloud Controller จากภายนอกนะ โดยต้องปิด Cloud Controller และ Load Balancer เริ่มต้นของมันออกไปก่อน ถ้าต้องการใช้ Traefik อาจจะเก็บไว้ก็ได้ แต่ผู้เขียนชอบใช้ NGINX Ingress มากกว่า ก็เลยจะปิด Traefik ด้วย&lt;/p&gt;
&lt;p&gt;หากคุณติดตั้ง K3s ไปแล้ว ให้แก้ไขไฟล์ Service ของ k3s โดยตรง:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# แก้ไขไฟล์ /etc/systemd/system/k3s.service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# หรือ /etc/systemd/system/k3s-agent.service สำหรับ Worker Node&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# เพิ่ม argument ท้ายบรรทัด ExecStart&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ExecStart&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;/usr/local/bin/k3s server &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# ... (argument อื่น ๆ) ... \&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;--disable=cloud-controller&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;--disable=servicelb&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;--disable=traefik&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;--cloud-provider=external&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;จากนั้น Reload Daemon และ Restart Service:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo systemctl daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo systemctl restart k3s
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ทำซ้ำแบบนี้กับทุก ๆ โนด&lt;/p&gt;
&lt;p&gt;แต่ถ้ากำลังจะติดตั้ง K3s ใหม่ สามารถระบุค่าเหล่านี้ผ่าน Environment Variable ได้เลย ซึ่งจะสะดวกกว่ามาก:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;--disable-cloud-controller --disable=servicelb --disable=traefik --kubelet-arg=cloud-provider=external&amp;#34;&lt;/span&gt; sh -
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;หลังจาก Restart หรือติดตั้งใหม่แล้ว จะพบว่าสามารถใช้คำสั่ง &lt;code&gt;kubectl&lt;/code&gt; ได้ตามปกติ แต่ Pod ทั้งหมดจะมีสถานะเป็น Pending เนื่องจากโนดมี taint &lt;code&gt;node.cloudprovider.kubernetes.io/uninitialized=true:NoSchedule&lt;/code&gt;&lt;/p&gt;
&lt;h3 id=&#34;ขนตอนท-2-สรางไฟลตงคา-cloudconf&#34;&gt;ขั้นตอนที่ 2: สร้างไฟล์ตั้งค่า cloud.conf
&lt;/h3&gt;&lt;p&gt;ไฟล์นี้ใช้สำหรับบอก Cloud Controller Manager ว่าจะเชื่อมต่อกับ OpenStack API ได้อย่างไร โดยต้องระบุ URL, Username, Password, Project (Tenant) ID และข้อมูลอื่น ๆ ที่จำเป็น สามารถดูตัวอย่างไฟล์ได้จาก &lt;a class=&#34;link&#34; href=&#34;https://github.com/kubernetes/cloud-provider-openstack/blob/master/manifests/controller-manager/cloud-config&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Official Repository ของ Cloud Provider OpenStack&lt;/a&gt; และแก้ไขให้ตรงกับ Public Cloud ที่เราใช้&lt;/p&gt;
&lt;h4 id=&#34;ตวอยาง-cloudconf&#34;&gt;ตัวอย่าง cloud.conf:
&lt;/h4&gt;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-editorconfig&#34; data-lang=&#34;editorconfig&#34;&gt;[Global]
auth-url=https://keystone.example.com/v3
username=your_username   
password=your_password
region=TH-BKK   
tenant-id=9f6dbf311397409a92cbbc761c7f8865
domain-id=c6b00adf4ed04fc5a958121fadb0e401

[LoadBalancer]
subnet-id=8b219e59-d293-4ce6-b364-1c9c7eb1a34e
floating-network-id=2aa7a98d-ae38-4844-82d6-9ab5c2460b69
# Uncomment if your cloud provider doesn&amp;#39;t set the default value
#flavor-id=fcbb978d-da0b-4ba6-8200-fd9228e6598e
#availability-zone=TH-BKK
# Uncomment if your cloud provider doesn&amp;#39;t support creating LBs via API
#internal-lb=true
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;ข้อมูลต่าง ๆ ที่จำเป็นสามารถดูได้โดยใช้คำสั่ง OpenStack Client เช่น&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;openstack versions show&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;openstack project list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;openstack project show&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;openstack subnet list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;openstack network list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;openstack loadbalancer availabilityzone list&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;openstack loadbalancer flavor list&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;ขนตอนท-3-สราง-secret-ใน-kubernetes&#34;&gt;ขั้นตอนที่ 3: สร้าง Secret ใน Kubernetes
&lt;/h3&gt;&lt;p&gt;เมื่อได้ไฟล์ &lt;code&gt;cloud.conf&lt;/code&gt; มาแล้ว เราจะนำไฟล์นี้ไปสร้างเป็น Secret ใน Kubernetes เพื่อให้ Cloud Controller Manager นำไปใช้งานได้&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl create secret -n kube-system generic cloud-config --from-file&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;cloud.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;สำคัญ:&lt;/strong&gt; secret จะต้องชื่อ &lt;code&gt;cloud-config&lt;/code&gt; และมีไฟล์ &lt;code&gt;cloud.conf&lt;/code&gt; อยู่ด้านใน&lt;/p&gt;
&lt;h3 id=&#34;ขนตอนท-4-ตดตง-openstack-cloud-controller-manager&#34;&gt;ขั้นตอนที่ 4: ติดตั้ง OpenStack Cloud Controller Manager
&lt;/h3&gt;&lt;p&gt;เราสามารถติดตั้งได้จาก &lt;a class=&#34;link&#34; href=&#34;https://github.com/kubernetes/cloud-provider-openstack/tree/master/manifests/controller-manager&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Manifest&lt;/a&gt; หรือ &lt;a class=&#34;link&#34; href=&#34;https://github.com/kubernetes/cloud-provider-openstack/tree/master/charts/openstack-cloud-controller-manager&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Helm Chart&lt;/a&gt; ใน Official Repository ซึ่งในที่นี้จะใช้ Manifest เพราะผู้เขียนขี้เกียจติดตั้ง Helm:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl apply -f https://raw.githubusercontent.com/kubernetes/cloud-provider-openstack/refs/tags/v1.32.0/manifests/controller-manager/cloud-controller-manager-roles.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl apply -f https://raw.githubusercontent.com/kubernetes/cloud-provider-openstack/refs/tags/v1.32.0/manifests/controller-manager/cloud-controller-manager-role-bindings.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl apply -f https://raw.githubusercontent.com/kubernetes/cloud-provider-openstack/refs/tags/v1.32.0/manifests/controller-manager/openstack-cloud-controller-manager-ds.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;หมายเหตุ 1:&lt;/strong&gt; แก้ไขเวอร์ชันให้ใกล้เคียงกับเวอร์ชันของ Kubernetes ที่เราใช้ ในที่นี้ผู้เขียนใช้ Kubernetes 1.32.6 จึงใช้ v1.32.0 โดยสามารถดูเวอร์ชันที่มีได้จาก&lt;a class=&#34;link&#34; href=&#34;https://github.com/kubernetes/cloud-provider-openstack/releases&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;หน้า release ของ Official Repository&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;หมายเหตุ 2:&lt;/strong&gt; ds ในชื่อไฟล์ย่อมาจาก DaemonSet ซึ่งหมายความว่า Controller Manager จะถูกติดตั้งลงบนโนดควบคุม (master node) ทุก ๆ โนด&lt;/p&gt;
&lt;h3 id=&#34;ขนตอนท-5-แกไข-daemonset-ใหรจก-k3s&#34;&gt;ขั้นตอนที่ 5: แก้ไข DaemonSet ให้รู้จัก k3s
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;อัพเดต:&lt;/strong&gt; เห็นว่ามี &lt;a class=&#34;link&#34; href=&#34;https://github.com/kubernetes/cloud-provider-openstack/pull/2902&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Pull Request ที่แก้ nodeSelector ให้เลือก &lt;code&gt;node-role.kubernetes.io/control-plane=true&lt;/code&gt; แล้ว&lt;/a&gt; ถ้าใครใช้ openstack-cloud-controller-manager รุ่นใหม่ ๆ อาจจะไม่ต้องทำตามขั้นตอนนี้แล้วก็ได้&lt;/p&gt;
&lt;p&gt;หลังจากติดตั้งในขั้นตอนที่แล้ว คุณอาจจะพบว่า&amp;hellip; ไม่มี Pod ของ openstack-cloud-controller-manager ถูกสร้างขึ้นมาเลย!&lt;/p&gt;
&lt;p&gt;ปัญหานี้เกิดจาก Manifest ของ DaemonSet ที่มีการระบุ nodeSelector ให้ Pod ทำงานบนโนดที่มี Label &lt;code&gt;node-role.kubernetes.io/master&lt;/code&gt; ที่มีค่าเป็น &lt;strong&gt;ค่าว่าง&lt;/strong&gt; (&amp;quot;&amp;quot;) เท่านั้น&lt;/p&gt;
&lt;p&gt;แต่ K3s กำหนด Label ให้โนดควบคุมเป็น &lt;code&gt;node-role.kubernetes.io/control-plane=true&lt;/code&gt; ทำให้ Selector ไม่ตรงกัน เราจึงต้องแก้ไข DaemonSet ด้วยคำสั่ง patch (หรือใครสะดวกใช้คำสั่ง edit ก็ได้เหมือนกัน)&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl patch daemonset openstack-cloud-controller-manager -n kube-system --type&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;json&amp;#39;&lt;/span&gt; -p&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;[{&amp;#34;op&amp;#34;: &amp;#34;replace&amp;#34;, &amp;#34;path&amp;#34;: &amp;#34;/spec/template/spec/nodeSelector/node-role.kubernetes.io~1control-plane&amp;#34;, &amp;#34;value&amp;#34;: &amp;#34;true&amp;#34;}]&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;คำอธิบาย:&lt;/strong&gt; &lt;code&gt;~1&lt;/code&gt; ใน path คือการ escape เครื่องหมาย / ใน JSON Patch เรากำลังบอกให้ Kubernetes เปลี่ยน Selector ไปมองหา Label &lt;code&gt;node-role.kubernetes.io/control-plane&lt;/code&gt; ที่มีค่าเป็น &lt;code&gt;true&lt;/code&gt; แทน&lt;/p&gt;
&lt;p&gt;หลังจากรันคำสั่งนี้แล้วไม่นาน Pod ของ Controller Manager ก็ควรจะถูกสร้างและอยู่ในสถานะ Running โดยเราสามารถตรวจสอบสถานะของ Pod ได้ด้วยคำสั่ง:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo kubectl get pods -n kube-system -l k8s-app&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;openstack-cloud-controller-manager -w
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;เมื่อ Pod ทำงานเรียบร้อยแล้ว taint &lt;code&gt;node.cloudprovider.kubernetes.io/uninitialized&lt;/code&gt; บนโนดก็จะหายไป และ Pod อื่น ๆ ที่เคยมีสถานะ Pending ก็จะเริ่มทำงานได้ตามปกติ&lt;/p&gt;
&lt;h3 id=&#34;ขนตอนท-6-ตรวจสอบ-security-groups&#34;&gt;ขั้นตอนที่ 6: ตรวจสอบ Security Groups
&lt;/h3&gt;&lt;p&gt;เพื่อให้ Load Balancer ของ OpenStack สามารถส่ง Traffic เข้ามายัง Node ใน Cluster ได้ เราต้องแน่ใจว่า Security Group ของโนดทุกตัวได้เปิด Port ในช่วง NodePort (ปกติจะอยู่ในช่วงพอร์ต 30000-32767) สำหรับ TCP Traffic ที่มาจาก CIDR (Subnet) ของ Load Balancer&lt;/p&gt;
&lt;h3 id=&#34;ขนตอนท-7-ทดสอบการทำงาน&#34;&gt;ขั้นตอนที่ 7: ทดสอบการทำงาน
&lt;/h3&gt;&lt;p&gt;ถึงเวลาทดสอบแล้ว! เราจะลองสร้าง Deployment ง่าย ๆ และเปิดให้เข้าถึงได้ผ่าน Service ชนิด LoadBalancer&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;สร้าง Deployment ของ Nginx: &lt;code&gt;sudo kubectl create deployment nginx --image=nginx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;เปิด Service เป็นชนิด LoadBalancer: &lt;code&gt;sudo kubectl expose deployment nginx --port=80 --type=LoadBalancer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;รอดูผลลัพธ์: &lt;code&gt;sudo kubectl get service nginx -w&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;รอสักครู่&amp;hellip; ถ้าทุกอย่างถูกต้อง คุณจะเห็นว่าช่อง EXTERNAL-IP ของ Service nginx จะมี IP Address ปรากฏขึ้นมา ซึ่ง IP นี้คือ IP ของ Load Balancer ที่ถูกสร้างขึ้นใน OpenStack นั่นเอง! แต่ถ้าไม่เจอก็ไม่เป็นไร ลองอ่านหัวข้อด้านล่างดู&lt;/p&gt;
&lt;h2 id=&#34;ขอจำกดทพบบน-cloud-provider-และวธแกปญหาเฉพาะหนา&#34;&gt;ข้อจำกัดที่พบบน Cloud Provider และวิธีแก้ปัญหาเฉพาะหน้า
&lt;/h2&gt;&lt;p&gt;ถึงแม้จะทำตามขั้นตอนมาทั้งหมด แต่ผู้เขียนก็ยังเจอปัญหาเฉพาะตัวของ Cloud Provider ที่ใช้งานอยู่ (Nipa Cloud) ซึ่งอาจจะเป็นประโยชน์กับคนที่เจอสถานการณ์คล้าย ๆ กัน&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;ต้องระบุ Flavor และ Availability Zone:&lt;/strong&gt; Load Balancer ที่ถูกสร้างขึ้นมาไม่ทำงาน หากไม่ระบุ flavor-id และ availability-zone ไปใน cloud.conf หรือใน Annotation ของ Service ไม่แน่ใจว่าเป็นเพราะทาง Cloud ลืมตั้งค่า Default ไว้หรือเปล่า&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ไม่สามารถสร้าง Floating IP ได้ เพราะ Description ยาวเกินไป:&lt;/strong&gt; Cloud Provider จำกัดความยาวของฟิลด์ description ของ Floating IP ไว้ที่ 20 ตัวอักษร แต่ openstack-cloud-controller-manager จะพยายามสร้าง Floating IP โดยระบุ description ที่มีความยาวไม่ต่ำกว่า 67 ตัวอักษร ทำให้เกิดข้อผิดพลาด HTTP 400 Bad Request วนลูปไม่รู้จบใน Log ของ Controller Manager (ทาง Provider รับทราบปัญหานี้แล้ว แต่ยังไม่มีกรอบเวลาแก้ไขที่ชัดเจน)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Attach IP แล้ว Traffic ไม่เข้า:&lt;/strong&gt; หากลอง Attach Floating IP ผ่าน API หรือ OpenStack Client ด้วยตัวเอง Controller Manager จะหยุดพยายามสร้าง IP และคำสั่ง &lt;code&gt;kubectl get service&lt;/code&gt; ก็จะแสดง External IP ถูกต้อง แต่&amp;hellip; Traffic ไม่สามารถเข้าได้!&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;จากข้อจำกัดข้างต้น (ซึ่งอาจจะเป็นความอ่อนประสบการณ์ของผู้เขียนเองก็ได้) จึงใช้วิธีแก้ปัญหาเฉพาะหน้าไปก่อน คือ เพิ่มการตั้งค่า internal-lb=true เข้าไปในไฟล์ cloud.conf เพื่อบอกให้ Controller Manager สร้างแค่ Load Balancer แต่ไม่ต้องพยายามสร้าง Floating IP เมื่อสร้าง Load Balancer เสร็จแล้วก็เข้าไปที่หน้า Web UI ของ Cloud Provider เพื่อ Attach Floating IP ให้กับ Load Balancer ที่ถูกสร้างขึ้นมาด้วยมือ&lt;/p&gt;
&lt;p&gt;วิธีนี้ทำให้สามารถสร้าง Load Balancer ได้แบบ (กึ่ง) อัตโนมัติ เพราะต้องเข้าไปจัดการส่วนที่เหลือใน Web UI ด้วยตนเอง&lt;/p&gt;
&lt;h2 id=&#34;บทสรป&#34;&gt;บทสรุป
&lt;/h2&gt;&lt;p&gt;การเชื่อมต่อ K3s เข้ากับ OpenStack Load Balancer เป็นประสบการณ์ที่ท้าทายแต่ก็ทำให้เข้าใจสถาปัตยกรรมเบื้องหลังของ Kubernetes บน Cloud มากขึ้น แม้จะต้องเจอกับปัญหาที่ได้กล่าวไปข้างต้นก็ตาม&lt;/p&gt;
&lt;p&gt;สุดท้ายนี้ ขอย้ำคำเตือนเดิมอีกครั้งว่า หากไม่จำเป็นจริง ๆ การใช้บริการ Managed Kubernetes ของ Cloud Provider ยังคงเป็นทางเลือกที่ดีและง่ายที่สุด แต่ถ้าคุณเป็นสายลุย ชอบเรียนรู้ และไม่กลัวปัญหา ก็จัดเลย&lt;/p&gt;
</description>
        </item>
        <item>
        <title>เขียนโปรแกรมช่วยกรอกฟอร์มบนเว็บจากข้อมูลในไฟล์ Excel โดยใช้ Python และ Playwright</title>
        <link>https://www.weeix.com/posts/automate-webform-filling-from-excel-files/</link>
        <pubDate>Sat, 15 Jul 2023 21:40:00 +0700</pubDate>
        
        <guid>https://www.weeix.com/posts/automate-webform-filling-from-excel-files/</guid>
        <description>&lt;img src="https://www.weeix.com/posts/automate-webform-filling-from-excel-files/images/cover.png" alt="Featured image of post เขียนโปรแกรมช่วยกรอกฟอร์มบนเว็บจากข้อมูลในไฟล์ Excel โดยใช้ Python และ Playwright" /&gt;&lt;p&gt;เคยต้องมานั่งกรอกฟอร์มบนเว็บซ้ำ ๆ ทั้งที่มีไฟล์ CSV หรือ Excel อยู่แล้วไหมครับ แล้วเว็บที่เรากรอกก็ดันไม่มีปุ่มให้นำเข้าข้อมูลจากไฟล์ที่มีอยู่แล้วอีก ทำให้ต้องมานั่งพิมพ์เองทีละรายการเป็นสิบรอบร้อยรอบ แทนที่จะได้เอาเวลาไป&lt;del&gt;แอบงีบ&lt;/del&gt;ทำอย่างอื่นที่มีประโยชน์กว่า บทความนี้เลยจะมาแนะนำวิธีเขียนโปรแกรมช่วยกรอกฟอร์มบนเว็บจากข้อมูลในไฟล์ Excel โดยใช้ภาษา Python และไรบรารี Playwright ครับ&lt;/p&gt;
&lt;div class=&#34;video-wrapper&#34;&gt;
    &lt;iframe loading=&#34;lazy&#34; 
            src=&#34;https://www.youtube.com/embed/zijiZt5eVEw&#34; 
            allowfullscreen 
            title=&#34;YouTube Video&#34;
    &gt;
    &lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;กลุ่มเป้าหมายหลักของบทความนี้คือคนที่พอจะมีประสบการณ์เขียน Python มาบ้าง สำหรับคนที่เพิ่งเริ่มฝึกเขียน ถ้าเข้าใจเกี่ยวกับ ตัวแปร ฟังก์ชัน การวนซ้ำ การติดตั้ง/เรียกใช้ไลบรารี และ ข้อความสั่ง &lt;code&gt;with&lt;/code&gt; แล้ว ก็น่าจะอ่านเข้าใจได้ (หรือเปล่านะ)&lt;/p&gt;
&lt;h2 id=&#34;playwright-คออะไร&#34;&gt;Playwright คืออะไร
&lt;/h2&gt;&lt;p&gt;Playwright เป็นไรบรารีที่เอาไว้สำหรับทดสอบการทำงานของเว็บแอปพลิเคชัน (web testing) และทำกระบวนการบนเว็บให้เป็นอัตโนมัติ (web automation) จากเดิมที่เราต้องเปิดเว็บบราวเซอร์แล้ว เข้าเว็บนั้น พิมพ์ช่องนี้ กดปุ่มโน้น ฯลฯ ก็เปลี่ยนมาเขียนโปรแกรมที่ใช้เว็บบราวเซอร์ทำสิ่งต่าง ๆ แทนเราครับ&lt;/p&gt;
&lt;h2 id=&#34;เมอไหรทควรจะใช-playwright&#34;&gt;เมื่อไหร่ที่ควรจะใช้ Playwright
&lt;/h2&gt;&lt;p&gt;ต้องบอกไว้ก่อนว่าควรจะเอา Playwright ไว้เป็นทางเลือกสุดท้ายนะครับ เพราะจะมีข้อจำกัดแบบเดียวกับการทำ web scraping เลย (การดึงข้อมูลจากเว็บไซต์ให้ออกมาอยู่ในรูปแบบที่เรากำหนด) เช่น&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;เปราะบาง&lt;/strong&gt; ถ้าเว็บที่เราใช้งานมีการเปลี่ยนข้อความ สมมุติแต่เดิมปุ่มชื่อ &amp;ldquo;บันทึก&amp;rdquo; แล้วถูกเปลี่ยนเป็น &amp;ldquo;ส่งข้อมูล&amp;rdquo; โปรแกรมเราก็อาจจะใช้งานไม่ได้แล้ว&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ช้า&lt;/strong&gt; ถ้าเทียบกับ API หรือฟังก์ชันสำหรับเพิ่มข้อมูลจากไฟล์ CSV/Excel แล้ว ย่อมช้ากว่าแน่นอนเพราะเป็นการจำลองการคลิก/พิมพ์ผ่านเว็บบราวเซอร์&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;มีโอกาสถูกปิดกั้น&lt;/strong&gt; ถ้าเจ้าของเว็บไซต์ไม่ต้องการให้เราเข้าไปกรอกข้อมูลอัตโนมัติ เขาสามารถใช้เครื่องมือ เช่น Captcha เพื่อป้องกันไม่ให้โปรแกรมเราเข้าไปกรอกได้&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ดังนั้นก่อนที่จะใช้ Playwright เราควรจะแน่ใจก่อนว่า&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ข้อมูลที่เราจะกรอกมีจำนวนมาก หรือต้องกรอกบ่อย ๆ&lt;/li&gt;
&lt;li&gt;เว็บที่เรากรอกไม่มีฟังก์ชันให้เพิ่มข้อมูลจากไฟล์ CSV/Excel&lt;/li&gt;
&lt;li&gt;เว็บที่เรากรอกไม่มี API ให้ใช้งาน&lt;/li&gt;
&lt;li&gt;เว็บที่เรากรอกไม่ได้ห้ามการใช้งานซอฟต์แวร์อัตโนมัติ&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;สงทตองเตรยม&#34;&gt;สิ่งที่ต้องเตรียม
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;ไฟล์ CSV หรือ Excel ข้อมูลที่จะกรอกเข้าไปในฟอร์ม&lt;/li&gt;
&lt;li&gt;ฟอร์มบนเว็บที่เราต้องการให้โปรแกรมช่วยกรอก&lt;/li&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://www.python.org/downloads/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;ดาวน์โหลดและติดตั้งโปรแกรม Python&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;ตดตง-playwright&#34;&gt;ติดตั้ง Playwright
&lt;/h2&gt;&lt;p&gt;ก่อนจะ&lt;a class=&#34;link&#34; href=&#34;https://playwright.dev/python/docs/library#installation&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;ติดตั้ง Playwright&lt;/a&gt; เราควรสร้าง virtual environment ขึ้นมาสำหรับ project นี้โดยเฉพาะ ซึ่งสามารถเลือกใช้ venv, pipenv, poetry, หรือ pdm ได้ตามความถนัด หรือถ้าใครคิดว่านี่จะเป็น project แรก และ project สุดท้ายในชีวิต แล้วก็ไม่กังวลว่าไรบรารีจะตีกันในอนาคต อาจจะลงสดเลยก็ได้ โดยใช้คำสั่ง&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pip install playwright
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;พอติดตั้งเสร็จแล้ว เราจะสามารถใช้คำสั่ง &lt;code&gt;playwright&lt;/code&gt; ได้ ให้เราใช้คำสั่งนี้เพื่อติดตั้งเว็บบราวเซอร์ที่จะใช้ในบทความนี้ครับ&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;playwright install
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;โดยคำสั่งด้านบน จะติดตั้งเว็บบราวเซอร์ทั้งหมด 3 ตัว คือ Chromium (ญาติของ Google Chrome), Firefox, และ WebKit ซึ่งถ้าใครรู้สึกว่ามันมากไปก็เลือกเฉพาะตัวที่เราจะใช้ก็ได้ครับ เช่น ถ้าจะติดตั้งแค่ Chromium ก็ใช้คำสั่ง&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;playwright install chromium
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;ใช-codegen-แปลงการกรอกขอมลใหเปนโคด&#34;&gt;ใช้ Codegen แปลงการกรอกข้อมูลให้เป็นโค้ด
&lt;/h2&gt;&lt;p&gt;Playwright มีเครื่องมือที่ชื่อว่า &lt;a class=&#34;link&#34; href=&#34;https://playwright.dev/python/docs/codegen&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Codegen&lt;/a&gt; ไว้อำนวยความสะดวกให้เราไม่ต้องเขียนโค้ดเองทั้งหมด โดยจะเปิดเว็บบราวเซอร์ กับหน้าต่าง Playwright Inspector ขึ้นมา เวลาที่เราทำอะไรก็ตามในเว็บบราวเซอร์นี้ เช่น เปิดเว็บ คลิก พิมพ์ ฯลฯ Codegen ก็จะแปลงการกระทำเหล่านั้นเป็นโค้ดในหน้าต่าง Playwright Inspector ให้โดยอัตโนมัติ&lt;/p&gt;
&lt;p&gt;สามารถเริ่มต้นใช้งาน Codegen ได้ด้วยคำสั่ง&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;playwright codegen
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;จะเป็นการเปิดเว็บบราวเซอร์ Chromium กับหน้าต่าง Playwright Inspector ขึ้นมา ถ้าในขั้นตอนการติดตั้ง Playwright เราไม่ได้ติดตั้ง Chromium ไว้ จะต้องระบุเว็บบราวเซอร์ที่เราจะใช้ด้วย เช่น&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;playwright codegen -b firefox
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;จากนั้น เราก็เปิดฟอร์มบนเว็บ เข้าสู่ระบบให้เรียบร้อย แล้วกรอกข้อมูลตามปกติ 1 รอบ ตัวอย่างตามวิดีโอด้านล่าง&lt;/p&gt;
&lt;div class=&#34;video-wrapper&#34;&gt;
    &lt;iframe loading=&#34;lazy&#34; 
            src=&#34;https://www.youtube.com/embed/XKO1UJHG9Ug&#34; 
            allowfullscreen 
            title=&#34;YouTube Video&#34;
    &gt;
    &lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;หลังจากกดปุ่มบันทึกแล้ว ให้คลิกกลับเข้ามาในหน้าสำหรับกรอกข้อมูลอีกครั้งเพื่อเตรียมพร้อมสำหรับการกรอกข้อมูลรอบถัดไป แต่ถ้ากดบันทึกแล้วระบบพามายังหน้าที่ให้กรอกข้อมูลอยู่แล้วก็ไม่ต้องทำอะไร&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;พอกรอกเสร็จ จะเห็นได้ว่ามีโค้ดเพิ่มขึ้นมาในหน้า Playwright Inspector ซึ่งเราสามารถคัดลอกไปใช้งานต่อได้ ตัวอย่างโค้ดที่ได้จะออกมาประมาณนี้&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; playwright.sync_api &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; Playwright, sync_playwright, expect
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;run&lt;/span&gt;(playwright: Playwright) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;None&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    browser &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; playwright&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;chromium&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;launch(headless&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;False&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    context &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; browser&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;new_context()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; context&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;new_page()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;goto(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;http://192.168.56.101:8000/auth/login&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_placeholder(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Email&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_placeholder(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Email&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;fill(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;admin@admin.com&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_placeholder(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Password&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_placeholder(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Password&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;fill(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;password&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_role(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;button&amp;#34;&lt;/span&gt;, name&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Sign In&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_role(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;button&amp;#34;&lt;/span&gt;, name&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34; Toggle navigation&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_role(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;link&amp;#34;&lt;/span&gt;, name&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34; Products &amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_role(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;link&amp;#34;&lt;/span&gt;, name&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34; Add Product&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_placeholder(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Enter product name&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_placeholder(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Enter product name&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;fill(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;เสื้อยืดลายดอก&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_placeholder(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Enter sku&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_placeholder(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Enter sku&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;fill(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;TS-002-01&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_placeholder(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Enter price&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_placeholder(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Enter price&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;fill(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;15&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_placeholder(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Enter Qty&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_placeholder(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Enter Qty&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;fill(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;20&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;frame_locator(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;iframe&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_text(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Enter description&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;frame_locator(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;iframe&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;locator(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;body&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;fill(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;เสื้อยืดลายดอก ใส่แล้วเฟี้ยว&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_role(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;combobox&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;first&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_role(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;treeitem&amp;#34;&lt;/span&gt;, name&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;ย้วยจัง&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_role(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;form&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_role(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;list&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;nth(&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_role(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;treeitem&amp;#34;&lt;/span&gt;, name&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;เสื้อยืด&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_role(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;button&amp;#34;&lt;/span&gt;, name&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Save Changes&amp;#34;&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    page&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get_by_role(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;link&amp;#34;&lt;/span&gt;, name&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Add Product&amp;#34;&lt;/span&gt;, exact&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;)&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#75715e&#34;&gt;# ---------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    context&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;close()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    browser&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;close()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;with&lt;/span&gt; sync_playwright() &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; playwright:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    run(playwright)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ถ้ารันโค้ดนี้เลย สิ่งที่จะเกิดขึ้นคือ โปรแกรมจะเปิดเว็บบราวเซอร์ขึ้นมา แล้วกรอกข้อมูลตามเราเป๊ะ ๆ (แต่เร็วกว่า) แต่เป้าหมายเราคือการกรอกข้อมูลจากไฟล์ CSV/Excel ไม่ใช่กรอกข้อมูลเดิมซ้ำ เพราะฉะนั้นเราต้องแก้ไขโค้ดเล็กน้อยเพื่อให้โปรแกรมทำงานตามที่เราต้องการ&lt;/p&gt;
&lt;p&gt;อย่างไรก็ตาม อาจมีบางกรณีที่ไม่สามารถใช้งาน Codegen ได้ เช่น หากหน้าเว็บสำหรับกรอกข้อมูลมีความซับซ้อนเกินไป Codegen อาจจะทำงานช้า หรือหยุดทำงานเลยก็ได้ (&lt;a class=&#34;link&#34; href=&#34;https://github.com/microsoft/playwright/issues/22041&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;เป็น bug ที่ยังไม่ได้รับการแก้ไขในขณะที่เขียนบทความนี้&lt;/a&gt;) ถ้าเป็นแบบนั้นก็จะลำบากหน่อย เพราะต้องเขียนโค้ดข้างบนเอง&lt;/p&gt;
&lt;h2 id=&#34;เขยนโคดสำหรบอานไฟล-csvexcel&#34;&gt;เขียนโค้ดสำหรับอ่านไฟล์ CSV/Excel
&lt;/h2&gt;&lt;p&gt;ในหัวข้อนี้เราจะมาเขียนโค้ดสำหรับอ่านไฟล์ CSV/Excel เพื่อเตรียมนำเข้าไปประกอบร่างกับโค้ดที่เขียนด้วย Codegen&lt;/p&gt;
&lt;p&gt;ตัวอย่างข้อมูลของผมเป็นไฟล์ Excel ชื่อ &lt;code&gt;example.xlsx&lt;/code&gt; ที่มีหน้าตาแบบนี้&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://www.weeix.com/posts/automate-webform-filling-from-excel-files/images/xlsx-example.png&#34;
	width=&#34;817&#34;
	height=&#34;391&#34;
	srcset=&#34;https://www.weeix.com/posts/automate-webform-filling-from-excel-files/images/xlsx-example_hu10764629014853894747.png 480w, https://www.weeix.com/posts/automate-webform-filling-from-excel-files/images/xlsx-example_hu11247769636137081012.png 1024w&#34;
	loading=&#34;lazy&#34;
	
		alt=&#34;ตัวอย่างไฟล์ Excel&#34;
	
	
		class=&#34;gallery-image&#34; 
		data-flex-grow=&#34;208&#34;
		data-flex-basis=&#34;501px&#34;
	
&gt;&lt;/p&gt;
&lt;p&gt;ไม่ว่าจะเป็น CSV หรือ Excel ก็ต้องจัดรูปแบบให้แถวแรกเป็นชื่อคอลัมน์ และแถวที่เหลือเป็นข้อมูลเหมือนภาพด้านบนนะครับ ห้าม merge คอลัมน์เด็ดขาด ไม่งั้นเละแน่นอน&lt;/p&gt;
&lt;p&gt;ก่อนอื่นก็เริ่มจากการติดตั้ง&lt;a class=&#34;link&#34; href=&#34;https://openpyxl.readthedocs.io/en/stable/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;ไรบรารี &lt;code&gt;openpyxl&lt;/code&gt;&lt;/a&gt; เพื่อใช้ในการอ่านไฟล์นามสกุล xlsx โดยใช้คำสั่งด้านล่าง (ถ้าเป็นไฟล์ CSV ก็ไม่ต้องติดตั้งไรบรารีใด ๆ เพิ่มเติม เพราะใน Python มีโมดูล &lt;code&gt;csv&lt;/code&gt; มาให้ใช้อยู่แล้ว)&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pip install openpyxl
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;เราสามารถให้ generative AI อย่าง ChatGPT, Google Bard, Bing Chat, ฯลฯ ช่วยเขียนโค้ดในส่วนนี้ได้ด้วยนะ อย่างผมใช้ Bing Chat ก็จะพิมพ์บอกเขาว่า&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;เขียนโค้ดภาษา Python โดยใช้ไรบรารี openpyxl เพื่ออ่านไฟล์ example.xlsx ที่มี 5 คอลัมน์ ได้แก่ name, sku, price, quantity, description มาเก็บไว้ในตัวแปร จากนั้นจึงแสดงผลออกทางหน้าจอ&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;ซึ่ง Bing Chat ก็ส่งโค้ดกลับมาประมาณนี้&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; openpyxl &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; load_workbook
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;workbook &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; load_workbook(filename&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;example.xlsx&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sheet &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; workbook&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;active
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; row &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; sheet&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;iter_rows(min_row&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;, values_only&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    name, sku, price, quantity, description &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; row
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    print(name, sku, price, quantity, description)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ถ้านำมารันได้เลย จะได้ผลลัพธ์ดังนี้&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;เสื้อยืดลายดอก TS-002-01 &lt;span style=&#34;color:#ae81ff&#34;&gt;15&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt; เสื้อยืดลายดอก ใส่แล้วเฟี้ยว
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;เสื้อยืดลายจุด TS-002-02 &lt;span style=&#34;color:#ae81ff&#34;&gt;15&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt; เสื้อยืดลายจุด ใส่แล้วเฟี้ยว
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;เสื้อยืดลายพราง TS-002-03 &lt;span style=&#34;color:#ae81ff&#34;&gt;18&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;10&lt;/span&gt; เสื้อยืดลายพราง ใส่แล้วเฟี้ยว
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;เสื้อยืดลายทาง TS-002-04 &lt;span style=&#34;color:#ae81ff&#34;&gt;15&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;15&lt;/span&gt; เสื้อยืดลายทาง ใส่แล้วเฟี้ยว
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ให้เรานำโค้ดที่ได้ไปรวมร่างกับโค้ดในหัวข้อ &amp;ldquo;ใช้ Codegen แปลงการกรอกข้อมูลให้เป็นโค้ด&amp;rdquo; โดย&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ย้ายบรรทัดนำเข้าไรบรารี เช่น &lt;code&gt;from openpyxl import load_workbook&lt;/code&gt; ไปอยู่ด้านบนด้วยกัน&lt;/li&gt;
&lt;li&gt;ย้ายตัวแปรที่เกิดจากการโหลดไฟล์ CSV/Excel ทั้งหมด เช่น &lt;code&gt;workbook&lt;/code&gt; และ &lt;code&gt;sheet&lt;/code&gt; ไปไว้ในฟังก์ชัน &lt;code&gt;run&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;ย้ายโค้ดวนซ้ำ &lt;code&gt;for&lt;/code&gt; ไปใส่ในฟังก์ชัน &lt;code&gt;run&lt;/code&gt; &lt;strong&gt;หลังจากบรรทัดที่เปิดไปถึงหน้าสำหรับกรอกข้อมูลแล้ว&lt;/strong&gt; จากนั้นเพิ่มระยะแท็ปให้บรรทัดที่เหลือในฟังก์ชันเข้าไปอยู่ในโค้ดวนซ้ำ&lt;/li&gt;
&lt;li&gt;แทนที่ข้อความที่เรากรอกตอนใช้ Codegen เป็นตัวแปรที่ถูกต้อง เช่น เปลี่ยน &lt;code&gt;&amp;quot;เสื้อยืดลายดอก&amp;quot;&lt;/code&gt; ให้เป็น &lt;code&gt;str(name)&lt;/code&gt; เป็นต้น (&lt;code&gt;.fill()&lt;/code&gt; รองรับเฉพาะตัวแปรที่เป็นข้อความ เราจึงต้องใช้ฟังก์ชัน &lt;code&gt;str()&lt;/code&gt; ครอบตัวแปร)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;ถ้าอ่านคำอธิบายด้านบนแล้วงง ดูจากตัวอย่างโค้ดด้านล่างก็ได้&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; from playwright.sync_api import Playwright, sync_playwright, expect
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+from openpyxl import load_workbook
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; def run(playwright: Playwright) -&amp;gt; None:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+    workbook = load_workbook(filename=&amp;#34;example.xlsx&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+    sheet = workbook.active
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;&lt;/span&gt;     browser = playwright.chromium.launch(headless=False)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     context = browser.new_context()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     page = context.new_page()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; def run(playwright: Playwright) -&amp;gt; None:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     page.get_by_role(&amp;#34;button&amp;#34;, name=&amp;#34; Toggle navigation&amp;#34;).click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     page.get_by_role(&amp;#34;link&amp;#34;, name=&amp;#34; Products &amp;#34;).click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     page.get_by_role(&amp;#34;link&amp;#34;, name=&amp;#34; Add Product&amp;#34;).click()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-    page.get_by_placeholder(&amp;#34;Enter product name&amp;#34;).click()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-    page.get_by_placeholder(&amp;#34;Enter product name&amp;#34;).fill(&amp;#34;เสื้อยืดลายดอก&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-    page.get_by_placeholder(&amp;#34;Enter sku&amp;#34;).click()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-    page.get_by_placeholder(&amp;#34;Enter sku&amp;#34;).fill(&amp;#34;TS-002-01&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-    page.get_by_placeholder(&amp;#34;Enter price&amp;#34;).click()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-    page.get_by_placeholder(&amp;#34;Enter price&amp;#34;).fill(&amp;#34;15&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-    page.get_by_placeholder(&amp;#34;Enter Qty&amp;#34;).click()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-    page.get_by_placeholder(&amp;#34;Enter Qty&amp;#34;).fill(&amp;#34;20&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-    page.frame_locator(&amp;#34;iframe&amp;#34;).get_by_text(&amp;#34;Enter description&amp;#34;).click()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-    page.frame_locator(&amp;#34;iframe&amp;#34;).locator(&amp;#34;body&amp;#34;).fill(&amp;#34;เสื้อยืดลายดอก ใส่แล้วเฟี้ยว&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-    page.get_by_role(&amp;#34;button&amp;#34;, name=&amp;#34;Save Changes&amp;#34;).click()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-    page.get_by_role(&amp;#34;link&amp;#34;, name=&amp;#34;Add Product&amp;#34;, exact=True).click()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+    for row in sheet.iter_rows(min_row=2, values_only=True):
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+        name, sku, price, quantity, description = row
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+        print(name, sku, price, quantity, description)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+        page.get_by_placeholder(&amp;#34;Enter product name&amp;#34;).click()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+        page.get_by_placeholder(&amp;#34;Enter product name&amp;#34;).fill(str(name))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+        page.get_by_placeholder(&amp;#34;Enter sku&amp;#34;).click()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+        page.get_by_placeholder(&amp;#34;Enter sku&amp;#34;).fill(str(sku))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+        page.get_by_placeholder(&amp;#34;Enter price&amp;#34;).click()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+        page.get_by_placeholder(&amp;#34;Enter price&amp;#34;).fill(str(price))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+        page.get_by_placeholder(&amp;#34;Enter Qty&amp;#34;).click()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+        page.get_by_placeholder(&amp;#34;Enter Qty&amp;#34;).fill(str(quantity))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+        page.frame_locator(&amp;#34;iframe&amp;#34;).get_by_text(&amp;#34;Enter description&amp;#34;).click()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+        page.frame_locator(&amp;#34;iframe&amp;#34;).locator(&amp;#34;body&amp;#34;).fill(str(description))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+        page.get_by_role(&amp;#34;button&amp;#34;, name=&amp;#34;Save Changes&amp;#34;).click()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;+        page.get_by_role(&amp;#34;link&amp;#34;, name=&amp;#34;Add Product&amp;#34;, exact=True).click()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     # ---------------------
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     context.close()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;จากนั้นจึงลองรันโค้ดดู ถ้าไม่มีอะไรผิดพลาด โปรแกรมก็จะใช้ข้อมูลจากไฟล์ Excel ไปกรอกฟอร์มบนเว็บให้เราจนครบ&lt;/p&gt;
&lt;h2 id=&#34;การนำไปใชงานจรง&#34;&gt;การนำไปใช้งานจริง
&lt;/h2&gt;&lt;p&gt;โค้ดในบทความนี้ถูกตัดทอนให้เหลือเฉพาะส่วนที่เกี่ยวข้องเพื่อให้อ่านและทำความเข้าใจได้ง่าย แต่เวลานำไปใช้งานจริง โดยเฉพาะกรณีที่เขียนโค้ดให้ผู้อื่นนำไปใช้งาน อาจจะปรับปรุงเพิ่มเติม เช่น&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;หลีกเลี่ยงการฝังชื่อผู้ใช้และรหัสผ่านไว้ในโค้ด ให้เขากรอกผ่าน prompt หรือหน้า UI แทน&lt;/li&gt;
&lt;li&gt;ทำให้ผู้ใช้งานสามารถเลือกไฟล์ CSV/Excel ได้ผ่าน argument ใน command line หรือหน้า UI&lt;/li&gt;
&lt;li&gt;แจ้งผู้ใช้ในกรณีที่บันทึกข้อมูลไม่สำเร็จ เช่น รหัสซ้ำ หรืออีเมลถูกใช้ไปแล้ว เป็นต้น&lt;/li&gt;
&lt;li&gt;ให้เว็บบราวเซอร์ทำงานในพื้นหลัง โดยเปลี่ยนจาก &lt;code&gt;headless=False&lt;/code&gt; เป็น &lt;code&gt;headless=True&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;สรป&#34;&gt;สรุป
&lt;/h2&gt;&lt;p&gt;บทความนี้ได้ชี้ให้เห็นถึงปัญหาเกี่ยวกับการที่ต้องกรอกฟอร์มบนเว็บซ้ำ ๆ พร้อมทั้งแนะนำการเขียนโปรแกรมช่วยกรอกฟอร์มบนเว็บโดยใช้ Python และไรบรารี Playwright ซึ่งจะเป็นประโยชน์กับผู้ที่ต้องกรอกข้อมูลมาก ๆ เป็นประจำบนฟอร์มในหน้าเว็บที่ไม่มี API และไม่สามารถนำเข้าข้อมูลจากไฟล์ CSV/Excel ที่มีอยู่ได้ ภายในบทความมีการแนะนำการใช้งาน Codegen และ generative AI อย่าง ChatGPT, Google Bard, Bing Chat, ฯลฯ เพื่อช่วยให้เขียนโค้ดได้สะดวกยิ่งขึ้น ไม่ต้องเปิดคู่มือบ่อย ๆ และปิดท้ายด้วยข้อแนะนำในการปรับปรุงโปรแกรมเพื่อให้มีความปลอดภัยและง่ายต่อการใช้งาน&lt;/p&gt;
</description>
        </item>
        <item>
        <title>Jekyll ต่างจาก WordPress ยังไง น่าใช้ไหม</title>
        <link>https://www.weeix.com/posts/jekyll-vs-wordpress/</link>
        <pubDate>Mon, 12 Jun 2023 22:47:00 +0700</pubDate>
        
        <guid>https://www.weeix.com/posts/jekyll-vs-wordpress/</guid>
        <description>&lt;img src="https://www.weeix.com/posts/jekyll-vs-wordpress/images/cover.jpg" alt="Featured image of post Jekyll ต่างจาก WordPress ยังไง น่าใช้ไหม" /&gt;&lt;p&gt;Jekyll เป็น static site generator (SSG) ที่เขียนขึ้นมาด้วยภาษา Ruby มีไว้สร้างเว็บ ซึ่งก็เหมือน WordPress ที่มีไว้สร้างเว็บเหมือนกัน แล้วซอฟต์แวร์ 2 ตัวนี้แตกต่างกันยังไงนะ&lt;/p&gt;
&lt;p&gt;จากที่เคยดูแล WordPress ที่ทำงาน แล้วกลับมาใช้ Jekyll ทำเว็บส่วนตัว ผมคิดว่ามีข้อแตกต่างหลัก ๆ ที่เอามาเปรียบเทียบได้ดังนี้ครับ&lt;/p&gt;
&lt;h2 id=&#34;หลกการทำงาน&#34;&gt;หลักการทำงาน
&lt;/h2&gt;&lt;p&gt;สำหรับ WordPress เราจะเอาเขาไปติดตั้งในที่ที่มี web server, PHP, และ MySQL (อยู่เครื่องแม่ข่ายเดียวกันหรือแยกเครื่องกันก็ได้) ตั้งค่านิดหน่อย แล้วก็เริ่มเขียนบทความได้เลย WordPress จะเอาบทความไปเก็บไว้ในฐานข้อมูล MySQL ให้ พอมีคนเข้ามาอ่านบทความ WordPress ก็จะไปอ่านข้อมูลจากฐานข้อมูลมาแสดง (ยกเว้นว่ามีการตั้งค่าให้ใช้แคช ซึ่งจะไม่พูดถึงในบทความนี้)&lt;/p&gt;
&lt;p&gt;ในส่วนของ Jekyll เราจะเขียนบทความหรือหน้าเว็บโดยสร้างไฟล์ที่เขียนด้วยภาษา Markdown เสร็จแล้วค่อยสั่งให้ Jekyll แปลงไฟล์เหล่านั้นให้เป็นหน้าเว็บ พอแปลงเสร็จเราก็จะเอาหน้าเว็บไปวางไว้บน web server (สังเกตว่าเครื่องแม่ข่ายไม่จำเป็นต้องมี PHP และ MySQL เหมือน WordPress)&lt;/p&gt;
&lt;h2 id=&#34;การใชงาน&#34;&gt;การใช้งาน
&lt;/h2&gt;&lt;p&gt;ถ้าใครเคยใช้ WordPress มาก่อนน่าจะพอนึกออกขั้นตอนออกอยู่ใช่ไหมครับ เวลาอยากจะเขียนบทความก็ เข้าสู่ระบบ กดเครื่องหมายบวก พิมพ์บทความ เลือกหมวดหมู่ แล้วกดเผยแพร่ ก็เป็นอันสิ้นสุดแล้ว&lt;/p&gt;
&lt;p&gt;กลับมาที่ Jekyll &amp;hellip; เอาตรง ๆ เลย ผมว่าใช้งานยากอยู่นะครับ แต่สำหรับคนที่ ถนัด command line, ชอบใช้ Markdown, ชื่นชอบ Git, และมีความสุขกับการแก้ไขปัญหา อาจจะรู้สึกว่าง่ายก็ได้ แต่ผมใช้มาซักพักแล้วก็ยังรู้สึกว่ายากอยู่ดี ลองนึกภาพตามนะครับ&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;เวลาจะเพิ่มบทความใหม่ ต้องสร้างไฟล์ Markdown ในโฟลเดอร์ &lt;code&gt;_posts&lt;/code&gt; โดยตั้งชื่อไฟล์ในรูปแบบ &lt;code&gt;ปปปป-ดด-วว-slug.md&lt;/code&gt; หากต้องการระบุ ชื่อเรื่อง หมวดหมู่ แท็ก และ/หรือ รูปประจำเรื่อง ต้องไปเขียนใส่ในสิ่งที่เรียกว่า &lt;a class=&#34;link&#34; href=&#34;https://jekyllrb.com/docs/front-matter/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;front matter&lt;/a&gt; ที่ด้านบทสุดของไฟล์&lt;/li&gt;
&lt;li&gt;พอเขียนบทความเสร็จแล้วจะเผยแพร่ ต้องใช้คำสั่ง &lt;code&gt;jekyll build&lt;/code&gt; เพื่อสร้างเว็บ โดยเว็บจะถูกสร้างขึ้นในโฟลเดอร์ &lt;code&gt;_site&lt;/code&gt; สำหรับให้เราอัปโหลดขึ้นไปวางบน web server หรือถ้าใช้ &lt;a class=&#34;link&#34; href=&#34;https://pages.github.com/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;GitHub Pages&lt;/a&gt; ก็จะง่ายกว่านั้นหน่อย คือเปิดเว็บ/แอป GitHub แล้วสร้างไฟล์ขึ้นมาได้เลย พอเรากด commit ปุ๊บ GitHub Pages ก็จะทำที่เหลือต่อให้เราเองโดยอัตโนมัติ&lt;/li&gt;
&lt;li&gt;หากจะแทรกรูปในบทความ ก่อนอื่นต้องนำไฟล์รูปไปวางในโฟลเดอร์ &lt;code&gt;assets/img&lt;/code&gt; จากนั้นจึงคัดลอกที่อยู่ไฟล์ไปวางในตำแหน่งที่จะเราแทรกรูป โดยใช้รูปแบบ Markdown ดังนี้ &lt;code&gt;![คำอธิบายรูป](/assets/img/path/to/your/image.jpg)&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;ส่วนตัวแล้วคิดว่า Jekyll น่าจะเหมาะกับโปรแกรมเมอร์หรือคนที่พอจะเคยเขียนโค้ดมาบ้างครับ&lt;/p&gt;
&lt;h2 id=&#34;การปรบแตงรปลกษณ&#34;&gt;การปรับแต่งรูปลักษณ์
&lt;/h2&gt;&lt;p&gt;ใน WordPress เราสามารถปรับแต่งรูปลักษณ์ได้โดยการเปลี่ยนธีม ซึ่งกดดาวน์โหลดติดตั้งจาก WordPress ได้เลย มีให้เลือกหลายพันธีม ส่วน Jekyll จะมีวิธีติดตั้งธีมหลัก ๆ อยู่ประมาณ 3 วิธี ขึ้นอยู่กับธีมที่เราจะใช้ ดังนี้&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;ติดตั้ง &lt;a class=&#34;link&#34; href=&#34;https://sundryanything.blogspot.com/2014/03/ruby-gem.html&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;gem&lt;/a&gt; ของปลั๊กอินที่จะใช้ แล้วไประบุ &lt;code&gt;theme: &amp;lt;ชื่อธีม&amp;gt;&lt;/code&gt; ในไฟล์ &lt;code&gt;_config.yml&lt;/code&gt; (ถ้าใช้ GitHub Pages จะติดตั้ง gem เพิ่มไม่ได้ แต่จะมีธีมให้เลือกใช้อยู่จำนวนนึง อย่างตอนที่เขียนบทความนี้ก็มี 13 ธีม ดูได้จากหน้า &lt;a class=&#34;link&#34; href=&#34;https://pages.github.com/themes/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Supported themes&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;ติดตั้งปลั๊กอิน &lt;a class=&#34;link&#34; href=&#34;https://github.com/benbalter/jekyll-remote-theme&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;jekyll-remote-theme&lt;/a&gt; (ถ้าใช้ GitHub Pages จะมีให้อยู่แล้ว) แล้วไประบุ &lt;code&gt;remote_theme: &amp;lt;ชื่อธีม&amp;gt;&lt;/code&gt; ในไฟล์ &lt;code&gt;_config.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;fork repository ธีมนั้นมาเป็นของเรา&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;การปรบแตงเชงลก&#34;&gt;การปรับแต่งเชิงลึก
&lt;/h2&gt;&lt;p&gt;ปรับแต่งเชิงลึกในที่นี้หมายถึงการแก้ไขโค้ดของโปรแกรมนะครับ&lt;/p&gt;
&lt;p&gt;ถ้าใช้ WordPress โดยทั่วไปแล้วมักจะไม่ต้องมานั่งปรับแต่งเชิงลึก เพราะว่าคุณสมบัติค่อนข้างครบถ้วนอยู่แล้ว และถ้าต้องการคุณสมบัติเพิ่มเติมที่ไม่มีใน WordPress เช่น ฟอร์มติดต่อ เว็บบอร์ด ระบบร้านค้า ฯลฯ ก็สามารถเลือกติดตั้งปลั๊กอินจากหน้าของผู้ดูแลเว็บไซต์ได้เลย แต่ถ้าต้องปรับแต่งเชิงลึกจริง ๆ วิธีมาตรฐานคือ การสร้างธีมลูก (ในกรณีที่เราต้องการแก้ไขธีม) หรือการสร้างปลั๊กอิน (ในกรณีที่เราต้องการเพิ่มหรือแก้ไขคุณสมบัติของ WordPress) ซึ่งอาจจะต้องทำความเข้าใจหลักการของ &lt;a class=&#34;link&#34; href=&#34;https://themevilles.com/wordpress-hooks/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;WordPress Hooks&lt;/a&gt; และศึกษาฟังก์ชันต่าง ๆ ที่ WordPress มีมาให้เราใช้เพิ่มเติม&lt;/p&gt;
&lt;p&gt;ส่วน Jekyll ถ้าเราพอจะมีความรู้เกี่ยวกับ HTML, CSS, และ JavaScript บ้างก็แทบจะเข้าไปแก้ตรง ๆ ได้เลย Jekyll จะใช้ภาษา template ที่ชื่อ &lt;a class=&#34;link&#34; href=&#34;https://shopify.github.io/liquid/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Liquid&lt;/a&gt; ซึ่งดูแล้วไม่ค่อยยากเลย (ถ้าเทียบกับการต้องไปศึกษากลไกของ WordPress)&lt;/p&gt;
&lt;h2 id=&#34;คาใชจาย&#34;&gt;ค่าใช้จ่าย
&lt;/h2&gt;&lt;p&gt;สำหรับ WordPress จำเป็นต้องไปฝากกับผู้บริการ cloud ที่รองรับภาษา PHP และระบบจัดการฐานข้อมูล MySQL ซึ่งจากที่ลองหาดูคร่าว ๆ ที่ถูกที่สุดในขณะที่เขียนบทความนี้ก็คือ 350 บาทต่อปี ซึ่งจะมีเนื้อที่ให้เก็บข้อมูล 100 MB และรับ-ส่งข้อมูลได้เดือนละ 2 GB เหมาะกับเว็บที่มีเนื้อหาน้อย และคิดไว้แล้วว่าจะมีคนเข้าไม่เยอะ หรือถ้าใช้บน WordPress.com ก็จะมีหลายราคาให้เลือก ตั้งแต่ฟรีจนไปถึง 800,000 กว่าบาทต่อปี ซึ่งถ้าเลือกใช้แบบฟรีก็จะมีข้อจำกัดอยู่บ้าง เช่น ต้องแสดงโฆษณาบนหน้าเว็บ ผูกกับชื่อโดเมน (domain name) ไม่ได้ เลือกใช้ธีม/ปลั๊กอินได้อย่างจำกัด เป็นต้น&lt;/p&gt;
&lt;p&gt;เรื่องค่าใช้จ่ายนี่เรียกได้ว่าเป็นจุดขายหลักของเว็บที่สร้างด้วย Jekyll เลยก็ว่าได้ ด้วยความที่เป็นแค่ static site ธรรมดา ก็เลยมีบริการฟรีค่อนข้างเยอะ เช่น&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://pages.github.com/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;GitHub Pages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://pages.cloudflare.com/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Cloudflare Pages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://www.netlify.com/&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;Netlify&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;หรือในอนาคต ถ้าเกิดว่าบริการข้างต้นไม่ฟรีอีกต่อไป เราก็สามารถรัน Jekyll บนคอมพิวเตอร์ของเราเพื่อสร้างเว็บ แล้วเอาไปฝากกับเว็บโฮสต์ static หรือบริการคลาวด์อย่าง Amazon S3 หรือ Google Cloud Storage ก็ได้ ยังไงค่าใช้จ่ายก็ถูกกว่าเว็บโฮสต์ที่ให้บริการ PHP และ MySQL แน่นอน&lt;/p&gt;
&lt;h2 id=&#34;สรป&#34;&gt;สรุป
&lt;/h2&gt;&lt;p&gt;Jekyll เป็นเครื่องมือที่เอาไว้แปลงไฟล์ Markdown ให้เป็นหน้าเว็บ อาจจะต้องใช้ทักษะเกี่ยวกับ การใช้ command line, Markdown, และ Git ในระดับนึง ไม่เหมือน WordPress ที่คลิก ๆ พิมพ์บทความ แล้วก็ได้หน้าเว็บเลย แต่ก็มีข้อดีคือสามารถนำเว็บที่สร้างด้วย Jekyll ไปวางบนเครื่องแม่ข่ายที่มีเฉพาะ web server ได้ ไม่จำเป็นต้องมีภาษาสำหรับประมวลผลหรือฐานข้อมูล ทำให้มีค่าใช้จ่ายน้อยกว่า&lt;/p&gt;
&lt;p&gt;สำหรับคนที่อยากลอง Jekyll สามารถไปอ่านบทความซีรีย์ &lt;a class=&#34;link&#34; href=&#34;https://aimetpgm.github.io/blog/series#%E0%B8%97%E0%B8%B3-blog-%E0%B8%94%E0%B9%89%E0%B8%A7%E0%B8%A2-jekyll&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;
    &gt;ทำ Blog ด้วย Jekyll&lt;/a&gt; ของคุณเอมได้เลยครับ เขียนไว้ละเอียดมาก อ่านสนุกด้วย&lt;/p&gt;
</description>
        </item>
        <item>
        <title>เกี่ยวกับ</title>
        <link>https://www.weeix.com/about/</link>
        <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
        
        <guid>https://www.weeix.com/about/</guid>
        <description>&lt;p&gt;ตั้งใจว่าจะใช้เว็บนี้เป็นสมุดจดบันทึกออนไลน์ แล้วก็หวังลึก ๆ
ว่าจะเกิดประโยชน์แก่คนที่หลงเข้ามาอ่านบ้าง ไม่มากก็น้อย&lt;/p&gt;
</description>
        </item>
        <item>
        <title>ค้นหา</title>
        <link>https://www.weeix.com/search/</link>
        <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
        
        <guid>https://www.weeix.com/search/</guid>
        <description></description>
        </item>
        <item>
        <title>หมวดหมู่</title>
        <link>https://www.weeix.com/archives/</link>
        <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
        
        <guid>https://www.weeix.com/archives/</guid>
        <description></description>
        </item>
        
    </channel>
</rss>
