How to Write Deployment-friendly Applications

How to Write Deployment-friendly Applications

The DevOps movement gave us many ways to put Python applications into production. But how can you practically structure and configure your applications to make them indifferent to the environment they run in? How do secrets fit into the picture? And where do you put that log file?

174e7b0ff60963f821d0b9a4f1a3ef52?s=128

Hynek Schlawack

May 12, 2018
Tweet

Transcript

  1. How to Write Deployment-friendly Applications Hynek Schlawack

  2. None
  3. None
  4. views.py from pyramid.response import Response from pyramid.view import view_config @view_config(route_name="hello")

    def hello_world(request): return Response("Hello World!")
  5. from pyramid.config import Configurator from .views import hello_world def make_app():

    config = Configurator() config.add_route("hello", "/hello") config.scan() return config.make_wsgi_app() app_maker.py
  6. from pyramid.config import Configurator from .views import hello_world def make_app():

    config = Configurator() config.add_route("hello", "/hello") config.scan() return config.make_wsgi_app() app_maker.py
  7. $ gunicorn \ --access-logfile - \ "sample.app_maker:make_app()"

  8. $ gunicorn \ --access-logfile - \ "sample.app_maker:make_app()" [2018-04-05 16:28:06 +0200]

    [54343] [INFO] Starting gunicorn 19.7.1 [2018-04-05 16:28:06 +0200] [54343] [INFO] Listening at: http://127.0.0.1:8000 (54343) [2018-04-05 16:28:06 +0200] [54343] [INFO] Using worker: sync [2018-04-05 16:28:06 +0200] [54346] [INFO] Booting worker with pid: 54346
  9. $ gunicorn \ --access-logfile - \ "sample.app_maker:make_app()" $ curl http://127.0.0.1:8000/hello

    Hello World! [2018-04-05 16:28:06 +0200] [54343] [INFO] Starting gunicorn 19.7.1 [2018-04-05 16:28:06 +0200] [54343] [INFO] Listening at: http://127.0.0.1:8000 (54343) [2018-04-05 16:28:06 +0200] [54343] [INFO] Using worker: sync [2018-04-05 16:28:06 +0200] [54346] [INFO] Booting worker with pid: 54346
  10. $ gunicorn \ --access-logfile - \ "sample.app_maker:make_app()" $ curl http://127.0.0.1:8000/hello

    Hello World! [2018-04-05 16:28:06 +0200] [54343] [INFO] Starting gunicorn 19.7.1 [2018-04-05 16:28:06 +0200] [54343] [INFO] Listening at: http://127.0.0.1:8000 (54343) [2018-04-05 16:28:06 +0200] [54343] [INFO] Using worker: sync [2018-04-05 16:28:06 +0200] [54346] [INFO] Booting worker with pid: 54346 127.0.0.1 - - [05/Apr/2018:16:28:08 +0200] "GET /hello HTTP/1.1" 200 12 "-" "curl/7.54.0"
  11. Zoom Out

  12. None
  13. #!/bin/bash exec 2>&1 \ gunicorn \ --access-logfile - \ "sample.app_maker:make_app()"

    run-app.sh
  14. #!/bin/bash exec 2>&1 \ gunicorn \ --access-logfile - \ "sample.app_maker:make_app()"

    run-app.sh
  15. #!/bin/bash exec 2>&1 \ gunicorn \ --access-logfile - \ "sample.app_maker:make_app()"

    run-app.sh
  16. ExecStart=/app/run-app.sh systemd

  17. ENTRYPOINT ["/app/run-app.sh"] Dockerfile

  18. web: ./run-app.sh Procfile

  19. App

  20. ./run-app.sh App

  21. 127.0.0.1:8000 Exposes ./run-app.sh App

  22. 127.0.0.1:8000 Exposes ./run-app.sh Logs stdout App

  23. Configuration

  24. What Varies?

  25. What Varies? Very Little

  26. [server] host=127.0.0.1 port=8000 log_level=INFO log_format=human

  27. Environment Variables

  28. Environment=LOG_FORMAT=human Environment=LOG_LEVEL=INFO ExecStart=/app/run-app.sh systemd

  29. ENV LOG_FORMAT=human ENV LOG_LEVEL=INFO ENTRYPOINT ["/app/run-app.sh"] Dockerfile

  30. None
  31. direnv

  32. envconsul direnv etcdenv

  33. envconsul direnv etcdenv os.environ

  34. envconsul direnv etcdenv envsubst os.environ

  35. envconsul direnv etcdenv envsubst consul-template confd os.environ

  36. $ env HOST=0.0.0.0 PORT=8888 LOG_LEVEL=INFO \ ./run-app.sh [2018-04-09 15:59:29 +0200]

    [35323] [INFO] Starting gunicorn 19.7.1 [2018-04-09 15:59:29 +0200] [35323] [INFO] Listening at: http://0.0.0.0:8888
  37. $ env HOST=0.0.0.0 PORT=8888 LOG_LEVEL=INFO \ ./run-app.sh [2018-04-09 15:59:29 +0200]

    [35323] [INFO] Starting gunicorn 19.7.1 [2018-04-09 15:59:29 +0200] [35323] [INFO] Listening at: http://0.0.0.0:8888
  38. $ env HOST=0.0.0.0 PORT=8888 LOG_LEVEL=INFO \ ./run-app.sh [2018-04-09 15:59:29 +0200]

    [35323] [INFO] Starting gunicorn 19.7.1 [2018-04-09 15:59:29 +0200] [35323] [INFO] Listening at: http://0.0.0.0:8888
  39. $ env HOST=0.0.0.0 PORT=8888 LOG_LEVEL=INFO \ ./run-app.sh [2018-04-09 15:59:29 +0200]

    [35323] [INFO] Starting gunicorn 19.7.1 [2018-04-09 15:59:29 +0200] [35323] [INFO] Listening at: http://0.0.0.0:8888 logging.basicConfig( level=getattr( logging, os.environ["LOG_LEVEL"], ), format="%(message)s", stream=sys.stdout, )
  40. import environ @environ.config(prefix="APP") class AppConfig: @environ.config class Log: level =

    environ.var() format = environ.var() log = environ.group(Log) config.py
  41. APP_LOG_LEVEL → app_cfg.log.level import environ @environ.config(prefix="APP") class AppConfig: @environ.config class

    Log: level = environ.var() format = environ.var() log = environ.group(Log) config.py
  42. import environ from .config import AppConfig from .app_maker import make_app

    app_cfg = environ.to_config(AppConfig) application = make_app(app_cfg) wsgi.py
  43. import environ from .config import AppConfig from .app_maker import make_app

    app_cfg = environ.to_config(AppConfig) application = make_app(app_cfg) wsgi.py
  44. #!/bin/bash exec 2>&1 \ gunicorn \ --access-logfile - \ "sample.app"

    run-app.sh
  45. None
  46. Don’t Put Sensitive Data Into Env Variables

  47. None
  48. HashiCorp Vault AWS Secrets Manager Google Cloud KMS Microsoft Azure

    Key Vault Docker Secrets
  49. HashiCorp Vault AWS Secrets Manager Google Cloud KMS Microsoft Azure

    Key Vault Docker Secrets
  50. Open Space: Secrets Room 10 @ 5pm

  51. secrets.py

  52. class VaultSecrets: # ... def get_db_url(self): return self.vault.read( f"secret/{self.env}/app-name" )["data"]["db_url"])

    secrets.py
  53. class FakeSecrets: def get_db_url(self): return ( "postgresql://user@localhost/db" ) secrets.py

  54. App

  55. env HOST=0.0.0.0 \ PORT=8000 \ LOG_LEVEL=INFO \ ./run-app.sh App

  56. env HOST=0.0.0.0 \ PORT=8000 \ LOG_LEVEL=INFO \ ./run-app.sh Secrets App

  57. 0.0.0.0:8000 Exposes env HOST=0.0.0.0 \ PORT=8000 \ LOG_LEVEL=INFO \ ./run-app.sh

    Secrets App
  58. 0.0.0.0:8000 Exposes env HOST=0.0.0.0 \ PORT=8000 \ LOG_LEVEL=INFO \ ./run-app.sh

    Secrets stdout @ INFO Logs @ INFO App
  59. 127.0.0.1:8000 Exposes env HOST=127.0.0.1 \ PORT=8000 \ ./run-app.sh App

  60. 127.0.0.1:8001 Exposes env HOST=127.0.0.1 \ PORT=8001 \ ./run-app.sh App 127.0.0.1:8000

    Exposes env HOST=127.0.0.1 \ PORT=8000 \ ./run-app.sh App
  61. 127.0.0.1:8001 Exposes env HOST=127.0.0.1 \ PORT=8001 \ ./run-app.sh App 127.0.0.1:8000

    Exposes env HOST=127.0.0.1 \ PORT=8000 \ ./run-app.sh App $PUBLIC_IP Load Balancer
  62. 127.0.0.1:8001 Exposes env HOST=127.0.0.1 \ PORT=8001 \ ./run-app.sh App 127.0.0.1:8000

    Exposes env HOST=127.0.0.1 \ PORT=8000 \ ./run-app.sh App $PUBLIC_IP Load Balancer Offline
  63. SIGTERM

  64. 127.0.0.1:8001 Exposes env HOST=127.0.0.1 \ PORT=8001 \ ./run-app.sh App 127.0.0.1:8000

    Exposes env HOST=127.0.0.1 \ PORT=8000 \ ./run-app.sh App $PUBLIC_IP Load Balancer
  65. 127.0.0.1:8001 Exposes env HOST=127.0.0.1 \ PORT=8001 \ ./run-app.sh App 127.0.0.1:8000

    Exposes env HOST=127.0.0.1 \ PORT=8000 \ ./run-app.sh App $PUBLIC_IP Load Balancer
  66. 127.0.0.1:8001 Exposes env HOST=127.0.0.1 \ PORT=8001 \ ./run-app.sh App $PUBLIC_IP

    Load Balancer
  67. None
  68. Readiness

  69. Readiness •/healthz

  70. Readiness •/healthz •/__heartbeat__

  71. Readiness •/healthz •/__heartbeat__ •/-/ready

  72. Readiness •/healthz •/__heartbeat__ •/-/ready •/-/readiness

  73. HAProxy http-request deny if { path_beg /-/ }

  74. Liveness

  75. Liveness •/-/healthy

  76. Liveness •/-/healthy •/-/liveness

  77. Liveness •/-/healthy •/-/liveness •__lbheartbeat__

  78. @view_config( route_name="ready", permission=NO_PERMISSION_REQUIRED, ) def ready(request): return Response("yep")

  79. @view_config( route_name="ready", permission=NO_PERMISSION_REQUIRED, ) def ready(request): return Response("yep") config.add_route("ready", "/-/ready")

  80. Moar

  81. Moar •__version__

  82. Moar •__version__ •/-/metrics

  83. Moar •__version__ •/-/metrics •/-/log-level

  84. 127.0.0.1:8001 Exposes env HOST=127.0.0.1 \ PORT=8001 \ ./run-app.sh App 127.0.0.1:8000

    Exposes env HOST=127.0.0.1 \ PORT=8000 \ ./run-app.sh App $PUBLIC_IP Load Balancer
  85. App 127.0.0.1:8003 Exposes env HOST=127.0.0.1 \ PORT=8002 \ ./run-app.sh 127.0.0.1:8002

    Exposes env HOST=127.0.0.1 \ PORT=8002 \ ./run-app.sh App 127.0.0.1:8001 Exposes env HOST=127.0.0.1 \ PORT=8001 \ ./run-app.sh App 127.0.0.1:8000 Exposes env HOST=127.0.0.1 \ PORT=8000 \ ./run-app.sh App $PUBLIC_IP Load Balancer
  86. App 10.0.0.4:8000 Exposes env HOST=10.0.0.4 \ PORT=8000 \ ./run-app.sh 10.0.0.3:8000

    Exposes env HOST=10.0.0.3 \ PORT=8000 \ ./run-app.sh App 10.0.0.2:8000 Exposes env HOST=10.0.0.2 \ PORT=8000 \ ./run-app.sh App 10.0.0.1:8000 Exposes env HOST=10.0.0.1 \ PORT=8000 \ ./run-app.sh App $PUBLIC_IP Load Balancer
  87. None
  88. None
  89. None
  90. None
  91. App 10.0.0.4:8000 Exposes env HOST=10.0.0.4 \ PORT=8000 \ ./run-app.sh 10.0.0.3:8000

    Exposes env HOST=10.0.0.3 \ PORT=8000 \ ./run-app.sh App 10.0.0.2:8000 Exposes env HOST=10.0.0.2 \ PORT=8000 \ ./run-app.sh App 10.0.0.1:8000 Exposes env HOST=10.0.0.1 \ PORT=8000 \ ./run-app.sh App $PUBLIC_IP Load Balancer
  92. None
  93. None
  94. Open Space: Docker Sunday Room 11 @ 11am

  95. App

  96. run-app.sh App

  97. run-app.sh env HOST=0.0.0.0 PORT=8000 … App

  98. run-app.sh SIGTERM env HOST=0.0.0.0 PORT=8000 … App

  99. Secrets run-app.sh SIGTERM env HOST=0.0.0.0 PORT=8000 … App

  100. Secrets run-app.sh SIGTERM External Resources Keeps data in env HOST=0.0.0.0

    PORT=8000 … App
  101. Secrets Exposes service 0.0.0.0:8000 run-app.sh SIGTERM External Resources Keeps data

    in env HOST=0.0.0.0 PORT=8000 … App
  102. Secrets Exposes service 0.0.0.0:8000 run-app.sh Exposes state 0.0.0.0:8000/-/* SIGTERM External

    Resources Keeps data in env HOST=0.0.0.0 PORT=8000 … App
  103. Logs stdout Secrets Exposes service 0.0.0.0:8000 run-app.sh Exposes state 0.0.0.0:8000/-/*

    SIGTERM External Resources Keeps data in env HOST=0.0.0.0 PORT=8000 … App
  104. None
  105. •Loose Coupling

  106. •Loose Coupling •Separate I/O & Logic

  107. •Loose Coupling •Separate I/O & Logic •Avoid Global State

  108. App Boundary = Just Another Boundary

  109. Epilogue

  110. Epilogue

  111. ox.cx/df @hynek vrmd.de