Test-Driven Infrastructure with Puppet

Puppet เกิดขึ้นมาเพื่อแก้ปัญหาการดูแลระบบ infrastructure เนื่องจากการดูแลระบบหนึ่งๆ เป็นสิ่งที่ยาก ยิ่งถ้ามีหลายๆ เซิฟเวอร์ในระบบนั้นแล้ว แค่การแก้ไขไฟล์ configuration ให้กับเครื่องเซิฟเวอร์แต่ละเครื่องก็สร้างความลำบากให้ชีวิตมากแล้ว การตรวจสอบเวอร์ชั่นของ package ต่างๆ ของแต่ละเซิฟเวอร์ก็สำคัญ บ่อยครั้งที่เซิฟเวอร์เราล่มเพราะว่าเวอร์ชั่นของ package ไม่ตรงกันก็มี

จริงๆ แค่การเขียนสคริปเพื่อทำ automated tasks ต่างๆ ก็สามารถช่วยในระดับหนึ่ง แต่ว่ายังมีหนทางที่ดีกว่านั่นคือคอนเซปของ Infrastructure as Code (IaC) ซึ่งเป็นกระบวนการที่เกิดขึ้นมาเพื่อให้การจัดการ infrastructure ดีขึ้น มันคือการเขียนโค้ด เขียนโปรแกรม เพื่อจัดการพวกไฟล์ configuration ต่างๆ รวมไปถึงการติดตั้ง package และการควบคุม service ต่างๆ ที่ทำงานอยู่ Puppet สามารถทำในส่วนนี้ได้เป็นอย่างดี ที่ Pronto เราก็ใช้ Puppet จัดการเครื่องเซิฟเวอร์บน AWS ซึ่งตอนนี้มีอยู่ประมาณ 20 กว่าเครื่อง และจะมีมากขึ้นเรื่อยๆ 🙂

การพัฒนาซอฟต์แวร์ก็มีคอนเซปของ test-driven development (TDD) อยู่ พูดง่ายๆ คือ การเขีย test ก่อนเขียนโค้ดนั่นเอง การเขียนโค้ดสำหรับ infrastructure ก็ไม่น้อยหน้านะ! มีการเขียน test เช่นกัน นี่เป็นที่มาของบทความนี้ที่จะพาไปลองทำ test-driven infrastructure development (TDID) กับ Puppet ดู เพราะว่าทีมเราทำกันแบบนี้แล้วเห็นว่าดีงามเลยขอแบ่งปันให้ทีมอื่นๆ ได้ลองกัน

หมายเหตุ ในทีนี้เราจะทำกันแค่ในระดับ unit นะครับ ไม่ใช่ integration ถ้าอยากลองระดับ integration แนะนำให้ลอง Serverspec กันนะ

ก่อนเริ่มสร้าง Infrastructure ของเราด้วย Puppet

ถ้าเนทแรงๆ ขั้นเหล่านี้จะใช้เวลาประมาณ 30 นาที

  1. ติดตั้ง VirtualBox บนเครื่อง
  2. ติดตั้ง Vagrant บนเครื่อง
  3. เอา code จาก Puppet Workshop at Untitled Conference 2016 จะ fork หรือ clone ก็ได้
  4. เข้าไปยัง folder ของ code ที่เพิ่งเอาลงมา
  5. สำหรับเครื่องที่มี RAM น้อยกว่า 8 GB ให้เปิดไฟล์ Vagrantfile ขึ้นมา แล้วลบส่วนของ a2 กับ a3 ทิ้งไป (ประมาณบรรทัดที่ 32-42) แล้วค่อยทำขั้นตอนที่ 6
  6. สั่ง vagrant up เพื่อโหลด box สร้าง virtual machine (VM) แต่ละตัว และ ติดตั้ง package ต่างๆ มาเตรียม (สั่งแค่ vagrant up พอ ที่เหลือ script ที่ทำไว้จะทำทุกอย่างโดยอัตโนมัติ)
  7. หลังจากเสร็จสิ้น ให้สั่ง vagrant halt เพื่อปิด VM ทุกตัว

เราจะทำอะไรบ้าง?

  • ดูว่า infrastructure setup ที่เราทำสุดท้ายแล้วหน้าตาเป็นอย่างไร
  • ติดตั้ง ปรับแต่ง Puppet master และ agent
  • เขียน test โดยใช้เครื่องมือที่มีชื่อว่า rspec-puppet
  • ลอง deploy code ของเราโดยใช้ Fabric เป็นตัวช่วย

หน้าตา Infrastructure ที่เรากำลังจะทำ

รูปที่ 1 แสดงหน้าตาของ infrastructure ที่เรากำลังจะทำในบทความนี้ จะมี master 1 ตัว แล้วก็จะมี agent คอยคุยกับ master อีก 3 ตัว ซึ่งตรงนี้เราสามารถมี agent มากกว่า 3 ตัวก็ได้ แต่ต้องคอยระลึกไว้หน่อยว่าเมื่อไหร่ก็ตามที่เรามีจำนวน agent เยอะมากเกินไป ตัว master อาจจะกลายมาเป็นคอขวดได้ อาจจะต้องมีการจัดการบางอย่าง อาจจะเพิ่ม master หรือว่าอาจจะลอง masterless ก็แล้วแต่ use case ของแต่ละทีมนะครับ 🙂

