Agile Software Architecture

Agile Software Architecture Common Artifacts

NB : This document will always be a work in progress as we are using an agile methodology.
These sections are placed here as to avoid repitition in each project.
A typical Agile Software Architecture Document is as follows (common section's highlighted) :

  • - Context/Oveview
  • - Functional Requirements
  • - Non-Functional Requirements
  • - Constraints
  • - Principles
  • - Software Architecture
  • - Code
  • - Data (bounded context)
  • - Infrastructure Architecture
  • - Deployment
  • - Local Development

Each project will only show these 4 sections :

    - Context/Overview
  • - Functional Requirements
  • - Softwre Architecture
  • - Data (bounded context)

SMART
We look at the quality attributes specific , measurable, achievable, relevant and timely.

Criteria
The following non-functional aspects will be addressed using our smart outline

Performance
Addressed by deploying lightweight microservices in an Openshift HA multinode cluster

Scalability
Addressed by deploying lightweight microservices in an Openshift HA multinode cluster

Availability
Making use of a multimaster (at least 3 nodes in different availablity zones) with no single point of failure.

Security (authentication, authorisation, GDPR etc)
TBD

Extensibility
Easy to use SDK, lightweight design, pluggable and loosely coupled architecture.

Flexibilty
Configurable via templates (yaml,json)

Auditing
Centralized logging of all events

Monitoring and management
Built into Openshift or with prometheus (preffered)

Reliability
Openshift 24/7 follow the sun support

Failover and disaster recovery (manual or automated)
Openshift 24/7 follow the sun support

We have identified a couple of areas with regards to constraints which are :-

  • - Integration with vue.js.
  • - Continuous reliance on the Middleware/Advantage systems
  • - Updates are not direct (if relevant) use of CQRS pattern
  • - Skill profile of development team

Architectural and software principles

All porjects will be deployed on a hosted Openshift instance
Extensive use of simple microservices
Use of linux containers
Simple bounded context design (DDD)
Database per microservice (where needed)
Loosely coupled architecture
Centralized logging
All requests to include unique requestId in http header
Use of Restful api's (OpenAPI for documentation)
Secured edge (TLS) termination
Front end developed in vue.js and hosted on an nginx container tuned for high performance and caching)
The code will adhere to the 12 factor app principles.

12 Factor App

Codebase
Each deployable app is tracked as one codebase tracked in revision control. It may have many deployed instances across multiple environments.

Dependencies
An app explicitly declares and isolates dependencies via appropriate tooling (e.g., Maven, Bundler, NPM) rather than depending on implicitly realized dependencies in its deployment environment.

Config
Configuration, or anything that is likely to differ between deployment environments (e.g., development, staging, production) is injected via operating system-level environment variables.

Backing services
Backing services, such as databases or message brokers, are treated as attached resources and consumed identically across all environments.

Build, release, run
The stages of building a deployable app artifact, combining that artifact with configuration, and starting one or more processes from that artifact/configuration combination, are strictly separated.

Processes
The app executes as one or more stateless processes (e.g., master/workers) that share nothing. Any necessary state is externalized to backing services (cache, object store, etc.).

Port binding
The app is self-contained and exports any/all services via port binding (including HTTP).

Concurrency
Concurrency is usually accomplished by scaling out app processes horizontally (though processes may also multiplex work via internally managed threads if desired).

Disposability
Robustness is maximized via processes that start up quickly and shut down gracefully. These aspects allow for rapid elastic scaling, deployment of changes, and recovery from crashes.

Dev/prod parity
Continuous delivery and deployment are enabled by keeping development, staging, and production environments as similar as possible.

Logs
Rather than managing logfiles, treat logs as event streams, allowing the execution environment to collect, aggregate, index, and analyze the events via centralized services.

Admin processes
Administrative or managements tasks, such as database migrations, are executed as one-off processes in environments identical to the app’s long-running processes.

