Przejdź do:
Serwer TCP (Transmission Control Protocol) na platformie Azure – ale po co?
W naszym projekcie, pracując nad rozwiązaniem pozwalającym systemom POS (ang. point of sale) na obsługiwanie płatności, musieliśmy zintegrować je z systemem obsługującym komunikację z terminalami płatniczymi. Wiązało się to z posiadaniem fizycznego urządzenia, w tym przypadku terminala, do celów testowych. Pracując w trybie zdalnym, z zespołem rozproszonym po kilku miastach w Polsce, niemożliwe było współdzielenie jednego takiego urządzenia. Zdecydowaliśmy, że do testów i na etapie pracy nad kodem będziemy używać stuba (czyt. staba) – aplikacji, która będzie zwracała predefiniowane odpowiedzi).
Lista wymagań wobec naszego zamiennika była bardzo krótka:
- Komunikacja musi odbywać się po TCP
- Żądanie musi zwracać oczekiwaną odpowiedź
- Serwer musi być w stanie obsłużyć równoległe żądania
Gotowe rozwiązanie w .NET było przygotowane tak, aby można je było uruchomić lokalnie na Dockerze.
Po upewnieniu się, że stub spełnia nasze oczekiwania i bez ryzyka może zastąpić rzeczywistą integrację, postanowiliśmy użyć go do testów E2E (ang. end to end), które są elementem pipeline’u DevOps. W tym celu trzeba było aplikację umieścić gdzieś na platformie Azure, z której korzystaliśmy.
Azure Serverless workflow orchestration
Automatyzacja procesów biznesowych jest naturalną transformacją. Poznaj rozwiązanie oparte na Azure Durable Function, które pozwoli ci przejść gładko przez ten proces.
Komunikacja po TCP – jakie są opcje?
Największą trudnością w znalezieniu odpowiedniego zasobu okazał się wymagany rodzaj protokołu, niewspierany przez serwisy bazujące głównie na komunikacji po HTTP. Jakie mieliśmy opcje?
- Maszyna wirtualna – można postawić maszynę wirtualną i na niej uruchomić aplikację, ale wymagałoby to dodatkowej konfiguracji dostępu do maszyny oraz utrudniało monitorowanie samej aplikacji.
- Azure Kubernetes Services – kolejną opcją jest AKS, który daje sporą elastyczność w kwestii automatyzacji zarządzania infrastrukturą, jednak na potrzeby prostego stuba jest to nadmiarowe rozwiązanie (wymagałoby odpowiedniej konfiguracji, a i tak nie wykorzystalibyśmy wielu opcji, jakie oferuje).
- Azure Container Instances – przeglądając portfolio usług Azure, natrafiłem na informację, że protokół TCP jest dostępny dla Azure Container Instances. Wyglądało to na idealne rozwiązanie, które można byłoby w prosty sposób uruchomić. Dodatkowym atutem była łatwość monitorowania i fakt, że lokalnie stub działał na kontenerze dockerowym.
Obejrzyj także wideo z serii BiteIT:
Konfigurujemy Azure Container Instances
Na potrzeby tego artykułu przygotowałem uproszczoną wersję docelowego rozwiązania. Działa ono jak echo, zwracając otrzymane żądanie do nadawcy, i to właśnie tę aplikację będę starał się udostępnić poprzez Azure Container Instances.
Poniżej fragment kodu serwera odpowiedzialny za obsługę żądań.
private void Echo(IAsyncResult result) { var listener = (TcpListener)result.AsynState!; if (!_active) { return } var client = listener.EndAcceptTcpClient(result); _logger.LogInformation(“Client connected from: {ip}”, client.Client.RemoteEndPoint); _allDone.Set(); Task.Run(async () => { using (var stream = client.GetStream()) var buffer = _array.Pool.Rent(256); Array.Pool.Return(await stream.Echo(buffer), clearArray: true); } client. Close(); }); }
Jako osoby zajmujące się wytwarzaniem oprogramowania powinniśmy dążyć do automatyzowania powtarzalnych czynności, dlatego całą konfigurację zasobów umieściłem w plikach Bicep. Takie rozwiązanie pozwala używać raz zdefiniowanej konfiguracji wielokrotnie podczas wdrażania jej na platformie Azure.
Podstawowa konfiguracja Azure Container Instances wymaga zdefiniowania:
1. Zasobów, z jakich może korzystać skonteneryzowana aplikacja.
resources: { requests: { cpu: 1 memoryInGB: 1 } }
2. Lokalizacji obrazu, na podstawie którego będzie budowany kontener, wraz ze zdefiniowanymi portami zewnętrznymi.
image: '${containerRegistryName}/tcp-server:latest’ ports: [ { port: port protocol 'TCP' } ]
3. Dostępu do rejestru, na którym znajduje się obraz.
imageRegistryCredentials: [ { server: containerRegistryName username: registryUserName password: registryPassword } ]
4. Konfiguracji adresu IP.
IpAddress: { type: ‘Public’ ports: [ { port: port protocol: ‘TCP’ } ] }
Najważniejsza rzecz, o której trzeba pamiętać, to aby wewnętrzny port odpowiadał portowi zewnętrznemu, inaczej komunikacja po TCP nie będzie działać.
Poniżej cały plik Bicep dla konfiguracji Azure Container Instances:
Czas na testy
Po wdrożeniu na środowisko Azure mogliśmy upewnić się, że aplikacja działa i nasłuchuje na żądania.
Do celów testowych napisałem prostego klienta TCP jako test w xUnit.
[Fact] public void MakeSingleCall() { var buffer = _arrayPool.Rent(256); try { using var client = GetClient(); using var stream = client.GetStream(); stream.Write(Encoding.UTF8.GetBytes(TestMessage)); while (!stream.DataAvailable) { Thread.Sleep(1000); } StringBuilder sb = new(); while (Stream.DataAvailable) { var readBytes = stream. Read(buffer); Sb.Append(Encoding.UTF8.GetString(buffer[...readBytes])); } Assert.Equal(TestMessage, sb.ToString()); } finally { _arrayPoolReturn(buffer); } }
Do nawiązywania połączenia służy prywatna metoda GetClient.
private static TcpClient GetClient() { var socketException = new SocketException(); ushort attempts = 0; while (attempts < RetryAttempt) { try { TcpClient client = new (HostName, Port); return client; } catch (socketException ex { socketException = ex; attempts++; Thread.Sleap(TimeSpan.FromSeconds(Math.Pow(2, attempts))); continue; } } throw SocketException; }
Jak widać, test zakończył się sukcesem, a w logach kontenera mamy informację o nawiązaniu połączenia.
Problem zmiany adresu IP po przeładowaniu…
Mógłbym w tym momencie przejść do podsumowania, jako że mamy działającą aplikację w Azure pozwalającą na komunikację po TCP. Niestety w dokumentacji Azure Container Instances jest informacja, że adres IP może się zmienić w przypadku przeładowania zasobu, co z kolei jest wymagane, aby pobrać najnowszą wersję obrazu aplikacji. Ryzyko zmiany adresu stuba sprawiało, że testy E2E nie mogły być częścią pipeline’u, ponieważ istniała możliwość, że będą fałszywie negatywne.
…i rozwiązanie tego problemu
Żeby nie wymuszać na kliencie serwera TCP pilnowania i zmieniania adresu IP po przeładowaniu zasobu, należy ten problem „schować” i obsłużyć wewnętrznie poprzez infrastrukturę. Na szczęście Azure zapewnia rozwiązania pozwalające to zrobić.
Docelowa architektura prezentuje się następująco:
I składa się z:
- Publicznego IP
- Load balancera
- Sieci wirtualnej (VNet)
- Container registry
- Container instances
Konfigurację w plikach Bicep podzieliłem na moduły per typ zasobu oraz jeden plik główny jako punkt startowy do postawienia wszystkich niezbędnych elementów.
Aby zmniejszyć liczbę adresów IP, jakie mogą zostać przydzielone do kontenera, zdefiniowana podsieć ma tylko 8 adresów, z czego 5 jest na wewnętrzne potrzeby Azure, co zostawia nas z 3 dostępnymi adresami.
resource virtualNetwork ‘Microsoft.Network/virtualNetworks@2022-05-01' existing = { name:shared-vnet' } resource subnet ‘Microsoft.Network/virtualNetworks/subnets@2022-05-01' = { name’tcp-server-sub' parent: virtualNetwork properties: { addressPrefix: ‘10.1.2.0/29 delegations: [ { name: ACIDelegationService’ properties: { serviceName: ‘Microsoft.ContainerInstance/containerGroups’ type: ‘Microsoft.Network/virtualNetworks/subnets/delegations’ } ] privateEndpointNetworkPolicies: ‘Disabled’ privateLinkServiceNetworkPolicies: ‘Enabled’ } } output subnetId string = subnet.id
Konfigurując load balancer, należy ustawić pulę dla dostępu do wewnętrznego zasobu.
backendAddressPools: [ { name: ‘tcp-server-be' properties: { loadBalancerBackendAddresses: [ { name: ‘tcp-server’ properties: { ipAddress: backendPrivateIPAddress virtualNetwork: { id:virtualNetworkId } } } ] } } ]
Gdzie „ipAddress” jest adresem dla Container Instances wewnątrz podsieci.
Dodatkowo zdefiniowałem sprawdzanie żywotności kontenera poprzez próbkowanie.
probes: [ { name: ‘tcp-server-hc' properties: { protocol: ‘Tcp’ port: tcpPort intervalInSeconds: 5 numberOfProbes: 1 } } ]
Wszystkie te ustawienia są niezbędne do ustalenia reguł przekierowywania ruchu z publicznego adresu IP na wewnętrzne.
loadBalancingRules: [ { name: ‘tcp-server-lb-rule' properties: { frontendPort: tcpPort protocol: ‘Tcp’ backendPort: tcpPort disableOutboundSnat: true frontendIPConfiguration: { id: resourceId( ‘Microsoft.Network/loadBalancers/frontendIPConfigurations’, ‘tcp-server-lb', ‘tcp-server-fe' ) } backendAddressPool: { id: resourceId( ‘Microsoft.Network/loadBalancers/backendAddressPools’, ‘tcp-server-lb', ‘tcp-server-be' ) } probe: { id: resourceId(‘ Microsoft.Network/loadBalancers/probes’, ‘tcp-server-lb', ‘tcp-server-hc' ) } } } ]
Jedyna zmiana w konfiguracji Container Instances dotyczy ustawień adresu IP, który teraz jest prywatny i przydzielony z podsieci.
ipAddress: { type: ‘Private’ ports: [ { port: port protocol : ‘TCP’ } ] } subnetIds:[ { id: subnetId } ]
Taka konfiguracja dalej wymaga pilnowania adresu IP po restarcie kontenera i upewniania się, że jest taki sam jak ustawiony na load balancerze, ale staje się to problemem wewnętrznym.
Container Apps z TCP jako kolejny etap ewolucji
Pod koniec 2022 roku Azure dał możliwość skonfigurowania Container Apps dla komunikacji po TCP w wersji zapoznawczej.
Ponieważ Azure Container Apps ułatwia skalowanie oraz pozwala zautomatyzować podnoszenie wersji aplikacji po pojawieniu się nowego obrazu w rejestrze, postanowiłem się temu przyjrzeć.
Zgodnie z dokumentacją należy umieścić środowisko zarządzane wewnątrz podsieci. Istotne jest skonfigurowanie poprawnego zakresu CIDR – minimum /23.
resource virtualNetwork ‘Microsoft.Network/virtualNetworks@2055-05-01' existing = { name: ‘shared-vnet’ } resource subnet ‘Microsoft.Network/virtualNetworks/subnets@2022-05-01 = { name: ‘tcp-server-ca-sub' parent: virtualNetwork properties: { addressPrefix: ‘10.1.0.0/23’ } } output subnetId string = subnet.id
Container Apps wymagają zdefiniowanego środowiska zarządzanego. Jak już wspomniałem, należy je umieścić wewnątrz podsieci, pamiętając o ustawieniu właściwości „internal” na false, aby aplikacja była dostępna z zewnątrz.
vnetConfiguration: { infrastructureSubnetId: subnetId internal: false }
Sama konfiguracja Container Apps wymaga zdefiniowania:
1. Szablonu dla rewizji, którą chcemy uruchomić.
template: { containers: [ { env: [ { name: ‘TcpServer_Port’ value: ‘${port}’ } ] image: ‘${containerRegistryName}/tcp-server:latest’ name: ‘tcp-server’ probes: [ { periodseconds: 10 successThreshold: 1 tcpSocket: { port: port } type: ‘Liveness’ } ] resources: { cpu: json(‘0.25’) memory: ‘0.5Gi’ } } ] revisionSuffix: guid(‘tcp-server') }
Dodałem, oczywiście, próbkowanie żywotności kontenera.
2. Punktu wejścia dla komunikacji zewnętrznej.
ingress: { allowInsecure: false exposedPort: port external: true targetPort: port transport: ‘tcp’ }
3. Dostępu do Azure Container Registry, na którym znajduje się obraz aplikacji.
registries: [ { server: ascoding.azurecr.io’ username: registryUsername passwordSecretRef: registryPasswordSecretId } ] secrets: [ { name: registryPasswordSecretId value: registryPassword } ]
Przydatnym dodatkiem jest możliwość przechowywania informacji wrażliwych wewnątrz zasobu, bez konieczności podłączania zewnętrznego Key Vaulta.
Po zakończonym wdrożeniu okazało się, że rozwiązanie Azure jest bardzo podobne do mojego.
I również działa.
Podsumowanie
Azure Container Instances pozwala na szybkie uruchamianie skonteneryzowanych aplikacji, które umożliwia zewnętrzną komunikację po TCP. Niestety, nie jest to rozwiązanie pozbawione wad (przypomnijmy: zmieniające się IP, brak skalowania), które utrudniają skonfigurowanie środowiska i w przypadku prostych aplikacji mogą być powodem do jego odrzucenia.
Na szczęście Azure wyszedł naprzeciw potrzebom użytkowników z nową usługą, jaką jest Container Apps, która jest relatywnie prosta do skonfigurowania i pozbawiona problemów, na jakie trafimy korzystając z Container Instances. Warto mieć to rozwiązanie na uwadze i śledzić nowe możliwości, jakie zespół developerów chmury Microsoft Azure będzie do niego dodawał.
Przeczytaj także: Big Data w chmurze Azure