{"id":14494,"date":"2023-02-08T00:00:00","date_gmt":"2023-02-07T08:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/cse\/?p=14494"},"modified":"2024-07-18T11:46:59","modified_gmt":"2024-07-18T18:46:59","slug":"maintenance_window_db_migrations","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/ise\/maintenance_window_db_migrations\/","title":{"rendered":"Maintenance window strategy for database migrations"},"content":{"rendered":"<p>In a recent project, we were tasked with implementing a database migration strategy.\nThe goal was to minimize the impact on our users and to ensure that during\nthe database migration database would not be corrupted with writes and reads\nin the process. This blog post will describe the strategy we implemented and\nhow we applied it.<\/p>\n<p>Database migrations can be a tricky task for any organization, especially when\nit comes to maintaining the availability of the system during the migration process.\nOne way to mitigate this risk is to implement a maintenance window strategy.<\/p>\n<p>In a maintenance window, the application is taken offline for maintenance or updates.\nThis approach enables IT teams to perform necessary updates or migrations\nwithout compromising the system&#8217;s availability for end users or risking a\ncorrupt database.<\/p>\n<p>As can be seen below in the diagram, the maintenance window strategy is\nimplemented by taking the system offline during the maintenance window and\nperforming the database migration. Once the migration is complete, the system\nis brought back online. During the maintenance window, the system is not\navailable for end users and any requests made to the system receive a 503\nhttp status code.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/cse\/wp-content\/uploads\/sites\/55\/2023\/02\/maintenance-window-database-migrations-0.jpg\" alt=\"maintenance window sequence diagram\" \/><\/p>\n<p>The criteria for implementing a maintenance window strategy for database\nmigrations are:<\/p>\n<ul>\n<li>Your database does not have migration tools that can be used to perform migrations\nwithout taking the database offline.<\/li>\n<li>Your database is not hosted on a cloud provider that provides a managed database\nservice that can be used to perform migrations without taking the database offline.<\/li>\n<li>You&#8217;re using a single database for your application and not a set of distributed database.<\/li>\n<\/ul>\n<h2>Real-world example<\/h2>\n<blockquote><p>All source code in this article can be found <a href=\"https:\/\/github.com\/microsoft\/cookiecutter-python-flask-clean-architecture\">here<\/a>.<\/p><\/blockquote>\n<p>For a real-world example, we will look at a maintenance window strategy implementation for\na <a href=\"https:\/\/flask.palletsprojects.com\/en\/2.2.x\/\">FLASK application<\/a> with a SQL based database. Given that the application uses <a href=\"https:\/\/www.sqlalchemy.org\/\">SQLAlchemy<\/a> and <a href=\"https:\/\/alembic.sqlalchemy.org\/en\/latest\/\">alembic<\/a> for\ndatabase migrations we will have the following components:<\/p>\n<h3>Service context database model<\/h3>\n<p>The service context database model is a database model that is used to store the\ncurrent state of the service. This model is used to determine if the service is\nin maintenance mode or not. The model is defined as follows:<\/p>\n<blockquote><p>Note: The service context database model can also be extended to store other\ninformation about the service such as the current version of the service.<\/p><\/blockquote>\n<pre><code class=\"language-python\">from src.infrastructure.databases import sqlalchemy_db as db\r\nfrom src.infrastructure.models.model_extension import ModelExtension\r\n\r\nclass ServiceContext(db.Model, ModelExtension):\r\n    __tablename__ = 'service_context'\r\n    id = db.Column(db.Integer, primary_key=True)\r\n    maintenance = db.Column(db.Boolean, default=False)<\/code><\/pre>\n<p>We use a database model to store the current state of the service because it allows\nus to store the state of the service persistently. This means that if the\nservice is restarted, the state of the service will be restored. Also, if the service\nis running on multiple instances, the state of the service will be consistent across\nall instances.<\/p>\n<h3>Service context service<\/h3>\n<p>The service context service is a service that is used to interact with the service\ncontext database model. The service context service is defined as follows:<\/p>\n<pre><code class=\"language-python\">from src.infrastructure.models import ServiceContext\r\nfrom src.infrastructure.databases import sqlalchemy_db as db\r\n\r\nclass ServiceContextService:\r\n\r\n    def update(self, data):\r\n        service_context = ServiceContext.query.first()\r\n\r\n        if service_context is None:\r\n            service_context = ServiceContext()\r\n\r\n        service_context.update(db, data)\r\n        return service_context\r\n\r\n    def get_service_context(self):\r\n        status = ServiceContext.query.first()\r\n\r\n        if status is None:\r\n            status = ServiceContext()\r\n            status.save(db)\r\n\r\n        return status<\/code><\/pre>\n<h3>Maintenance mode activation and deactivation<\/h3>\n<p>To activate of deactivate the maintenance mode, we can use a route as shown below:<\/p>\n<pre><code class=\"language-python\">@blueprint.route('\/service-context', methods=['PATCH'])\r\n@post_data_required\r\n@inject\r\ndef update_service_context(\r\n    json_data,\r\n    service_context_service=Provide[\r\n        DependencyContainer.service_context_service\r\n    ]\r\n):\r\n    validated_data = ServiceContextSchema().load(json_data)\r\n    service_context = service_context_service.update(validated_data)\r\n    return create_response(service_context, ServiceContextSchema)<\/code><\/pre>\n<p>When creating this public route, make sure that you have a way to authenticate the user that makes the request in order to determine that the user has the necessary permissions.<\/p>\n<p>You can also create a management command with <a href=\"https:\/\/flask.palletsprojects.com\/en\/2.2.x\/cli\/\">Flask cli<\/a> to\nactivate the maintenance mode from within the application:<\/p>\n<pre><code class=\"language-python\">@app.cli.command(\"activate_maintenance_mode\")\r\ndef activate_maintenance_mode():\r\n    service_context_service = app.container.service_context_service()\r\n    service_context_service.update({\"maintenance\": True})\r\n    logger.info(\"Maintenance mode activated\")\r\n\r\n@app.cli.command(\"deactivate_maintenance_mode\")\r\ndef deactivate_maintenance_mode():\r\n    service_context_service = app.container.service_context_service()\r\n    service_context_service.update({\"maintenance\": False})\r\n    logger.info(\"Maintenance mode deactivated\")<\/code><\/pre>\n<p>This allows you to activate or deactivate the maintenance mode from the command line if\nyou have access to the server where the application is running.<\/p>\n<h3>Maintenance mode check<\/h3>\n<p>We are using the <code>before_request<\/code> decorator to run the maintenance mode check before\neach request. The maintenance mode check is defined as follows:<\/p>\n<pre><code class=\"language-python\">@app.before_request\r\ndef check_for_maintenance():\r\n    service_context_service = app.container.service_context_service()\r\n    status = service_context_service.get_service_context()\r\n\r\n    if not (\"maintenance\" in request.path or \"status\" in request.path):\r\n        if status.maintenance:\r\n            return jsonify(\r\n                {\"message\": \"Service is currently enduring maintenance\"}\r\n            ), 503<\/code><\/pre>\n<p>With this implementation, the service will return a 503 http status code for all\nrequests that are not made to the maintenance mode activation, deactivation\nor status routes.<\/p>\n<h2>Applying migrations during maintenance window<\/h2>\n<p>You have several ways to apply migrations during a maintenance window.<\/p>\n<p>One way is to do the migration manually by running the migration commands from\nwithin the application. This is the simplest way to apply migrations and gives\nyou the most control over the migration process.<\/p>\n<h3>Manual migrations<\/h3>\n<p>An example of applying the migration manually is shown below in a kubernetes cluster:<\/p>\n<ol>\n<li>Access a pod that has access to a database and has the maintenance window strategy implemented\n<pre><code class=\"language-bash\">kubectl exec -it &lt;pod_name&gt; -- \/bin\/bash<\/code><\/pre>\n<\/li>\n<li>Activate the maintenance mode\n<pre><code class=\"language-bash\">python manage.py activate_maintenance_mode<\/code><\/pre>\n<\/li>\n<li>Apply the migration\n<pre><code class=\"language-bash\">python manage.py db upgrade<\/code><\/pre>\n<\/li>\n<li>Deactivate the maintenance mode\n<pre><code class=\"language-bash\">python manage.py deactivate_maintenance_mode<\/code><\/pre>\n<\/li>\n<\/ol>\n<h3>Pipeline based migration<\/h3>\n<p>This is a more advanced way to apply migrations during a maintenance window.\nThe pipeline below is an azure devops pipeline and an example of an azure\napp service deployment where a maintenance window is used to apply the migration.<\/p>\n<p>The pipeline is defined as follows:<\/p>\n<pre><code class=\"language-bash\">trigger:\r\n  paths:\r\n    include:\r\n    - &lt;service_path_from_rooth&gt;\/migrations\/*\r\n\r\nvariables:\r\n  production_web_app_service_url: \"https:\/\/&lt;production_service_url&gt;\"\r\n  staging_web_app_service_url: \"https:\/\/&lt;staging_web_app_service_url&gt;\"\r\n  projectRoot: $CI_PROJECT_DIR\r\n  vmImageName: ubuntu-latest\r\n  pythonVersion: 3.8\r\n  isMain: $[eq(variables['Build.SourceBranch'], 'refs\/heads\/main')]\r\n\r\nstages:\r\n- stage: \"deploy_stable\"\r\n  condition: and(always(), eq(variables['isMain'], True))\r\n  jobs:\r\n  - job: \"build_web_app\"\r\n    displayName: \"Build web app\"\r\n    pool:\r\n      vmImage: $(vmImageName)\r\n    steps:\r\n    - task: UsePythonVersion@0\r\n      inputs:\r\n        versionSpec: '$(pythonVersion)'\r\n    - script: |\r\n        python -m venv venv\r\n        source  venv\/bin\/activate\r\n        python -m pip install --upgrade pip\r\n        pip install setup\r\n        pip install --target=\".\/.python_packages\/lib\/site-packages\" -r .\/requirements.txt\r\n      workingDirectory: $(projectRoot)\r\n      displayName: \"Install requirements\"\r\n    - task: ArchiveFiles@2\r\n      inputs:\r\n        rootFolderOrFile: '$(Build.SourcesDirectory)'\r\n        includeRootFolder: false\r\n        archiveType: 'zip'\r\n        archiveFile: '$(Build.ArtifactStagingDirectory)\/Application$(Build.BuildId).zip'\r\n        replaceExistingArchive: true\r\n    - publish: $(Build.ArtifactStagingDirectory)\/Application$(Build.BuildId).zip\r\n      displayName: 'Upload package'\r\n      artifact: drop\r\n    - script: |\r\n        curl -X PATCH -H \"Content-Type: application\/json\" -d '{\"maintenance\": true}' $(production_web_app_service_url)\/service-context\r\n      name: activate_maintenance_mode_production\r\n      displayName: Activate maintenance mode on production\r\n    - task: AzureWebApp@1\r\n      displayName: \"Deploy web app to staging\"\r\n      inputs:\r\n        azureSubscription: '&lt;Azure service connection&gt;'\r\n        appType: webAppLinux\r\n        appName: '&lt;name of web app&gt;'\r\n        deployToSlotOrASE: true\r\n        resourceGroupName: '&lt;name of resource group&gt;'\r\n        slotName: staging\r\n    - script: |\r\n        curl -X PATCH -H \"Content-Type: application\/json\" -d '{\"maintenance\": true}' $(staging_web_app_service_url)\/service-context\r\n      name: activate_maintenance_mode_staging\r\n      displayName: Activate maintenance mode on staging\r\n    - task: AzureKeyVault@2\r\n      displayName: Load key vault db connection string\r\n      inputs:\r\n        connectedServiceName: azure-keyvault-connection\r\n        keyVaultName: $(keyVaultName)\r\n        secretsFilter: '*'\r\n    - script: |\r\n        &gt; .env\r\n        echo SQLALCHEMY_DATABASE_URI=\"$(SQLALCHEMY_DATABASE_URI)\" &gt;&gt; .env\r\n      name: setup_env_file\r\n      displayName: Setup env file\r\n    - script: |\r\n        python manage.py db upgrade\r\n      name: apply_migrations\r\n      displayName: Apply migrations\r\n    - task: AzureAppServiceManage@0\r\n      displayName: 'Swap staging and production slots'\r\n      inputs:\r\n        azureSubscription: '&lt;Azure service connection&gt;'\r\n        appType: webAppLinux\r\n        WebAppName: '&lt;name of web app&gt;'\r\n        ResourceGroupName: '&lt;name of resource group&gt;'\r\n        SourceSlot: staging\r\n        SwapWithProduction: true\r\n    - script: |\r\n        curl -X PATCH -H \"Content-Type: application\/json\" -d '{\"maintenance\": false}' $(production_web_app_service_url)\/service-context\r\n      name: deactivate_maintenance_mode_production\r\n      displayName: Deactivate maintenance mode on production<\/code><\/pre>\n<p>The pipeline is triggered when a migration is pushed to the main branch.\nThe pipeline will then build the application,\nset the production and staging app in maintenance mode, apply the migration and\nswap the production and staging slots.<\/p>\n<h2>Closing remarks<\/h2>\n<p>When planning a maintenance window for a database migration, it&#8217;s important to\nconsider the following:<\/p>\n<p>1) Identify the right time: Choose a time that has the least impact on end users, such as during off-peak hours or weekends.<\/p>\n<p>2) Communicate with stakeholders: Notify all stakeholders, including customers and internal teams, of the planned maintenance window well in advance. This will give them ample time to plan and prepare for the interruption in service.<\/p>\n<p>3) Test the migration process: Before the actual migration, test the process in a non-production environment to ensure it runs smoothly. This will also help identify any potential issues that need to be addressed.<\/p>\n<p>4) Have a rollback plan: In case something goes wrong during the migration, it&#8217;s important to have a rollback plan in place to quickly restore the previous version of the database.<\/p>\n<p>5) Monitor the migration: Continuously monitor the migration process to ensure it&#8217;s running smoothly and to quickly address any issues that may arise.<\/p>\n<p>6) By implementing a maintenance window strategy, organizations can minimize the impact of database migrations on end users and ensure the availability of the system during the migration process.<\/p>\n<p>It&#8217;s also important to note that some migrations such as those of very large databases or\nthose that require a lot of data transformation may require more than one maintenance window.\nIn such scenarios it is important to plan and test the migration in chunks, allowing for a more\ngradual and controlled migration process.<\/p>\n<p>Overall, a maintenance window strategy can be a useful tool for organizations looking to perform database migrations\nwhile minimizing disruption to end users. With careful planning, testing and monitoring, IT teams can ensure a\nsmooth and successful migration process.<\/p>\n<p>If you want to learn more or want to check out the code for yourself, you can find all source code <a href=\"https:\/\/github.com\/microsoft\/cookiecutter-python-flask-clean-architecture\">here<\/a>. This will give a cookiecutter repository that provides you with a flask onion architecture with support for maintenance mode and tests with <a href=\"https:\/\/github.com\/testcontainers\/testcontainers-python\">test containers<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Maintenance window implementation for database migrations using Flask.<\/p>\n","protected":false},"author":106000,"featured_media":14508,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[3367,178,3366,3368,3364,300],"class_list":["post-14494","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cse","tag-database","tag-flask","tag-frameworks","tag-migrations","tag-open-source","tag-python"],"acf":[],"blog_post_summary":"<p>Maintenance window implementation for database migrations using Flask.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/14494","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/users\/106000"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/comments?post=14494"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/14494\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media\/14508"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media?parent=14494"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/categories?post=14494"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/tags?post=14494"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}