All microservice's will be written using Golang. The reason for selecting golang is due to strict typing, its a compiled language and as a result improves human error with regards to variables, types, error handling, good code practice and overall bug elimination.
Its used in the opensource project for Docker, Kubernetes and Openshift (the platform that these microservices (linux containers) will be deployed in). The compiled footprint is low, cpu and memory (ideal for microservices) usage is superior to that of older languages like Java and C# due its lightweight concurrency model and maximized usage of multicore processors.
The code base will adhere to the 12 factor app principles.

  • - All API's will be in the form of Restful API's using http, the internal services are in a VPN and hence we don't need to use https for inter process communication. Use openAPI 3.0.0 to doucment and verify
  • - Under no circumstances will state be kept in any microservice.
  • - As mentioned in the principles section a microservice is well defined, it does one thing and does it well, the microservice will have it's own database and will not share a database with any other microservice.
  • - All microservice's will be deployed with golang base linux containers.
  • - Containers should be built using UBI (universal base image form Rehat)
  • - The front end will be written in Vue.js and deployed in a nginx linux container with improved perfomance tuning and artifact caching.
  • - The code can be deployed in a mounted persistent volume in the nginx container for DEV & UAT but not for PRODUCTION.
  • - Each microservice and front end application will be hosted in it's own repo under a MyPortfolio project space.
  • - Git or Gitlab shell will be used for the base repo
  • - Quay.io image repository will be used to push all images to.
  • - TDD Test Driven Development. Code coverage should be 80% or better. Develop unit tests before coding.
  • - CICD will be used - please refer to the "Deployment" Section for further details.

Overview
As mentioned the services will be deployed as linux containers on a hosted Openshift instance.
The infrastructure for the hosted Openshift will be as follows for HA (high availability)

Openshift Overview Diagram

Overview
We address the SDLC (system development lifecycle) and how we will fully automate the process. We also discuss the test and deployment strategy for local, uat, pre-prod, performance and production deployment.

Advanced Deployment Strategies/Techniques

As this is a fairly straight forward design we won't be making use of Blue/Green or Canary deployments.
Blue/Green deployment is mainly used in production environments. Once our new production image is ready for deploy, the system router is currently pointing/set to blue, once we have successfully deployed and ensured the service is running on green, we then simply switch the router to green (This is common DevOps practice). In Openshift we make use of routes and services to implement this.

Canary deployment (from the use of canaries in mines) are also mainly used in production environments. Basically we deploy to our current blue live environment , we then allow a small amount of traffic to flow to the canary deployed microservice. Once we are happy that the service is working correctly we allow more traffic through until all traffic has been diverted to our canary deploy.

Automated Deploy (DevOps)
We will make full use of ArgoCD and Openshift (tekton) pipelines

Trunk Driven Development Diagram
The process is straightforward. The developer checks out code from the master branch creates Push Requests against master (Trunk). All changes are made to this branch. Once the developer is satisfied that the changes made has :
- been tested locally and working
- met the code linting requirements
- met the code analysis requirement (code coverage, commenting, critical and major bugs/issues, code smell and vulnerability/security issues)
- Peer reviewed

The reasons for using Trunk Driven Development (as stated in the book "ACCELERATE - Gene Kim et al.") is as follows

Small batches (with statistical data proving this is a better process)
High Performance teams keep branches short lived - with frequent integrations into master
Each change triggers a build process that includes running unit tests
If any step fails - the developers fix it immediately
It avoids merge hell (with long lived feature branches)

Trunk Driven Develpoment Diagram



CICD Pipeline Diagram



Overview (local development)

Docker is a pre-requisite, please refer to this link to setup docker and a openshift cluster (single node) for local dev.
The openshift command line will be used extensively in the dev process.
Create a docker base image for the language (in my case golang) to work with, below is an example of the Dockerfile I created.

NB For Dev I make use of a Persistent Volume, this allows me to mount my golang binary in the dir /go to the PV
This is really neat as it allows me then to push a new golang binary to the current container pod without having to rebuild and push (then pull) the image every time I make a code change.

