Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ccollicutt committed Apr 24, 2023
0 parents commit 1c51456
Show file tree
Hide file tree
Showing 21 changed files with 1,310 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
. .venv/bin/activate

export KUBE_NAMESPACE=chain-link
export KUBE_LABEL_SELECTOR=app=chain-link
export SERVICE_NAME=chain-link-0
export SERVICES_LIST=http://localhost:5001,http://localhost:5002,http://localhost:5003

alias m=make
30 changes: 30 additions & 0 deletions .github/workflows/build-and-push-image.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Build and Push Docker Image

on:
push:
branches:
- main # Replace with the default branch name of your repository

jobs:
build_and_push:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_PAT }}

- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ghcr.io/${{ github.repository_owner }}/chain-link:latest
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
__pycache__
.venv
.vscode
chain-link-cli.conf
chain-link-manifests
NOTES.md
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.11.3
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Dockerfile
FROM python:3.11-slim
COPY requirements.txt /
RUN set -ex && \
pip install -r requirements.txt
COPY app.py gunicorn-run.sh /app/
RUN useradd gunicorn -u 10001 --user-group
USER 10001
WORKDIR /app

ENTRYPOINT [ "/app/gunicorn-run.sh" ]
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# chain-link

This is a not a crypto app. :) It's really meant as a demo app that can be deployed into Kubernetes and then used by some other tooling to visualize connectivity.

Using the app and CLI tool in this repository, several pods will be deployed to Kubernetes, and when the first one of those pods is connected to on port 80, it will connect to the next "chain-link" pod, which will forward it to the next pod, and so on, forming a "service chain". That chain can then be visualized in something like the Zipkin GUI.

```
+-------------------+ +--------------------+ +--------------------+ +--------------------+
| | | | | | | |
| loadgenerator +--------> chain-link-0 +---------> chain-link-1 +--------> chain-link-(n) |
| | | | | | | |
+-------------------+ +--------------------+ +--------------------+ +--------------------+
```

The application is configured to send traces to a Zipkin instance that is deployed into the same namespace.

There is a loadgenerator pod that will continuously poll the first deployment to create traffic.

## How to Deploy

1. Check out the git repository
1. OPTIONAL: Build the image and put it into a registry that the Kubernetes nodes can pull from. Otherwise the default image will be used.
2. Deploy with the CLI provided (which might require setting up a suitable Python environment)

### OPTIONAL: Build the Image

Build the image and push it into your registry.

```
docker build -t chain-link .
docker tag chain-link <your registry>/chain-link:latest
docker push <your registry>/chain-link:latest
```

### Deploy the Kubernetes Resources

Use the provided CLI to deploy into Kubernetes. There are a few options that can be set, so use `--help` to determine the options.

```
./chain-link-cli -h
```

The number of instances is configurable:

```
./chain-link-cli --instances 5 deploy
```

## What is Deployed

* A specified number of instances of the chain-link application
* loadgenerator instance
* Zipkin instance

Below is an example of a deployment with five chain link instances.

```
$ k get pods
NAME READY STATUS RESTARTS AGE
chain-link-deployment-0-785c6fc8c9-mfx8s 1/1 Running 0 20m
chain-link-deployment-1-74b5c7bc7f-4d5zr 1/1 Running 0 19m
chain-link-deployment-2-77cdf4f74c-lsmb2 1/1 Running 0 19m
loadgenerator 1/1 Running 0 19m
zipkin-deployment-5c44dd85ff-5vdmw 1/1 Running 0 20m
```

## Zipkin

The application is configured to send traces to the Zipkin service.

### Access Zipkin

Port forward to it and access the service from your localhost in the browser on port 9411.

```
kubectl port-forward svc/zipkin-service 9411:80
```

### What it Looks Like in Zipkin

![zipkin](img/zipkin.png)

![zipkin](img/zipkin-deps.png)


169 changes: 169 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
"""
This app forwards a request to the next service in the chain
"""

import os
import logging
import json
import random
import requests
import time
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.exporter.zipkin.json import ZipkinExporter
from flask import Flask, request, jsonify, make_response


#
# OpenTelemetry and Zipkin
#

