Port the Database Migrations doc
Moves the Database Migrations page from https://github.com/cloudkeep/barbican/wiki/Database-Migrations to the Sphinx documentation, with added notes on automatically generating version files. Change-Id: Ie6b0a63af90c27439e82889b0f1cd0c0fc0a6bea
This commit is contained in:
parent
bfae7fc508
commit
888b68a531
231
doc/source/contribute/database_migrations.rst
Normal file
231
doc/source/contribute/database_migrations.rst
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
Database Migrations
|
||||||
|
====================
|
||||||
|
|
||||||
|
Database migrations are managed using the Alembic_ library. The consensus for
|
||||||
|
`OpenStack and SQLAlchemy`_ is that this library is preferred over
|
||||||
|
sqlalchemy-migrate.
|
||||||
|
|
||||||
|
Database migrations can be performed two ways: (1) via the API startup
|
||||||
|
process, and (2) via a separate script.
|
||||||
|
|
||||||
|
Database migrations can be optionally enabled during the API startup process.
|
||||||
|
Corollaries for this are that a new deployment should begin with only one node
|
||||||
|
to avoid migration race conditions.
|
||||||
|
|
||||||
|
Alternatively, the automatic update startup behavior can be disabled, forcing
|
||||||
|
the use of the migration script. This latter mode is probably safer to use in
|
||||||
|
production environments.
|
||||||
|
|
||||||
|
Policy
|
||||||
|
-------
|
||||||
|
|
||||||
|
A Barbican deployment goal is to update application and schema versions with
|
||||||
|
zero downtime. The challenge is that at all times the database schema must be
|
||||||
|
able to support two deployed application versions, so that a single migration
|
||||||
|
does not break existing nodes running the previous deployment. For example,
|
||||||
|
when deleting a column we would first deploy a new version that ignores the
|
||||||
|
column. Once all nodes are ignoring the column, a second deployment would be
|
||||||
|
made to remove the column from the database.
|
||||||
|
|
||||||
|
To achieve this goal, the following rules will be observed for schema changes:
|
||||||
|
|
||||||
|
1. Do not remove columns or tables directly, but rather:
|
||||||
|
|
||||||
|
a. Create a version of the application not dependent on the removed
|
||||||
|
column/table
|
||||||
|
b. Replace all nodes with this new application version
|
||||||
|
c. Create an Alembic version file to remove remove the column/table
|
||||||
|
d. Apply this change in production manually, or automatically with a future
|
||||||
|
version of the application
|
||||||
|
|
||||||
|
2. Changing column attributes (types, names or widths) should be handled as
|
||||||
|
follows:
|
||||||
|
|
||||||
|
a. TODO: This Stack Overflow `Need to alter column types in production
|
||||||
|
database`_ page and many others summarize the grief involved in doing
|
||||||
|
these sort of migrations
|
||||||
|
b. TODO: What about old and new application versions happening
|
||||||
|
simultaneously?
|
||||||
|
|
||||||
|
i. Maybe have the new code perform migration to new column on each read
|
||||||
|
...similar to how a no-sql db migration would occur?
|
||||||
|
|
||||||
|
3. Transforming column attributes (ex: splitting one ``name`` column into a
|
||||||
|
``first`` and ``last`` name):
|
||||||
|
|
||||||
|
a. TODO: An `Alembic example`_, but not robust for large datasets.
|
||||||
|
|
||||||
|
Overview
|
||||||
|
---------
|
||||||
|
|
||||||
|
*Prior to invoking any migration steps below, change to your* ``barbican`` *project's
|
||||||
|
folder and activate your virtual environment per the* `Developer Guide`_.
|
||||||
|
|
||||||
|
**If you are using PostgreSQL, please ensure you are using SQLAlchemy version
|
||||||
|
0.9.3 or higher, otherwise the generated version files will not be correct.**
|
||||||
|
|
||||||
|
**You cannot use these migration tools and techniques with SQLite databases.**
|
||||||
|
|
||||||
|
Consider taking a look at the `Alembic tutorial`_. As a brief summary: Alembic
|
||||||
|
keeps track of a linked list of version files, each one applying a set of
|
||||||
|
changes to the database schema that a previous version file in the linked list
|
||||||
|
modified. Each version file has a unique Alembic-generated ID associated with
|
||||||
|
it. Alembic generates a table in the project table space called
|
||||||
|
``alembic_version`` that keeps track of the unique ID of the last version file
|
||||||
|
applied to the schema. During an update, Alembic uses this stored version ID
|
||||||
|
to determine what if any follow on version files to process.
|
||||||
|
|
||||||
|
Generating Change Versions
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
To make schema changes, new version files need to be added to the
|
||||||
|
``barbican/model/migration/alembic_migrations/versions/`` folder. This section
|
||||||
|
discusses two ways to add these files.
|
||||||
|
|
||||||
|
Automatically
|
||||||
|
''''''''''''''
|
||||||
|
|
||||||
|
Alembic autogenerates a new script by comparing a clean database (i.e., one
|
||||||
|
without your recent changes) with any modifications you make to the Models.py
|
||||||
|
or other files. This being said, automatic generation may miss changes... it
|
||||||
|
is more of an 'automatic assist with expert review'. See `What does
|
||||||
|
Autogenerate Detect`_ in the Alembic documentation for more details.
|
||||||
|
|
||||||
|
First, you must start Barbican using a version of the code that does not
|
||||||
|
include your changes, so that it creates a clean database. This example uses
|
||||||
|
Barbican launched with DevStack (see `Barbican DevStack`_ wiki page for
|
||||||
|
instructions).
|
||||||
|
|
||||||
|
1. Make changes to the 'barbican/model/models.py' SQLAlchemy models or
|
||||||
|
checkout your branch that includes your changes using git.
|
||||||
|
2. Execute ``bin/barbican-db-manage.py -d <Full URL to database, including
|
||||||
|
user/pw> revision -m '<your-summary-of-changes>' --autogenerate``
|
||||||
|
|
||||||
|
a. For example: ``bin/barbican-db-manage.py -d
|
||||||
|
mysql://root:password@127.0.0.1/barbican?charset=utf8
|
||||||
|
revision -m 'Make unneeded verification columns nullable' --autogenerate``
|
||||||
|
|
||||||
|
3. Examine the generated version file, found in
|
||||||
|
``barbican/model/migration/alembic_migrations/versions/``:
|
||||||
|
|
||||||
|
a. **Verify generated update/rollback steps, especially for modifications
|
||||||
|
to existing columns/tables**
|
||||||
|
b. **If you added new tables, follow this guidance**:
|
||||||
|
|
||||||
|
1. Make sure you added your new table to the ``MODELS`` element of the
|
||||||
|
``barbican/model/models.py`` module.
|
||||||
|
2. Note that when Barbican boots up, it will add the new table to the
|
||||||
|
database. It will also try to apply the database version (that also
|
||||||
|
tries to add this table) via alembic. Therefore, please edit the
|
||||||
|
generated script file to add these lines:
|
||||||
|
|
||||||
|
a. ``ctx = op.get_context()`` (to get the alembic migration context in
|
||||||
|
current transaction)
|
||||||
|
b. ``con = op.get_bind()`` (get the database connection)
|
||||||
|
c. ``table_exists = ctx.dialect.has_table(con.engine,
|
||||||
|
'your-new-table-name-here')``
|
||||||
|
d. ``if not table_exists:``
|
||||||
|
e. ``...remaining create table logic here...``
|
||||||
|
|
||||||
|
*Note: For anything but trivial or brand new columns/tables, database backups
|
||||||
|
and maintenance-window downtimes might be called for.*
|
||||||
|
|
||||||
|
Manually
|
||||||
|
'''''''''
|
||||||
|
|
||||||
|
1. Execute: ``bin/barbican-db-manage.py revision -m "<insert your change
|
||||||
|
description here>"``
|
||||||
|
2. This will generate a new file in the
|
||||||
|
``barbican/model/migration/alembic_migrations/versions/`` folder, with this
|
||||||
|
sort of file format:
|
||||||
|
``<unique-Alembic-ID>_<your-change-description-from-above-but-truncated>.py``.
|
||||||
|
Note that only the first 20 characters of the description are used.
|
||||||
|
3. You can then edit this file per tutorial and the `Alembic Operation
|
||||||
|
Reference`_ page for available operations you may make from the version
|
||||||
|
files. **You must properly fill in both the** ``upgrade()`` **and**
|
||||||
|
``downgrade()`` **methods.**
|
||||||
|
|
||||||
|
Applying Changes
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Barbican utilizes the Alembic version files as managing delta changes to the
|
||||||
|
database. Therefore the first Alembic version file does **not** contain all
|
||||||
|
time-zero database tables.
|
||||||
|
|
||||||
|
To create the initial Barbican tables in the database, execute the Barbican
|
||||||
|
application per the 'Via Application' section.
|
||||||
|
|
||||||
|
Thereafter, it is suggested that only the ``barbican-db-manage.py`` script
|
||||||
|
above be used to update the database schema per the 'Manually' section. Also,
|
||||||
|
automatic database updates from the Barbican application should be disabled by
|
||||||
|
adding/updating ``db_auto_create = False`` in the ``barbican-api.conf``
|
||||||
|
configuration file.
|
||||||
|
|
||||||
|
Via Application
|
||||||
|
''''''''''''''''
|
||||||
|
|
||||||
|
The last section of the `Alembic tutorial`_ describes the process used by the
|
||||||
|
Barbican application to create and update the database table space
|
||||||
|
automatically.
|
||||||
|
|
||||||
|
By default, when the Barbican API boots up it will try to create the Barbican
|
||||||
|
database tables (using SQLAlchemy), and then try to apply the latest version
|
||||||
|
files (using Alembic). In this mode, the latest version of the Barbican
|
||||||
|
application can create a new database table space updated to the latest schema
|
||||||
|
version, or else it can update an existing database table space to the latest
|
||||||
|
schema revision (called ``head`` in the docs).
|
||||||
|
|
||||||
|
*To bypass this automatic behavior, add* ``db_auto_create = False`` *to the*
|
||||||
|
``barbican-api.conf`` *file*.
|
||||||
|
|
||||||
|
Manually
|
||||||
|
'''''''''
|
||||||
|
|
||||||
|
Run ``bin/barbican-db-manage.py -d <Full URL to database, including user/pw>
|
||||||
|
upgrade -v head``, which will cause Alembic to apply the changes found in all
|
||||||
|
version files after the version currently written in the target database, up
|
||||||
|
until the latest version file in the linked chain of files.
|
||||||
|
|
||||||
|
To upgrade to a specific version, run this command:
|
||||||
|
``bin/barbican-db-manage.py -d <Full URL to database, including user/pw>
|
||||||
|
upgrade -v <Alembic-ID-of-version>``. The ``Alembic-ID-of-version`` is a
|
||||||
|
unique ID assigned to the change such ``as1a0c2cdafb38``.
|
||||||
|
|
||||||
|
To downgrade to a specific version, run this command:
|
||||||
|
``bin/barbican-db-manage.py -d <Full URL to database, including user/pw>
|
||||||
|
downgrade -v <Alembic-ID-of-version>``.
|
||||||
|
|
||||||
|
TODO Items
|
||||||
|
-----------
|
||||||
|
|
||||||
|
1. *[Done - It works!]* Verify alembic works with the current SQLAlchemy model
|
||||||
|
configuration in Barbican (which was borrowed from Glance).
|
||||||
|
2. *[Done - It works, I was able to add/remove columns while app was running]*
|
||||||
|
Verify that SQLAlchemy is tolerant of schema miss-matches. For example, if
|
||||||
|
a column is added to a table schema, will this break existing deployments
|
||||||
|
that aren't expecting this column?
|
||||||
|
3. *[Done - It works]* Add auto-migrate code to the boot up of models (see the
|
||||||
|
``barbican\model\repositories.py`` file).
|
||||||
|
4. *[Done - It works]* Add guard in Barbican model logic to guard against
|
||||||
|
running migrations with SQLite databases.
|
||||||
|
5. Add detailed deployment steps for production, so how new nodes are rolled
|
||||||
|
in and old ones rolled out to complete move to new versions.
|
||||||
|
6. *[In Progress]* Add a best-practices checklist section to this page.
|
||||||
|
|
||||||
|
a. This would provide guidance on safely migrating schemas, do's and
|
||||||
|
don'ts, etc.
|
||||||
|
b. This could also provide code guidance, such as ensuring that new schema
|
||||||
|
changes (eg. that new column) aren't required for proper functionality
|
||||||
|
of the previous version of the code.
|
||||||
|
c. If a server bounce is needed, notification guidelines to the devop team
|
||||||
|
would be spelled out here.
|
||||||
|
|
||||||
|
.. _Alembic: https://alembic.readthedocs.org/en/latest/
|
||||||
|
.. _Alembic Example: https://julo.ch/blog/migrating-content-with-alembic/
|
||||||
|
.. _Alembic Operation Reference: https://alembic.readthedocs.org/en/latest/ops.html
|
||||||
|
.. _Alembic tutorial: https://alembic.readthedocs.org/en/latest/tutorial.html
|
||||||
|
.. _Barbican DevStack: https://wiki.openstack.org/wiki/BarbicanDevStack
|
||||||
|
.. _Developer Guide: https://github.com/cloudkeep/barbican/wiki/Developer-Guide
|
||||||
|
.. _Need to alter column types in production database: http://stackoverflow.com/questions/5329255/need-to-alter-column-types-in-production-database-sql-server-2005
|
||||||
|
.. _OpenStack and SQLAlchemy: https://wiki.openstack.org/wiki/OpenStack_and_SQLAlchemy#Migrations
|
||||||
|
.. _What does Autogenerate Detect: http://alembic.readthedocs.org/en/latest/autogenerate.html#what-does-autogenerate-detect-and-what-does-it-not-detect
|
@ -18,6 +18,7 @@ Getting Started
|
|||||||
|
|
||||||
contribute/getting_involved
|
contribute/getting_involved
|
||||||
contribute/dependencies
|
contribute/dependencies
|
||||||
|
contribute/database_migrations
|
||||||
setup/index
|
setup/index
|
||||||
testing
|
testing
|
||||||
plugin/index
|
plugin/index
|
||||||
|
Loading…
Reference in New Issue
Block a user