You are currently viewing Plant Identifier Web Application Deployment Using Google Kubernetes Engine (GKE), Ingress, Prometheus and Grafana stack

Plant Identifier Web Application Deployment Using Google Kubernetes Engine (GKE), Ingress, Prometheus and Grafana stack

This is an implementation guide to deploying a production-ready web application that can scale from zero to handling thousands of requests. In this comprehensive guide, I’ll walk you through deploying a Plant Identifier application on Google Kubernetes Engine including the use of Ingress, ESO (External Secrets Operator), alongside a setup of monitoring stack.

Introduction

This is an implementation guide to deploying a production-ready web application that can scale from zero to handling thousands of requests. In this comprehensive guide, I’ll walk you through deploying a Plant Identifier application on Google Kubernetes Engine including the use of Ingress, ESO (External Secrets Operator), alongside a setup of monitoring stack.

Plant Identifier Architecture
        graph TB
            subgraph Client["Client"]
                A[User Browser]:::client
            end

            subgraph GCP["GCP"]
                subgraph LB["Google Cloud Load Balancer"]
                    B[GKE Ingress]:::gcp
                    C[Managed Certificate/SSL]:::gcp
                end

                subgraph GKE["GKE Cluster"]
                    subgraph Monitor["Monitoring"]
                        D[Prometheus]:::monitoring
                        E[Grafana]:::monitoring
                        F[ServiceMonitor]:::monitoring
                    end

                    subgraph App["Plant-ID"]
                        G[Plant Identifier App]:::app
                        H[Service Account]:::k8s
                        I[ClusterRole Binding]:::k8s
                    end

                    subgraph Secrets["External-Secrets"]
                        J[External Secrets Operator]:::k8s
                    end
                end
            end

            subgraph AWS["AWS Cloud"]
                K[Secrets Manager]:::aws
            end

            A --> B
            B --> C
            B --> G
            G --> F
            F --> D
            D --> E
            J --> K
            J --> G
            H --> I
            I --> G

            classDef client fill:#1E88E5,stroke:#0D47A1,stroke-width:4px,color:white;
            classDef gcp fill:#4285F4,stroke:#1B5E20,stroke-width:4px,color:white;
            classDef aws fill:#FF9900,stroke:#E65100,stroke-width:4px,color:black;
            classDef k8s fill:#326CE5,stroke:#1A237E,stroke-width:4px,color:white;
            classDef monitoring fill:#33A852,stroke:#1B5E20,stroke-width:4px,color:white;
            classDef app fill:#9C27B0,stroke:#4A148C,stroke-width:4px,color:white;
        

This project showcases the integration of:

  • Google’s Gemini AI for plant identification (the use of API KEY in our .env)
  • Google Cloud Platform (GCP) for infrastructure
  • Kubernetes (GKE) for container orchestration
  • AWS Secrets Manager for secure secret store
  • Prometheus and Grafana for monitoring

This setup demonstrates several important concepts in modern web development:

  • HPA (HorizontalPodAutoscaler) – Kubernetes deployment
  • Cloud-native security practices
  • Multi-cloud integration (GCP + AWS)
  • Automated SSL certificate management
  • Real-time monitoring

For the plant identifier App, it has to run on https for the following reasons:

  • Camera access for plant photos
  • API calls to Google Gemini
  • User trust and security

What We’ll Cover

  1. Containerizing the application
  2. Managing secrets securely
  3. Deploying to GKE
  4. Configuring HTTPS and domain
  5. Implementing monitoring
  6. Testing and validation

Required Accounts & Permissions

Google Cloud Account

  • Billing enabled
  • Project created

AWS Account (for Secrets Management)

  • AWS Access Key ID
  • AWS Secret Access Key
  • Appropriate IAM permissions for Secrets Manager

Let’s get started!