- Improves dev time
- No build
- No push
- No deploy (pull) in Openshift
- I can verify my binary is working on my local dev machine by just executing it !!!
- It's important to change the imagePullPolicy to IfNotPresent after the first deploy (this prevents Openshift from pulling the image on each change)
- Use the oc cli to rsync the files
- Strip the golang binary (gives you about 25% compact) i.e from 8M to about 2M
- You can also use -ldflags

The basic flow is as follows :
- build linux container
- push container to repo
- use oc cli to login into Openshift cluster
- create project (namespace)
- deploy application (using template)
- once deployed change the image pull policy
- use the oc rsync cli to push new binaries to the pod/container

The time saved using this process is imense. It was taking about 1min - 2min to build and push a container and another minute or so to pull the new image in Openshift
This process takes about 20 to 30 sec in total (smile)
The first time around the volume (claim) will be empty - use the debug mode (Openshift tab) to start the pod and use "oc rsync" to push the binary

Create a dockerfile


FROM registry.access.redhat.com/ubi8/ubi-init:latest

LABEL maintainer="lzuccarelli@tfd.ie"

ENV GOLANG_VERSION 1.13.1
ENV GOLANG_DOWNLOAD_URL https://golang.org/dl/go$GOLANG_VERSION.linux-amd64.tar.gz
ENV GOLANG_DOWNLOAD_SHA256 94f874037b82ea5353f4061e543681a0e79657f787437974214629af8407d124

