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