Prerequisites

  • Familiarity with Docker and Kubernetes concepts
  • GCP and AWS accounts
  • Docker
  • AWS cli
  • Google Cloud SDK (Download and install from: https://cloud.google.com/sdk/docs/install)
  • Command-line tools (gcloud, kubectl, helm)
  • GKE gcloud auth plugin (gcloud components install gke-gcloud-auth-plugin)
  • A registered domain name
  • Required APIs & Services – Enable required GCP APIs
    gcloud services enable \
    container.googleapis.com \
    compute.googleapis.com \
    containerregistry.googleapis.com


  • Domain & DNS
    Requirements:
    • Registered domain name
    • Access to domain’s DNS settings
    • Access to Add/modify A records (to be mapped to our Ingress static IP)

Step 1: GCP Authentication

gcloud auth login
gcloud config set project PROJECT_ID

Verify APIs are enabled

gcloud services list –enabled

Configure Docker to use GCP Container Registry

For specific region (recommended): gcloud auth configure-docker gcr.io

Step 2: Prepare Docker

We will build our docker image and push it to Google Container Registry.
Download the Application code from my GitHub repo here. By the way I wrote the Plant Identifier App code with the help of Claude Sonnet, and it turned out fine *yay*.

Build your image:

docker build -t gcr.io/YOUR_PROJECT_ID/plant-identifier:latest_1 .

You can get your PROJECT_ID from Google Cloud Console or run “gcloud info | grep Project”.

Push to GCR:

docker push gcr.io/YOUR_PROJECT_ID/plant-identifier:latest_1


Step 3: Reserve a static IP

Provision a Public static IP, we will be using this in our deployment later on.

gcloud compute addresses create plant-id-ip --global

Get the IP address:

gcloud compute addresses describe plant-id-ip --global --format='get(address)'

List all addresses:

gcloud compute addresses list


Step 4: Create cluster

Now let us create our GKE Cluster, I am going with a cost-effective approach as the App is lightweight but we will also configure HPA to allow for scaling if traffic grows, run the below command:

gcloud container clusters create plant-identifier-cluster \
--zone europe-west2-a \
--num-nodes 2 \
--machine-type e2-standard-4 \
--disk-size 30

Get credentials for kubectl:

gcloud container clusters get-credentials plant-identifier-cluster --zone europe-west2-a

Now you can run kubectl commands and check our clusters and resources.

kubectl cluster-info
kubectl get nodes

Also visit the Console to check all the provisioned resources.

STEP 5: Setup External Secrets Operator

We need to create secret for our API Key. If you want to use this application for yourself, you will need a Google Gemini API Key, you can check outside of this document on how to get it, then use the API key you have copied in your .env and also follow the steps below. Remember not to commit your key or any sensitive data to you repository.

In this setup guide, we are going to use AWS Secrets Manager with our GKE cluster to securely store our key (Note that there are many other approaches to doing this, you can make use of other Vaults and you can also directly set it as secret in your pipeline, for example in Jenkins, GitHub etc).

This step involves setting up the External Secrets Operator (ESO) to fetch secrets from AWS Secrets Manager.

First, install the External Secrets Operator:

  1. Add the External Secrets helm repository
helm repo add external-secrets https://charts.external-secrets.io
helm repo update

2. Install External Secrets Operator

helm install external-secrets \
external-secrets/external-secrets \
--namespace external-secrets \
--create-namespace

3. Verify ESO installation

kubectl get pods -n external-secrets
kubectl get crds | grep externalsecrets

4. Create IAM User/Policy/Role in AWS:

4a. Create policy for Secrets Manager access

aws iam create-policy --policy-name plant-id-secrets-policy --policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Resource": "arn:aws:secretsmanager:::secret:*"
}
]
}'

For example: “Resource”: “arn:aws:secretsmanager:us-east-1::secret:plant-id*”

4b. Create IAM user and attach policy

aws iam create-user --user-name plant-id-secrets-user

4c. Attach Policy to the user


