The 12 Factor App
Deploying is my business... And business is Good!
# BIO
ROMAN SACHENKO
- BEER
- MUSIC INSTRUMENTS
- bass
- drums
- CAMPING
- WEIRD JOKES
- HOT SAUCES
- SOFTWARE ENGINEERING
# BRIEF HISTORY
- Reduce on-boarding time
- Follow DevOps standards
- Increase portability
- Reduce communication needs
- Adapt for CI/CD usage
- Improve scalability
# WHY
DISCO - Deploy Infrastructure Safely, Consistently, and Optimally
# Coverage
DEVELOPMENT
CONFIGURATION
SCALING
ADMINISTRATION
DEPLOYMENT
ERROR HANDLING
DEPENDENCIES MANAGEMENT
THE 12 FACTOR APP
# (I) CODEBASE
"One codebase tracked in revision control, many deploys"
# (I) CODEBASE
- Same codebase for everyone and everywhere
- Shared code in the same repository
- Packed and exported shared code
- NPM
- Artifactory
# (II) DEPENDENCIES
"Explicitly declare and isolate dependencies"
// .nvmrc
v18.12.0
// dockerfile
FROM node:18-alpine
- in .nvmrc
- in dockerfile
- always commit package-lock.json/yarn.lock
# (II) DEPENDENCIES
Fix dependencies versions
# (II) DEPENDENCIES
- Explicit dependencies declaration
- Dockerfile and Package Manager - FRIENDS
- Fix dependencies versions
- No global dependencies
- Check dependencies during service startup
- Clean up
- npm prune --production
# (III) CONFIG
"Store config in the environment"
# (III) CONFIG
- Do not hardcode config variables
- Use environment variables everywhere
- process.env (NodeJS)
- use config service for access
- .env for local development is not a shame
- Validate config variables during the service startup
# (III) CONFIG
const config = () => ({
dbConnectionString: process.env.DB_CONNECTION_STRING,
customConf: {
auth: process.env.CUSTOM_AUTH,
options: process.env.CUSTOM_OPTIONS,
}
})
const validate = (config) => {
const errors = validateConfig(config)
if (errors?.length) {
throw new Error('Invalid config error')
}
}
# (IV) BACKING SERVICES
"Treat backing services as attached resources"
# (IV) BACKING SERVICES
- Databases, cache, queue (etc.) providers are resources
- Local, development, staging, production parity
- Use URL-based connection strings
- Easy to attach and detach
[DEV]
[STAGING]
[PROD]
URL
URL
URL
# (V) BUILD, RELEASE, RUN
"Strictly separate build and run stages"
# (V) BUILD, RELEASE, RUN
- Define build, release and run steps - split them
- Tag artifacts and releases
- Apply "Config" factor
# (VI) PROCESSES
"Execute the app as one or more stateless processes"
# (VI) PROCESSES
LOAD BALANCER
PROXY
# (VI) PROCESSES
- Race conditions
- Inaccurate calculations
- Security issues
- Reliability issues
Oops...
# (VI) PROCESSES
- Stateless service, stateless process
- Share nothing
- Do not rely on in-memory, disk cached data
- Rely on "Backing services"
- Keep in mind horizontal scaling
# (VII) PORT BINDING
"Export services via port binding"
# (VII) PORT BINDING
- Do not hardcode ports
- Runtime ports injection
- "Config" factor
- Dynamic port biding
app.listen(process.env.PORT, () => {
log(`What's up?`)
})
FROM nginx
EXPOSE 80 443
# (VIII) CONCURRENCY
"Scale out via the process model"
# (VIII) CONCURRENCY
- Keep in mind horizontal scaling
- Share nothing
- Containerize
- "Backing services" factor
- Unix process model
- Use process manager
# (IX) DISPOSABILITY
"Maximize robustness with fast startup and graceful shutdown"
# (IX) DISPOSABILITY
- Start up fast
- Shutdown gracefully
- Aim to catch what is uncaugh
# (IX) DISPOSABILITY
/**
* finish operations
* release resources
* log
* re-route requests/handlers if needed
*/
const stopHandler = async () => {
logger.info('Shutting down server...')
setShutDownStatus() // respond with 503
await releaseResources()
await closeServer()
}
['SIGTERM', 'SIGINT', 'SIGHUP']
.forEach((signal) => process.on(signal, stopHandler))
# (IX) DISPOSABILITY
process.on('uncaughtException', async (err) => {
logger.error('Uncaught Exception', err)
await releaseResources()
process.exit(1)
})
process.on('unhandledRejection', async (err) => {
logger.error('Unhandled Rejection', err)
await releaseResources()
process.exit(1)
})
process.on('exit', (code) => {
logger.info(`Process exited with code ${code}`)
process.exit(code)
})
# (X) DEV/PROD PARITY
"Keep development, staging, and production as similar as possible"
# (X) DEV/PROD PARITY
- Same code
- Same dependencies
- Same backing services
- Same configurations method
- Minimize the gaps
Aim to
# (XI) LOGS
"Treat logs as event streams"
# (XI) LOGS
- Write logs to STDOUT / STDERR
- Use structured output (JSON)
- Aggregate logs outside of your app
What
# (XI) LOGS
> STDOUT
LOGS AGENT
> STDOUT
# (XII) ADMIN PROCESSES
"Run admin/management tasks as one-off processes"
# (XII) ADMIN PROCESSES
- Define app logic and admin tasks - split
- same repository, different folders
- db migrations, seeding, hot changes
- manual re-do operations
- Don't run admin tasks from the app startup script
- Re-use your code
- Learn built-in instruments and libraries
What
# SUMMARY
DISCO - Deploy Infrastructure Safely, Consistently, and Optimally
The 12 Factor App
Slides
Get in touch
roman.sachenko@volvocars.com
roman.sachenko
Questions?
The 12 Factor App
By Roman Sachenko
The 12 Factor App
- 241