Puppet Infrastructure Setup

รูปที่ 1: Puppet Infrastructure Setup

ปรับแต่ง Puppet Master

  1. เปิดเครื่อง master สั่ง vagrant up puppet
  2. เข้าเครื่องไป ใช้คำสั่ง vagrant ssh puppet
  3. เซต host (แก้ไฟล์ /etc/hosts) เตรียมไว้สำหรับเครื่อง agent เลย เรามี 3 เครื่อง ส่วน IP แต่ละเครื่องดูได้จาก Vagrantfile
    192.168.33.11 a1
    192.168.33.12 a2
    192.168.33.13 a3
  4. แก้ไฟล์ /etc/puppet/puppet.conf ลบส่วน template ออก เพราะ deprecated
  5. สั่ง sudo puppet master --verbose --no-daemonize จะได้เห็น message ต่างๆ (จริงๆ เราสามารถใช้ upstart เปิด service ได้)
  6. คำสั่ง sudo puppet cert list -all เอาไว้ดู certificate ทั้งหมด
  7. คำสั่ง sudo puppet cert sign <agent> เอาไว้ sign certificate ของ agent ที่เรียกเข้ามา

ปรับแต่ง Puppet Agent

  1. ลองเปิดเครื่องแรกมาก่อน สั่ง vagrant up a1
  2. สำหรับคนที่มี RAM มากกว่าหรือเท่ากับ 8 GB สามารถสั่ง vagrant up ได้เลย เพื่อเปิดเครื่อง agent มาให้ครบทุกเครื่อง
  3. เข้าเครื่องไป สั่ง vagrant ssh a1
  4. เซต host (แก้ไฟล์ /etc/hosts) เพื่อให้ a1 รู้จักกับ master
    192.168.33.10 puppet
  5. แก้ /etc/puppet/puppet.conf ลบ template กับ [master] ออก แล้วเซต runinterval=10 เพื่อให้มัน poll ไวๆ (โดยค่าตั้งต้นแล้ว agent จะ poll ทุกๆ 30 นาที)
  6. แก้ /etc/default/puppet เปิดโหมด agent โดยเปลี่ยนค่า START ให้เป็น yes
  7. สั่ง sudo service puppet restart เพื่อโหลด configuration ใหม่ จังหวะนี้ตัว agent จะเจอ master ของเราแล้ว ดูได้จาก message ที่เครื่อง master
  8. กลับไปที่ master แล้วให้เรา sign certificate ของเครื่อง a1 โดยสั่ง sudo puppet cert sign a1
  9. แล้วลองลิสต์ดู certificate ทั้งหมดดู สั่ง sudo puppet cert list -all แล้วเราจะเป็นว่ามีเครื่องหมาย + อยู่ด้านหน้า a1 ซึ่งแปลว่า master ได้รู้จักเครื่อง a1 เรียบร้อยแล้ว
  10. เสร็จแล้วให้เรา restart ตัว master สั่ง 1 ที
  11. กลับไปที่ agent เพื่อจะเปิด agent ให้ poll สั่ง sudo puppet agent --enable
  12. เสร็จแล้วก็ลองเพิ่ม package ที่ตัว master ดู แล้วดูว่าที่เครื่อง agent ของเรานั้นได้ติดตั้ง package นั้นๆ หรือไม่ (ถ้าไม่ติดตั้งสักที ก็ลอง restart ตัว master ใหม่ แล้วให้ agent ไป poll ตัว master แบบ manual เลย โดยใช้คำสั่ง sudo puppet agent --test

ใช้ rspec-puppet ทำ Automated Test

  1. เปิดเครื่อง rspec-puppet มา สั่ง vagrant up rspec-puppet
  2. เราจะสร้าง Puppet module ใหม่ชื่อ git เนื่องจากเราอยากติดตั้ง git ให้กับเครื่อง agent ของเรา ขั้นตอนการสร้าง Puppet module ใหม่มีดังนี้ เริ่มจากการเขียน test ก่อน
    1. สั่ง cd puppet/modules/
    2. สั่ง mkdir git
    3. สั่ง cd git/
    4. สั่ง mkdir -p spec/classes
    5. สั่ง mkdir -p spec/fixtures/manifests
    6. สั่ง touch spec/fixtures/manifests/site.pp เพราะว่า Puppet จะคาดหวังว่าจะต้องได้อ่านไฟล์ site.pp ดังนั้นเราจึงจำเป็นต้องสร้างไฟล์เปล่าๆ ไว้
    7. สั่ง mkdir -p spec/fixtures/modules/git
    8. สั่ง cd spec/fixtures/modules/git
    9. สั่ง for i in files lib manifests templates; do ln -s ../../../../$i $i; done
    10. สั่ง cd ../../../../
    11. สร้างไฟล์ spec/spec_helper.rb แล้วใส่ code ตามนี้
      require "rspec-puppet"
      
      fixture_path = File.expand_path(File.join(__FILE__, "..", "fixtures"))
      
      RSpec.configure do |c|
        c.module_path = File.join(fixture_path, "modules")
        c.manifest_dir = File.join(fixture_path, "manifests")
      end
      
    12. เสร็จแล้วลองสั่ง rspec ที่ path puppet/modules/git/ ถ้าทำถูกควรจะเห็น output ตามนี้ เนื่องจากยังไม่ได้เขียน test เลย จึงไม่มี test ไหนโดนรัน
      No examples found.
      
      Finished in 0.00023 seconds (files took 0.05145 seconds to load)
      0 examples, 0 failures
      
  3. เริ่มเขียน test จะมีขั้นตอนตามนี้
    1. สร้างไฟล์ spec/classes/git_spec.rb (ไฟล์จะลงท้ายด้วย _spec.rb) แล้วเริ่มด้วย code ตามนี้
      require "spec_helper"
      
      describe "git" do
        let(:title) { "git" }
      end
      
    2. เสร็จแล้วเราก็ค่อยๆ เขียน assertion ลงไป
      require "spec_helper"
      
      describe "git" do
        let(:title) { "git" }
        it { should contain_class("git") }
      end
      
    3. เสร็จแล้วลองกลับมาสั่ง rspec ดู ควรจะได้ output ตามนี้
      F
      
      Failures:
      
        1) git should contain Class[git]
           Failure/Error: it { should contain_class("git") }
      
      …
      
      Finished in 0.09668 seconds (files took 0.91318 seconds to load)
      1 example, 1 failure
      
      Failed examples:
      
      rspec ./spec/classes/git_spec.rb:6 # git should contain Class[git]
      
    4. ตรงตามจุดประสงค์ของเราที่เขียน test มาให้ fail ก่อน
    5. เราค่อยไปเขียน code โดย
      1. สั่ง mkdir manifests
      2. สั่ง touch manifests/init.pp
    6. แล้วใส่ code ตามนี้
      class git {
      }
      
    7. แล้วรัน rspec ใหม่ ควรจะได้ output ตามนี้
      .
      
      Finished in 0.12943 seconds (files took 0.93615 seconds to load)
      1 example, 0 failures
      
    8. ผ่านแล้วสำหรับ test แรก 🙂 เราไปเขียน test ต่อไปเลย เราจะติดตั้ง git-core
      require "spec_helper"
      
      describe "git" do
        let(:title) { "git" }
        it { should contain_class("git") }
        it {
          should contain_package("git-core").with(
            "ensure" => "installed"
          )
        }
      end
      
    9. รัน test แล้วก็ควรจะได้ fail แบบนี้
      Failed examples:
      
      rspec ./spec/classes/git_spec.rb:8 # git should contain Package[git-core] with ensure => "installed"
      
    10. กลับมาเพิ่ม code ของเรา
      class git {
        package { "git-core":
          ensure => "installed"
        }
      }
      
    11. กลับไปรัน test ใหม่ ควรจะผ่านสำหรับ module นี้
  4. สุดท้ายแล้ว ให้เรากลับไปที่ไฟล์ puppet/manifests/site.pp แก้ไขส่วนที่เป็น package ของ git-core แล้วใส่ include git เข้าไปแทน ก็เป็นอันเสร็จสิ้น ทีนี้เราก็จะลอง deploy code ของเราดู 😉
  5. ถ้าต้องการสร้าง Puppet module ใหม่ ก็ให้ย้อนกลับไปขั้นตอนที่ 2 อีกรอบ

