1. Omówienie
Microsoft .NET Core to wieloplatformowa, open source wersja .NET, która może działać natywnie w kontenerach. Oprogramowanie .NET Core jest dostępne w GitHubie i utrzymywane przez Microsoft oraz społeczność .NET. W tym module wdrażamy skonteneryzowaną aplikację .NET Core w Google Kubernetes Engine (GKE).
W tym module powtarza się typowy schemat programowania – aplikacje są tworzone w lokalnym środowisku programistycznym, a następnie wdrażane w środowisku produkcyjnym. W pierwszej części modułu weryfikowana jest przykładowa aplikacja podstawowa .NET, korzystając z kontenera działającego w Cloud Shell. Po zweryfikowaniu aplikacja jest wdrażana w Kubernetes przy użyciu GKE. Moduł obejmuje kroki potrzebne do utworzenia klastra GKE.
W drugiej części modułu wprowadzamy niewielką zmianę w aplikacji, w której wyświetla się nazwa hosta kontenera, w którym działa dana instancja aplikacji. Zaktualizowana aplikacja jest następnie weryfikowana w Cloud Shell, a wdrożenie jest aktualizowane pod kątem nowej wersji. Poniższa ilustracja przedstawia sekwencję zadań w tym module:
Koszty
Jeśli przeprowadzisz ten moduł dokładnie w takiej formie, zaczną obowiązywać normalne opłaty za poniższe usługi
2. Konfiguracja i wymagania
Wymagania wstępne
Do ukończenia tego modułu wymagane są konto i projekt Google Cloud. Szczegółowe instrukcje tworzenia nowego projektu znajdziesz w tych ćwiczeniach z programowania.
W tym module wykorzystano narzędzie Dockera uruchomione w Cloud Shell, które jest dostępne w konsoli Google Cloud i jest wstępnie skonfigurowane oraz zawiera wiele przydatnych narzędzi, takich jak gcloud i Docker. Poniżej dowiesz się, jak uzyskać dostęp do Cloud Shell. Kliknij ikonę Cloud Shell w prawym górnym rogu, aby wyświetlić ją w dolnym panelu okna konsoli.
Alternatywne opcje konfiguracji klastra GKE (opcjonalnie)
Ten moduł wymaga klastra Kubernetes. W następnej sekcji zostanie utworzony klaster GKE z prostą konfiguracją. W tej sekcji znajdziesz niektóre polecenia gcloud
z alternatywnymi opcjami konfiguracji do użycia podczas tworzenia klastra Kubernetes przy użyciu GKE. Za pomocą poniższych poleceń można na przykład zidentyfikować różne typy maszyn, strefy, a nawet GPU (akceleratory).
- Wyświetl typy maszyn za pomocą tego polecenia
gcloud compute machine-types list
- Wyświetl listę GPU za pomocą tego polecenia
gcloud compute accelerator-types list
- Wyświetl strefy obliczeniowe za pomocą tego polecenia
gcloud compute zones list
- Pomoc dotycząca dowolnego polecenia gcloud
gcloud container clusters --help
- Na przykład tutaj znajdziesz szczegółowe informacje o tworzeniu klastra Kubernetes
gcloud container clusters create --help
- Na przykład tutaj znajdziesz szczegółowe informacje o tworzeniu klastra Kubernetes
Pełną listę opcji konfiguracyjnych GKE znajdziesz w tym dokumencie.
Przygotowanie do utworzenia klastra Kubernetes
W Cloud Shell konieczne jest ustawienie pewnych zmiennych środowiskowych i skonfigurowanie klienta gcloud. Można to zrobić za pomocą następujących poleceń.
export PROJECT_ID=YOUR_PROJECT_ID
export DEFAULT_ZONE=us-central1-c
gcloud config set project ${PROJECT_ID}
gcloud config set compute/zone ${DEFAULT_ZONE}
Tworzenie klastra GKE
W tym module wdrażana jest aplikacja .NET Core w Kubernetes, dlatego musisz utworzyć klaster. Użyj poniższego polecenia, aby utworzyć nowy klaster Kubernetes w Google Cloud przy użyciu GKE.
gcloud container clusters create dotnet-cluster \
--zone ${DEFAULT_ZONE} \
--num-nodes=1 \
--node-locations=${DEFAULT_ZONE},us-central1-b \
--enable-stackdriver-kubernetes \
--machine-type=n1-standard-1 \
--workload-pool=${PROJECT_ID}.svc.id.goog \
--enable-ip-alias
--num-nodes
to liczba węzłów, które należy dodać na strefę. Można ją później skalować.--node-locations
to rozdzielona przecinkami lista stref. W tym przypadku strefa określona w powyższej zmiennej środowiskowej i strefaus-central1-b
jest używana- UWAGA: ta lista nie może zawierać duplikatów
--workload-pool
określa tożsamość zadań, aby zadania GKE miały dostęp do usług Google Cloud
Podczas tworzenia klastra wyświetlane są następujące elementy
Creating cluster dotnet-cluster in us-central1-b... Cluster is being deployed...⠼
Konfigurowanie kubectl
Interfejs wiersza poleceń kubectl
to główny sposób interakcji z klastrem Kubernetes. Aby można było używać go z nowo utworzonym klastrem, należy go skonfigurować pod kątem uwierzytelniania w klastrze. Odbywa się to za pomocą tego polecenia.
$ gcloud container clusters get-credentials dotnet-cluster --zone ${DEFAULT_ZONE}
Fetching cluster endpoint and auth data.
kubeconfig entry generated for dotnet-cluster.
Korzystanie z usługi kubectl
do interakcji z klastrem powinno być teraz możliwe.
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-dotnet-cluster-default-pool-02c9dcb9-fgxj Ready <none> 2m15s v1.16.13-gke.401
gke-dotnet-cluster-default-pool-ed09d7b7-xdx9 Ready <none> 2m24s v1.16.13-gke.401
3. Przetestuj lokalnie funkcje i potwierdź odpowiednie działanie
W tym module używane są poniższe obrazy kontenerów z oficjalnego repozytorium .NET w centrum Dockera.
Uruchom kontener lokalnie, aby sprawdzić jego działanie
Sprawdź w Cloud Shell, czy Docker działa prawidłowo i czy kontener .NET działa zgodnie z oczekiwaniami, uruchamiając to polecenie Dockera:
$ docker run --rm mcr.microsoft.com/dotnet/samples
Hello from .NET!
__________________
\
\
....
....'
....
..........
.............'..'..
................'..'.....
.......'..........'..'..'....
........'..........'..'..'.....
.'....'..'..........'..'.......'.
.'..................'... ......
. ......'......... .....
. ......
.. . .. ......
.... . .......
...... ....... ............
................ ......................
........................'................
......................'..'...... .......
.........................'..'..... .......
........ ..'.............'..'.... ..........
..'..'... ...............'....... ..........
...'...... ...... .......... ...... .......
........... ....... ........ ......
....... '...'.'. '.'.'.' ....
....... .....'.. ..'.....
.. .......... ..'........
............ ..............
............. '..............
...........'.. .'.'............
............... .'.'.............
.............'.. ..'..'...........
............... .'..............
......... ..............
.....
Environment:
.NET 5.0.1-servicing.20575.16
Linux 5.4.58-07649-ge120df5deade #1 SMP PREEMPT Wed Aug 26 04:56:33 PDT 2020
Potwierdź funkcje aplikacji internetowej
Przykładowa aplikacja internetowa w usłudze można również zweryfikować w Cloud Shell. Poniższe polecenie uruchomienia Dockera tworzy nowy kontener, który udostępnia port 80
i mapuje go na port localhost
8080
. Pamiętaj, że w tym przypadku obiekt localhost
znajduje się w Cloud Shell.
$ docker run -it --rm -p 8080:80 --name aspnetcore_sample mcr.microsoft.com/dotnet/samples:aspnetapp
warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
Storing keys in a directory '/root/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
No XML encryptor configured. Key {64a3ed06-35f7-4d95-9554-8efd38f8b5d3} may be persisted to storage in unencrypted form.
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: /app
Ponieważ jest to aplikacja internetowa, należy ją wyświetlić i zweryfikować w przeglądarce internetowej. W następnej sekcji dowiesz się, jak to zrobić w Cloud Shell przy użyciu podglądu w przeglądarce.
4. Dostęp do usług w Cloud Shell za pomocą funkcji „Podgląd w przeglądarce”
Cloud Shell udostępnia podgląd w przeglądarce, który umożliwia interakcję z procesami uruchomionymi w instancji Cloud Shell za pomocą przeglądarki.
Użyj „Podglądu w przeglądarce” aby wyświetlać aplikacje w Cloud Shell
W Cloud Shell kliknij przycisk podglądu w przeglądarce i wybierz „Podejrzyj na porcie 8080”. (lub innego portu w podglądzie w przeglądarce).
Spowoduje to otwarcie okna przeglądarki z adresem podobnym do tego:
https://8080-cs-754738286554-default.us-central1.cloudshell.dev/?authuser=0
Wyświetl przykładową aplikację .NET za pomocą podglądu w przeglądarce
Przykładowa aplikacja uruchomiona w ostatnim kroku można teraz wyświetlić, uruchamiając podgląd w przeglądarce i wczytując podany adres URL. Powinna wyglądać mniej więcej tak:
5. Wdróż w Kubernetes
Tworzenie i stosowanie pliku YAML
Następny krok wymaga pliku YAML z opisem 2 zasobów Kubernetes: wdrożenia i usługi. Utwórz w Cloud Shell plik o nazwie dotnet-app.yaml
i dodaj do niego tę zawartość.
apiVersion: apps/v1
kind: Deployment
metadata:
name: dotnet-deployment
labels:
app: dotnetapp
spec:
replicas: 3
selector:
matchLabels:
app: dotnetapp
template:
metadata:
labels:
app: dotnetapp
spec:
containers:
- name: dotnet
image: mcr.microsoft.com/dotnet/samples:aspnetapp
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: dotnet-service
spec:
selector:
app: dotnetapp
ports:
- protocol: TCP
port: 8080
targetPort: 80
Teraz użyj narzędzia kubectl
, aby zastosować ten plik w Kubernetes.
$ kubectl apply -f dotnet-app.yaml
deployment.apps/dotnet-deployment created
service/dotnet-service created
Zwróć uwagę na komunikaty wskazujące, że odpowiednie zasoby zostały utworzone.
Zapoznaj się z dostępnymi materiałami
Za pomocą interfejsu wiersza poleceń kubectl
możemy zbadać zasoby utworzone powyżej. Najpierw przyjrzyjmy się zasobom dotyczącym wdrażania i sprawdźmy, czy nowe wdrożenie już istnieje.
$ kubectl get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
dotnet-deployment 3/3 3 3 80s
Teraz przyjrzyjmy się obiektom ReplicaSets. Powyższe wdrożenie powinno tworzyć obiekt ReplicaSet.
$ kubectl get replicaset
NAME DESIRED CURRENT READY AGE
dotnet-deployment-5c9d4cc4b9 3 3 3 111s
Na koniec przyjrzyj się Podom. Zasób Deployment wskazuje, że powinny istnieć 3 instancje. Poniższe polecenie powinno pokazywać 3 instancje. Opcja -o wide
została dodana, dzięki czemu będą wyświetlane węzły, w których działają te instancje.
$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
dotnet-deployment-5c9d4cc4b9-cspqd 1/1 Running 0 2m25s 10.16.0.8 gke-dotnet-cluster-default-pool-ed09d7b7-xdx9 <none> <none>
dotnet-deployment-5c9d4cc4b9-httw6 1/1 Running 0 2m25s 10.16.1.7 gke-dotnet-cluster-default-pool-02c9dcb9-fgxj <none> <none>
dotnet-deployment-5c9d4cc4b9-vvdln 1/1 Running 0 2m25s 10.16.0.7 gke-dotnet-cluster-default-pool-ed09d7b7-xdx9 <none> <none>
Sprawdzanie zasobu usługi
Zasób usługi w Kubernetes to system równoważenia obciążenia. Punkty końcowe są określane przez etykiety w podach. Dzięki temu po dodaniu do wdrożenia nowych podów za pomocą powyższej operacji kubectl scale deployment
utworzone pody są od razu dostępne dla żądań obsługiwanych przez tę usługę.
Poniższe polecenie powinno wyświetlić zasób Service.
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
dotnet-service ClusterIP 10.20.9.124 <none> 8080/TCP 2m50s
...
Za pomocą poniższego polecenia możesz wyświetlić więcej informacji o usłudze.
$ kubectl describe svc dotnet-service
Name: dotnet-service
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=dotnetapp
Type: ClusterIP
IP: 10.20.9.124
Port: <unset> 8080/TCP
TargetPort: 80/TCP
Endpoints: 10.16.0.7:80,10.16.0.8:80,10.16.1.7:80
Session Affinity: None
Events: <none>
Zwróć uwagę, że typ usługi to ClusterIP
. Oznacza to, że dowolny pod w klastrze może rozpoznać nazwę usługi (dotnet-service
) na swój adres IP. Żądania wysyłane do usługi będą równoważone obciążenie we wszystkich instancjach (podach). Powyższa wartość Endpoints
to adresy IP podów obecnie dostępnych dla tej usługi. Porównaj je z adresami IP podów, które zostały wyświetlone powyżej.
Sprawdź uruchomioną aplikację
W tym momencie aplikacja jest aktywna i gotowa do obsługi żądań użytkowników. Aby uzyskać do niego dostęp, użyj serwera proxy. Poniższe polecenie tworzy lokalny serwer proxy, który akceptuje żądania na porcie 8080
i przekazuje je do klastra Kubernetes.
$ kubectl proxy --port 8080
Starting to serve on 127.0.0.1:8080
Aby uzyskać dostęp do aplikacji internetowej, użyj teraz podglądu w przeglądarce w Cloud Shell.
Do adresu URL wygenerowanego przez Podgląd w przeglądarce dodaj ten fragment: /api/v1/namespaces/default/services/dotnet-service:8080/proxy/
. Efekt będzie wyglądał mniej więcej tak:
https://8080-cs-473655782854-default.us-central1.cloudshell.dev/api/v1/namespaces/default/services/dotnet-service:8080/proxy/
Gratulujemy wdrożenia aplikacji .NET Core w Google Kubernetes Engine. Następnie wprowadzimy zmianę w aplikacji i wprowadzimy ją ponownie.
6. Modyfikowanie aplikacji
W tej sekcji aplikacja zostanie zmodyfikowana, aby wyświetlić hosta, na którym działa instancja. Dzięki temu będzie można sprawdzić, czy równoważenie obciążenia działa i czy dostępne pody odpowiadają zgodnie z oczekiwaniami.
Pobieranie kodu źródłowego
git clone https://github.com/dotnet/dotnet-docker.git
cd dotnet-docker/samples/aspnetapp/
Zaktualizuj aplikację, dodając do niej nazwę hosta.
vi aspnetapp/Pages/Index.cshtml
<tr>
<td>Host</td>
<td>@Environment.MachineName</td>
</tr>
Tworzenie nowego obrazu kontenera i testowanie go lokalnie
Utwórz nowy obraz kontenera ze zaktualizowanym kodem.
docker build --pull -t aspnetapp:alpine -f Dockerfile.alpine-x64 .
Tak jak poprzednio przetestuj nową aplikację lokalnie
$ docker run --rm -it -p 8080:80 aspnetapp:alpine
warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
Storing keys in a directory '/root/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
No XML encryptor configured. Key {f71feb13-8eae-4552-b4f2-654435fff7f8} may be persisted to storage in unencrypted form.
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: /app
Tak jak wcześniej, dostęp do aplikacji można uzyskać za pomocą podglądu w przeglądarce. Tym razem parametr Host powinien być widoczny, jak tutaj:
Otwórz nową kartę w Cloud Shell i uruchom polecenie docker ps
, aby sprawdzić, czy identyfikator kontenera odpowiada powyższej wartości Host.
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ab85ce11aecd aspnetapp:alpine "./aspnetapp" 2 minutes ago Up 2 minutes 0.0.0.0:8080->80/tcp relaxed_northcutt
Dodaj tag do obrazu i przekaż go, aby był dostępny w Kubernetes
Obraz musi zostać otagowany i wypchnięty, aby można go było pobrać przez Kubernetes. Zacznij od wyświetlenia listy obrazów kontenerów i określenia odpowiedniego obrazu.
$ docker image list
REPOSITORY TAG IMAGE ID CREATED SIZE
aspnetapp alpine 95b4267bb6d0 6 days ago 110MB
Następnie otaguj obraz i przekaż go do Google Container Registry. Przy użyciu powyższego identyfikatora IMAGE będzie on wyglądać tak
docker tag 95b4267bb6d0 gcr.io/${PROJECT_ID}/aspnetapp:alpine
docker push gcr.io/${PROJECT_ID}/aspnetapp:alpine
7. Wdróż ponownie zaktualizowaną aplikację
Edytuj plik YAML
Wróć do katalogu, w którym zapisany jest plik dotnet-app.yaml
. Znajdź następujący wiersz w pliku YAML
image: mcr.microsoft.com/dotnet/core/samples:aspnetapp
Trzeba to zmienić, aby odwołać się do obrazu kontenera, który został utworzony i przeniesiony do gcr.io powyżej.
image: gcr.io/PROJECT_ID/aspnetapp:alpine
Nie zapomnij go zmodyfikować, aby korzystać z PROJECT_ID
. Po zakończeniu pracy powinien on wyglądać mniej więcej tak
image: gcr.io/myproject/aspnetapp:alpine
Zastosuj zaktualizowany plik YAML
$ kubectl apply -f dotnet-app.yaml
deployment.apps/dotnet-deployment configured
service/dotnet-service unchanged
Zwróć uwagę, że zasób Deployment wyświetla się jako zaktualizowany, a zasób Service – bez zmian. Zaktualizowane pody można zobaczyć tak jak poprzednio przy użyciu polecenia kubectl get pod
, ale tym razem dodamy -w
, który będzie na bieżąco obserwować wszystkie zmiany.
$ kubectl get pod -w
NAME READY STATUS RESTARTS AGE
dotnet-deployment-5c9d4cc4b9-cspqd 1/1 Running 0 34m
dotnet-deployment-5c9d4cc4b9-httw6 1/1 Running 0 34m
dotnet-deployment-5c9d4cc4b9-vvdln 1/1 Running 0 34m
dotnet-deployment-85f6446977-tmbdq 0/1 ContainerCreating 0 4s
dotnet-deployment-85f6446977-tmbdq 1/1 Running 0 5s
dotnet-deployment-5c9d4cc4b9-vvdln 1/1 Terminating 0 34m
dotnet-deployment-85f6446977-lcc58 0/1 Pending 0 0s
dotnet-deployment-85f6446977-lcc58 0/1 Pending 0 0s
dotnet-deployment-85f6446977-lcc58 0/1 ContainerCreating 0 0s
dotnet-deployment-5c9d4cc4b9-vvdln 0/1 Terminating 0 34m
dotnet-deployment-85f6446977-lcc58 1/1 Running 0 6s
dotnet-deployment-5c9d4cc4b9-cspqd 1/1 Terminating 0 34m
dotnet-deployment-85f6446977-hw24v 0/1 Pending 0 0s
dotnet-deployment-85f6446977-hw24v 0/1 Pending 0 0s
dotnet-deployment-5c9d4cc4b9-cspqd 0/1 Terminating 0 34m
dotnet-deployment-5c9d4cc4b9-vvdln 0/1 Terminating 0 34m
dotnet-deployment-5c9d4cc4b9-vvdln 0/1 Terminating 0 34m
dotnet-deployment-85f6446977-hw24v 0/1 Pending 0 2s
dotnet-deployment-85f6446977-hw24v 0/1 ContainerCreating 0 2s
dotnet-deployment-5c9d4cc4b9-cspqd 0/1 Terminating 0 34m
dotnet-deployment-5c9d4cc4b9-cspqd 0/1 Terminating 0 34m
dotnet-deployment-85f6446977-hw24v 1/1 Running 0 3s
dotnet-deployment-5c9d4cc4b9-httw6 1/1 Terminating 0 34m
dotnet-deployment-5c9d4cc4b9-httw6 0/1 Terminating 0 34m
Te dane wyjściowe pokazują bieżącą aktualizację kroczącą. Po pierwsze uruchamiane są nowe kontenery, a po uruchomieniu wyłączane są stare kontenery.
Sprawdź uruchomioną aplikację
W tym momencie aplikacja jest aktualizowana i gotowa do obsługi żądań użytkowników. Tak jak poprzednio można uzyskać do niego dostęp za pomocą serwera proxy.
$ kubectl proxy --port 8080
Starting to serve on 127.0.0.1:8080
Aby uzyskać dostęp do aplikacji internetowej, użyj teraz podglądu w przeglądarce w Cloud Shell.
Do adresu URL wygenerowanego przez Podgląd w przeglądarce dodaj ten fragment: /api/v1/namespaces/default/services/dotnet-service:8080/proxy/
. Efekt będzie wyglądał mniej więcej tak:
https://8080-cs-473655782854-default.us-central1.cloudshell.dev/api/v1/namespaces/default/services/dotnet-service:8080/proxy/
Sprawdzanie, czy usługa Kubernetes rozkłada obciążenie
Odśwież ten adres URL kilka razy i zauważ, że host się zmienia, ponieważ obciążenie żądaniami jest równoważone w różnych podach przez usługę. Porównaj wartości hosta z powyższą listą podów, aby sprawdzić, czy wszystkie pody odbierają ruch.
Skalowanie instancji w górę
Skalowanie aplikacji w Kubernetes jest proste. Widoczne poniżej polecenie przeskaluje wdrożenie do 6 instancji aplikacji.
$ kubectl scale deployment dotnet-deployment --replicas 6
deployment.apps/dotnet-deployment scaled
Nowe pody i ich bieżący stan można wyświetlić za pomocą tego polecenia
kubectl get pod -w
Zwróć uwagę, że odświeżenie tego samego okna przeglądarki pokazuje, że ruch jest teraz równoważony we wszystkich nowych podach.
8. Gratulacje!
W tym module zweryfikowano przykładową aplikację internetową .NET Core w środowisku programistycznym, a następnie wdrożono ją w Kubernetes przy użyciu GKE. Następnie aplikacja została zmodyfikowana, tak aby wyświetlała nazwę hosta kontenera, w którym działała. Następnie wdrożenie Kubernetes zostało zaktualizowane do nowej wersji, a aplikację przeskalowano w górę, aby zademonstrować rozkład obciążenia między dodatkowymi instancjami.
Aby dowiedzieć się więcej o .NET i Kubernetes, zapoznaj się z tymi samouczkami. Bazują one na wiedzy zdobytej w tym module przez wprowadzenie Istio Service Mesh na potrzeby bardziej zaawansowanych wzorców routingu i odporności.
9. Czyszczenie danych
Aby uniknąć niezamierzonych kosztów, usuń klaster i obraz kontenera utworzone w tym module za pomocą podanych niżej poleceń.
gcloud container clusters delete dotnet-cluster --zone ${DEFAULT_ZONE}
gcloud container images delete gcr.io/${PROJECT_ID}/aspnetapp:alpine