aws iam attach-user-policy --user-name plant-id-secrets-user --policy-arn

For example: aws iam attach-user-policy –user-name plant-id-secrets-user –policy-arn arn:aws:iam:::policy/plant-id-secrets-policy

It is always advised to apply principle of least privilege, avoid granting broad permissions, however, in a case of local development or you are working alone, you can also use the below AWS policy:

aws iam attach-user-policy --user-name plant-id-secrets-user --policy-arn arn:aws:iam::aws:policy/SecretsManagerReadWrite

4d. You can list the attached policy for the user as below:

aws iam list-attached-user-policies --user-name plant-id-secrets-user

5. Create access keys and save the details

aws iam create-access-key --user-name plant-id-secrets-user

6. Store secret in AWS Secrets Manager

Run the below command to store your secret:

aws secretsmanager create-secret \
--name plant-id \
--secret-string '{"GOOGLE_API_KEY":"your-api-key-value"}'

Create namespace

Before we continue, let us create a namespace where we deploy our resources.

kubectl create namespace plant-id

7. Configure AWS Credentials

#Create AWS credentials secret
kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
name: aws-credentials
namespace: plant-id
type: Opaque
stringData:
access-key-id: YOUR_AWS_ACCESS_KEY_ID
secret-access-key: YOUR_AWS_SECRET_ACCESS_KEY
EOF

Verify secret creation:

kubectl get secret aws-credentials

8. Configure SecretStore
Let’s configure SecretStore, note that you can configure it globally using kind: ClusterSecretStore instead of limiting it to a particular namespace.

secretStore-auth.yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secret-store
namespace: plant-id
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
secretRef:
accessKeyIDSecretRef:
name: aws-credentials
key: access-key-id
namespace: plant-id
secretAccessKeySecretRef:
name: aws-credentials
key: secret-access-key
namespace: plant-id

kubectl apply -f secretStore-auth.yaml

Verify SecretStore:

kubectl get secretstore
kubectl describe secretstore aws-secret-store
  1. Configure ExternalSecret
# external-secrets.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: plant-id-external-secret
namespace: plant-id
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secret-store
kind: SecretStore
target:
name: plant-id-secrets
creationPolicy: Owner
data:
- secretKey: GOOGLE_API_KEY
remoteRef:
key: plant-id
property: GOOGLE_API_KEY

kubectl apply -f external-secrets.yaml

Verify ExternalSecret:

kubectl get externalsecret
kubectl describe externalsecret plant-id-external-secret

STEP 6: Deploy to GKE

Points to note:

  • Managed certificates for SSL
  • Proper NEG annotations
  • Service with NEG annotations
  • GKE HTTP(S) Load Balancing enabled
  • Domain configuration
  • DNS Configuration pointing to Ingress IP
  1. Deployment with Secrets
    Let’s work on deploying our application. Ensure your secretKeyRef:name matches the name given in your target:name in external-secrets.yaml.
    If your application is not able to get the API-Key or any other values in your env, the pods will get stuck in some errors.
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: plant-identifier
namespace: plant-id
spec:
replicas: 1
selector:
matchLabels:
app: plant-identifier
template:
metadata:
labels:
app: plant-identifier
spec:
containers:
- name: plant-identifier
image: gcr.io/<project-ID>/plant-identifier:latest_1
ports:
- containerPort: 3000
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 300m
memory: 256Mi
env:
- name: GOOGLE_API_KEY
valueFrom:
secretKeyRef:
name: plant-id-secrets
key: GOOGLE_API_KEY

  1. BackendConfig (GKE-specific)
    BackendConfig customizes the behaviour of GCP load balancer backends like the Custom health checks and timeout settings. Here you can also enable CDN, configure session affinity, connection draining and other settings you require, you can refer to Google Cloud documentation for more available options.
