Skalujemy kontenery w usłudze Amazon ECS
Chmura może być dużo tańsza od własnej serwerowni. Może też być droższa. Wszystko zależy od tego, jak zaprojektujemy nasze rozwiązania i w jaki sposób je wdrożymy. Dużą zaletą chmury publicznej jest możliwość łatwego skalowania zasobów, które konsumuje nasza aplikacja. Oprogramowanie powinno oczywiście być w odpowiedni sposób zaprojektowane i napisane. Ale to temat na inny odcinek.
W AWS w razie potrzeby szybko zwiększymy ilość pracujących dla naszych klientów zasobów.

A jeżeli klientów nam ubędzie lub ich ilość zmienia się w czasie, możemy także infrastrukturę zmniejszyć i ponosić niższe koszty

W rozwiązaniach on-premises oba te kroki takie łatwe nie są.
Amazon ECS
Ale nie samymi maszynami wirtualnymi chmura stoi. We wdrożeniach wykorzystywane są także kontenery. Tu prawie każdemu do głowy przyjdzie od razu Kubernetes, ale są także inne rozwiązania. Często wygodniejsze, a na pewno prostsze i tańsze. Przykładem jest usługa Amazon ECS, za pomocą której możemy orkiestrować (mądre słowo) nasze aplikacje w AWS. I to nie tylko te proste. Wraz z usługami load balancingu i service discovery na ECS “stoją” poważne rozwiązania.
Jako zwolennik zrzucania jak największej odpowiedzialności na dostawcę chmury proponuję często klientom skorzystanie z modelu Fargate, w którym zupełnie nie przejmujemy się zasobami, na których uruchamiane są aplikacje.
Ale wróćmy do skalowania.
Autoskalowanie
Chcemy oczywiście aby nasza aplikacja skalowała się bez naszego udziału, na podstawie zużytych zasobów. Pomoże nam w tym usługa Application Auto Scaling, która umożliwia między innymi zwiększanie i zmniejszanie ilości tasków pracujących w ramach serwisu ECS.
W naszym przypadku rozwiązanie będzie się składało z następujących komponentów:
- ECS Service
- CloudWatch Alarm
- Scaling Policy
- Scalable Target
Całość będzie działała oczywiście w VPC i udostępniona zostanie przez Application Load Balancer.
Scalable Target
W przypadku serwisu ECS wymiarem, który możemy skalować jest ilość tasków. Za pomocą Scalable Target przypiszemy dla konkretnego serwisu ECS minimalną i maksymalną wartość.
ScalableTarget: Type: AWS::ApplicationAutoScaling::ScalableTarget DependsOn: Service Properties: RoleARN: !GetAtt AutoScalingRole.Arn ResourceId: !Join - "/" - [service, !Ref ECSCluster, !GetAtt Service.Name] ServiceNamespace: ecs ScalableDimension: ecs:service:DesiredCount MinCapacity: 1 MaxCapacity: 3
W przykładowym template określamy minimum na 1, a maksimum na 3 taski.
Scaling Policy
Application Autoscaling będzie zarządzał liczbą pasków na podstawie Scaling Policy, która opisuje jak taki proces ma się odbywać. Mamy już utworzony target, możemy więc zająć się naszą polityką.
Ważny jest typ polityki. W naszym przypadku użyjemy TargetTrackingScaling, który dobrze sprawdzi się w naszym przypadku, w którym jako monitorowaną wartość ustalamy utylizację CPU. Gdybyśmy potrzebowali bardziej granularnych ustawień, można skorzystać ze StepScaling, gdzie dokładnie możemy określić ilość dodawanych instancji. Na przykład w przypadku gdy utylizacja procesowa przekroczyła 75% dodajemy jednego taska, a gdy nasza aplikacja używa więcej niż 90%, od razu moglibyśmy dodać 3 taski.
ScalingPolicy: Type: AWS::ApplicationAutoScaling::ScalingPolicy Properties: PolicyName: !Sub "${Prefix}-Auto-Scaling-Policy" PolicyType: TargetTrackingScaling ScalingTargetId: !Ref ScalableTarget TargetTrackingScalingPolicyConfiguration: PredefinedMetricSpecification: PredefinedMetricType: ECSServiceAverageCPUUtilization TargetValue: 75.0 ScaleOutCooldown: 60 ScaleInCooldown: 60
Ważnymi parametrami polityki są także ScaleOutCooldown oraz ScaleInCooldown. Domyślnie ustawione są one na 5 minut. Zapobiegają ciągłemu przeskalowywaniu naszej usługi w górę i w dół. Za ich pomocą określamy jak długo proces skalowania ma ignorować alarmy CloudWatch, zanim ponownie przeskaluje nasz serwis.
Nasza polityka utworzy w usłudze CloudWatch alarmy. Aby mogła to zrobić potrzebuje do tego oczywiście odpowiednich uprawnień. Dodatkowo proces auto skalujący musi dostać uprawnienia do sprawdzania stanu naszego serwisu i dokonywania na nim zmian.
Statement: - Effect: Allow Action: - cloudwatch:PutMetricAlarm - cloudwatch:DescribeAlarms - cloudwatch:DeleteAlarms - ecs:DescribeServices - ecs:UpdateService
Aplikacja
Nasza “aplikacja” będzie prostym serwisem, który oblicza i zwraca sumę pierwiastków kwadratowych dla ciągu liczb.
func s(w http.ResponseWriter, r *http.Request) { sum := 0.0 e := 10000000 * (rand.Intn(10) + 1) for i := 1; i < e; i++ { sum += math.Sqrt(float64(i)) } fmt.Fprintln(w, sum) }
Dodatkowym endpointem będzie prosty healthcheck, który będzie informował o tym, że task już działa.
func h(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "OK") }
Całość oczywiście będzie skonteneryzowana. Dla uproszczenia obrad dostępny jest na DockerHub. Nie tworzyłem repozytorium obrazów w AWS.
FROM golang:1.15.3-alpine AS build COPY main.go ./ RUN CGO_ENABLED=0 go build -o /bin/stress FROM scratch COPY --from=build /bin/stress /bin/stress EXPOSE 8080 CMD ["/bin/stress"]
Deployment i testy
Cały template CloudFormation z wszystkimi zasobami dostępny jest w repozytorium na GitHub.
Po utworzeniu stacka z naszego template za pomocą polecenia aws cloudformation describe-stacks --stack-name <NAZWA STACKA> --region <REGION> --output text --query "Stacks[0].Outputs[0].OutputValue"
możemy sprawdzić jak wygląda adres naszego load balancera, który umożliwia komunikację z taskami w naszym serwisie. Spróbujmy sprawdzić za pomocą przeglądarki czy dostaniemy odpowiedź.