# gcc for cgo
RUN dnf clean all
RUN rm -rf /var/cache/dnf
RUN dnf install -y git gcc make && rm -rf /var/lib/apt/lists/*

RUN curl -fsSL "$GOLANG_DOWNLOAD_URL" -o golang.tar.gz \
&& echo "$GOLANG_DOWNLOAD_SHA256 golang.tar.gz" | sha256sum -c - \
&& tar -C /usr/local -xzf golang.tar.gz \
&& rm golang.tar.gz

ENV GOPATH /go
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
COPY microservice uid_entrypoint.sh /go/
COPY swaggerui/ /go/swaggerui/

RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 0755 "$GOPATH"
WORKDIR $GOPATH

USER 1001

ENTRYPOINT [ "./uid_entrypoint.sh" ]

# This will change depending on each microservice entry point
CMD ["./microservice"]
                                


Build a linux container

Build the docker file

# change the name push image
$ podman push  tfld-docker-dev-local.repo.14west.io/:1.13.1

# push the image
$ podman build -t tfld-docker-dev-local.repo.14west.io/:1.13.1 .
                                


Create a template

{
  "kind": "Template",
  "apiVersion": "v1",
  "metadata": {
    "name": "",
    "annotations": {
      "description": "Threefold Systems POC",
      "tags": "tfs,poc,golang",
      "iconClass": "icon-golang"
    }
  },
  "parameters": [
    {
      "name": "GOLANG_IMAGE",
      "value": "tfld-docker-dev-local.repo.14west.io/",
      "description": "The name of the golang image",
      "required": true
    },
    {
      "name": "GOLANG_IMAGE_VERSION",
      "value": "1.13.1",
      "description": "The version for the golang image",
      "required": true
    },
    {
      "name": "LOG_LEVEL",
      "value": "info",
      "description": "A default log level for all components",
      "required": true
    },
    {
      "name": "NAME",
      "value": "",
      "description": "A default name",
      "required": true
    },
    {
      "name": "VERSION",
      "value": "1.13.1",
      "description": "A default version",
      "required": true
    },
    {
      "name": "PORT",
      "value": "9000",
      "description": "A default server port",
      "required": true
    }
  ],
  "objects": [
    {
      "apiVersion": "v1",
      "kind": "PersistentVolumeClaim",
      "metadata": {
        "name": ""
      },
      "spec": {
        "accessModes": [ "ReadWriteOnce" ],
        "resources": {
          "requests": {
             "storage": "1G"
          }
        }
      }
    },
    {
      "kind": "Service",
      "apiVersion": "v1",
      "metadata": {
        "name": "",
        "labels": {
          "name": ""
        }
      },
      "spec": {
        "selector": {
          "name": ""
        },
        "ports": [
          {
            "name": "-port",
            "port": 9000,
            "targetPort": 9000,
            "protocol" : "TCP"
          }
        ]
      }
    },
    {
      "kind": "DeploymentConfig",
      "apiVersion": "v1",
      "metadata": {
        "name": "",
        "labels": {
          "name": ""
        }
      },
      "spec": {
        "triggers": [
          {
            "type": "ConfigChange"
          }
        ],
        "replicas": 1,
        "selector": {
          "name": ""
        },
        "template": {
          "metadata": {
            "labels": {
              "name": ""
            }
          },
          "spec": {
            "containers": [
              {
                "name": "",
                "image": "${GOLANG_IMAGE}:${GOLANG_IMAGE_VERSION}",
                "ports": [
                  {
                    "containerPort": 9000
                  }
                ],
                "livenessProbe": {
                  "httpGet": {
                    "path": "/api/v2/sys/info/isalive",
                    "port": 9000
                  },
                  "initialDelaySeconds": 55,
                  "timeoutSeconds": 5,
                  "periodSeconds": 60,
                  "successThreshold": 1,
                  "failureThreshold": 2
                },
                "readinessProbe": {
                  "httpGet": {
                    "path": "/api/v2/sys/info/isalive",
                    "port": 9000
                  },
                  "initialDelaySeconds": 0,
                  "timeoutSeconds": 5,
                  "periodSeconds": 10,
                  "successThreshold": 1,
                  "failureThreshold": 1
                },
                "env": [
                  {
                    "name": "LOG_LEVEL",
                    "value": "${LOG_LEVEL}"
                  },
                  {
                    "name": "NAME",
                    "value": "${NAME}"
                  },
                  {
                    "name": "VERSION",
                    "value": "${VERSION}"
                  },
                  {
                    "name": "PORT",
                    "value": "${PORT}"
                  }
                ],
                "volumeMounts": [
                  {
                    "name": "gomount",
                    "mountPath": "/go"
                  }
                ],
                "imagePullPolicy": "Always",
                "resources": {
                  "limits": {
                    "cpu": "250m",
                    "memory": "100Mi"
                  },
                  "requests": {
                    "cpu": "10m",
                    "memory": "20Mi"
                  }
                }
              }
            ],
            "volumes": [
              {
                "name": "gomount",
                "persistentVolumeClaim": {
                  "claimName": ""
                }
              }
            ]
          }
        }
      }
    }
  ]
}
                                


Deploy and sync

# Once created use the oc cli to deploy
$ oc login 
$ oc new-project my-dev

# deploy the app
$ oc new-app  - f  xyz.json
$ oc get pods


# This is an example

# build and strip binary
$ GOOS=linux go build -ldflags="-s -w" -o bin/microservice

# OR
$ go build -o bin/microservice
$ strip bin/microservice

# NOTE
# You can also use upx to compact the binary even further
# This did cause segment faults in the UBI rhel 8 image
# I skipped this step
$ upx -9 bin/microservice

oc rsync bin/ simple-jsonservice-3-7gcpm-debug:/go
WARNING: cannot use rsync: rsync not available in container
microservice
project.json
uid_entrypoint.sh

# close the dialog and execute - this will restart the pod with your latest binary
# N.B !!! remember to change your imagePullPolicy -> IfNotPresent (i.e pull only if the image is not present)
oc rollout latest "your-deploy-config"
                                

About
This is a collection of data for a quick lookup / reference
 
Its not an exhaustive reference (nor will it be), but as stated before its a quick lookup / reference
LMZ 2020