# backend-config.yaml
apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
name: plant-id-backend-config
namespace: plant-id
spec:
healthCheck:
checkIntervalSec: 30
timeoutSec: 5
healthyThreshold: 1
unhealthyThreshold: 2
type: HTTP
requestPath: /
port: 3000

After we apply our config file, we can check these:

kubectl get backendconfig -n plant-id
gcloud compute health-checks list

3. Configure Managed Certificate

# managed-cert.yaml
apiVersion: networking.gke.io/v1
kind: ManagedCertificate
metadata:
name: plant-id-cert
namespace: plant-id
spec:
domains:
- <your-domain-name>

Verification commands:

kubectl get managedcertificate -n plant-id
kubectl describe managedcertificate plant-id-cert -n plant-id

Check certificate status in GCP:

gcloud compute ssl-certificates list

For example, you will see an output like this:

$ gcloud compute ssl-certificates list
NAME TYPE CREATION_TIMESTAMP EXPIRE_TIME REGION MANAGED_STATUS
mcrt-7cde66e0-56bd-460c-85c0-230d1b849716 MANAGED 2024-11-09T04:51:42.750-08:00 2025-02-07T04:51:44.000-08:00 ACTIVE
projectrom.co.uk: ACTIVE

You need to give 10 to 30 minutes for the ssl certificate to become ACTIVE. If it is however stuck in PROVISIONING state as below, check your DNS mapping, managed certificate configuration, check status of the DNS records and your domain.

NAME TYPE CREATION_TIMESTAMP EXPIRE_TIME REGION MANAGED_STATUS
mcrt-c0e796a1-28df-4567-9ef5-d0b7d1cae948 MANAGED 2024-11-12T04:32:55.344-08:00 PROVISIONING
projectrom.co.uk: PROVISIONING

4. Horizontal Pod Autoscaling (HPA)
The HPA automatically scales the number of pods based on resource utilization or custom metrics. Some of the benefits of this are cost optimization, improved application performance, customized thresholds, resource efficiency and improved application performance.
Let’s add HPA config.

# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: plant-identifier-hpa
namespace: plant-id
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: plant-identifier
minReplicas: 1
maxReplicas: 3
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70

To verify hpa status:

kubectl get hpa -n plant-id
kubectl describe hpa plant-identifier-hpa -n plant-id

5. Configure Service
Lets configure our Service and add some annotations.

# service.yaml
apiVersion: v1
kind: Service
metadata:
name: plant-identifier
namespace: plant-id
annotations:
cloud.google.com/neg: '{"ingress": true}'
cloud.google.com/app-protocols: '{"http":"HTTPS"}'
cloud.google.com/backend-config: '{"default": "plant-id-backend-config"}'
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 3000
selector:
app: plant-identifier

Verify Status:

kubectl get service plant-identifier -n plant-id
kubectl describe service plant-identifier -n plant-id

A quick check of the BackendConfig:

kubectl describe backendconfig plant-id-backend-config -n plant-id

After applying the config, you should see outputs similar to these:

6. FrontendConfig
FrontendConfig customizes the Google Cloud Load Balancer’s frontend settings. We will be associating it with our Ingress later on, using annotation. Here, we can enable HTTPS redirection, configure SSL policies, configure appropriate security headers and other options available. Consult the Google Cloud documentation for more customizable features.

# frontend-config.yaml
apiVersion: networking.gke.io/v1beta1
kind: FrontendConfig
metadata:
name: plant-id-https
namespace: plant-id
spec:
redirectToHttps:
enabled: true
responseCodeName: MOVED_PERMANENTLY_DEFAULT # 301 redirect
sslPolicy: gke-ingress-ssl-policy

If you want to apply FrontendConfig if in a separate file:

kubectl apply -f frontend-config.yaml

Check status:

kubectl get frontendconfig -n plant-id
kubectl describe frontendconfig plant-id-https -n plant-id

7. Apply Deployment file
I have put all the configuration in one file, let us apply it:

kubectl apply -f deployment-frontend.yaml

