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
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
- 71