Merge pull request #378 from Waterdrips/ah-day84

This commit is contained in:
Michael Cade 2023-03-25 07:50:09 +00:00 committed by GitHub
commit 87126c8754
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 570 additions and 1 deletions

View File

@ -156,7 +156,7 @@ Or contact us via Twitter, my handle is [@MichaelCade1](https://twitter.com/Mich
### Engineering for Day 2 Ops
- [] 👷🏻‍♀️ 84 > [](2023/day84.md)
- [] 👷🏻‍♀️ 84 > [Writing an API - What is an API?](2023/day84.md)
- [] 👷🏻‍♀️ 85 > [](2023/day85.md)
- [] 👷🏻‍♀️ 86 > [](2023/day86.md)
- [] 👷🏻‍♀️ 87 > [](2023/day87.md)

View File

@ -0,0 +1,44 @@
# Getting started
This repo expects you to have a working kubernetes cluster already setup and
available with kubectl
We expect you already have a kubernetes cluster setup and available with kubectl and helm.
I like using (Civo)[https://www.civo.com/] for this as it is easy to setup and run clusters
The code is available in this folder to build/push your own images if you wish - there are no instructions for this.
## Start the Database
```shell
kubectl apply -f database/mysql.yaml
```
## deploy the day1 - sync
```shell
kubectl apply -f synchronous/k8s.yaml
```
Check your logs
```shell
kubectl logs deploy/generator
kubectl logs deploy/requestor
```
## deploy nats
helm repo add nats https://nats-io.github.io/k8s/helm/charts/
helm install my-nats nats/nats
## deploy day 2 - async
```shell
kubectl apply -f async/k8s.yaml
```
Check your logs
```shell
kubectl logs deploy/generator
kubectl logs deploy/requestor
```

View File

@ -0,0 +1,77 @@
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
ports:
- port: 3306
selector:
app: mysql
clusterIP: None
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
spec:
selector:
matchLabels:
app: mysql
strategy:
type: Recreate
template:
metadata:
labels:
app: mysql
spec:
containers:
- image: mysql:5.6
name: mysql
env:
# Use secret in real usage
- name: MYSQL_ROOT_PASSWORD
value: password
- name: MYSQL_DATABASE
value: example
- name: MYSQL_USER
value: example
- name: MYSQL_PASSWORD
value: password
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pv-claim
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: mysql-pv-volume
labels:
type: local
spec:
storageClassName: manual
capacity:
storage: 20Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/mnt/data"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pv-claim
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
# https://kubernetes.io/docs/tasks/run-application/run-single-instance-stateful-application/

View File

@ -0,0 +1,17 @@
# Set the base image to use
FROM golang:1.17-alpine
# Set the working directory inside the container
WORKDIR /app
# Copy the source code into the container
COPY . .
# Build the Go application
RUN go build -o main .
# Expose the port that the application will run on
EXPOSE 8080
# Define the command that will run when the container starts
CMD ["/app/main"]

View File

@ -0,0 +1,5 @@
module main
go 1.20
require github.com/go-sql-driver/mysql v1.7.0

View File

@ -0,0 +1,2 @@
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=

View File

@ -0,0 +1,139 @@
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"math/rand"
"net/http"
"time"
)
func generateAndStoreString() (string, error) {
// Connect to the database
db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/mysql")
if err != nil {
return "", err
}
defer db.Close()
// Generate a random string
// Define a string of characters to use
characters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
// Generate a random string of length 10
randomString := make([]byte, 64)
for i := range randomString {
randomString[i] = characters[rand.Intn(len(characters))]
}
// Insert the random number into the database
_, err = db.Exec("INSERT INTO generator(random_string) VALUES(?)", string(randomString))
if err != nil {
return "", err
}
fmt.Printf("Random string %s has been inserted into the database\n", string(randomString))
return string(randomString), nil
}
func main() {
// Create a new HTTP server
server := &http.Server{
Addr: ":8080",
}
err := createGeneratordb()
if err != nil {
panic(err.Error())
}
ticker := time.NewTicker(60 * time.Second)
quit := make(chan struct{})
go func() {
for {
select {
case <-ticker.C:
checkStringReceived()
case <-quit:
ticker.Stop()
return
}
}
}()
// Handle requests to /generate
http.HandleFunc("/new", func(w http.ResponseWriter, r *http.Request) {
// Generate a random number
randomString, err := generateAndStoreString()
if err != nil {
http.Error(w, "unable to generate and save random string", http.StatusInternalServerError)
return
}
print(fmt.Sprintf("random string: %s", randomString))
w.Write([]byte(randomString))
})
// Start the server
fmt.Println("Listening on port 8080")
err = server.ListenAndServe()
if err != nil {
panic(err.Error())
}
}
func createGeneratordb() error {
db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/mysql")
if err != nil {
return err
}
defer db.Close()
// try to create a table for us
_, err = db.Exec("CREATE TABLE IF NOT EXISTS generator(random_string VARCHAR(100), seen BOOLEAN)")
return err
}
func checkStringReceived() {
// get a list of strings from database that dont have the "seen" bool set top true
// loop over them and make a call to the requestor's 'check' endpoint and if we get a 200 then set seen to true
// Connect to the database
db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/mysql")
if err != nil {
print(err)
}
defer db.Close()
// Insert the random number into the database
results, err := db.Query("SELECT random_string FROM generator WHERE seen IS NOT true")
if err != nil {
print(err)
}
// loop over results
for results.Next() {
var randomString string
err = results.Scan(&randomString)
if err != nil {
print(err)
}
// make a call to the requestor's 'check' endpoint
// if we get a 200 then set seen to true
r, err := http.Get("http://requestor-service:8080/check/" + randomString)
if err != nil {
print(err)
}
if r.StatusCode == 200 {
_, err = db.Exec("UPDATE generator SET seen = true WHERE random_string = ?", randomString)
if err != nil {
print(err)
}
} else {
fmt.Println(fmt.Sprintf("Random string has not been received by the requestor: %s", randomString))
}
}
}

View File

@ -0,0 +1,69 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: requestor
spec:
replicas: 1
selector:
matchLabels:
app: requestor
template:
metadata:
labels:
app: requestor
spec:
containers:
- name: requestor
image: heyal/requestor:sync
imagePullPolicy: Always
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: requestor-service
spec:
selector:
app: requestor
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 8080
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: generator
spec:
replicas: 1
selector:
matchLabels:
app: generator
template:
metadata:
labels:
app: generator
spec:
containers:
- name: generator
image: heyal/generator:sync
imagePullPolicy: Always
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: generator-service
spec:
selector:
app: generator
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 8080
type: ClusterIP

View File

@ -0,0 +1,17 @@
# Set the base image to use
FROM golang:1.17-alpine
# Set the working directory inside the container
WORKDIR /app
# Copy the source code into the container
COPY . .
# Build the Go application
RUN go build -o main .
# Expose the port that the application will run on
EXPOSE 8080
# Define the command that will run when the container starts
CMD ["/app/main"]

View File

@ -0,0 +1,5 @@
module main
go 1.20
require github.com/go-sql-driver/mysql v1.7.0

View File

@ -0,0 +1,2 @@
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=

View File

@ -0,0 +1,134 @@
package main
import (
"database/sql"
"errors"
"fmt"
_ "github.com/go-sql-driver/mysql"
"io"
"net/http"
"strings"
"time"
)
func storeString(input string) error {
// Connect to the database
db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/mysql")
defer db.Close()
// Insert the random number into the database
_, err = db.Exec("INSERT INTO requestor(random_string) VALUES(?)", input)
if err != nil {
return err
}
fmt.Printf("Random string %s has been inserted into the database\n", input)
return nil
}
func getStringFromDB(input string) error {
// see if the string exists in the db, if so return nil
// if not, return an error
db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/mysql")
defer db.Close()
result, err := db.Query("SELECT * FROM requestor WHERE random_string = ?", input)
if err != nil {
return err
}
for result.Next() {
var randomString string
err = result.Scan(&randomString)
if err != nil {
return err
}
if randomString == input {
return nil
}
}
return errors.New("string not found")
}
func getStringFromGenerator() {
// make a request to the generator
// save sthe string to db
r, err := http.Get("http://generator-service:8080/new")
if err != nil {
fmt.Println(err)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(fmt.Sprintf("body from generator: %s", string(body)))
storeString(string(body))
}
func main() {
// setup a goroutine loop calling the generator every minute, saving the result in the DB
ticker := time.NewTicker(60 * time.Second)
quit := make(chan struct{})
go func() {
for {
select {
case <-ticker.C:
getStringFromGenerator()
case <-quit:
ticker.Stop()
return
}
}
}()
// Create a new HTTP server
server := &http.Server{
Addr: ":8080",
}
err := createRequestordb()
if err != nil {
panic(err.Error())
}
// Handle requests to /generate
http.HandleFunc("/check/", func(w http.ResponseWriter, r *http.Request) {
// get the value after check from the url
id := strings.TrimPrefix(r.URL.Path, "/check/")
// check if it exists in the db
err := getStringFromDB(id)
if err != nil {
http.Error(w, "string not found", http.StatusNotFound)
return
}
fmt.Fprintf(w, "string found", http.StatusOK)
})
// Start the server
fmt.Println("Listening on port 8080")
err = server.ListenAndServe()
if err != nil {
panic(err.Error())
}
}
func createRequestordb() error {
db, err := sql.Open("mysql", "root:password@tcp(mysql:3306)/mysql")
if err != nil {
return err
}
defer db.Close()
// try to create a table for us
_, err = db.Exec("CREATE TABLE IF NOT EXISTS requestor(random_string VARCHAR(100))")
return err
}

View File

@ -0,0 +1,58 @@
# Writing an API - What is an API?
The acronym API stands for “application programming interface”. What does this really mean though? Its a way of
controlling an application programmatically. So when you use a website that displays some data to you (like Twitter)
there will be an action taken by the interface to get data or send data to the application (the twitter backend in this
example) - this is done programmatically in the background by code running in the user interface.
In the example given above we looked at an example of a public API, however the vast majority of APIs are private, one
request to the public twitter API will likely cause a cascade of interactions between programs in the backend. These
could be to save the tweet text into a datastore, to update the number of likes or views a tweet has or to take an image
that has been uploaded and resize it for a better viewing experience.
We build programs with APIs that other people can call so that we can expose program logic to other developers, teams
and our customers or suppliers. They are a predefined way of sharing information. For example, we can define an API
using [openapi specification](https://swagger.io/resources/open-api/) which is used
for [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) API design. This api specification forms a
contract that we can fulfil. For example, If you make an API request to me and pass a specific set of content, such as a
date range, I will respond with a specific set of data. Therefore you can reliably expect to receive data of a certain
type when calling my API.
We are going to build up an example set of applications that communicate using an API for this section of the learning
journey to illustrate the topics and give you a hands-on look at how things can be done.
Design:
2 programs that communicate bi-directionally, every minute or so one application will request a random string from the
other, once one is received it will store this number in a database for future use
The Random Number generator will generate a random string when requested and save this into a database, the application
will then ask the first program for confirmation that it received the string, and store this information against the
string in the database
The applications will be called:
generator
requestor
This may sound like a silly example but it allows us to quickly look into the various tasks involved with building,
deploying, monitoring and owning a service that runs in production. There are bi-directional failure modes as each
application needs something from the other to complete successfully and things we can monitor such as API call rates -
We can see if one application stops running.
We need to now decide what our API Interfaces should look like. We have 2 API calls that will be used to communicate
here. Firstly, the `requestor` will call the `generator` and ask for a string. This is likely going to be an API call
without any additional content other than making a request for a string. Secondly, the `generator` will start to ask
the `requestor` for confirmation that it received and stored the string, in this case we need to pass a parameter to
the `requestor` which will be the string we are interested in knowing about.
The `generator` will use a URL path of `/new` to serve up random strings
The `requestor` is going to use URL paths to receive string information from the `generator` to check the status, so we
will setup a URL of `/strings/<STRING>` where <STRING> is the string of interest.
## Building the API
There is a folder on the Github repository under the 2023 section called `day2-ops-code` and we will be using this
folder to store our code for this, and future, section of the learning journey.
We are using Golang's built in HTTP server to serve up our endpoints and asynchronous goroutines to handle the
checks. Every 60 seconds we will look into the generators database and get all the strings which we dont have
conformation for and then calling the requesters endpoint to check if the string is there.