Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add gke-whereami to samples #132

Merged
merged 9 commits into from
Aug 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions gke-whereami/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM python:3.8-alpine

#MAINTAINER Alex Mattson "alex.mattson@gmail.com"

COPY ./requirements.txt /app/requirements.txt

WORKDIR /app

RUN pip install -r requirements.txt

COPY . /app

RUN addgroup -S appuser && adduser -S -G appuser appuser
USER appuser

ENTRYPOINT [ "python" ]

CMD [ "app.py" ]
1 change: 1 addition & 0 deletions gke-whereami/Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: python app.py
130 changes: 130 additions & 0 deletions gke-whereami/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# gke-whereami

A simple Kubernetes-oriented app for describing the location of the pod serving a request via its attributes (cluster name, cluster region, pod name, namespace, service account, etc). The response payload includes an emoji that is hashed from the pod name, which makes it a little easier for a human to visually identify the pod you're dealing with.

This was originally written for testing & debugging multi-cluster ingress use cases on GKE (now Ingress for Anthos). The app's response payload will omit fields that it's unable to provide, meaning you can run this app on non-GKE K8s clusters, but the payload's fields will reflect that gap.

### Setup

#### Step 1 - Create a GKE cluster

First define your environment variables (substituting where #needed#):

```
export PROJECT_ID=#YOUR_PROJECT_ID#

export COMPUTE_REGION=#YOUR_COMPUTE_REGION# # this expects a region, not a zone

export CLUSTER_NAME=whereami
```

Now create your cluster:

```
gcloud beta container clusters create $CLUSTER_NAME \
--enable-ip-alias \
--enable-stackdriver-kubernetes \
--region=$COMPUTE_REGION \
--num-nodes=1 \
theemadnes marked this conversation as resolved.
Show resolved Hide resolved
--release-channel=regular

gcloud container clusters get-credentials $CLUSTER_NAME --region $COMPUTE_REGION
```

This will create a regional cluster with a single node per zone (3 nodes in total).

#### Step 2 - Deploy the service/pods:

```
kubectl apply -k k8s
```

*or via [kustomize](https://kustomize.io/)*

```
kustomize build k8s | kubectl apply -f -
```

Get the service endpoint:
theemadnes marked this conversation as resolved.
Show resolved Hide resolved
> Note: this may be `pending` for a few minutes while the service provisions
```
WHEREAMI_ENDPOINT=$(kubectl get svc whereami | grep -v EXTERNAL-IP | awk '{ print $4}')
```

Wrap things up by `curl`ing the `EXTERNAL-IP` of the service.

```
curl $WHEREAMI_ENDPOINT
```

Result:

```{"cluster_name":"cluster-1","host_header":"34.72.90.134","metadata":"frontend","node_name":"gke-cluster-1-default-pool-c91b5644-v8kg.c.alexmattson-scratch.internal","pod_ip":"10.4.2.34","pod_name":"whereami-7b79956dd6-vmm9z","pod_name_emoji":"🧚🏼‍♀️","pod_namespace":"default","pod_service_account":"whereami-ksa","project_id":"alexmattson-scratch","timestamp":"2020-07-30T05:44:14","zone":"us-central1-c"}```

### [Optional] Setup backend service call

`gke-whereami` has an optional flag within its configmap that will cause it to call another backend service within your GKE cluster (for example, a different, non-public instance of itself). This is helpful for demonstrating a public microservice call to a non-public microservice, and then including the responses of both microservices in the payload delivered back to the user.

#### [Optional] Step 1 - Remove existing deployment

First, remove the default deployment, as the default deployment won't attempt to call the downstream service, since updating a configmap referenced by a pod will not automatically redeploy that pod:

```
kubectl delete -k k8s
```

#### [Optional] Step 2 - Deploy the backend instance

```
kubectl apply -k k8s-backend-overlay-example
```

*or via [kustomize](https://kustomize.io/)*

```
kustomize build k8s-backend-overlay-example | kubectl apply -f -
```

#### [Optional] Step 3 - Configure & deploy the frontend

Modify `k8s/configmap.yaml`'s `BACKEND_ENABLED` field to `"True"`.

Next, redeploy the "frontend" instance of `gke-whereami`:

```
kubectl apply -k k8s
```

*or via [kustomize](https://kustomize.io/)*

```
kustomize build k8s | kubectl apply -f -
```

Get the service endpoint:
> Note: this may be `pending` for a few minutes while the service provisions
```
WHEREAMI_ENDPOINT=$(kubectl get svc whereami | grep -v EXTERNAL-IP | awk '{ print $4}')
```

Wrap things up by `curl`ing the `EXTERNAL-IP` of the service.

```
curl $WHEREAMI_ENDPOINT
```

The (*slightly* busy-looking) result should look like this:
theemadnes marked this conversation as resolved.
Show resolved Hide resolved

```{"backend_result":{"cluster_name":"cluster-1","host_header":"whereami-backend","metadata":"backend","node_name":"gke-cluster-1-default-pool-c91b5644-v8kg.c.alexmattson-scratch.internal","pod_ip":"10.4.2.37","pod_name":"whereami-backend-86bdc7b596-z4dqk","pod_name_emoji":"💪🏾","pod_namespace":"default","pod_service_account":"whereami-ksa-backend","project_id":"alexmattson-scratch","timestamp":"2020-07-30T05:56:15","zone":"us-central1-c"},"cluster_name":"cluster-1","host_header":"34.72.90.134","metadata":"frontend","node_name":"gke-cluster-1-default-pool-c91b5644-1z7l.c.alexmattson-scratch.internal","pod_ip":"10.4.1.29","pod_name":"whereami-7888579d9d-qdmbg","pod_name_emoji":"🧜","pod_namespace":"default","pod_service_account":"whereami-ksa","project_id":"alexmattson-scratch","timestamp":"2020-07-30T05:56:15","zone":"us-central1-c"}```

Look at the `backend_result` field from the response. That portion of the JSON is from the backend service.

If you wish to call a different backend service, modify `k8s/configmap.yaml`'s `BACKEND_SERVICE` to some other service name.


### Notes

If you'd like to build & publish via Google's [buildpacks](https://github.com/GoogleCloudPlatform/buildpacks), something like this should do the trick (leveraging the local `Procfile`):

```pack build --builder gcr.io/buildpacks/builder:v1 --publish gcr.io/${PROJECT_ID}/whereami```

147 changes: 147 additions & 0 deletions gke-whereami/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import requests
from flask import Flask, request, Response, jsonify
import logging
import json
import sys
import socket
import os
from datetime import datetime
import emoji
import random
from flask_cors import CORS

METADATA_URL = 'http://metadata.google.internal/computeMetadata/v1/'
METADATA_HEADERS = {'Metadata-Flavor': 'Google'}

app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False # otherwise our emojis get hosed
CORS(app) # enable CORS

# set up emoji list
emoji_list = list(emoji.unicode_codes.UNICODE_EMOJI.keys())


@app.route('/healthz') # healthcheck endpoint
def i_am_healthy():
return ('OK')


@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def home(path):

# define the response payload
payload = {}

# get GCP project ID
try:
theemadnes marked this conversation as resolved.
Show resolved Hide resolved
r = requests.get(METADATA_URL +
'project/project-id',
headers=METADATA_HEADERS)
if r.ok:
payload['project_id'] = r.text
except:

logging.warning("Unable to capture project ID.")

# get GCP zone
try:
r = requests.get(METADATA_URL +
'instance/zone',
headers=METADATA_HEADERS)
if r.ok:
payload['zone'] = str(r.text.split("/")[3])
except:

logging.warning("Unable to capture zone.")

# get GKE node name
try:
r = requests.get(METADATA_URL +
'instance/hostname',
headers=METADATA_HEADERS)
if r.ok:
payload['node_name'] = str(r.text)
except:

logging.warning("Unable to capture node name.")

# get GKE cluster name
try:
r = requests.get(METADATA_URL +
'instance/attributes/cluster-name',
headers=METADATA_HEADERS)
if r.ok:
payload['cluster_name'] = str(r.text)
except:

logging.warning("Unable to capture GKE cluster name.")

# get host header
try:
payload['host_header'] = request.headers.get('host')
except:
logging.warning("Unable to capture host header.")

# get pod name, emoji & datetime
payload['pod_name'] = socket.gethostname()
payload['pod_name_emoji'] = emoji_list[hash(socket.gethostname()) %
len(emoji_list)]
payload['timestamp'] = datetime.now().replace(microsecond=0).isoformat()

# get namespace, pod ip, and pod service account via downstream API
if os.getenv('POD_NAMESPACE'):
payload['pod_namespace'] = os.getenv('POD_NAMESPACE')
else:
logging.warning("Unable to capture pod namespace.")

if os.getenv('POD_IP'):
payload['pod_ip'] = os.getenv('POD_IP')
else:
logging.warning("Unable to capture pod IP address.")

if os.getenv('POD_SERVICE_ACCOUNT'):
payload['pod_service_account'] = os.getenv('POD_SERVICE_ACCOUNT')
else:
logging.warning("Unable to capture pod KSA.")

# get the whereami METADATA envvar
metadata = os.getenv('METADATA')
if os.getenv('METADATA'):
payload['metadata'] = os.getenv('METADATA')
else:
logging.warning("Unable to capture metadata.")

# should we call a backend service?
call_backend = os.getenv('BACKEND_ENABLED')

if call_backend == 'True':

backend_service = os.getenv('BACKEND_SERVICE')

try:
r = requests.get('http://' + backend_service)
if r.ok:
backend_result = r.json()
else:
backend_result = None
except:

print(sys.exc_info()[0])
backend_result = None

payload['backend_result'] = backend_result

return jsonify(payload)


if __name__ == '__main__':
out_hdlr = logging.StreamHandler(sys.stdout)
fmt = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
out_hdlr.setFormatter(fmt)
out_hdlr.setLevel(logging.INFO)
logging.getLogger().addHandler(out_hdlr)
logging.getLogger().setLevel(logging.INFO)
app.logger.handlers = []
app.logger.propagate = True
app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))
theemadnes marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 7 additions & 0 deletions gke-whereami/k8s-backend-overlay-example/cm-flag.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: whereami-configmap
data:
BACKEND_ENABLED: "False" # assuming you don't want a chain of backend calls
METADATA: "backend"
8 changes: 8 additions & 0 deletions gke-whereami/k8s-backend-overlay-example/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
nameSuffix: "-backend"
commonLabels:
app: whereami-backend
bases:
- ../k8s
patches:
- cm-flag.yaml
- service-type.yaml
6 changes: 6 additions & 0 deletions gke-whereami/k8s-backend-overlay-example/service-type.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: "v1"
kind: "Service"
metadata:
name: "whereami"
spec:
type: ClusterIP
8 changes: 8 additions & 0 deletions gke-whereami/k8s/configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: whereami-configmap
data:
BACKEND_ENABLED: "False" # flag to enable backend service call "False" || "True"
BACKEND_SERVICE: "whereami-backend" # substitute with corresponding service name - this example assumes both services are in the same namespace
METADATA: "frontend" # arbitrary string that gets returned in payload - can be used to track which services you're interacting with
Loading