Cloud Architecture The Good, the bad and the ugly

Robus Gauli

@robusgauli

About {me}

Name

Work

More about me @ https://robus.dev

Architecture, Infrastructure, Operations a.k.a Drawing boxes and arrows

Robus Gauli

Tech

Go, Python, Rust, C/C++, K8s, Open X ....

8. Wait postgres version need to be above 10 for issues we can't figure out yet

......

{Simplicity}

{Simplicity}

Microservices

Service broken down to multiple services

As function of concern (cart, billing, user, etc)

Single responsibility per Service

Thus simple in design

{Simplicity}

Monolith

Separate modules/packages instead of separate service

Usually single database instead of database per service

In memory lock instead of Distributed lock

Thus simple in design

{Agility} Microservice

Independent Deployment of services

Faster Development to Deployment Cycle

Faster Build and Tests

Thus Agile

{Agility} Monolith

No service to service dependencies

No versioning and contract managements

No smoke tests or chaos testing

Thus Agile

{Architecture} Level 101

Client

Server

Database

Monolith

{Architecture} Level 102

server
Database
Client
server
Database
server
Database
server
Database
Microservice | SOA

{Architecture} Level 103

server
Database
Client
server
Database
server
Database
server
Database
Client
Client
Microservice | Microfrontend | SOA

{The GOOD}

  • {Container} as unit of deployment

  • {Container} as build and test runtime

{The GOOD}

  • Configuration management
    
  • Cloud Resources Provisioning Tools
    
  • Infrastructure as a Code
    

{The BAD}

  • Configuration management
    
  • Cloud Resources Provisioning Tools
    
  • Infrastructure as a Code
    

{The UGLY}

  • Configuration management
    
  • Cloud Resources Provisioning Tools
    
  • Infrastructure as a Code
    

MONOLITH

MICROSERVICE

Scalability

Scalability

{Circle of Lambda Life}

{Quests for Architectural Decision}

Scalability

Simplicity

Agility

Reliability

Traceability

Testability

Debugability

Transport ( remote calls vs memory address hops JMP/RET)

Security

Operation

## Create a command: terraform init (via operational docker image proxy)
docker/terraform-init:
  docker run \
    --rm \
    --entrypoint=terraform \
    -v $(APP_ROOT):/app \
    -w /app \
    --env AWS_ACCESS_KEY_ID=$(AWS_ACCESS_KEY_ID) \
    --env AWS_SECRET_ACCESS_KEY=$(AWS_SECRET_ACCESS_KEY) \
    --env TF_CLI_CONFIG_FILE=$(TF_CLI_CONFIG_FILE) \
     \
     awsregistry/operational-deps-image init

2. Create build/test scripts that wraps your Operational Docker Image

Makefile

## Create a command
docker/build:
  docker run \
    --rm \
    --entrypoint=yarn \
    -v $(APP_ROOT):/app \
    -w /app \
     awsregistry/operational-deps-image build

Another example of build react app

Makefile

3. Execute the command

~/home
> make docker/terraform-init

From your terminal

docker/terraform-init:
  docker run \
    --rm \
    --entrypoint=terraform \
    -v $(APP_ROOT):/app \
    -w /app \
    --env AWS_ACCESS_KEY_ID=$(AWS_ACCESS_KEY_ID) \
    --env AWS_SECRET_ACCESS_KEY=$(AWS_SECRET_ACCESS_KEY) \
    --env TF_CLI_CONFIG_FILE=$(TF_CLI_CONFIG_FILE) \
     \
     awsregistry/operational-deps-image init

Let's approach visually

~/home
> make docker/terraform-init

Command to execute

Docker image with operational deps

upload

test

build

apply

Execution inside container

mounted source code inside container

Difference

~/home
> yarn build
~/home
> make docker/build

Before

Now

What did it buy you?

Yup that's it. No joke

Yup that's it. No joke

No dependencies to Build, Test, Lint, Upload, Deploy or anything that is required in your development lifecycle

What about CI?

Local

CI/CD

make docker/build

make docker/build

make docker/test

make docker/test

make docker/deploy

make docker/deploy

To build

To test

To deploy

make docker/dry-run

make docker/dry-run

make docker/lint

make docker/lint

make docker/format

make docker/format

To dry run

To lint

To format

master:
  - step:
      name: Run Lint and Plan
      deployment: prod-plan
      script:
        - make docker/terraform-lint
        - make docker/terraform-plan-out
        
  - step:
      name: Run Test
      deployment: prod-plan
      script:
        - make docker/test
        - make docker/integration-test
        
  - step:
      name: Apply terraform plan
      deployment: prod-apply
      trigger: manual
      script:
        - make docker/terraform-apply

Sample Bitbucket CI scripts from project

{Conclusion}

Don't expect developer to manage dependencies.

Isolating Operational Complexity is equally, if not more important than isolating Application Complexity

Your CI/CD, Staging environments & Local should share common operational and application dependencies.

TOPIC {2}