Verify deployment:

kubectl get deployments -n plant-id
kubectl describe deployment plant-identifier -n plant-id
kubectl get pods -n plant-id
kubectl describe pod -l app=plant-identifier -n plant-id


Let us check the managed certificate as well.

Let’s check our HPA as well:

8. Configure Ingress
Let us expose our Kubernetes App using Ingress-gce. This will help with load balancing and redirect HTTP traffic to HTTPS.

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: plant-id-ingress
namespace: plant-id
annotations:
kubernetes.io/ingress.class: "gce"
networking.gke.io/managed-certificates: "plant-id-cert"
kubernetes.io/ingress.global-static-ip-name: "plant-id-ip" # If using static IP
networking.gke.io/v1beta1.FrontendConfig: "plant-id-https"
spec:
rules:
- host: xxxpom.co.uk # Replace with your domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: plant-identifier
port:
number: 80

Apply the config:
kubectl apply -f ingress.yaml

Validate:

kubectl get ingress -n plant-id
kubectl describe ingress plant-id-ingress -n plant-id


gcloud compute forwarding-rules list

Health check status:

gcloud compute health-checks list

kubectl get apiservice v1beta1.metrics.k8s.io -o yaml #verify metrics server

Check BE and NEGs

gcloud compute network-endpoint-groups list

Check all the deployed services

kubectl get all -n plant-id

9. DNS Configuration Verification

You can carry out this step at any point before or after creating Managed Certificate and Ingress. This is just a guide to follow if you need to setup your domain and A records.

Get Ingress IP

kubectl get ingress plant-id-ingress -n plant-id -o jsonpath='{.status.loadBalancer.ingress[0].ip}'

Go to your domain registrar
Add an A record pointing to your static IP:
Type: A
Name: plant-id (or @ for root domain) – I used “@”

Verify DNS resolution:

nslookup <your-domain-name>

Test HTTPS certificate:

curl -v https://<your-domain-name>

Access your domain
Now our App should be reachable, browse to your domain to confirm.

Test the redirect we configured in Frontendconfig


curl -I http://<yourdomain>

It Should show a 301 redirect to https://

Check ingress events

kubectl get events -n plant-id

Check service and application logs

kubectl logs -l app=plant-identifier -n plant-id

View logs for the last hour with timestamps

kubectl logs -l app=plant-identifier -n plant-id --since=1h

$ kubectl logs -l app=plant-identifier -n plant-id --since=40h
{
"name": "Jute",
"description": "Jute is a natural fiber obtained from the stem of the jute plant, Corchorus olitorius.",
"scientificName": "Corchorus olitorius",
"family": "Tiliaceae",
"nativeRegion": "Tropical Asia",
"sunlightNeeds": "Full sun to partial shade",
"wateringFrequency": "Regular watering, keeping soil moist but not soggy"
}

Lastly, let’s play around with the App, download different plant pictures, upload each one for identification by the App, you can also capture image of plant directly if any plant is close to you, image capturing is easier with your phone camera, so browse to your site address and play around with it.

Step 7: Install Prometheus, Grafana and AlertManager

We will be creating easy monitoring with Grafana and Prometheus. Grafana server serves as our dashboard and data source management and it supports multiple data sources including Prometheus, Loki, AWS Cloudwatch, Elasticsearch as data sources.

Using kube-prometheus-stack

Let’s proceed with using kube-prometheus-stack. kube-prometheus-stack is a preconfigured solution to spin up Prometheus quickly.
It involves a pre-configured Prometheus installation that automatically collects metrics from Kubernetes resources (pods, services, storage, etc.). Additionally, the kube-prometheus-stack provides Grafana integration with ready-made dashboards and visualizations.

Add Helm repos

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

Install kube-prometheus-stack:
This will install Prometheus, Alertmanager, and Grafana, along with several pre-configured dashboards and alerts for Kubernetes monitoring.