# Configure the TracerProvider and SpanExporter
# service_name is set in the cli.py script as a env var
service_name = os.environ.get("CHAIN_LINK_SERVICE_NAME", "unknown")
trace.set_tracer_provider(
TracerProvider(resource=Resource.create({"service.name": service_name}))
)

# create a ZipkinSpanExporter - this is specific to Zipkin deployed into
# Kubernetes with the cli.py script
# NOTE(curtis): this is expecting a service called zipkin-service-0 listening on
# port 80!
zipkin_exporter = ZipkinExporter(
endpoint="http://zipkin-service/api/v2/spans",
)

# Create a BatchSpanProcessor and add the exporter to it
span_processor = BatchSpanProcessor(zipkin_exporter)

# add to the tracer
trace.get_tracer_provider().add_span_processor(span_processor)

#
# Flask
#

# Instrument the Flask app and Requests library
app = Flask(__name__)
FlaskInstrumentor().instrument_app(app)
RequestsInstrumentor().instrument()

#
# Logging
#

logging.basicConfig(level=logging.INFO)
app.logger.info("service_name %s", service_name)


# the services.json will be mounted from a configmap
def get_service_urls():
"""
Get the list of services from the configmap which is mounted into the pod
"""
with open(
"/etc/chain-link.conf.d/services.json", encoding="utf-8"
) as services_file:
services_json = services_file.read()
return json.loads(services_json)


services = get_service_urls()
app.logger.info("new services: %s", services)


def is_valid_service(svc_name):
"""
Check if the service_name is in the services list
"""
return svc_name in services


#
# Routes
#


@app.route("/", methods=["GET"])
def process_request():
"""
Process the request and forward it to the next service in the chain
"""

# one of the nodes should take longer to respond, so we are going to randomly
# sleep for 2 seconds on one of the nodes (well, this isn't perfect but you
# get the idea)
random_number = random.random()
app.logger.info(f"Node's random_number: {random_number}")
if random_number < 1 / len(services):
sleep_duration = 2 # Sleep for 2 seconds
app.logger.info("This node is sleeping for %s seconds", sleep_duration)
time.sleep(sleep_duration)

current_service = request.headers.get("X-Current-Service", service_name)

# check if the current service is valid and then set the index in the chain
# to the current service
if current_service and not is_valid_service(current_service):
return make_response(jsonify({"message": "Invalid service"}), 400)
elif current_service:
app.logger.info("current_service: %s", current_service)
index = services.index(current_service)
else:
index = 0

# if the current service is not the last service in the chain, then forward
# the request to the next service in the chain
if index + 1 < len(services):
next_service = services[index + 1]
app.logger.info("next_service: %s", next_service)
headers = {"X-Current-Service": next_service}
response = requests.get(
f"http://{next_service}/forward", headers=headers, timeout=3
)
return response.text, response.status_code
else:
return (
jsonify(
{"message": f"You have reached the final chain link {current_service}"}
),
200,
)


# I just want a route named /forward :)
@app.route("/forward", methods=["GET"])
def forward_request():
"""
Forward the request to the next service in the chain
"""
return process_request()


@app.route("/readiness", methods=["GET"])
def readiness():
"""
Readiness probe
"""
return make_response(jsonify({"message": "ok"}), 200)


#
# Main
#

# if running out of gunicorn, use the gunicorn logger
# https://trstringer.com/logging-flask-gunicorn-the-manageable-way/
if __name__ != "__main__":
gunicorn_logger = logging.getLogger("gunicorn.error")
app.logger.handlers = gunicorn_logger.handlers
app.logger.setLevel(gunicorn_logger.level)

if __name__ == "__main__":
port = int(os.environ.get("FLASK_RUN_PORT", "8080"))
app.logging.info("port: %s", port)
app.run(host="0.0.0.0", port=port)
10 changes: 10 additions & 0 deletions chain-link-cli
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env python3
"""
ChainLink CLI - Deploy and validate the chain-link application in to a Kubernetes cluster
"""

from cli.cli_manager import run_cli


if __name__ == "__main__":
run_cli()
Empty file added cli/__init__.py
Empty file.
Loading

0 comments on commit 1c51456

Please sign in to comment.