Programming language, features & patterns

{Features & Patterns}

Language features

* Map/Reduce/Filter

* Classes & Objects

* First class functions, Immutability, closure, polymorphism, inheritance, ABC, etc

* Generators, async/await, coroutines, pattern matching, etc

* Modern error handling. I call it "throw everything and handle nothing"

Programming patterns*

* Singleton pattern

* Factory pattern

* Try/Except control flow

* Multi hierarchy pattern

Step back

Let's evaluate programming from the First principle/Axioms

What does it mean to program?

INPUT

f(x)

OUTPUT

3

sqr(x)

9

"robus"

len(x)

5

Query(user)

find(x)

{name: "john"}

robus.dev

http(x)

<html >

Side effects

None

None

IO

IO

Requirements for computation

Read from memory

int x = &y

Write to memory

x = 56

Conditional branching

if (x > 45) {...}

Control flow construct

for (..) {break}

Yup that's all. Verified by Turing and Alonzo Church

Example Case study of Golang

No try/catch/exception

No classes & objects

No generators & coroutines

And yet, it empowers 100% of cloud native infrastructure (docker, kubernetes, fargate, etcd, falco, lambdas, etc)

No inheritance

No map, reduce, filters, while, do while

No abstract base class and super/sub class

No macros and templates

What did they do right?

No try/catch/exception

No classes & objects

No generators & coroutines

No inheritance

No map, reduce, filters, while, do while

No abstract base class and super/sub class

No macros and templates

Errors are just values.

Struct

X

for(...)

X

Interface

X

Example Error pattern

user := User.get(id)
mentors.add(user)
parents.add(user)

1

No error handling

try {
  user := User.get(id)
} catch (USERMISSINGEXC) {
  user := User.create({name: "mac"})
  notifiy(USER_CREATED)
} finally {
  mentors.add(user)
  parents.add(user)
}

2

Using try/catch

err, user := User.get(id)

if (err == USER.MISSING) {
  user = User.create({name: "mac"})
  notify(USER_CREATED)
}

mentors.add(user)
parents.add(user)

3

Error as a value

{Conclusion}

One way of doing things

You might not require Singleton pattern if you don't have global state in your language

Rule of thumb: Less features => Good feature

Make sure errors are first class values. Ideally, no compilation without explicit error handling

Use language that has native construct for side effects management. For example: Interface to swap out I/O dependencies in Go/Java/Typescript, Monad to wrap side effects in Haskell and JS, etc 

{Programming Language}

Compilation/Transpilation free

Dynamic Types

* Less code => Good code

* I call it "any type programming pattern"

Time to 200 OK

Things that goes wrong

No compiler in your tooling means you are the compiler

Runtime crashes like these "234" + 34

Monkey Patching, Duck Typing

* Readability  & Correctness

Dynamic types

* Performance

* Refactor

* Contracts via Types

{Conclusion}

Rely on strongly typed language as a guide during refactoring/changes.

Use language & tools that ensures program correctness using Static Types

* Value correctness [NEXT SLIDE]

* Memory and thread safety

* mypy, go race, rust analyzer, flow, typescript, elm, etc

Avoid "Monotype" language, or make it compilation target

* For example, JS is more of a compilation target than a language. [NEXT SLIDE]

* Dropbox had to introduce  type annotation  in  a language to make sense of their 4 million lines of code.

Coding = 10% new code & 90% changes in average. Look at VIM's default mode

Compiler errors

Compilation target

// Compile to JS from C
emcc -o main.js main.c

// Run via Node
node main.js
#include <stdio.h>

int sum(int* numbers, int size) {
  int result = 0;
  for (int i = 0; i < size; i++) {
    int number = numbers[i];
    result += number;
  }
  return result;
}

int main() {
  int numbers[5] = {1, 2, 3, 4, 5};

  int result = sum(numbers, 5);
  printf("The result is: %d\n", result);
}

C to JS/WASM using emscripten compiler tool chain

TOPIC {3}

Cloud & Microservices

Unicorn

Services with bounded concern

Scale

Distributed

Observability

Messaging

Gateways

Tools

AWS/Azure/GCP Services

Container orchestrations

Storage(s3/File Store, EBS/ABS)

Databases

SNS, SQS, Events Hub, Azure Service Bus

Durable & Steps

? Local development

Development Perspective

? Debugability

? Metrics, Logs & Traces

10x = 1/10x

How can we potentially fix this?

Microservice trait

? Compute

? Storage

? Functions

? Databases

? Observability

Note: Not a microservice architecture

? Cost

{CONCLUSION}

Tools & Solutions with open standards & protocols

Tools & solutions with ease of use in local settings

Tools & Solutions with managed support from cloud

Tools & Solutions with cloud agnostic property

Initial Architecture with Microservice trait ???

Thank {you}

Find me at https://robus.dev

Github handle https://github.com/robusgauli

Copy of Better engineering practices

By robus

Copy of Better engineering practices

  • 36