Jak postawić serwer TCP (Transmission Control Protocol) na platformie Azure – krok po kroku

Adam Sosiński | Usługi chmurowe | 08.02.2023

TCP

Gdy rozwijamy aplikację, często zachodzi potrzeba integrowania jej z zewnętrznymi systemami. Zdarza się, że komunikacja z nimi jest niestabilna albo sama konfiguracja wymusza posiadanie fizycznych urządzeń, aby developer na etapie programowania mógł całościowo przetestować rozwiązanie. Programując pod kątem komunikacji z urządzeniem (gdy np. w kodzie jest jego adres IP), jesteśmy zależni od jego funkcjonalności i dostępności. Dobrą praktyką jest wtedy pewnego rodzaju odizolowanie naszego systemu, aby warunki zewnętrzne nie wpływały na codzienną pracę nad oprogramowaniem. Z tego artykułu dowiesz się, jak odpowiedzieliśmy na to wyzwanie, wdrażając komunikację po TCP z wykorzystaniem Azure. Jeżeli kiedykolwiek stanąłbyś przed zadaniem postawienia serwera TCP z wykorzystaniem Microsoft Azure, ten materiał przeprowadzi cię przez ten proces krok po kroku.

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:

  1. Komunikacja musi odbywać się po TCP
  2. Żądanie musi zwracać oczekiwaną odpowiedź
  3. 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

Azure Serverless workflow orchestration

Przeczytaj artykuł

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?

  1. 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.
  2. 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).
  3. 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:

Protokół tcp zapewnia usługi połączeniowe

Czas na testy

Po wdrożeniu na środowisko Azure mogliśmy upewnić się, że aplikacja działa i nasłuchuje na żądania.

Protokół tcp zapewnia usługi połączeniowe
Segment tcp zawiera sumę kontrolną

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.

TCP  pozwala przesyłać i odebrać dane między procesami 
 IP to identyfikator liczbowy

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:

 Czy Container Apps  z TCP może być nowym najczęściej stosowanym rozwiązaniem?

I składa się z:

  1. Publicznego IP
  2. Load balancera
  3. Sieci wirtualnej (VNet)
  4. Container registry
  5. 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.

Jak postawić serwer TCP (Transmission Control Protocol) na platformie Azure – krok po kroku - blog 2023.02.08 graphic 6
Jak postawić serwer TCP (Transmission Control Protocol) na platformie Azure – krok po kroku - blog 2023.02.08 graphic 7

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.

  1. 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.

Jak postawić serwer TCP (Transmission Control Protocol) na platformie Azure – krok po kroku - blog 2023.02.08 graphic 8

I również działa.

Jak postawić serwer TCP (Transmission Control Protocol) na platformie Azure – krok po kroku - blog 2023.02.08 graphic 9

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

Autorem wpisu jest:

Senior .NET Developer / Technical Leader

Programista rozwiązań webowych, pracujący z nowoczesnymi stackami technologicznymi. Stawia na pierwszym miejscu rodzinę, ale znajduje też czas na sport i poszerzanie wiedzy o nowinki ze świata .NET. Uważa, że jeżeli można się podzielić wiedzą i doświadczeniem, to czemu tego nie robić?

Dodaj komentarz: