Agile Software Architecture
- Projects
- Agile Software Architecture
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
- - 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"