Dowodzenie flotą EC2 z konsoli
Klikanie po konsoli AWS nie skaluje się poza garstkę serwerów. Jak sterujemy flotą EC2 z Ruby — deklarując instancje jako dane, uzgadniając stan pożądany z faktycznym i tagując się ku zdrowiu psychicznemu.
Webowa konsola AWS to świetne miejsce, by uruchomić pierwszy serwer, i okropne, by zarządzać dziesiątym. Każda instancja utworzona kliknięciem to płatek śniegu: nie zdiffujesz jej, nie zrobisz code review, a pół roku później nikt nie pamięta, w której grupie bezpieczeństwa jest ani dlaczego. W chwili, gdy masz więcej niż parę maszyn, chcesz mieć flotę opisaną jako kod — coś, co przeczytasz, zwersjonujesz i uruchomisz ponownie.
To epoka tuż przed Terraformem, a JSON CloudFormation to własny rodzaj bólu, więc
budowaliśmy to w Ruby z gemem aws-sdk. Okazuje się, że zaskakująco mała ilość
kodu daje ci narzędzie konsolowe, które uruchamia, inspekcjonuje i wyłącza całą
flotę na komendę. Oto jego kształt.
Deklaruj instancje jako dane
Kluczowa idea — ta, która przetrwa, jakiegokolwiek narzędzia ostatecznie użyjesz — to przestać wykonywać zmiany infrastruktury, a zacząć deklarować stan pożądany. Zapisz, jak flota ma wyglądać, w czystych danych, i pozwól narzędziu dopasować rzeczywistość:
FLEET = {
"web-1" => { type: "m3.medium", role: "web", az: "eu-west-1a" },
"web-2" => { type: "m3.medium", role: "web", az: "eu-west-1b" },
"worker-1" => { type: "c3.large", role: "worker", az: "eu-west-1a" },
"db-1" => { type: "m3.large", role: "db", az: "eu-west-1a" },
}
To konfiguracja, nie komendy. Żyje w twoim repo, czysto się diffuje w pull requeście i jest jedynym źródłem prawdy o tym, co powinno istnieć. Wszystko, co narzędzie robi, służy temu, by AWS zgodził się z tym hashem.
Rozmawiaj z EC2 z Ruby
Gem aws-sdk opakowuje API EC2. Uruchomienie instancji to wywołanie metody:
require "aws-sdk"
ec2 = AWS::EC2.new(region: "eu-west-1")
def launch(name, spec, ec2)
instance = ec2.instances.create(
image_id: ami_for(spec[:role]),
instance_type: spec[:type],
availability_zone: spec[:az],
key_name: "deploy",
security_groups: [spec[:role]],
)
instance.tags["Name"] = name
instance.tags["Role"] = spec[:role]
instance.tags["ManagedBy"] = "fleet-tool"
instance
end
Dwie rzeczy tutaj liczą się bardziej niż samo uruchomienie. AMI na rolę
(ami_for) oznacza, że każdy rodzaj maszyny bootuje ze znanego, wcześniej
upieczonego obrazu, zamiast być konfigurowanym ręcznie po starcie — maszyna webowa i
maszyna workera zaczynają życie już różne i już poprawne. A tagi to nie
dekoracja; to sposób, w jaki znajdziesz tę instancję ponownie.
Tagi są twoim kluczem głównym
To lekcja, której przyswojenie zajęło nam najdłużej: w chmurze, gdzie instancje
przychodzą i odchodzą, a ich identyfikatory są nieprzejrzyste (i-0a1b2c3d),
tagi są sposobem adresowania floty. Otaguj każdą instancję przynajmniej
Name, Role i znacznikiem, że to twoje narzędzie ją utworzyło (ManagedBy).
Teraz możesz zadawać AWS pytania we własnym słownictwie:
# wszystko, czym narzędzie zarządza
managed = ec2.instances.filter("tag:ManagedBy", "fleet-tool")
# wszystkie serwery webowe, działające
web = ec2.instances
.filter("tag:Role", "web")
.filter("instance-state-name", "running")
Bez zdyscyplinowanego tagowania jesteś sprowadzony do gapienia się na listę
tajemniczych ID i adresów IP. Z nim „zrestartuj wszystkich workerów” albo „pokaż mi,
co działa w eu-west-1b” staje się jednolinijkowcem. Szczególnie tag ManagedBy
trzyma twoje narzędzie z dala od ręcznie uruchomionej instancji, której powinno dać
spokój.
Uzgadniaj pożądane z faktycznym
Mając stan pożądany jako dane i stan faktyczny odpytywalny po tagu, rdzeń narzędzia pisze się sam: porównaj oba i działaj na różnicy.
def reconcile(fleet, ec2)
running = ec2.instances
.filter("tag:ManagedBy", "fleet-tool")
.each_with_object({}) { |i, h| h[i.tags["Name"]] = i if i.status == :running }
# uruchom wszystko zadeklarowane, lecz nie działające
(fleet.keys - running.keys).each do |name|
puts "uruchamiam #{name}"
launch(name, fleet[name], ec2)
end
# oznacz wszystko działające, lecz już nie zadeklarowane
(running.keys - fleet.keys).each do |name|
puts "SIEROTA: #{name} działa, ale nie ma jej we flocie — wyłączyć? "
end
end
Ten wzorzec pożądane − faktyczne jest sercem każdego narzędzia
infrastructure-as-code, które przyszło później, z Terraformem włącznie. Instancje
zadeklarowane, lecz brakujące, są uruchamiane; instancje działające, lecz nie
zadeklarowane, są wynoszone na wierzch jako sieroty, byś o nich zdecydował (celowo
nie auto-wyłączane — niszczenie maszyn nigdy nie powinno być cichym efektem
ubocznym). Uruchom raz, a zbiega flotę; uruchom ponownie, a nie robi nic, bo
rzeczywistość już pasuje. Ta idempotencja to właściwość, która czyni narzędzie
bezpiecznym do uruchamiania w harmonogramie albo w panice.
Od floty do konsoli
Owiń kawałki w kilka podkomend, a masz konsolę operacyjną dla swojej infrastruktury:
case ARGV.first
when "status" then print_status(FLEET, ec2)
when "reconcile" then reconcile(FLEET, ec2)
when "ssh" then exec_ssh(ARGV[1], ec2) # ssh po tagu Name, nie IP
when "run" then run_everywhere(ARGV[1..], ec2) # komenda na każdej maszynie web
end
Podkomenda ssh to drobiazg, który opłaca całe narzędzie: wpisujesz ssh web-1, a
ono rozwiązuje tag Name na bieżące publiczne IP i łączy się — koniec z
kopiowaniem adresów z konsoli. run rozsyła komendę powłoki na każdą instancję
roli, dzięki czemu sprawdzisz wersję albo prześledzisz log w całej flocie bez
logowania się na każdą maszynę ręcznie. (Taki adresowalny po nazwie, działający na
całą flotę runner komend to dokładnie to, czym ma być narzędzie jak guignol.)
Znaj granice
To podejście jest celowo skromne i warto być uczciwym co do tego, gdzie się kończy. Zarządza instancjami; nie zarządza ich wnętrzem — to zadanie twoich upieczonych AMI plus narzędzia do konfiguracji. Nie ma prawdziwego pliku stanu, więc wnioskuje stan faktyczny z tagów przy każdym przebiegu, co jest odporne, ale zgrubne. I nie zastępuje dedykowanych narzędzi nadchodzących teraz: CloudFormation potrafi wyrazić cały stos deklaratywnie, a właśnie ogłoszony Terraform obiecuje robić to między dostawcami z porządnym cyklem plan/apply i modelem stanu. Gdy twoja infrastruktura przerośnie pojedynczy hash, przejdź na nie.
Ale nie pomijaj zbudowania małej wersji najpierw. Napisanie kilkuset linii Ruby wobec API EC2 uczy cię, konkretnie, idei, które wielkie narzędzia formalizują: infrastruktura jako dane, tagi jako tożsamość i uzgadnianie pożądanego z faktycznym. Gdy raz masz je w rękach, każde narzędzie infrastruktury, które potem spotkasz, jest tylko potężniejszym wyrazem tych samych trzech idei — a użyjesz ich lepiej, bo zbudowałeś je samodzielnie.