การ Deploy Code เข้า Puppet Master

ใช้เครื่องมือที่มีชื่อว่า Fabric มีหลักการคือเราจะเขียนเป็น task แล้วก็จะสั่งรัน task นั้นๆ เปิดดูไฟล์ fabfile.py ในนั้นจะมีการประกาศ task ต่างๆ ไว้แล้ว ทีนี้เวลาเราจะ deploy code เราแค่สั่ง fab puppet deploy

ปิดท้าย

การเขียน test อาจจะดูช้าในช่วงแรก อาจจะไม่เห็นผลเท่าไหร่ แต่มันส่งผลโดยตรงในระยะยาวแน่นอน ในวันที่เรามีระบบที่ใหญ่ขึ้น และซับซ้อนยิ่งขึ้น การที่มี test โดยเฉพาะแบบ automated ชีวิตการทำงานของเราจะไม่ต้องมาเสียเวลากับการนั่งงมหาบั๊ก เอาเวลาไปสร้างสรรค์สิ่งใหม่ๆ ดีกว่าเยอะ

จบแล้วครับ Happy Testing and Coding กันทุกคนนะ 😀

ปล. โค้ดทั้งหมดในนี้จะอยู่ที่ branch ชื่อ final นะครับ กดดู Puppet Workshop at Untitled Conference 2016 (final) ได้เลย


Kan Ouivirach

Kan Ouivirach

Lead Software Architect

Being interested in Agile software development, I joined an Agile team at Pronto Tools as a Research & Development Architect (as Lead Software Architect now). I am an enthusiastic architect who not only has a scientific mindset, but also a practical approach to software solutions.