Install Helm Chart using values.yaml (Download the prom-stack-values.yaml from my GitHub repo Here) for customization and set the “grafana.adminPassword=?”

helm upgrade --install prom-stack prometheus-community/kube-prometheus-stack -n monitoring --create-namespace -f prom-stack-values.yaml --set grafana.adminPassword="<your-password>"

After installing kube-prometheus-stack Helm chart, we can use the “kubectl get all -n monitoring” commmand to view all its components.

kubectl get all -n monitoring
kubectl --namespace monitoring get pods

Access Grafana
To expose our Grafana, we can:

  • Port-forward to access Grafana:
    kubectl port-forward svc/kube-prometheus-grafana -n monitoring 3000:80
  • You can change svc ClusterIP to LoadBalancer type by editing the file directly.
  • You can also pass the option in helm upgrade.
helm upgrade --install prom-stack prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--set prometheus.server.service.type=LoadBalancer

If you notice that in our values file (prom-stack-values.yaml), we have stated LoadBalancer as service type, so for us, no need to change anything.

If you did not set the Grafana admin password in values.yaml file or pass it in helm install command, you can retrieve it by using the below command:

kubectl get secret --namespace monitoring prom-stack-grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo

After installing, check the external IP assigned to the Grafana service:


check services –

kubectl get svc -n monitoring

kubectl get svc -n monitoring prom-stack-grafana
kubectl get svc -n monitoring prom-stack-kube-prometheus-prometheus
kubectl get svc -n monitoring prom-stack-kube-prometheus-alertmanager
You can access Grafana at http://<EXTERNAL_IP>:80
You can access Prometheus at http://<EXTERNAL_IP:9090

So that our Prometheus stack can access the resources in our cluster across different namespaces including our Plant-ID App, we need to configure ServiceMonitor and rbac – ClusterRole and ClusterRoleBinding, refer to the GitHub documentation for the files.

Download files from Here

Apply the config files:
kubectl service-monitor.yaml
kubectl apply -f prometheus-rbac.yaml

Let’s check our service monitor:

kubectl get servicemonitor -A

By examining the metric values in the pre-packaged dashboard, we can visually inspect the metric values of all resources in the Kubernetes cluster. Navigate to “Targets” on Prometheus.

A simulated service down scenario:

Dashboards visualization:

If you need to add any other data source, you can go to Connection –> Add new data source –> select your Data source –> Enter key details like URL, Authentication etc –> Test and Save

In our next project, we will delve deeper into configuring AlertManager, Grafana Loki for log aggregation and Grafana for visualization.

A few tips If you need to delete resources you have used

helm list -A

Delete Prometheus Stack

helm uninstall prom-stack -n monitoring

Delete namespace

kubectl delete namespace monitoring
kubectl delete namespace plant-id

Delete IP and certs:

gcloud compute addresses delete plant-id-ip --global
gcloud compute ssl-certificates list
gcloud compute ssl-certificates delete [CERT_NAME] --global

List node pools

gcloud container node-pools list \
--cluster plant-identifier-cluster \
--zone europe-west2-a

Delete node pools

gcloud container node-pools delete default-pool \
--cluster plant-identifier-cluster \
--zone europe-west2-a

List and delete disks

gcloud compute disks list
gcloud compute disks delete DISK_NAME --zone europe-west2-a

Delete persistent volumes

kubectl delete pv --all

Delete cluster

gcloud container clusters delete plant-identifier-cluster \
--zone europe-west2-a \
--async

Conclusion

The Plant Identifier application deployment showcases a secure, cloud-native deployment on GKE with AWS Secrets Manager, Prometheus, and Grafana, highlighting robust multi-cloud integration and DevOps best practices. This setup includes automated SSL/TLS management, secure secret handling via External Secrets Operator, and scalable, well-configured infrastructure for high performance and maintainability.

Happy Learning!

Leave a Reply