Jak pamiętacie automatycznie zostały utworzone dwa alarmy w usłudze CloudWatch. Jeżeli zajrzycie tam po chwili od uruchomienia serwisu, zobaczycie, że jeden z alarmów będzie w stanie OK, a drugi w stanie ALARM.

Tak naprawdę nic złego się nie dzieje. Po prostu nasz serwis nie jest prawie obciążony, dlatego alarm skalujący w dół jest włączony.
Spróbujmy trochę obciążyć nasz system. Użyję do tego artillery. Za pomocą polecenia
artillery quick \
--count 20 \
--num 100 \
http://ecs-autoscaling-Load-Balancer-554735364.eu-central-1.elb.amazonaws.com
wykonamy 2000 requestów.
Po chwili w metrykach klastra będzie można zobaczyć jak wzrasta utylizacja procesora.

Podobnie będzie to widoczne w CloudWatch. Wyzwoli się także alarm odpowiedzialny za skalowanie w górę.

Po chwili w naszym klastrze będą pracowały już dwa taski, które zajmą się obsługą requestów.

Kiedy wszystkie nasze requesty zostaną obsłużone, obciążenie CPU spadnie i zostanie wyzwolony alarm, który odpowiada za skalowanie w dół.

Ilość tasków, które mają pracować w ramach serwisu zostanie zmniejszona. W naszym przypadku do jedennego.

Task zostanie wyrejestrowany z target grupy i po domyślnych pięciu minutach na obsłużenie istniejących połączeń zostanie zatrzymany.

Podsumowując
Kontenery w chmurze to niekoniecznie Kubernetes. Czasami, a nawet często, bez tego całego „oprzyrządowania” CNCF jesteśmy w stanie uruchomić w AWS dobrze działające aplikacje.
Horizontal Pod Auroscaler i Cluster Autoscaler działają. Ale czy naprawdę ich potrzebujemy?
Ktoś powie, że to vendor lock. Tak, zgoda. Ale jak często zdarzyło Ci się przenosić działające i zarabiające rozwiązania pomiędzy różnymi chmurami? Może wolisz mieć po prostu działające rozwiązanie, za które w dużej mierze będzie odpowiadał vendor, a Ty z drinkiem z parasolką będziesz spokojnie obserwował swoje działające usługi.