<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Naveen Kannan</title><link>https://naveenkannan.dev/</link><description>Recent content on Naveen Kannan</description><generator>Hugo</generator><language>en</language><lastBuildDate>Sun, 26 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://naveenkannan.dev/index.xml" rel="self" type="application/rss+xml"/><item><title>Configuring FastAPI for use with PowerBI when using Entra ID for OAuth2.</title><link>https://naveenkannan.dev/posts/fastapi_entraid_redirect/</link><pubDate>Sun, 26 Jul 2026 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/fastapi_entraid_redirect/</guid><description>A guide to enabling a FastAPI integrated with Entra ID to be accessible via PowerBI.</description><content:encoded><![CDATA[<h1 id="introduction">Introduction</h1>
<p><a href="https://fastapi.tiangolo.com/">FastAPI</a> is a framework for building APIs with Python based on standard Python type hints. It&rsquo;s fast and easy to stand up, and as a Python developer, it was relatively easy to pick up and get started with it.</p>
<p>As part of deploying a FastAPI instance in production, I integrated FastAPI with Entra ID for OAuth2 via <a href="https://vibber-ai.github.io/fastapi-azure-auth/">this really cool library, FastAPI-Azure-Auth.</a></p>
<p>After integrating this library into my FastAPI instance, I was able to secure access to the API server. However, I quickly ran into an interesting problem. While trying to connect a PowerBI instance to the API, I found that the API settings needed some more configuration to enable interactive logging in via Microsoft SSO within PowerBI.</p>
<h2 id="some-context">Some context</h2>
<p>FastAPI needs to be configured to return a <code>WWW-Authenticate</code> header when a user attempts to access the API without authorization (a <code>HTTP 401 Unauthorized</code>) error.</p>
<p>When an unauthenticated access request is made to a secure API, it can be configured to return a header called <code>WWW-Authenticate</code> within it&rsquo;s response when it returns a <code>HTTP 401 Unauthorized</code> error.</p>
<p>The <code>WWW-Authenticate</code> header returns information that can be used for the user to authenticate themselves and return to the API with a valid, unexpired authentication token.</p>
<p>As there is no element of interaction with a HTTP API, the onus is on the application/user that is querying the API to extract the information returned in the <code>WWW-Authenticate</code> header and to proceed to the identity provider (in this case, Microsoft Entra ID.)</p>
<p>In our case, when PowerBI (via Power Query) requests access to a Web API via an Organizational account, it sends a request to the endpoint to the URL with an empty bearer token. It then expects a <code>WWW-Authenticate</code> header along with the Microsoft Entra ID authorization URI to use. This authorization URI should return the tenant that is to be used for the OAuth2 process. Therefore, all we need to do is configure an error handler within the FastAPI instance to return a <code>WWW-Authenticate</code> header when it returns a <code>HTTP 401 Unauthorized</code> error.</p>
<p>The <a href="https://learn.microsoft.com/en-us/power-query/connector-authentication">Power Query documentation</a> documents the supported workflow when trying to access a Web API via an Organizational account.</p>
<p>From the documentation, this is the reponse expected by Power Query when it is given a <code>HTTP 401 Unauthorized</code> error.</p>
<pre tabindex="0"><code>HTTP/1.1 401 Unauthorized
Cache-Control: private
Content-Type: text/html
Server:
WWW-Authenticate: Bearer authorization_uri=https://login.microsoftonline.com/aaaabbbb-0000-cccc-1111-dddd2222eeee/oauth2/authorize
Date: Wed, 15 Aug 2018 15:02:04 GMT
Content-Length: 49
</code></pre><p>So to sum it up, we need to configure FastAPI to return a <code>WWW-Authenticate</code> header when it returns a <code>HTTP 401 Unauthorized</code> error, and this header needs to return the authorization URI associated with the tenant that is able to access the API service via OAuth2.</p>
<h2 id="the-solution">The Solution</h2>
<p>Note that this guide assumes that you have already configured your FastAPI instance to work with Entra ID for OAuth2 flows.</p>
<p>As a general example (which may differ from your set-up), I usually have my code that handles the Entra ID authentication flow within a folder called <code>modules</code> in the project root, in a file called <code>azure.py</code>, which I can then import into my <code>main.py</code> file.</p>
<p>Following from the basic example within the FastAPI-Azure-Auth documentation, here&rsquo;s what <code>azure.py</code> looks like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi_azure_auth <span style="color:#f92672">import</span> SingleTenantAzureAuthorizationCodeBearer
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> pydantic <span style="color:#f92672">import</span> AnyHttpUrl, computed_field
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> pydantic_settings <span style="color:#f92672">import</span> BaseSettings
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Settings</span>(BaseSettings):
</span></span><span style="display:flex;"><span>    BACKEND_CORS_ORIGINS: list[str <span style="color:#f92672">|</span> AnyHttpUrl] <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;https://your.server.com&#34;</span>,
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>    OPENAPI_CLIENT_ID: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    APP_CLIENT_ID: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    TENANT_ID: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    SCOPE_DESCRIPTION: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;user_impersonation&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@computed_field</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@property</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">SCOPE_NAME</span>(self) <span style="color:#f92672">-&gt;</span> str:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;api://</span><span style="color:#e6db74">{</span>self<span style="color:#f92672">.</span>APP_CLIENT_ID<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">{</span>self<span style="color:#f92672">.</span>SCOPE_DESCRIPTION<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@computed_field</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@property</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">SCOPES</span>(self) <span style="color:#f92672">-&gt;</span> dict:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> {
</span></span><span style="display:flex;"><span>            self<span style="color:#f92672">.</span>SCOPE_NAME: self<span style="color:#f92672">.</span>SCOPE_DESCRIPTION,
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@computed_field</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@property</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">OPENAPI_AUTHORIZATION_URL</span>(self) <span style="color:#f92672">-&gt;</span> str:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;https://login.microsoftonline.com/</span><span style="color:#e6db74">{</span>self<span style="color:#f92672">.</span>TENANT_ID<span style="color:#e6db74">}</span><span style="color:#e6db74">/oauth2/v2.0/authorize&#34;</span>
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@computed_field</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@property</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">OPENAPI_TOKEN_URL</span>(self) <span style="color:#f92672">-&gt;</span> str:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;https://login.microsoftonline.com/</span><span style="color:#e6db74">{</span>self<span style="color:#f92672">.</span>TENANT_ID<span style="color:#e6db74">}</span><span style="color:#e6db74">/oauth2/v2.0/token&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>settings <span style="color:#f92672">=</span> Settings()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>azure_scheme <span style="color:#f92672">=</span> SingleTenantAzureAuthorizationCodeBearer(
</span></span><span style="display:flex;"><span>    app_client_id<span style="color:#f92672">=</span>settings<span style="color:#f92672">.</span>APP_CLIENT_ID,
</span></span><span style="display:flex;"><span>    tenant_id<span style="color:#f92672">=</span>settings<span style="color:#f92672">.</span>TENANT_ID,
</span></span><span style="display:flex;"><span>    scopes<span style="color:#f92672">=</span>settings<span style="color:#f92672">.</span>SCOPES,
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div>
    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>This example doesn&rsquo;t go into how you supply the module with the required <code>OPENAPI_CLIENT_ID</code>, <code>APP_CLIENT_ID</code> and <code>TENANT_ID</code> variables.
It is good practice to use a secret vault to supply these variables when calling <code>Settings()</code> to override the default empty strings.
For example:</p>
<blockquote>
<pre tabindex="0"><code>settings = Settings(
   APP_CLIENT_ID=app_client_id,
   OPENAPI_CLIENT_ID=openapi_client_id,
   TENANT_ID=tenant_id,
)
</code></pre></blockquote>
<p>In this example, the three variables supplied to <code>Settings()</code> have been called from a secret vault.</p>
      </div>
    </div><h2 id="the-error-handler">The Error Handler</h2>
<p>Define a custom error handler within your FastAPI code. For example, create a folder called <code>modules</code> in your project root, and create a file called <code>errors.py</code> within <code>modules</code>. Put the following in that file.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi <span style="color:#f92672">import</span> Request, HTTPException
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi.responses <span style="color:#f92672">import</span> JSONResponse
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> modules <span style="color:#f92672">import</span> azure
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">custom_http_exception_handler</span>(request: Request, exc: HTTPException):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> exc<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">401</span>:
</span></span><span style="display:flex;"><span>        auth_url <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;https://login.microsoftonline.com/</span><span style="color:#e6db74">{</span>azure<span style="color:#f92672">.</span>settings<span style="color:#f92672">.</span>TENANT_ID<span style="color:#e6db74">}</span><span style="color:#e6db74">/oauth2/authorize&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> JSONResponse(
</span></span><span style="display:flex;"><span>            status_code<span style="color:#f92672">=</span><span style="color:#ae81ff">401</span>,
</span></span><span style="display:flex;"><span>            content<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;detail&#34;</span>: <span style="color:#e6db74">&#34;Not authenticated&#34;</span>},
</span></span><span style="display:flex;"><span>            headers<span style="color:#f92672">=</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;WWW-Authenticate&#34;</span>: <span style="color:#e6db74">f</span><span style="color:#e6db74">&#39;Bearer authorization_uri=&#34;</span><span style="color:#e6db74">{</span>auth_url<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;&#39;</span>
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> JSONResponse(
</span></span><span style="display:flex;"><span>            status_code<span style="color:#f92672">=</span>exc<span style="color:#f92672">.</span>status_code, 
</span></span><span style="display:flex;"><span>            content<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;detail&#34;</span>: exc<span style="color:#f92672">.</span>detail})
</span></span></code></pre></div><p>In this code snippet, we are doing the following:</p>
<ul>
<li>When a HTTP Exception occurs, the functions checks to see if a 401 error is returned.</li>
<li>We define the URL for OAuth2 authorization flow by using the tenant ID taken from the Azure config.</li>
<li>We construct and return the <code>WWW-Authenticate</code> header to be compliant with the format expected by Power Query.</li>
<li>For HTTP Errors that are not 401, the default response is passed back.</li>
</ul>
<p>We can then import this error handler in <code>main.py</code> as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Annotated, AsyncGenerator
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> contextlib <span style="color:#f92672">import</span> asynccontextmanager
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> starlette.middleware.cors <span style="color:#f92672">import</span> CORSMiddleware
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi <span style="color:#f92672">import</span> FastAPI, HTTPException, Security
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> fastapi_azure_auth.user <span style="color:#f92672">import</span> User
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> modules <span style="color:#f92672">import</span> azure
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> modules.errors <span style="color:#f92672">import</span> custom_http_exception_handler
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>settings <span style="color:#f92672">=</span> azure<span style="color:#f92672">.</span>settings
</span></span><span style="display:flex;"><span>azure_scheme <span style="color:#f92672">=</span> azure<span style="color:#f92672">.</span>azure_scheme
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@asynccontextmanager</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">lifespan</span>(app: FastAPI) <span style="color:#f92672">-&gt;</span> AsyncGenerator[<span style="color:#66d9ef">None</span>, <span style="color:#66d9ef">None</span>]:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Load OpenID config on startup.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">await</span> azure_scheme<span style="color:#f92672">.</span>openid_config<span style="color:#f92672">.</span>load_config()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">yield</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>app <span style="color:#f92672">=</span> FastAPI(
</span></span><span style="display:flex;"><span>    lifespan<span style="color:#f92672">=</span>lifespan,
</span></span><span style="display:flex;"><span>    swagger_ui_oauth2_redirect_url<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/api/oauth2-redirect&#34;</span>,
</span></span><span style="display:flex;"><span>    swagger_ui_init_oauth<span style="color:#f92672">=</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;usePkceWithAuthorizationCodeGrant&#34;</span>: <span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;clientId&#34;</span>: settings<span style="color:#f92672">.</span>OPENAPI_CLIENT_ID,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;scopes&#34;</span>: settings<span style="color:#f92672">.</span>SCOPE_NAME,
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> settings<span style="color:#f92672">.</span>BACKEND_CORS_ORIGINS:
</span></span><span style="display:flex;"><span>    app<span style="color:#f92672">.</span>add_middleware(
</span></span><span style="display:flex;"><span>        CORSMiddleware,
</span></span><span style="display:flex;"><span>        allow_origins<span style="color:#f92672">=</span>[str(origin) <span style="color:#66d9ef">for</span> origin <span style="color:#f92672">in</span> settings<span style="color:#f92672">.</span>BACKEND_CORS_ORIGINS],
</span></span><span style="display:flex;"><span>        allow_credentials<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>        allow_methods<span style="color:#f92672">=</span>[<span style="color:#e6db74">&#34;*&#34;</span>],
</span></span><span style="display:flex;"><span>        allow_headers<span style="color:#f92672">=</span>[<span style="color:#e6db74">&#34;*&#34;</span>],
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>app<span style="color:#f92672">.</span>add_exception_handler(HTTPException, custom_http_exception_handler)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@app.get</span>(<span style="color:#e6db74">&#34;/&#34;</span>, dependencies<span style="color:#f92672">=</span>[Security(azure_scheme)])
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">root</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> {<span style="color:#e6db74">&#34;message&#34;</span>: <span style="color:#e6db74">&#34;Hello World&#34;</span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">&#39;__main__&#39;</span>:
</span></span><span style="display:flex;"><span>    uvicorn<span style="color:#f92672">.</span>run(<span style="color:#e6db74">&#39;main:app&#39;</span>, reload<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span></code></pre></div><p>With this error handler imported, PowerBI should be able to use the Power Query Connector to enable SSO via your Microsoft login when it is attempting to connect to your API!</p>
<h1 id="references">References</h1>
<ul>
<li><a href="https://vibber-ai.github.io/fastapi-azure-auth/">FastAPI-Azure-Auth documentation</a></li>
<li><a href="https://learn.microsoft.com/en-us/power-query/connector-authentication">Microsoft Learn documentation on the Power Query connector and authentication flows</a></li>
<li><a href="https://www.rfc-editor.org/info/rfc7235/#section-4.1">Documenation on the <code>WWW-Authenticate</code> header standards</a></li>
</ul>
]]></content:encoded></item><item><title>Private Intranet SSL certificate generation using DNS-01 challenges.</title><link>https://naveenkannan.dev/posts/private_domain_https/</link><pubDate>Tue, 17 Feb 2026 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/private_domain_https/</guid><description>Using a command-line based ACME client to automate the generation of SSL certificates for intranet servers.</description><content:encoded><![CDATA[<h1 id="introduction">Introduction</h1>
<p>When deploying internal services/tools on a private intranet, these services are usually hosted on servers that do not have a public IP address interface enabled, and only have a private IP address that only accepts connections through a firewall/IP whitelist. This approach ensures that these tools are only accessible internally, and are not public facing.</p>
<p>An approach like this, however, has a few caveats to be aware of. If you want to make requests to these services hosted on private servers, there are two options.</p>
<ul>
<li>Referring to the IP address directly</li>
<li>Referring to a domain name. This requires that your DNS service is correctly configured to have the domain name resolve to the IP address associated with it.</li>
</ul>
<p>Either way, without any further configuration, if you were to simply call these services either by the IP address or the domain name, you are greeted by warnings and disclaimers that state that your connection to this service is <strong>insecure</strong>. By default, these tools will not allow you to access these services without manually allowing the connection or by permitting the use of an insecure connection.</p>
<p>For example, if I was to host an API server on a VM with a private IP address of <code>10.0.0.1</code>, and accessed it as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl https://10.0.0.1/api
</span></span></code></pre></div><p>Or, if your DNS server resolves the domain name <code>dev.company.com</code> to the <code>10.0.0.1</code> IP address,</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl https://dev.company.com/api
</span></span></code></pre></div><p>This is the following output you can expect:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl failed to verify the legitimacy of the server and therefore could not
</span></span><span style="display:flex;"><span>establish a secure connection to it. To learn more about this situation and
</span></span><span style="display:flex;"><span>how to fix it, please visit the web page mentioned above.
</span></span></code></pre></div><p>These errors occur because these services expect a SSL certificate signed by a <strong>trusted CA</strong> by default. You will run into similar warnings when accessing front-end tools hosted on these servers as well.</p>
<p>There are a few options here.</p>
<ul>
<li>Use a self-signed SSL certificate.</li>
</ul>
<p>This approach doesn&rsquo;t negate the warnings! A self-signed SSL certificate is still considered insecure by default.</p>
<ul>
<li>Spin up an internal CA</li>
</ul>
<p>When you spin up an internal CA and generate certificates signed by this local CA, you will then have to <strong>install this certificate</strong> on the end user&rsquo;s devices, which might not be a feasible task. Certificates can expire, and you will have to automate the renewal, propagation and installation of the new certificates on the end-user devices on a regular basis.</p>
<p>The first approach doesn&rsquo;t fix the problem, and the second approach requires a lot of infrastructure and effort to be built.</p>
<p>By far the easiest approach would be to have a <strong>publicly signed SSL certificate</strong>. This would remove the need to create your own internal CA service and the need to then install these local SSL certs on end-user devices.</p>
<p>However, this is where we run into the problem I&rsquo;m hoping to address with this blog post. Globally trusted CAs usually <strong>do not issue SSL certificates for private IP addresses</strong>.</p>
<p>Even if you decided to issue an SSL certificate for your private domain name, you would need to create an external A record for it associated with your server&rsquo;s public IP address, which might be not be something you want to expose.</p>
<p>However, if your company already has a public domain name (which most companies do), with an associated DNS provider, it is entirely possible to have a publicly signed SSL certificate hosted on your private intranet server, without ever needing to expose your server&rsquo;s public IP address.</p>
<p>In this blog post, I will demonstrate how you can set this up and then automate the process of generating publicly signed SSL certificates for your private domain without the need for publicly exposing your server&rsquo;s IP address.</p>
<p>First, let&rsquo;s go over some basic fundamentals.</p>
<h2 id="lets-encrypt-and-the-acme-protocol">Let&rsquo;s Encrypt and the ACME protocol</h2>
<figure class="align-center ">
    <img loading="lazy" src="https://letsencrypt.org/images/letsencrypt-logo-horizontal.svg#center"
         alt="The Let&rsquo;s Encrypt Logo."/> <figcaption>
            <p>The Let&rsquo;s Encrypt Logo.</p>
        </figcaption>
</figure>

<p><a href="https://letsencrypt.org/">Let&rsquo;s Encrypt</a> is a Certificate Authority that provides free TLS/SSL certificates, and has been doing so for a full decade. It is a truly fundamental part of modern internet infrastructure, and is a project of the nonprofit Internet Security Research Group. Let&rsquo;s Encrypt automates the issuing of certificates through an API that is based on the ACME protocol.</p>
<figure class="align-center ">
    <img loading="lazy" src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/16/ACME%E2%80%93protocol-icon.svg/500px-ACME%E2%80%93protocol-icon.svg.png#center"
         alt="The ACME protocol."/> <figcaption>
            <p>The ACME protocol.</p>
        </figcaption>
</figure>

<p>The ACME (Automated Certificate Management Environment) protocol is a communication protocol that makes it possible to automatically obtain trusted certificates without human intervention, at very low cost. ACME is on version 2 of it&rsquo;s API, which was released on March 13, 2018, and this protocol was designed by the Internet Security Research Group for the Let&rsquo;s Encrypt Service.</p>
<p>Since Let&rsquo;s Encrypt requires the use of the ACME protocol, you will need an ACME client to interact with it programmatically.</p>
<h2 id="dehydrated-a-bash-based-acme-client"><code>dehydrated</code>, a bash based ACME client</h2>
<figure class="align-center ">
    <img loading="lazy" src="https://github.com/dehydrated-io/dehydrated/blob/master/docs/logo.png?raw=true#center"
         alt="The dehydrated Logo."/> <figcaption>
            <p>The <code>dehydrated</code> Logo.</p>
        </figcaption>
</figure>

<p><a href="https://github.com/dehydrated-io/dehydrated"><code>dehydrated</code></a> is a bash-based ACME Client that supports both the ACME v1 and ACME v2 protocols.</p>
<p>Lightweight (it&rsquo;s pretty much a bash script!) and simple with an MIT license, this is what we&rsquo;ll be using as our ACME client.</p>
<h2 id="acme-challenges">ACME Challenges</h2>
<p>When requesting a signed certificate from Let&rsquo;s Encrypt via an ACME client, Let&rsquo;s Encrypt validates that you control the domain name by issuing <strong>challenges</strong> defined by the ACME standard. There are three types of challenges, HTTP-01, DNS-01 and TLS-ALPN-01.</p>
<p>TLS-ALPN-01 is highly technical and uses a custom ALPN protocol, and isn&rsquo;t very relevant for this post, so we won&rsquo;t go over it this time.</p>
<h3 id="http-01">HTTP-01</h3>
<p>This is the most common challenge type, where the client proves control over a domain name by proving that it can provision HTTP resources on a server accessible under that domain name.</p>
<p>This challenge type requires the creation of A/AAAA records with your DNS provider. During the challenge process, Let&rsquo;s Encrypt servers will connect to at least one of the hosts found in the DNS A and AAAA records. For this challenge to successfully validate, the server must be reachable at the specified IP address.</p>
<p>This is why we won&rsquo;t be going with HTTP-01 challenges in this blog post, since that would defeat the purpose of having a private intranet to begin with.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>DNS <code>A</code> records indicate the IP address of a given domain name, in the IPv4 format.
DNS <code>AAAA</code> records indicate the IP address of a given domain name in the IPv6 format.</p>
      </div>
    </div><h3 id="dns-01">DNS-01</h3>
<p>DNS-01 challenges require the client to provision a <code>TXT</code> record containing a designated value for a specific validation domain name.</p>
<p>After Let&rsquo;s Encrypt issues the ACME client a token as part of validation, the client will create a <code>TXT</code> record with your DNS provider derived from that token and account key. Let&rsquo;s Encrypt will then query the DNS system for that record, and will then issue a certificate if it finds a match.</p>
<p>This is the challenge type we will be using in this blog post, since this approach <strong>does not</strong> require us to expose the servers to the public internet. However, this approach requires that your DNS provider has API functionality to automate this process.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>DNS <code>TXT</code> records store text information often used for verification.</p>
      </div>
    </div><h1 id="prerequisites">Prerequisites</h1>
<p>With a brief explanation of the fundamentals completed, let&rsquo;s move forward with the prerequisites you will need before following along with this post.</p>
<p>You will need:</p>
<ul>
<li>A DNS provider with API functionality.</li>
</ul>

    <div class="admonition tip">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M272 384c9.6-31.9 29.5-59.1 49.2-86.2c0 0 0 0 0 0c5.2-7.1 10.4-14.2 15.4-21.4c19.8-28.5 31.4-63 31.4-100.3C368 78.8 289.2 0 192 0S16 78.8 16 176c0 37.3 11.6 71.9 31.4 100.3c5 7.2 10.2 14.3 15.4 21.4c0 0 0 0 0 0c19.8 27.1 39.7 54.4 49.2 86.2l160 0zM192 512c44.2 0 80-35.8 80-80l0-16-160 0 0 16c0 44.2 35.8 80 80 80zM112 176c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-61.9 50.1-112 112-112c8.8 0 16 7.2 16 16s-7.2 16-16 16c-44.2 0-80 35.8-80 80z"/></svg>
        <span>Tip</span>
      </div>
      <div class="admonition-content">
        <p>While it is possible to manually update <code>TXT</code> records every 60-90 days, that approach is highly discouraged. ACME was built on the principle of automation,
so if your DNS provider does not have API functionality, I highly recommend that you swap to a DNS provider that supports an API.</p>
<p>The Let&rsquo;s Encrypt community has created a <a href="https://community.letsencrypt.org/t/dns-providers-who-easily-integrate-with-lets-encrypt-dns-validation/86438">resource to track DNS providers who provide API support</a>, which is a very useful resource.</p>
      </div>
    </div>
    <div class="admonition warning">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"/></svg>
        <span>Warning</span>
      </div>
      <div class="admonition-content">
        <p>My experience in this process has been with NameCheap, so this post will involve the creation of a NameCheap hook.</p>
<p>However, NameCheap does not offer an API by default, and it isn&rsquo;t listed in the list of approved DNS providers with API access on the Let&rsquo;s Encrypt website.
NameCheap requires a minimum account balance and a whitelisted IP address.</p>
      </div>
    </div><ul>
<li>
<p>Your DNS server (different from your DNS provider) maps the IP address of your private server to the domain name you intend to use.</p>
</li>
<li>
<p>cURL, sed, grep, awk, mktemp and <code>openssl</code> on your Unix OS.</p>
</li>
</ul>
<h1 id="setup">Setup</h1>
<h2 id="installing-the-dehydrated-client">Installing the <code>dehydrated</code> client</h2>
<p>The <a href="https://github.com/dehydrated-io/dehydrated"><code>dehydrated</code></a> ACME client can be installed as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Downloading the Dehydrated client</span>
</span></span><span style="display:flex;"><span>cd /etc
</span></span><span style="display:flex;"><span>git clone https://github.com/dehydrated-io/dehydrated
</span></span></code></pre></div><p>Running this code will pull the <code>dehydrated</code> library to the <code>/etc</code>. Further configuration is needed.</p>
<h2 id="configuring-the-dehydrated-client">Configuring the <code>dehydrated</code> client</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Creating the necessary folders in the `dehydrated` folder</span>
</span></span><span style="display:flex;"><span>cd /etc/dehydrated
</span></span><span style="display:flex;"><span>mkdir certs accounts records_backup
</span></span></code></pre></div><p>Running this code will create three folders in the <code>dehydrated</code> folder, which will now have the following content:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>dehydrated/
</span></span><span style="display:flex;"><span>├── CHANGELOG
</span></span><span style="display:flex;"><span>├── LICENSE
</span></span><span style="display:flex;"><span>├── README.md
</span></span><span style="display:flex;"><span>├── accounts
</span></span><span style="display:flex;"><span>├── certs
</span></span><span style="display:flex;"><span>├── dehydrated
</span></span><span style="display:flex;"><span>├── docs
</span></span><span style="display:flex;"><span>└── records_backup
</span></span></code></pre></div><h2 id="installing-a-dns-hook-for-dns-01-challenges">Installing a DNS hook for DNS-01 challenges</h2>
<p>The <a href="https://github.com/dehydrated-io/dehydrated/wiki">wiki for <code>dehydrated</code> has a list of community-created DNS hooks for DNS providers.</a></p>
<p>For this example, we will use the <a href="https://github.com/wdouglascampbell/dehydrated_namecheap_dns_api_hook">NameCheap DNS hook.</a></p>
<p>This library contains two scripts, <code>namecheap_dns_api_hook.sh</code> and <code>reload_services.sh</code>. Place them in your <code>dehydrated</code> directory.</p>
<h2 id="configuring-dehydrated">Configuring <code>dehydrated</code></h2>
<p>Create empty files called <code>config</code> and <code>domains.txt</code> in the <code>dehydrated</code> folder.</p>
<p>Your <code>dehydrated</code> directory should now have the following content:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>dehydrated/
</span></span><span style="display:flex;"><span>├── CHANGELOG
</span></span><span style="display:flex;"><span>├── LICENSE
</span></span><span style="display:flex;"><span>├── README.md
</span></span><span style="display:flex;"><span>├── accounts
</span></span><span style="display:flex;"><span>├── certs
</span></span><span style="display:flex;"><span>├── config
</span></span><span style="display:flex;"><span>├── dehydrated
</span></span><span style="display:flex;"><span>├── docs
</span></span><span style="display:flex;"><span>├── domains.txt
</span></span><span style="display:flex;"><span>├── namecheap_dns_api_hook.sh
</span></span><span style="display:flex;"><span>├── records_backup
</span></span><span style="display:flex;"><span>└── reload_services.sh
</span></span></code></pre></div><h3 id="config"><code>config</code></h3>
<p>Place the following content in <code>config</code>:</p>
<pre tabindex="0"><code>########################################################
# This is the main config file for the Dehydrated      #
# Namecheap DNS 01 Hook script                         #
#                                                      #
# Default values of this config are in comments        #
########################################################

# Namecheap API Credentials
# These are not default dehydrated config params!
apiusr=# Replace with your API username from NameCheap
apikey=# Replace with your API key from NameCheap
BASEDIR=&#34;/etc/dehydrated&#34;
CHALLENGETYPE=&#34;dns-01&#34;

# Configure notifications

## Replace these parameters with the appropriate email addresses and domain names
#SENDER=&#34;sender@example.com&#34;
#RECIPIENT=&#34;recipient@example.com&#34;
#DOMAIN=&#34;domain.name.com&#34;

DEBUG=no
OPENSSL=&#34;openssl&#34;
# Extra options passed to the curl binary (default: &lt;unset&gt;)
#CURL_OPTS=

# Records backup location
RECORDS_BACKUP=${BASEDIR}/records_backup
DOMAINS_TXT=&#34;${BASEDIR}/domains.txt&#34;
CERTDIR=&#34;${BASEDIR}/certs&#34;
HOOK=&#34;${BASEDIR}/namecheap_dns_api_hook.sh&#34;

# Configure certificate and key locations for deployment
DEPLOYED_CERTDIR=&#34;${BASEDIR}/certs&#34;
DEPLOYED_KEYDIR=&#34;${BASEDIR}/certs&#34;

# Email Sending Method Options
#  + SENDMAIL (default)
#  + SMTP
#MAIL_METHOD=SENDMAIL

# SMTP options
#SMTP_DOMAIN=localhost
#SMTP_SERVER=
#SMTP_PORT=25
</code></pre>
    <div class="admonition warning">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"/></svg>
        <span>Warning</span>
      </div>
      <div class="admonition-content">
        <p>The above is an example to begin testing with.
This isn&rsquo;t the ideal way to store your NameCheap API details.
Ideally you manage them with a key vault or via injection of environment variables at the time of running the script.</p>
      </div>
    </div><h3 id="domainstxt"><code>domains.txt</code></h3>
<p><code>dehydrated</code> uses the <code>domains.txt</code> file as the configuration for which certificates should be requested.</p>
<p>In this example, if your publicly accessible domain is <code>company.com</code>, and you want to have your private intranet have the subdomain <code>dev.company.com</code>, place the following in the <code>domains.txt</code> file.</p>
<pre tabindex="0"><code>dev.company.com
company.com www.company.com dev.company.com
</code></pre><p>The first line will create a certificate for the <code>dev.company.com</code> domain, and the last line will create a multi-domain certificate.</p>
<p>These two lines produce certificates stored under different directory names (<code>dev.company.com</code> vs <code>company.com</code>) within the <code>certs</code> directory.</p>
<h3 id="reload-servicessh"><code>reload-services.sh</code></h3>
<p>Make the following changes to <code>reload-services.sh</code>:</p>
<pre tabindex="0"><code>#!/usr/bin/env bash

# Script to reloading services that use SSL certificates to ensure
# all services are using the latest versions of the certificates.

DOMAIN=&#34;${1}&#34; KEYFILE=&#34;${2}&#34; CERTFILE=&#34;${3}&#34; FULLCHAINFILE=&#34;${4}&#34; CHAINFILE=&#34;${5}&#34; TIMESTAMP=&#34;${6}&#34;

# Nginx
echo &#34; + Reloading Nginx configuration&#34;
systemctl reload nginx.service
</code></pre><h1 id="running-dehydrated">Running <code>dehydrated</code></h1>

    <div class="admonition tip">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M272 384c9.6-31.9 29.5-59.1 49.2-86.2c0 0 0 0 0 0c5.2-7.1 10.4-14.2 15.4-21.4c19.8-28.5 31.4-63 31.4-100.3C368 78.8 289.2 0 192 0S16 78.8 16 176c0 37.3 11.6 71.9 31.4 100.3c5 7.2 10.2 14.3 15.4 21.4c0 0 0 0 0 0c19.8 27.1 39.7 54.4 49.2 86.2l160 0zM192 512c44.2 0 80-35.8 80-80l0-16-160 0 0 16c0 44.2 35.8 80 80 80zM112 176c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-61.9 50.1-112 112-112c8.8 0 16 7.2 16 16s-7.2 16-16 16c-44.2 0-80 35.8-80 80z"/></svg>
        <span>Tip</span>
      </div>
      <div class="admonition-content">
        <p>The first run of dehydrated requires accepting the Let&rsquo;s Encrypt Terms of Service, which is done with <code>dehydrated --register --accept-terms</code>.</p>
<p>Run the command before proceeding!</p>
      </div>
    </div><p>Run <code>dehydrated</code> with the command <code>/etc/dehydrated/dehydrated -c</code>.</p>
<p>This runs the <code>dehydrated</code> client using the <code>config</code> file&rsquo;s parameters. It should get the challenge from Let’s Encrypt, and provision a DNS TXT record with the response via the DNS hook and the API. When validated the certificates will be placed in the <code>certs</code> directory, and from there they can be distributed to the appropriate applications. The certificates will be valid for 90 days.</p>
<h3 id="updating-nginx">Updating <code>nginx</code></h3>
<p>The following snippet from a <code>server</code> block in the <code>nginx</code> configuration shows how <code>nginx</code> will be configured to use the newly generated SSL certificates and keys.</p>

    <div class="admonition important">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24l0 112c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-112c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>
        <span>Important</span>
      </div>
      <div class="admonition-content">
        <p>This is a snippet from one of the <code>server</code> blocks, and is not comprehensive. It just shows an example of how the SSL configuration can be deployed within the <code>server</code> block.
In this snippet, we&rsquo;re pointing to the multi-domain cert we created.</p>
      </div>
    </div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#ae81ff">443</span> <span style="color:#e6db74">ssl</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:443</span> <span style="color:#e6db74">ssl</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server_name</span> <span style="color:#e6db74">dev.company.com</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ssl_certificate</span> <span style="color:#e6db74">/etc/dehydrated/certs/company.com/fullchain.pem</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ssl_certificate_key</span> <span style="color:#e6db74">/etc/dehydrated/certs/company.com/privkey.pem</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ssl_protocols</span> <span style="color:#e6db74">TLSv1.2</span> <span style="color:#e6db74">TLSv1.3</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ssl_ciphers</span> <span style="color:#e6db74">&#39;EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH&#39;</span>;
</span></span></code></pre></div><p><code>nginx</code> can then be reloaded with <code>sudo systemctl reload nginx</code>. Once reloaded, services hosted by the <code>server</code> block should use the newly generated SSL certs, and are now hosted securely. These certificates expire in 90 days, so the process of certificate renewal should be automated.</p>
<h3 id="automating-certificate-renewal">Automating certificate renewal</h3>
<p>Place the following entry into the <code>root</code> user&rsquo;s crontab:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#ae81ff">0</span> <span style="color:#ae81ff">0</span>   *  *  *     <span style="color:#f92672">(</span>/etc/dehydrated/dehydrated -c<span style="color:#f92672">)</span> 2&gt;&amp;<span style="color:#ae81ff">1</span> | logger -t dehydrated
</span></span></code></pre></div><p>This should run the <code>dehydrated</code> client every night at 12AM, and the log output can be captured using <code>cat /var/log/syslog | grep dehydrated</code>.</p>
<p>This CRON jon checks the expiry date of the current certificate, and if the lifetime on these certificates is less than 30 days, it will renew them with Let&rsquo;s Encrypt.  The hook should also reload the <code>nginx</code> config to use the new certs after they&rsquo;ve been obtained.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>Even though this CRON job runs <code>dehydrated</code> every night, the Let&rsquo;s Encrypt service <strong>does not get queried immediately</strong>.
<code>dehydrated</code> checks the expiry date of the current certificates, and only makes a request to Let&rsquo;s Encrypt
if the certificate validity is <strong>less than 30 days</strong>.</p>
<p>This approach ensures that <code>dehydrated</code> doesn&rsquo;t indavertently get you rate-limited by Let&rsquo;s Encrypt.</p>
      </div>
    </div><h1 id="citations">Citations</h1>
<ul>
<li>Bryan, B. (2016, September 12). <strong>Intranet SSL Certificates Using Let’s Encrypt | DNS-01.</strong> <a href="https://b3n.org/intranet-ssl-certificates-using-lets-encrypt-dns-01/">https://b3n.org/intranet-ssl-certificates-using-lets-encrypt-dns-01/</a></li>
</ul>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>The inspiration and guide for this blog post is this <a href="https://b3n.org/intranet-ssl-certificates-using-lets-encrypt-dns-01/">blog post by Benjamin Bryan</a> that was published on September 12, 2016.
Ben&rsquo;s <a href="https://b3n.org/">blog is excellent</a> and he writes on a lot of interesting topics.</p>
<p>I highly recommend that you go check out his <a href="https://b3n.org/intranet-ssl-certificates-using-lets-encrypt-dns-01/">original blog post,</a> which is what I followed while setting up the scripts and automations that I discussed in this blog.</p>
<p>This post would have been impossible without the original subject matter as a guide!</p>
      </div>
    </div><h1 id="references">References</h1>
<ul>
<li>The <a href="https://letsencrypt.org/">Let&rsquo;s Encrypt Website.</a></li>
<li>Let&rsquo;s Encrypt&rsquo;s <a href="https://letsencrypt.org/docs/challenge-types/">documentation on Challenge Types.</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc8555#section-8.4">RFC8555 documentation on DNS-01 challenges.</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc8555#section-8.3">RFC8555 documentation on HTTP-01 challenges.</a></li>
<li>Let&rsquo;s Encrypt&rsquo;s <a href="https://letsencrypt.org/docs/client-options/">documentation on recommended ACME clients.</a></li>
<li>The <a href="https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment">ACME protocol on Wikipedia.</a></li>
<li>The <a href="https://github.com/dehydrated-io/dehydrated">dehydrated ACME client.</a></li>
<li>The <a href="https://github.com/wdouglascampbell/dehydrated_namecheap_dns_api_hook">repo for the NameCheap DNS hook.</a></li>
</ul>
]]></content:encoded></item><item><title>PostgreSQL Entity type Version Control with Alembic Utils.</title><link>https://naveenkannan.dev/posts/alembic_utils_introduction/</link><pubDate>Sun, 08 Feb 2026 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/alembic_utils_introduction/</guid><description>Committing PostgreSQL entity types to version control using Alembic Utils.</description><content:encoded><![CDATA[<h1 id="introduction">Introduction</h1>
<p>PostgreSQL (and other RDBMS) have entity types such as <a href="https://www.postgresql.org/docs/current/sql-createfunction.html">functions</a>, <a href="https://www.postgresql.org/docs/current/sql-createview.html">views</a>, <a href="https://www.postgresql.org/docs/current/sql-creatematerializedview.html">materialized views</a>, <a href="https://www.postgresql.org/docs/current/sql-createtrigger.html">triggers</a>, and <a href="https://www.postgresql.org/docs/current/sql-createpolicy.html">policies</a>. These entity types do a lot of useful work, and a well designed database will use them heavily.</p>
<p>These entity types are not typically considered part of the SQLAlchemy ORM, and by extension, the data model itself. By default, Alembic has no functionality to detect the creation of new entity types when it autogenerates migration files. The default Alembic ORM also has no functionality to define these entity types with classes.</p>
<p>However, these entity types are part of a database&rsquo;s schema. Creation of/changes to them should be accounted for within the ORM and as part of migration version control, in much the same way as changes in a table are recorded in an ORM as part of migration version control.</p>
<p>In this blog post, we will pick up from the progress in the previous entry in this series and go over how we can use an extension to Alembic called <a href="https://github.com/olirice/alembic_utils">Alembic Utils</a> to add support for autogenerating migrations for some PostgreSQL entity types.</p>
<h2 id="what-weve-already-covered">What we&rsquo;ve already covered</h2>
<p>In <a href="/posts/sqlalchemy_data_modeling/">part 1:</a></p>
<ul>
<li>We used SQLAlchemy to create the first iteration of a rudimentary data model, with three tables.</li>
<li>We configured Alembic to connect to a fresh development database and migrated our rudimentary data model.</li>
</ul>
<p>In <a href="/posts/alembic_model_expansion/">part 2:</a></p>
<ul>
<li>We created a <strong>many-to-many</strong> association between orders and products, and a <strong>many-to-many</strong> association between products and tags.</li>
<li>We generated migration scripts using Alembic.</li>
<li>We committed the migration scripts to version control.</li>
</ul>
<figure class="align-center ">
    <img loading="lazy" src="../alembic_model_expansion/erd_diagram_producttags.png#center"
         alt="The data model at the end of the previous post."/> <figcaption>
            <p>The data model at the end of the previous post.</p>
        </figcaption>
</figure>

<h2 id="what-well-cover">What we&rsquo;ll cover</h2>
<p>In this blog post, we will cover how you can install Alembic Utils and begin incorporating entity types into the commerce data model.</p>
<p>We will do the following:</p>
<ul>
<li>Update the virtual environment with Alembic Utils.</li>
<li>Configure Alembic to use the newly installed extension.</li>
<li>Create a function and a view in the ORM.</li>
<li>Register these entities with Alembic Utils.</li>
<li>Autogenerate a migration script with Alembic that we will review, and then apply the revisions.</li>
</ul>
<h1 id="installing-alembic-utils">Installing Alembic Utils</h1>
<p>In <a href="/posts/sqlalchemy_data_modeling/#prerequisites">part 1</a> of this series, we created a micromamba virtual environment called <code>data_model</code> with SQLAlchemy and Alembic installed.</p>
<p>We will activate this virtual envrionment and then install Alembic Utils.</p>
<p>First, activate the virtual environment.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>micromamba activate data_model
</span></span></code></pre></div><p>Then, install the Alembic Utils extension within the environment. The documentation recommends using <code>pip</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pip install alembic_utils
</span></span></code></pre></div><p>Export the new environment details using the following command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>micromamba env export &gt; environment.yaml
</span></span></code></pre></div>
    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>Updating your <code>environment.yaml</code> file any time your dependencies change is good practice.</p>
<p>It makes your virtual environment reproducible and accessible!</p>
      </div>
    </div><h1 id="adding-a-view-to-the-database">Adding a view to the database</h1>
<h2 id="creating-a-view-in-the-orm">Creating a View in the ORM</h2>
<p>To explain what a view is, we can start by asking a straightforward high level question. What are the names of the products associated with a particular order? The stakeholders who have access to data might need to be able to access the products associated with a single specific order, or a group of specific orders.</p>
<p>In our database, there is a table called <code>order_products</code> that maps the associations between <code>orders</code> and <code>products</code> using the primary keys from that table. However, just looking at this table alone won&rsquo;t give us the contextual information we need. We will need to write an SQL query to join the <code>order_products</code> table with the <code>products</code> table in order to retrieve the name and description of a particular order&rsquo;s products.</p>
<p>This is a query that can be easily <strong>generalizable</strong>. A query can be written to join <strong>all</strong> the products referenced in <code>order_products</code> with the relevant product data from <code>products</code>. This query can then be filtered to return the associations with a single order or group of orders.</p>
<p>This query can be abstracted into a view. Views are named queries that allow for data to be queried from it <strong>like a regular table</strong>. The view is not physically stored, however. The query in the view is run each time the view itself is referenced in a query.</p>
<p>To return the product data associated with each <code>product_id</code> reference in <code>order_products</code>, we could write a query like this.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">select</span>
</span></span><span style="display:flex;"><span>  op.order_id,
</span></span><span style="display:flex;"><span>  op.product_id,
</span></span><span style="display:flex;"><span>  op.product_count,
</span></span><span style="display:flex;"><span>  p.product_name,
</span></span><span style="display:flex;"><span>  p.description,
</span></span><span style="display:flex;"><span>  p.price
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">from</span>
</span></span><span style="display:flex;"><span>  order_products op
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">join</span> products p <span style="color:#66d9ef">on</span> op.product_id <span style="color:#f92672">=</span> p.product_id
</span></span></code></pre></div><p>We can add an adjustment to this query to return the total price of each product associated with an order when accounting for the order count.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">select</span>
</span></span><span style="display:flex;"><span>  op.order_id,
</span></span><span style="display:flex;"><span>  op.product_id,
</span></span><span style="display:flex;"><span>  op.product_count,
</span></span><span style="display:flex;"><span>  p.product_name,
</span></span><span style="display:flex;"><span>  p.description,
</span></span><span style="display:flex;"><span>  p.price <span style="color:#66d9ef">as</span> unit_price,
</span></span><span style="display:flex;"><span>  op.product_count <span style="color:#f92672">*</span> p.price <span style="color:#66d9ef">as</span> order_price
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">from</span>
</span></span><span style="display:flex;"><span>  order_products op
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">join</span> products p <span style="color:#66d9ef">on</span> op.product_id <span style="color:#f92672">=</span> p.product_id
</span></span></code></pre></div><p>We multiply the product count with the unit price to get the total cost of the product within an order. With this query, we can now filter to a specific set of <code>order_id</code> values to return all the products associated with those orders and their total cost!</p>
<p>Creating a view from a query like this, that has the potential to be referenced multiple times, is great database design. We can now add this view to our ORM.</p>
<p>First, we will create a script called <code>db_views.py</code> in the root of the project. This script will be where we store the views that we create for our ORM.</p>
<p><code>db_views.py</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic_utils.pg_view <span style="color:#f92672">import</span> PGView
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>order_line_items <span style="color:#f92672">=</span> PGView(
</span></span><span style="display:flex;"><span>    schema<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public&#34;</span>,
</span></span><span style="display:flex;"><span>    signature<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;order_line_items&#34;</span>,
</span></span><span style="display:flex;"><span>    definition<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">select op.order_id ,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">	op.product_id,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">	op.product_count,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">	p.product_name,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">	p.description,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">	p.price as unit_price,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">	op.product_count * p.price as order_price
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">	from order_products op
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">join products p
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">on op.product_id = p.product_id
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>With this code, we first create a <code>PGView</code> class object called <code>order_line_items</code>. This defines a PostgreSQL view also called <code>order_line_items</code> that references the query that we wrote to return the line items (products) associated with orders.</p>
<h2 id="registering-the-newly-defined-entity">Registering the newly defined entity</h2>
<p>We will update <code>alembic/env.py</code> to then register our newly created entity with alembic_utils.</p>
<p><code>alembic/env.py</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> logging.config <span style="color:#f92672">import</span> fileConfig
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> engine_from_config
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> pool
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic_utils.replaceable_entity <span style="color:#f92672">import</span> register_entities
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> context
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> models <span style="color:#f92672">import</span> Base
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#f92672">from</span> db_views <span style="color:#f92672">import</span> order_line_items
</span></span><span style="display:flex; background-color:#3c3d38"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>register_entities([order_line_items])
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># this is the Alembic Config object, which provides</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># access to the values within the .ini file in use.</span>
</span></span><span style="display:flex;"><span>config <span style="color:#f92672">=</span> context<span style="color:#f92672">.</span>config
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Interpret the config file for Python logging.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># This line sets up loggers basically.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> config<span style="color:#f92672">.</span>config_file_name <span style="color:#f92672">is</span> <span style="color:#f92672">not</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    fileConfig(config<span style="color:#f92672">.</span>config_file_name)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># add your model&#39;s MetaData object here</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># for &#39;autogenerate&#39; support</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># from myapp import mymodel</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># target_metadata = mymodel.Base.metadata</span>
</span></span><span style="display:flex;"><span>target_metadata <span style="color:#f92672">=</span> Base<span style="color:#f92672">.</span>metadata
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># other values from the config, defined by the needs of env.py,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># can be acquired:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># my_important_option = config.get_main_option(&#34;my_important_option&#34;)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ... etc.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">run_migrations_offline</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Run migrations in &#39;offline&#39; mode.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    This configures the context with just a URL
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and not an Engine, though an Engine is acceptable
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    here as well.  By skipping the Engine creation
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    we don&#39;t even need a DBAPI to be available.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Calls to context.execute() here emit the given string to the
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    script output.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    url <span style="color:#f92672">=</span> config<span style="color:#f92672">.</span>get_main_option(<span style="color:#e6db74">&#34;sqlalchemy.url&#34;</span>)
</span></span><span style="display:flex;"><span>    context<span style="color:#f92672">.</span>configure(
</span></span><span style="display:flex;"><span>        url<span style="color:#f92672">=</span>url,
</span></span><span style="display:flex;"><span>        target_metadata<span style="color:#f92672">=</span>target_metadata,
</span></span><span style="display:flex;"><span>        literal_binds<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>        dialect_opts<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;paramstyle&#34;</span>: <span style="color:#e6db74">&#34;named&#34;</span>},
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> context<span style="color:#f92672">.</span>begin_transaction():
</span></span><span style="display:flex;"><span>        context<span style="color:#f92672">.</span>run_migrations()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">run_migrations_online</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Run migrations in &#39;online&#39; mode.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    In this scenario we need to create an Engine
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and associate a connection with the context.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    connectable <span style="color:#f92672">=</span> engine_from_config(
</span></span><span style="display:flex;"><span>        config<span style="color:#f92672">.</span>get_section(config<span style="color:#f92672">.</span>config_ini_section, {}),
</span></span><span style="display:flex;"><span>        prefix<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;sqlalchemy.&#34;</span>,
</span></span><span style="display:flex;"><span>        poolclass<span style="color:#f92672">=</span>pool<span style="color:#f92672">.</span>NullPool,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> connectable<span style="color:#f92672">.</span>connect() <span style="color:#66d9ef">as</span> connection:
</span></span><span style="display:flex;"><span>        context<span style="color:#f92672">.</span>configure(connection<span style="color:#f92672">=</span>connection, target_metadata<span style="color:#f92672">=</span>target_metadata)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">with</span> context<span style="color:#f92672">.</span>begin_transaction():
</span></span><span style="display:flex;"><span>            context<span style="color:#f92672">.</span>run_migrations()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> context<span style="color:#f92672">.</span>is_offline_mode():
</span></span><span style="display:flex;"><span>    run_migrations_offline()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>    run_migrations_online()
</span></span></code></pre></div><p>The additions to the code import the newly defined view and register it. This view should now be detected by <code>alembic revision --autogenerate</code>.</p>
<h2 id="autogenerating-a-migration">Autogenerating a migration</h2>
<p>We now autogenerate a revision with the command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic revision --autogenerate -m <span style="color:#e6db74">&#34;Created a view called order_line_items that returns order line items.&#34;</span>
</span></span></code></pre></div><p>This should create a new migration script located at <code>/path_to_your_project/alembic/versions</code>.</p>
<p><code>/path_to_your_project/alembic/versions/e2ba9d70fc4d_created_a_view_called_order_line_items_.py</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;Created a view called order_line_items that returns order line items.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revision ID: e2ba9d70fc4d
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revises: 6e58a26e5c70
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Create Date: 2026-02-07 19:26:28.816521
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Sequence, Union
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> op
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sqlalchemy <span style="color:#66d9ef">as</span> sa
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic_utils.pg_view <span style="color:#f92672">import</span> PGView
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> text <span style="color:#66d9ef">as</span> sql_text
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># revision identifiers, used by Alembic.</span>
</span></span><span style="display:flex;"><span>revision: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;e2ba9d70fc4d&#39;</span>
</span></span><span style="display:flex;"><span>down_revision: Union[str, <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;6e58a26e5c70&#39;</span>
</span></span><span style="display:flex;"><span>branch_labels: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>depends_on: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">upgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    public_order_line_items <span style="color:#f92672">=</span> PGView(
</span></span><span style="display:flex;"><span>        schema<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public&#34;</span>,
</span></span><span style="display:flex;"><span>        signature<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;order_line_items&#34;</span>,
</span></span><span style="display:flex;"><span>        definition<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;select op.order_id , </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">op.product_id, </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">op.product_count,</span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">p.product_name, </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">p.description, </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">p.price as unit_price, </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">op.product_count * p.price as order_price</span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">from order_products op </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">join products p </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">on op.product_id = p.product_id&#39;</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>create_entity(public_order_line_items)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">downgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    public_order_line_items <span style="color:#f92672">=</span> PGView(
</span></span><span style="display:flex;"><span>        schema<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public&#34;</span>,
</span></span><span style="display:flex;"><span>        signature<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;order_line_items&#34;</span>,
</span></span><span style="display:flex;"><span>        definition<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;select op.order_id , </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">op.product_id, </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">op.product_count,</span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">p.product_name, </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">p.description, </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">p.price as unit_price, </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">op.product_count * p.price as order_price</span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">from order_products op </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">join products p </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">on op.product_id = p.product_id&#39;</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_entity(public_order_line_items)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</span>
</span></span></code></pre></div><p>This migration script now creates a PostgreSQL view with the name <code>order_line_items</code>.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>The <code>PGView</code> object here is called <code>public_order_line_items</code> as the autogenerate function uses the schema name as a prefix to the object name. In this case, the schema used is <code>public</code>.</p>
      </div>
    </div><h2 id="applying-the-migration">Applying the migration</h2>
<p>We now apply the migration with the command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic upgrade head
</span></span></code></pre></div><p>Connecting to the database shows us that the view has been migrated to the database.</p>
<figure class="align-center ">
    <img loading="lazy" src="erd_diagram_view.png#center"
         alt="The newly migrated view."/> <figcaption>
            <p>The newly migrated view.</p>
        </figcaption>
</figure>

<h1 id="expanding-the-data-models-scope">Expanding the data model&rsquo;s scope</h1>
<p>We would like to now expand the data model to track when the attributes of a particular product have been edited. To do this, we will do the following:</p>
<ul>
<li>Add a <code>last_edited</code> timestamp column to the <code>products</code> table.</li>
<li>Create a function that will update the <code>last_edited</code> column of a table.</li>
<li>Create a trigger that will call the aforementioned function whenever a row of <code>products</code> is edited.</li>
</ul>
<h2 id="adding-a-timestamp-column-to-the-products-table">Adding a timestamp column to the <code>products</code> table</h2>
<p>Amend the data model in <code>models.py</code> to have a <code>last_edited</code> timestamp column.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>    Column,
</span></span><span style="display:flex;"><span>    Integer,
</span></span><span style="display:flex;"><span>    String,
</span></span><span style="display:flex;"><span>    Float,
</span></span><span style="display:flex;"><span>    ForeignKey,
</span></span><span style="display:flex;"><span>    Identity,
</span></span><span style="display:flex;"><span>    Text,
</span></span><span style="display:flex;"><span>    DateTime,
</span></span><span style="display:flex;"><span>    Table,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.orm <span style="color:#f92672">import</span> declarative_base, relationship
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.sql <span style="color:#f92672">import</span> func
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Base <span style="color:#f92672">=</span> declarative_base()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Customer</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;customers&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    first_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    last_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    email_address <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Order&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;customer&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">OrderProduct</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;order_products&#34;</span>
</span></span><span style="display:flex;"><span>    order_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer,
</span></span><span style="display:flex;"><span>        ForeignKey(<span style="color:#e6db74">&#34;orders.order_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex;"><span>        primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    product_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer,
</span></span><span style="display:flex;"><span>        ForeignKey(<span style="color:#e6db74">&#34;products.product_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex;"><span>        primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    product_count <span style="color:#f92672">=</span> Column(Integer, server_default<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;1&#34;</span>, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    order <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Order&#34;</span>, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;products&#34;</span>)
</span></span><span style="display:flex;"><span>    product <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Product&#34;</span>, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Order</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;orders&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    order_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer, ForeignKey(<span style="color:#e6db74">&#34;customers.customer_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    order_date <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        DateTime(timezone<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>),
</span></span><span style="display:flex;"><span>        server_default<span style="color:#f92672">=</span>func<span style="color:#f92672">.</span>now(),
</span></span><span style="display:flex;"><span>        nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    customer <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Customer&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    products <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;OrderProduct&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;order&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>product_tags <span style="color:#f92672">=</span> Table(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;product_tags&#34;</span>,
</span></span><span style="display:flex;"><span>    Base<span style="color:#f92672">.</span>metadata,
</span></span><span style="display:flex;"><span>    Column(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;product_id&#34;</span>,
</span></span><span style="display:flex;"><span>        ForeignKey(<span style="color:#e6db74">&#34;products.product_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex;"><span>        primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    ),
</span></span><span style="display:flex;"><span>    Column(<span style="color:#e6db74">&#34;tag_id&#34;</span>, ForeignKey(<span style="color:#e6db74">&#34;tags.tag_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>),
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Product</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;products&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    product_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    product_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    description <span style="color:#f92672">=</span> Column(Text)
</span></span><span style="display:flex;"><span>    price <span style="color:#f92672">=</span> Column(Float, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex; background-color:#3c3d38"><span>    last_edited <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex; background-color:#3c3d38"><span>        DateTime(timezone<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), server_default<span style="color:#f92672">=</span>func<span style="color:#f92672">.</span>now(), onupdate<span style="color:#f92672">=</span>func<span style="color:#f92672">.</span>now()
</span></span><span style="display:flex; background-color:#3c3d38"><span>    )
</span></span><span style="display:flex;"><span>    orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;OrderProduct&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;product&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    tags <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Tag&#34;</span>, secondary<span style="color:#f92672">=</span>product_tags, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;products&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Tag</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tags&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    tag_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>, unique<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    products <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Product&#34;</span>, secondary<span style="color:#f92672">=</span>product_tags, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;tags&#34;</span>)
</span></span></code></pre></div>
    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>The <code>onupdate=func.now()</code> parameter for <code>last_edited</code> will update the column when changes are issued through the ORM.</p>
<p>To guarantee database level correctness, we will need to create a trigger as well to guarantee data accuracy
even when changes come from outside of any applications that use the ORM.</p>
      </div>
    </div><p>Generate a revision with <code>alembic --autogenerate</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;Added a column to products to track timestamps for row-level edits.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revision ID: eebe110ef9d2
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revises: e2ba9d70fc4d
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Create Date: 2026-02-08 16:29:55.536261
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Sequence, Union
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> op
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sqlalchemy <span style="color:#66d9ef">as</span> sa
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># revision identifiers, used by Alembic.</span>
</span></span><span style="display:flex;"><span>revision: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;eebe110ef9d2&#39;</span>
</span></span><span style="display:flex;"><span>down_revision: Union[str, <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;e2ba9d70fc4d&#39;</span>
</span></span><span style="display:flex;"><span>branch_labels: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>depends_on: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">upgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>add_column(<span style="color:#e6db74">&#39;products&#39;</span>, sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;last_edited&#39;</span>, sa<span style="color:#f92672">.</span>DateTime(timezone<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), server_default<span style="color:#f92672">=</span>sa<span style="color:#f92672">.</span>text(<span style="color:#e6db74">&#39;now()&#39;</span>), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">downgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_column(<span style="color:#e6db74">&#39;products&#39;</span>, <span style="color:#e6db74">&#39;last_edited&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</span>
</span></span></code></pre></div><p>We will then apply this migration with the <code>alembic upgrade head</code> command.</p>
<h2 id="creating-a-function-to-update-the-last_edited-column-of-products">Creating a function to update the <code>last_edited</code> column of <code>products</code></h2>
<p>Create a script called <code>db_functions.py</code> in the project root.</p>
<p><code>db_functions.py</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic_utils.pg_function <span style="color:#f92672">import</span> PGFunction
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Creating a function that updates the `last_edited` column of a table</span>
</span></span><span style="display:flex;"><span>timestamp_update_on_edit <span style="color:#f92672">=</span> PGFunction(
</span></span><span style="display:flex;"><span>    schema<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public&#34;</span>,
</span></span><span style="display:flex;"><span>    signature<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;timestamp_update_on_edit()&#34;</span>,
</span></span><span style="display:flex;"><span>    definition<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">RETURNS TRIGGER AS $$ BEGIN NEW.last_edited = now();
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">RETURN NEW;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">END;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">$$ language PLPGSQL
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>With this code, we create a <code>PGFunction</code> class object that defines a PostgreSQL function called <code>timestamp_update_on_edit()</code>. When this function is called, the <code>last_edited</code> column is updated to have the time and date of the point in time when it was called.</p>
<h2 id="creating-a-trigger-to-call-the-timestamp-function-on-row-edits">Creating a trigger to call the timestamp function on row edits</h2>
<p>Create a script called <code>db_triggers.py</code> in the project root.</p>
<p><code>db_triggers.py</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic_utils.pg_trigger <span style="color:#f92672">import</span> PGTrigger
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Creating a trigger to apply `timestamp_update_on_edit()` to `products` on a row being updated</span>
</span></span><span style="display:flex;"><span>timestamp_update_apply_products_trigger <span style="color:#f92672">=</span> PGTrigger(
</span></span><span style="display:flex;"><span>    schema<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public&#34;</span>,
</span></span><span style="display:flex;"><span>    signature<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;timestamp_update_apply_products_trigger&#34;</span>,
</span></span><span style="display:flex;"><span>    on_entity<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public.products&#34;</span>,
</span></span><span style="display:flex;"><span>    is_constraint<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span>    definition<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">BEFORE
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">UPDATE
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  on products FOR EACH ROW EXECUTE FUNCTION timestamp_update_on_edit()
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>This code defines a <code>PGTrigger</code> class object with the signature <code>timestamp_update_apply_products_trigger</code>. When a row is updated on <code>products</code>, this trigger is activated and calls the <code>timestamp_update_on_edit()</code> function, which will update the <code>last_edited</code> column.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>In the ORM, as part of the definition of the <code>Product</code> object, the <code>onupdate=func.now()</code> parameter
updates the <code>last_edited</code> column when changes are issued through the ORM.</p>
<p>When we create a trigger to perform the timestamp update for <code>products</code> in the database upon a row being edited,
this guarantees correctness on a database level even when the changes come outside of the application.</p>
      </div>
    </div><h2 id="registering-the-newly-defined-entities">Registering the newly defined entities</h2>
<p>We will update <code>alembic/env.py</code> to then register our newly created entities with alembic_utils.</p>
<p><code>alembic/env.py</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> logging.config <span style="color:#f92672">import</span> fileConfig
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> engine_from_config
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> pool
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic_utils.replaceable_entity <span style="color:#f92672">import</span> register_entities
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> context
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> models <span style="color:#f92672">import</span> Base
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> db_views <span style="color:#f92672">import</span> order_line_items
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#f92672">from</span> db_functions <span style="color:#f92672">import</span> timestamp_update_on_edit
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#f92672">from</span> db_triggers <span style="color:#f92672">import</span> timestamp_update_apply_products_trigger
</span></span><span style="display:flex; background-color:#3c3d38"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>register_entities(
</span></span><span style="display:flex; background-color:#3c3d38"><span>    [
</span></span><span style="display:flex; background-color:#3c3d38"><span>        order_line_items,
</span></span><span style="display:flex; background-color:#3c3d38"><span>        timestamp_update_on_edit,
</span></span><span style="display:flex; background-color:#3c3d38"><span>        timestamp_update_apply_products_trigger,
</span></span><span style="display:flex; background-color:#3c3d38"><span>    ]
</span></span><span style="display:flex; background-color:#3c3d38"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># this is the Alembic Config object, which provides</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># access to the values within the .ini file in use.</span>
</span></span><span style="display:flex;"><span>config <span style="color:#f92672">=</span> context<span style="color:#f92672">.</span>config
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Interpret the config file for Python logging.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># This line sets up loggers basically.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> config<span style="color:#f92672">.</span>config_file_name <span style="color:#f92672">is</span> <span style="color:#f92672">not</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    fileConfig(config<span style="color:#f92672">.</span>config_file_name)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># add your model&#39;s MetaData object here</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># for &#39;autogenerate&#39; support</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># from myapp import mymodel</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># target_metadata = mymodel.Base.metadata</span>
</span></span><span style="display:flex;"><span>target_metadata <span style="color:#f92672">=</span> Base<span style="color:#f92672">.</span>metadata
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># other values from the config, defined by the needs of env.py,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># can be acquired:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># my_important_option = config.get_main_option(&#34;my_important_option&#34;)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ... etc.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">run_migrations_offline</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Run migrations in &#39;offline&#39; mode.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    This configures the context with just a URL
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and not an Engine, though an Engine is acceptable
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    here as well.  By skipping the Engine creation
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    we don&#39;t even need a DBAPI to be available.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Calls to context.execute() here emit the given string to the
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    script output.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    url <span style="color:#f92672">=</span> config<span style="color:#f92672">.</span>get_main_option(<span style="color:#e6db74">&#34;sqlalchemy.url&#34;</span>)
</span></span><span style="display:flex;"><span>    context<span style="color:#f92672">.</span>configure(
</span></span><span style="display:flex;"><span>        url<span style="color:#f92672">=</span>url,
</span></span><span style="display:flex;"><span>        target_metadata<span style="color:#f92672">=</span>target_metadata,
</span></span><span style="display:flex;"><span>        literal_binds<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>        dialect_opts<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;paramstyle&#34;</span>: <span style="color:#e6db74">&#34;named&#34;</span>},
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> context<span style="color:#f92672">.</span>begin_transaction():
</span></span><span style="display:flex;"><span>        context<span style="color:#f92672">.</span>run_migrations()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">run_migrations_online</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Run migrations in &#39;online&#39; mode.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    In this scenario we need to create an Engine
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and associate a connection with the context.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    connectable <span style="color:#f92672">=</span> engine_from_config(
</span></span><span style="display:flex;"><span>        config<span style="color:#f92672">.</span>get_section(config<span style="color:#f92672">.</span>config_ini_section, {}),
</span></span><span style="display:flex;"><span>        prefix<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;sqlalchemy.&#34;</span>,
</span></span><span style="display:flex;"><span>        poolclass<span style="color:#f92672">=</span>pool<span style="color:#f92672">.</span>NullPool,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> connectable<span style="color:#f92672">.</span>connect() <span style="color:#66d9ef">as</span> connection:
</span></span><span style="display:flex;"><span>        context<span style="color:#f92672">.</span>configure(connection<span style="color:#f92672">=</span>connection, target_metadata<span style="color:#f92672">=</span>target_metadata)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">with</span> context<span style="color:#f92672">.</span>begin_transaction():
</span></span><span style="display:flex;"><span>            context<span style="color:#f92672">.</span>run_migrations()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> context<span style="color:#f92672">.</span>is_offline_mode():
</span></span><span style="display:flex;"><span>    run_migrations_offline()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>    run_migrations_online()
</span></span></code></pre></div><p>With this update, our newly created function and trigger have been registered. They should now be detected when using <code>autogenerate</code>.</p>
<h2 id="autogenerating-and-applying-a-revision-to-migrate-the-newly-added-entities">Autogenerating and applying a revision to migrate the newly added entities</h2>
<p>Create a revision script with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic revision --autogenerate -m <span style="color:#e6db74">&#34;Created a function to update the last_edited timestamp column, and created a trigger to call this function for the products table.&#34;</span>
</span></span></code></pre></div><p>This should generate a revision file with the following content:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;Created a function to update the last_edited timestamp column, and created a trigger to call this function for the products table.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revision ID: 172f9e67a364
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revises: eebe110ef9d2
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Create Date: 2026-02-08 16:41:47.178179
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Sequence, Union
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> op
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sqlalchemy <span style="color:#66d9ef">as</span> sa
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic_utils.pg_function <span style="color:#f92672">import</span> PGFunction
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> text <span style="color:#66d9ef">as</span> sql_text
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic_utils.pg_trigger <span style="color:#f92672">import</span> PGTrigger
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> text <span style="color:#66d9ef">as</span> sql_text
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># revision identifiers, used by Alembic.</span>
</span></span><span style="display:flex;"><span>revision: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;172f9e67a364&#39;</span>
</span></span><span style="display:flex;"><span>down_revision: Union[str, <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;eebe110ef9d2&#39;</span>
</span></span><span style="display:flex;"><span>branch_labels: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>depends_on: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">upgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    public_timestamp_update_on_edit <span style="color:#f92672">=</span> PGFunction(
</span></span><span style="display:flex;"><span>        schema<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public&#34;</span>,
</span></span><span style="display:flex;"><span>        signature<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;timestamp_update_on_edit()&#34;</span>,
</span></span><span style="display:flex;"><span>        definition<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;RETURNS TRIGGER AS $$ BEGIN NEW.last_edited = now();</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">RETURN NEW;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">END;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">$$ language PLPGSQL&#39;</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>create_entity(public_timestamp_update_on_edit)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    public_products_timestamp_update_apply_products_trigger <span style="color:#f92672">=</span> PGTrigger(
</span></span><span style="display:flex;"><span>        schema<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public&#34;</span>,
</span></span><span style="display:flex;"><span>        signature<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;timestamp_update_apply_products_trigger&#34;</span>,
</span></span><span style="display:flex;"><span>        on_entity<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public.products&#34;</span>,
</span></span><span style="display:flex;"><span>        is_constraint<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span>        definition<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;BEFORE </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">UPDATE </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">  on products FOR EACH ROW EXECUTE FUNCTION timestamp_update_on_edit()&#39;</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>create_entity(public_products_timestamp_update_apply_products_trigger)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">downgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    public_products_timestamp_update_apply_products_trigger <span style="color:#f92672">=</span> PGTrigger(
</span></span><span style="display:flex;"><span>        schema<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public&#34;</span>,
</span></span><span style="display:flex;"><span>        signature<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;timestamp_update_apply_products_trigger&#34;</span>,
</span></span><span style="display:flex;"><span>        on_entity<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public.products&#34;</span>,
</span></span><span style="display:flex;"><span>        is_constraint<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span>        definition<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;BEFORE </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">UPDATE </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">  on products FOR EACH ROW EXECUTE FUNCTION timestamp_update_on_edit()&#39;</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_entity(public_products_timestamp_update_apply_products_trigger)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    public_timestamp_update_on_edit <span style="color:#f92672">=</span> PGFunction(
</span></span><span style="display:flex;"><span>        schema<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public&#34;</span>,
</span></span><span style="display:flex;"><span>        signature<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;timestamp_update_on_edit()&#34;</span>,
</span></span><span style="display:flex;"><span>        definition<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;RETURNS TRIGGER AS $$ BEGIN NEW.last_edited = now();</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">RETURN NEW;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">END;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">$$ language PLPGSQL&#39;</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_entity(public_timestamp_update_on_edit)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</span>
</span></span></code></pre></div><p>The migration script should create the function and the trigger that we defined in the ORM within the database. After reviewing it, we apply the migration with the command <code>alembic upgrade head</code>.</p>
<p>Once these changes have been migrated, run the following SQL code in the database to get the triggers associated with the <code>products</code> table.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span>    tgname <span style="color:#66d9ef">AS</span> <span style="color:#66d9ef">trigger_name</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span>
</span></span><span style="display:flex;"><span>    pg_trigger
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span>
</span></span><span style="display:flex;"><span>    tgrelid <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;public.products&#39;</span>::regclass
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">trigger_name</span>;
</span></span></code></pre></div><p>This should give you the output:</p>
<pre tabindex="0"><code>RI_ConstraintTrigger_a_16447
RI_ConstraintTrigger_a_16448
RI_ConstraintTrigger_a_16471
RI_ConstraintTrigger_a_16472
timestamp_update_apply_products_trigger
</code></pre><p>Indicating the trigger has been successfully migrated. Run the following code to get the functions associated with the <code>public</code> schema:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">routine_name</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span>
</span></span><span style="display:flex;"><span>    information_schema.routines
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span>
</span></span><span style="display:flex;"><span>    routine_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;FUNCTION&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">AND</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">routine_schema</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;public&#39;</span>;
</span></span></code></pre></div><p>This should give you the output:</p>
<pre tabindex="0"><code>routine_name
timestamp_update_on_edit
</code></pre><p>showing that our new function has been migrated to the database as well!</p>
<p>We&rsquo;ve successfully added a view, a function and a trigger to our database via migrations applied from our SQLAlchemy ORM.</p>
<p>Browsing through <code>alembic/versions</code> should give us an idea of the growth of our data model and how it evolves to match the change in scope of our e-commerce workflow.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic/versions/
</span></span><span style="display:flex;"><span>├── 172f9e67a364_created_a_function_to_update_the_last_.py
</span></span><span style="display:flex;"><span>├── 6e58a26e5c70_created_a_table_for_tags_and_created_a_.py
</span></span><span style="display:flex;"><span>├── 960969e75871_created_orderproduct_as_an_association_.py
</span></span><span style="display:flex;"><span>├── a0cf7f5d705d_initial_commit_created_customers_orders_.py
</span></span><span style="display:flex;"><span>├── e2ba9d70fc4d_created_a_view_called_order_line_items_.py
</span></span><span style="display:flex;"><span>└── eebe110ef9d2_added_a_column_to_products_to_track_.py
</span></span></code></pre></div><h1 id="next-steps">Next Steps</h1>
<p>In subsequent blog posts, we will cover:</p>
<ul>
<li>Applying our migrations to a production environment</li>
<li>Using the ORM as a module when coding a FastAPI server</li>
</ul>
<h1 id="references">References</h1>
<ul>
<li>The <a href="https://github.com/olirice/alembic_utils">repo for Alembic Utils.</a></li>
<li>The <a href="https://olirice.github.io/alembic_utils/">documentation for Alembic Utils.</a></li>
</ul>
]]></content:encoded></item><item><title>Data Model Version Control with Alembic.</title><link>https://naveenkannan.dev/posts/alembic_model_expansion/</link><pubDate>Mon, 26 Jan 2026 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/alembic_model_expansion/</guid><description>Developing version-controlled relational data models using Alembic.</description><content:encoded><![CDATA[<h1 id="introduction">Introduction</h1>
<p>As the scope of a data model (and by extension, its downstream APIs) changes, it will need to be updated and expanded to account for this new scope.</p>
<p>When changes are made to a data model, especially to a model that is split across development and production environments, schema drift becomes a constant problem that looms in the background.</p>
<p>Typical coding workflows are managed by version control as an industry standard. Database schemas, however, are typically not submitted to a version control system. As different team members collaborate on a data model split across different environments, the possibility of schema drift gradually grows in an environment where the data model is not tracked in a central repository.</p>
<p>This is where Alembic shines! It is the de-facto migration tool for SQLAlchemy, and is the ideal tool to version control a data model&rsquo;s development.</p>
<p>In this blog post, we will pick up from the progress in the previous entry in this series and continue to develop our data model. We will build more tables and relationships into our data model, and commit all of these changes to version control using Alembic.</p>
<h2 id="what-weve-already-covered">What we&rsquo;ve already covered</h2>
<p>In our <a href="/posts/sqlalchemy_data_modeling/">previous blog post:</a></p>
<ul>
<li>We used SQLAlchemy to create the first iteration of a rudimentary data model, with three tables.</li>
<li>We configured Alembic to connect to a fresh development database and migrated our rudimentary data model.</li>
</ul>
<p>Please refer to the first post in this series for a detailed explanation of SQLAlchemy and Alembic, and how it is relevant in our workflows here.</p>
<h2 id="what-well-cover-in-this-post">What we&rsquo;ll cover in this post</h2>
<ul>
<li>Creating a <strong>many-to-many</strong> association between orders and products.</li>
<li>Creating a <strong>many-to-many</strong> association between products and tags.</li>
<li>Committing all of our changes to version control with Alembic.</li>
</ul>
<h1 id="getting-started">Getting started</h1>
<p>At this point, we have a rudimentary data model with three tables, and only two relationships.</p>
<p>There&rsquo;s <code>orders</code>,<code>customers</code> and <code>products</code>, with a one-to-many relationship between <code>customers</code> and <code>orders</code>, and a many-to-one relationship between <code>orders</code> and <code>customers</code>.</p>
<figure class="align-center ">
    <img loading="lazy" src="../sqlalchemy_data_modeling/basic_data_model_erd.png#center"
         alt="Our initial data model."/> <figcaption>
            <p>Our initial data model.</p>
        </figcaption>
</figure>

<p>The classes for these tables in our ORM were defined as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Customer</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;customers&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    first_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    last_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    email_address <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Order&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;customer&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Order</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;orders&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    order_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer,
</span></span><span style="display:flex;"><span>        ForeignKey(<span style="color:#e6db74">&#34;customers.customer_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex;"><span>        nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    order_date <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        DateTime(timezone<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>),
</span></span><span style="display:flex;"><span>        server_default<span style="color:#f92672">=</span>func<span style="color:#f92672">.</span>now(),
</span></span><span style="display:flex;"><span>        nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    customer <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Customer&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Product</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;products&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    product_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    product_name <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    description <span style="color:#f92672">=</span> Column(Text)
</span></span><span style="display:flex;"><span>    price <span style="color:#f92672">=</span> Column(Float, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span></code></pre></div>
    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>We generated an initial migration, but right now, this migration environment lives locally. We need to push this to a GitHub repository!</p>
<p>The data model at the end of the first blog post has been pushed to GitHub and can be <a href="https://github.com/naveenk2022/commerce_data_model">found here</a>.</p>
<p>This repository will evolve along with the blog! Every change we make moving forward will be committed to version control.</p>
      </div>
    </div><p>There are a lot of changes we need to make to make this data model usable. To start with:</p>
<ul>
<li>A connection needs to be built between orders and products. A single order can have multiple products, and a single product can be associated with multiple orders.</li>
<li>Products need to be associated with tags to enable better categorization within the product catalog. Each product can be associated with multiple tags, and
a single tag can be associated with multiple products.</li>
</ul>

    <div class="admonition tip">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M272 384c9.6-31.9 29.5-59.1 49.2-86.2c0 0 0 0 0 0c5.2-7.1 10.4-14.2 15.4-21.4c19.8-28.5 31.4-63 31.4-100.3C368 78.8 289.2 0 192 0S16 78.8 16 176c0 37.3 11.6 71.9 31.4 100.3c5 7.2 10.2 14.3 15.4 21.4c0 0 0 0 0 0c19.8 27.1 39.7 54.4 49.2 86.2l160 0zM192 512c44.2 0 80-35.8 80-80l0-16-160 0 0 16c0 44.2 35.8 80 80 80zM112 176c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-61.9 50.1-112 112-112c8.8 0 16 7.2 16 16s-7.2 16-16 16c-44.2 0-80 35.8-80 80z"/></svg>
        <span>Tip</span>
      </div>
      <div class="admonition-content">
        <p>This change in relationship between orders and products is a <strong>many-to-many</strong> relationship!</p>
<p>When we expand our data model to include tags, the relationship between tags and products will also be a <strong>many-to-many</strong> relationship.</p>
      </div>
    </div><p>Let&rsquo;s begin implementing this change in scope within the data model. With each step, we will also commit our work to version control using Alembic.</p>
<h2 id="many-to-many-relationship-between-orders-and-products">Many-to-many relationship between <code>orders</code> and <code>products</code></h2>
<p>We want to build a many-to-many relationship between <code>orders</code> and <code>products</code>. We also want to expand this relationship to track the <strong>count</strong> of each product in an order.</p>
<p>To accomplish that, we will create an <strong>association table.</strong></p>

    <div class="admonition tip">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M272 384c9.6-31.9 29.5-59.1 49.2-86.2c0 0 0 0 0 0c5.2-7.1 10.4-14.2 15.4-21.4c19.8-28.5 31.4-63 31.4-100.3C368 78.8 289.2 0 192 0S16 78.8 16 176c0 37.3 11.6 71.9 31.4 100.3c5 7.2 10.2 14.3 15.4 21.4c0 0 0 0 0 0c19.8 27.1 39.7 54.4 49.2 86.2l160 0zM192 512c44.2 0 80-35.8 80-80l0-16-160 0 0 16c0 44.2 35.8 80 80 80zM112 176c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-61.9 50.1-112 112-112c8.8 0 16 7.2 16 16s-7.2 16-16 16c-44.2 0-80 35.8-80 80z"/></svg>
        <span>Tip</span>
      </div>
      <div class="admonition-content">
        <p>Association tables are also referred to as join tables, junction tables or cross-reference tables.</p>
      </div>
    </div><p>An association table maps two (or more) tables together by using their primary keys as foreign keys. Each foreign key forms a <strong>many-to-one</strong> relationship between the association table and the individual data table. An association table creates a two-way link between the tables that represent each entity.</p>
<p>The best way to demonstrate this is by example! Let&rsquo;s begin by creating an association table to group together the products associated with each order.</p>
<p>To generate an association table, the following changes need to be made to <code>models.py</code>. Additions have been highlighted.</p>
<p><strong><code>models.py</code></strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>    Column,
</span></span><span style="display:flex;"><span>    Integer,
</span></span><span style="display:flex;"><span>    String,
</span></span><span style="display:flex;"><span>    Float,
</span></span><span style="display:flex;"><span>    ForeignKey,
</span></span><span style="display:flex;"><span>    Identity,
</span></span><span style="display:flex;"><span>    Text,
</span></span><span style="display:flex;"><span>    DateTime,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.orm <span style="color:#f92672">import</span> declarative_base, relationship
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.sql <span style="color:#f92672">import</span> func
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Base <span style="color:#f92672">=</span> declarative_base()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Customer</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;customers&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    first_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    last_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    email_address <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Order&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;customer&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">OrderProduct</span>(Base):
</span></span><span style="display:flex; background-color:#3c3d38"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;order_products&#34;</span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>    order_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex; background-color:#3c3d38"><span>        Integer,
</span></span><span style="display:flex; background-color:#3c3d38"><span>        ForeignKey(<span style="color:#e6db74">&#34;orders.order_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex; background-color:#3c3d38"><span>        primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>    )
</span></span><span style="display:flex; background-color:#3c3d38"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>    product_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex; background-color:#3c3d38"><span>        Integer,
</span></span><span style="display:flex; background-color:#3c3d38"><span>        ForeignKey(<span style="color:#e6db74">&#34;products.product_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex; background-color:#3c3d38"><span>        primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>    )
</span></span><span style="display:flex; background-color:#3c3d38"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>    product_count <span style="color:#f92672">=</span> Column(Integer, server_default<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;1&#34;</span>, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex; background-color:#3c3d38"><span>    order <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Order&#34;</span>, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;products&#34;</span>)
</span></span><span style="display:flex; background-color:#3c3d38"><span>    product <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Product&#34;</span>, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Order</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;orders&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    order_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer, ForeignKey(<span style="color:#e6db74">&#34;customers.customer_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    order_date <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        DateTime(timezone<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>),
</span></span><span style="display:flex;"><span>        server_default<span style="color:#f92672">=</span>func<span style="color:#f92672">.</span>now(),
</span></span><span style="display:flex;"><span>        nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    customer <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Customer&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex; background-color:#3c3d38"><span>    products <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex; background-color:#3c3d38"><span>        <span style="color:#e6db74">&#34;OrderProduct&#34;</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;order&#34;</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Product</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;products&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    product_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    product_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    description <span style="color:#f92672">=</span> Column(Text)
</span></span><span style="display:flex;"><span>    price <span style="color:#f92672">=</span> Column(Float, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex; background-color:#3c3d38"><span>    orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex; background-color:#3c3d38"><span>        <span style="color:#e6db74">&#34;OrderProduct&#34;</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;product&#34;</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>    )
</span></span></code></pre></div><h3 id="creating-the-association-table">Creating the association table</h3>
<p>We create a class to define the association table, since the association table contains an additional column beyond just the associations with <code>Order</code> and <code>Product</code>. This table is <code>OrderProduct</code>.</p>
<p>The<code>order_id</code> and <code>product_id</code> columns in this table are designated as <strong>foreign keys</strong>.</p>
<p>We then create associations between <code>OrderProduct</code> and the entity tables using the <code>relationship</code> parameter. <code>OrderProducts</code> has relationships built to both <code>Order</code> and <code>Product</code>.</p>
<h3 id="explaining-the-relationship-between-order-and-product">Explaining the relationship between <code>Order</code> and <code>Product</code></h3>
<p>There is no <strong>direct  relationship</strong> between <code>Order</code> and <code>Product</code>. This relationship is mediated by the <code>OrderProduct</code> class, which is an association table that decomposes the relationship between <code>Order</code> and <code>Product</code>.</p>
<p>A single instance of an <code>Order</code> can belong to multiple rows of <code>OrderProduct</code>.</p>
<p>A single instance of a <code>Product</code> can belong to multiple rows of <code>OrderProduct</code>.</p>
<p>However, every instance of <code>OrderProduct</code> can only have:</p>
<ul>
<li>
<p>A <strong>single</strong> <code>Order</code></p>
</li>
<li>
<p>A <strong>single</strong> <code>Product</code>.</p>
</li>
</ul>
<p>In essence, the many-to-many relationship between <code>Order</code> and <code>Product</code> is abstracted into being indirect via <code>OrderProduct</code>. Each row in <code>OrderProduct</code> represents a single product within a single order, along with additional metadata about that association, such as the product count for that order.</p>
<p>The following code snippets establish this indirect relationship between <code>Order</code> and <code>Product</code>.</p>
<p><strong><code>OrderProduct</code></strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>    order <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Order&#34;</span>, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;products&#34;</span>)
</span></span><span style="display:flex;"><span>    product <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Product&#34;</span>, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</span>)
</span></span></code></pre></div><p><strong><code>Order</code></strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>products <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;OrderProduct&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;order&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span></code></pre></div><p><strong><code>Product</code></strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;OrderProduct&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;product&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span></code></pre></div><p><strong>For the <code>OrderProduct</code> Class, this code:</strong></p>
<ul>
<li>
<p>Creates a relationship called <code>order</code>, that points <strong>towards</strong> the <code>Order</code> class. This now establishes a many-to-one relationship between <code>OrderProduct</code> and <code>Order</code>. Each instance of <code>OrderProduct</code> references exactly <strong>one</strong> <code>Order</code>.</p>
</li>
<li>
<p>Creates a relationship called <code>product</code>, that points <strong>towards</strong> the <code>Product</code> class. This now establishes a many-to-one relationship between <code>OrderProduct</code> and <code>Product</code>. Each instance of <code>OrderProduct</code> references exactly <strong>one</strong> <code>Product</code>.</p>
</li>
</ul>
<p><strong>For the <code>Order</code> Class, this code:</strong></p>
<ul>
<li>Creates a relationship called <code>products</code>, that points <strong>towards</strong> the <code>OrderProduct</code> class. This now establishes a one-to-many relationship between <code>Order</code> and <code>OrderProducts</code>. A single <code>Order</code> can be referenced by <strong>multiple</strong> instances of <code>OrderProduct</code>.</li>
</ul>

    <div class="admonition tip">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M272 384c9.6-31.9 29.5-59.1 49.2-86.2c0 0 0 0 0 0c5.2-7.1 10.4-14.2 15.4-21.4c19.8-28.5 31.4-63 31.4-100.3C368 78.8 289.2 0 192 0S16 78.8 16 176c0 37.3 11.6 71.9 31.4 100.3c5 7.2 10.2 14.3 15.4 21.4c0 0 0 0 0 0c19.8 27.1 39.7 54.4 49.2 86.2l160 0zM192 512c44.2 0 80-35.8 80-80l0-16-160 0 0 16c0 44.2 35.8 80 80 80zM112 176c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-61.9 50.1-112 112-112c8.8 0 16 7.2 16 16s-7.2 16-16 16c-44.2 0-80 35.8-80 80z"/></svg>
        <span>Tip</span>
      </div>
      <div class="admonition-content">
        <p>Even though this relationship points to <code>OrderProduct</code>, it is named <code>products</code>.
This is intentional, and it refers to the conceptual representation of this relationship, not the literal schema itself.</p>
<ul>
<li>Conceptually:
An order has <strong>multiple products.</strong></li>
<li>Relationally:
An <code>Order</code> can be referenced by multiple <code>OrderProduct</code> rows.</li>
</ul>
<p>This relationship is named <code>products</code> to reflect the <em>conceptual</em> link between orders and products, and not the direct <em>relational</em> link between <code>Orders</code> and <code>OrderProducts</code>.</p>
<p>Remember that there is no <strong>direct</strong> connection between <code>Order</code> and <code>Product</code> in the data model!</p>
      </div>
    </div><p><strong>For the <code>Product</code> Class, this code:</strong></p>
<ul>
<li>
<p>Creates a relationship called <code>orders</code>, that points <strong>towards</strong> the <code>OrderProduct</code> class. This now establishes a one-to-many relationship between <code>Product</code> and <code>OrderProducts</code>. A single <code>Product</code> can be referenced by <strong>multiple</strong> instances of <code>OrderProduct</code>.</p>
</li>
<li>
<p>Similar to the <code>products</code> relationship defined in <code>Order</code>, this is a conceptual representation, which is why it is called <code>orders</code>.</p>
</li>
</ul>
<p>By wiring these connections, we abstract away the many-to-many relationship between <code>Orders</code> and <code>Products</code>.</p>

    <div class="admonition tip">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M272 384c9.6-31.9 29.5-59.1 49.2-86.2c0 0 0 0 0 0c5.2-7.1 10.4-14.2 15.4-21.4c19.8-28.5 31.4-63 31.4-100.3C368 78.8 289.2 0 192 0S16 78.8 16 176c0 37.3 11.6 71.9 31.4 100.3c5 7.2 10.2 14.3 15.4 21.4c0 0 0 0 0 0c19.8 27.1 39.7 54.4 49.2 86.2l160 0zM192 512c44.2 0 80-35.8 80-80l0-16-160 0 0 16c0 44.2 35.8 80 80 80zM112 176c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-61.9 50.1-112 112-112c8.8 0 16 7.2 16 16s-7.2 16-16 16c-44.2 0-80 35.8-80 80z"/></svg>
        <span>Tip</span>
      </div>
      <div class="admonition-content">
        <p>The <code>back_populates</code> parameter establishes a bidirectional link between the two ORM relationships. It ensures that objects on both sides of each relationship synchronize in-Python state changes.</p>
      </div>
    </div><h1 id="auto-generating-migrations">Auto-generating migrations</h1>
<p>We now have a end state of our data model defined in the ORM. To generate a migration, we need to allow Alembic to compare the current state of the database against the ORM, and have it automatically generate a migration script that we <strong>must review</strong> before applying.</p>
<p>We configured and set up Alembic in the previous blog post, so this time, we just need to run the following command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic revision --autogenerate -m <span style="color:#e6db74">&#34;Created OrderProduct as an association table to track Order-Product association.&#34;</span>
</span></span></code></pre></div><p>This should autogenerate a migration script, located at <code>/path_to_your_project/alembic/versions/960969e75871_created_orderproduct_as_an_association_.py</code>. The <code>versions</code> directory will have the following content:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic/versions/
</span></span><span style="display:flex;"><span>└── a0cf7f5d705d_initial_commit_created_customers_orders_.py
</span></span><span style="display:flex;"><span>└── 960969e75871_created_orderproduct_as_an_association_.py
</span></span></code></pre></div><p>The <code>versions</code> directory now contains two migration scripts! One is the migration script for our initial commit in the previous blog post, and one is the newly generated script for creating the OrderProducts table.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>In the previous blog post, the initial commit migration script had the ID <code>de5f32c11361</code>, and not <code>a0cf7f5d705d</code>.</p>
<p>The reason for this change is that I created a new revision script for the initial migration, and the revision ID is randomly generated!</p>
<p>Other aspects of the migration script are otherwise exactly the same, and the scripts are otherwise functionally identical.</p>
      </div>
    </div><p>The newly generated migration should have the following content:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;Created OrderProduct as an association table to track Order-Product association.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revision ID: 960969e75871
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revises: a0cf7f5d705d
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Create Date: 2026-01-26 17:30:18.314422
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Sequence, Union
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> op
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sqlalchemy <span style="color:#66d9ef">as</span> sa
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># revision identifiers, used by Alembic.</span>
</span></span><span style="display:flex;"><span>revision: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;960969e75871&#39;</span>
</span></span><span style="display:flex;"><span>down_revision: Union[str, <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;a0cf7f5d705d&#39;</span>
</span></span><span style="display:flex;"><span>branch_labels: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>depends_on: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">upgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>create_table(<span style="color:#e6db74">&#39;order_products&#39;</span>,
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;order_id&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;product_id&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;product_count&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), server_default<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;1&#39;</span>, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>ForeignKeyConstraint([<span style="color:#e6db74">&#39;order_id&#39;</span>], [<span style="color:#e6db74">&#39;orders.order_id&#39;</span>], ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;CASCADE&#39;</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>ForeignKeyConstraint([<span style="color:#e6db74">&#39;product_id&#39;</span>], [<span style="color:#e6db74">&#39;products.product_id&#39;</span>], ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;CASCADE&#39;</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>PrimaryKeyConstraint(<span style="color:#e6db74">&#39;order_id&#39;</span>, <span style="color:#e6db74">&#39;product_id&#39;</span>)
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">downgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_table(<span style="color:#e6db74">&#39;order_products&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</span>
</span></span></code></pre></div><h1 id="reviewing-the-auto-generated-migration">Reviewing the auto-generated migration</h1>
<p>Remember that Alembic&rsquo;s documentation states,</p>
<blockquote>
<p>It is <strong>always</strong> necessary to manually review and correct the <strong>candidate migrations</strong> that autogenerate produces.</p>
</blockquote>
<p>Reviewing our migration script, it looks like:</p>
<ul>
<li>Alembic has created a table called <code>order_products</code>.</li>
<li><code>order_id</code> and <code>product_id</code> are both <strong>foreign keys</strong>. They are the primary keys from the tables <code>orders</code> and <code>products</code> respectively.</li>
<li><code>order_id</code> and <code>product_id</code> are also <strong>primary keys</strong> for the <code>order_products</code> table.</li>
<li><code>order_products</code> has a column called <code>product_count</code>, with a default value of 1.</li>
</ul>

    <div class="admonition tip">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M272 384c9.6-31.9 29.5-59.1 49.2-86.2c0 0 0 0 0 0c5.2-7.1 10.4-14.2 15.4-21.4c19.8-28.5 31.4-63 31.4-100.3C368 78.8 289.2 0 192 0S16 78.8 16 176c0 37.3 11.6 71.9 31.4 100.3c5 7.2 10.2 14.3 15.4 21.4c0 0 0 0 0 0c19.8 27.1 39.7 54.4 49.2 86.2l160 0zM192 512c44.2 0 80-35.8 80-80l0-16-160 0 0 16c0 44.2 35.8 80 80 80zM112 176c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-61.9 50.1-112 112-112c8.8 0 16 7.2 16 16s-7.2 16-16 16c-44.2 0-80 35.8-80 80z"/></svg>
        <span>Tip</span>
      </div>
      <div class="admonition-content">
        <p><code>order_id</code> and <code>product_id</code> are both selected as primary keys to ensure that duplicate rows won’t be persisted within the <code>order_products</code> regardless of issues on the application side.</p>
      </div>
    </div><p>This migration script looks good! We can go ahead and apply it to the database.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Why does the migration script look so sparse compared to the complex ORM relationships?</span>
      </div>
      <div class="admonition-content">
        <p><strong><code>Order</code></strong>:</p>
<pre tabindex="0"><code>products = relationship(
        &#34;OrderProduct&#34;,
        back_populates=&#34;order&#34;,
        cascade=&#34;all, delete-orphan&#34;,
    )
</code></pre><p>This code defines the behavior within the <strong>ORM</strong>, and will become relevant when you interact with the database via SQLAlchemy.
It defines the behaviour of these classes within Python and also documents the relationship on a conceptual level.
It <strong>doesn&rsquo;t create anything within the database</strong>.</p>
<p>On the database level, it is really straightforward to build this relationship,
which is why the migration script seems so sparse in comparison.
Within the migration script, on the database level, we&rsquo;re just creating <code>order_products</code>
as a table with foreign keys pointing to the parent entity tables.
The relational aspect of the data model is much lower level than the conceptual data model, which is what we define in the ORM.</p>
<p>The ORM helps us build a conceptual data model with rich, complex relationships as seen in our code.
Alembic reads the ORM and generates migrations to create a lower-level relational data model <strong>from</strong> the ORM.</p>
      </div>
    </div><h1 id="applying-the-migration">Applying the migration</h1>
<p>We can then apply the migration script with the command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic upgrade head
</span></span></code></pre></div><p>This should give us the following output:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.runtime.migration<span style="color:#f92672">]</span> Context impl PostgresqlImpl.
</span></span><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.runtime.migration<span style="color:#f92672">]</span> Will assume transactional DDL.
</span></span><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.runtime.migration<span style="color:#f92672">]</span> Running upgrade a0cf7f5d705d -&gt; 960969e75871, Created OrderProduct as an association table to track Order-Product association.
</span></span></code></pre></div><p>Connecting to the database shows the following ERD:</p>
<figure class="align-center ">
    <img loading="lazy" src="erd_diagram_orderproducts.png#center"
         alt="Our newly iterated data model!"/> <figcaption>
            <p>Our newly iterated data model!</p>
        </figcaption>
</figure>

<p>We&rsquo;ve successfully created the association table, and we have built an indirect <strong>many-to-many</strong> association between <code>Orders</code> and <code>Products</code>! When we examine the contents of the <code>alembic_version</code> table, it now contains a single row with the value <code>960969e75871</code>, which is the revision it has applied. It applied this one specifically because we had alembic upgrade to the <code>head</code> version, which is the latest version.</p>
<h1 id="committing-to-github">Committing to GitHub</h1>
<p>At this point, we&rsquo;ve made changes to <code>models.py</code> and we have a new migration script called <code>960969e75871_created_orderproduct_as_an_association_.py</code>. Both these changes need to be pushed GitHub. I recommend committing and pushing these changes as a single commit with the same commit message that we used for auto-generating a migration script.</p>
<p>Remember to always commit migration scripts <strong>together with</strong> model changes.</p>
<h1 id="further-iterating">Further iterating</h1>
<p>Let&rsquo;s expand this data model more. A great addition to this data model would be the ability to create and assign tags to products for greater searchability.</p>
<p>To start with, these tags will be really simple. A tag will be a text-based descriptor that can be applied to any product. For example, a laptop as a product might have a tag called &lsquo;computer&rsquo; assigned to it.</p>
<p>To reflect this, we will create a table containing tags, and build a <strong>many-to-many</strong> relationship between products and tags. Each product can have multiple tags, and each tag can be assigned to multiple products. For this, like before, we will build an association table.</p>
<p>We will do the following:</p>
<ul>
<li>Create a class for <code>Tags</code> in the ORM</li>
<li>Create an association table called <code>product_tags</code> to associate products and tags</li>
<li>Update the relationships for <code>Products</code> to reflect the new relationship to the <code>Tags</code> class.</li>
</ul>

    <div class="admonition tip">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M272 384c9.6-31.9 29.5-59.1 49.2-86.2c0 0 0 0 0 0c5.2-7.1 10.4-14.2 15.4-21.4c19.8-28.5 31.4-63 31.4-100.3C368 78.8 289.2 0 192 0S16 78.8 16 176c0 37.3 11.6 71.9 31.4 100.3c5 7.2 10.2 14.3 15.4 21.4c0 0 0 0 0 0c19.8 27.1 39.7 54.4 49.2 86.2l160 0zM192 512c44.2 0 80-35.8 80-80l0-16-160 0 0 16c0 44.2 35.8 80 80 80zM112 176c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-61.9 50.1-112 112-112c8.8 0 16 7.2 16 16s-7.2 16-16 16c-44.2 0-80 35.8-80 80z"/></svg>
        <span>Tip</span>
      </div>
      <div class="admonition-content">
        <p>In the interest of brevity, this section won&rsquo;t be as thoroughly explanatory as the section for creating <code>OrderProducts</code>.
This process is very similar to what we&rsquo;ve covered thus far!</p>
      </div>
    </div><h2 id="creating-a-tags-class">Creating a <code>Tags</code> class</h2>
<p><strong><code>models.py</code></strong> (Changes to code have been highlighted):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>    Column,
</span></span><span style="display:flex;"><span>    Integer,
</span></span><span style="display:flex;"><span>    String,
</span></span><span style="display:flex;"><span>    Float,
</span></span><span style="display:flex;"><span>    ForeignKey,
</span></span><span style="display:flex;"><span>    Identity,
</span></span><span style="display:flex;"><span>    Text,
</span></span><span style="display:flex;"><span>    DateTime,
</span></span><span style="display:flex; background-color:#3c3d38"><span>    Table,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.orm <span style="color:#f92672">import</span> declarative_base, relationship
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.sql <span style="color:#f92672">import</span> func
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Base <span style="color:#f92672">=</span> declarative_base()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Customer</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;customers&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    first_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    last_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    email_address <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Order&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;customer&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">OrderProduct</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;order_products&#34;</span>
</span></span><span style="display:flex;"><span>    order_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer,
</span></span><span style="display:flex;"><span>        ForeignKey(<span style="color:#e6db74">&#34;orders.order_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex;"><span>        primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    product_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer,
</span></span><span style="display:flex;"><span>        ForeignKey(<span style="color:#e6db74">&#34;products.product_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex;"><span>        primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    product_count <span style="color:#f92672">=</span> Column(Integer, server_default<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;1&#34;</span>, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    order <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Order&#34;</span>, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;products&#34;</span>)
</span></span><span style="display:flex;"><span>    product <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Product&#34;</span>, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Order</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;orders&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    order_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer, ForeignKey(<span style="color:#e6db74">&#34;customers.customer_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    order_date <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        DateTime(timezone<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>),
</span></span><span style="display:flex;"><span>        server_default<span style="color:#f92672">=</span>func<span style="color:#f92672">.</span>now(),
</span></span><span style="display:flex;"><span>        nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    customer <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Customer&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    products <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;OrderProduct&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;order&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>product_tags <span style="color:#f92672">=</span> Table(
</span></span><span style="display:flex; background-color:#3c3d38"><span>    <span style="color:#e6db74">&#34;product_tags&#34;</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>    Base<span style="color:#f92672">.</span>metadata,
</span></span><span style="display:flex; background-color:#3c3d38"><span>    Column(
</span></span><span style="display:flex; background-color:#3c3d38"><span>        <span style="color:#e6db74">&#34;product_id&#34;</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>        ForeignKey(<span style="color:#e6db74">&#34;products.product_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex; background-color:#3c3d38"><span>        primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>    ),
</span></span><span style="display:flex; background-color:#3c3d38"><span>    Column(<span style="color:#e6db74">&#34;tag_id&#34;</span>, ForeignKey(<span style="color:#e6db74">&#34;tags.tag_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>),
</span></span><span style="display:flex; background-color:#3c3d38"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Product</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;products&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    product_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    product_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    description <span style="color:#f92672">=</span> Column(Text)
</span></span><span style="display:flex;"><span>    price <span style="color:#f92672">=</span> Column(Float, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;OrderProduct&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;product&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex; background-color:#3c3d38"><span>    tags <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Tag&#34;</span>, secondary<span style="color:#f92672">=</span>product_tags, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;products&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Tag</span>(Base):
</span></span><span style="display:flex; background-color:#3c3d38"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tags&#34;</span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>    tag_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex; background-color:#3c3d38"><span>    name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>, unique<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex; background-color:#3c3d38"><span>    products <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Product&#34;</span>, secondary<span style="color:#f92672">=</span>product_tags, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;tags&#34;</span>)
</span></span></code></pre></div>
    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>The <code>product_tags</code> implementation here is different from the <code>OrderProducts</code> class.</p>
<p>This is because <code>product_tags</code> is a straightforward, simple association table,
which is what the documentation for SQLAlchemy says to use when your association table has no metadata.</p>
<p>This <code>secondary</code> table pattern is only usable when all you have in the table are foreign keys.
<code>product_tags</code> has no attributes apart from its foreign keys, so we use the association table with the <code>secondary</code> table pattern.</p>
<p>The reason <code>OrderProducts</code> was mapped as a class was because we have additional metadata beyond just the foreign keys in that table.</p>
<p><code>OrderProducts</code> goes from just a simple association table to a full entity in it&rsquo;s own right when you add even a single extra column beyond the foreign keys.
The recommended approach for adding metadata to an association table is to map the association table directly to a full class.</p>
      </div>
    </div><h2 id="generating-a-migration">Generating a migration</h2>
<p>We create an auto-generated migration script with the following command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic revision --autogenerate -m <span style="color:#e6db74">&#34;Created a table for tags, and created a table called product_tags as an association table between products and tags.&#34;</span>
</span></span></code></pre></div><h2 id="reviewing-the-migration-script">Reviewing the migration script</h2>
<p>The contents of the migration script are:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;Created a table for tags, and created a table called product_tags as an association table between products and tags.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revision ID: 6e58a26e5c70
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revises: 960969e75871
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Create Date: 2026-01-26 18:32:43.007353
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Sequence, Union
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> op
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sqlalchemy <span style="color:#66d9ef">as</span> sa
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># revision identifiers, used by Alembic.</span>
</span></span><span style="display:flex;"><span>revision: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;6e58a26e5c70&#39;</span>
</span></span><span style="display:flex;"><span>down_revision: Union[str, <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;960969e75871&#39;</span>
</span></span><span style="display:flex;"><span>branch_labels: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>depends_on: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">upgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>create_table(<span style="color:#e6db74">&#39;tags&#39;</span>,
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;tag_id&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), sa<span style="color:#f92672">.</span>Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;name&#39;</span>, sa<span style="color:#f92672">.</span>String(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>PrimaryKeyConstraint(<span style="color:#e6db74">&#39;tag_id&#39;</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>UniqueConstraint(<span style="color:#e6db74">&#39;name&#39;</span>)
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>create_table(<span style="color:#e6db74">&#39;product_tags&#39;</span>,
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;product_id&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;tag_id&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>ForeignKeyConstraint([<span style="color:#e6db74">&#39;product_id&#39;</span>], [<span style="color:#e6db74">&#39;products.product_id&#39;</span>], ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;CASCADE&#39;</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>ForeignKeyConstraint([<span style="color:#e6db74">&#39;tag_id&#39;</span>], [<span style="color:#e6db74">&#39;tags.tag_id&#39;</span>], ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;CASCADE&#39;</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>PrimaryKeyConstraint(<span style="color:#e6db74">&#39;product_id&#39;</span>, <span style="color:#e6db74">&#39;tag_id&#39;</span>)
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">downgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_table(<span style="color:#e6db74">&#39;product_tags&#39;</span>)
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_table(<span style="color:#e6db74">&#39;tags&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</span>
</span></span></code></pre></div><p>This migration script (with the revision ID <code>6e58a26e5c70</code>) looks good!</p>
<p>You should now have three scripts inside <code>alembic/versions</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic/versions/
</span></span><span style="display:flex;"><span>├── 6e58a26e5c70_created_a_table_for_tags_and_created_a_.py
</span></span><span style="display:flex;"><span>├── 960969e75871_created_orderproduct_as_an_association_.py
</span></span><span style="display:flex;"><span>└── a0cf7f5d705d_initial_commit_created_customers_orders_.py
</span></span></code></pre></div><h2 id="applying-the-migration-script">Applying the migration script</h2>
<p>Apply the migration script by running:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic upgrade head
</span></span></code></pre></div><p>Connecting to the database shows the following ERD:</p>
<figure class="align-center ">
    <img loading="lazy" src="erd_diagram_producttags.png#center"
         alt="Tags are now associated with Products!"/> <figcaption>
            <p>Tags are now associated with Products!</p>
        </figcaption>
</figure>

<p>We&rsquo;ve successfully built a connection between Products and Tags via an association table!</p>
<p>Browsing through <code>alembic/versions</code> should give us an idea of the growth of our data model and how it evolves to match the change in scope of our e-commerce workflow. With these three migration scripts, we&rsquo;ve gone from a rudimentary data model to a more complex one, while being able to track how the data model has evolved from its conception.</p>
<h1 id="next-steps">Next Steps</h1>
<p>In subsequent blog posts, we will cover:</p>
<ul>
<li>Using an Async engine for asynchronous database connections</li>
<li>Using the <code>alembic_utils</code> library to incorporate views, functions and triggers into the version control system.</li>
<li>Configuring Alembic to connect to a production environment, and seamlessly bring the production environment&rsquo;s schema up-to-date with our final revision.</li>
</ul>
<h1 id="references">References</h1>
<ul>
<li>SQLAlchemy&rsquo;s <a href="https://docs.sqlalchemy.org/en/14/orm/basic_relationships.html#many-to-many">documentation on many-to-many relationships.</a></li>
<li>Alembic&rsquo;s <a href="https://alembic.sqlalchemy.org/en/latest/autogenerate.html#what-does-autogenerate-detect-and-what-does-it-not-detect">documentation on auto-generating migrations.</a></li>
</ul>
]]></content:encoded></item><item><title>Relational Data Model design with SQLAlchemy.</title><link>https://naveenkannan.dev/posts/sqlalchemy_data_modeling/</link><pubDate>Tue, 13 Jan 2026 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/sqlalchemy_data_modeling/</guid><description>An introduction to using SQLAlchemy and Alembic for relational data model design.</description><content:encoded><![CDATA[<h1 id="introduction">Introduction</h1>
<h2 id="data-modeling">Data Modeling</h2>
<p>Data modeling is the process of creating a representation of data that defines the way it is structured and used in complex systems. When it comes to relational databases, designing a normalized data model is essential for efficient database operation, and to make your data consistent and clean.</p>
<p>With a fully normalized data model, data in your relational database becomes:</p>
<ul>
<li>Understandable</li>
</ul>
<p>A good data model can decompose the complexity of real-world systems and the data they generate into a relational model that is easy to read and comprehend, and standardizes the relationship of one data domain with every other data domain.</p>
<ul>
<li>Scalable</li>
</ul>
<p>A well defined and <strong>normalized</strong> data model becomes scalable, and allows for sustainable development of data-driven solutions for users of your database as the scope of your data grows.</p>
<ul>
<li>Modular</li>
</ul>
<p>A database with a well designed and thoroughly documented data model becomes easy to use by downstream teams such as API designers and front-end developers, minimizing the communication needed for these teams to function independently. Good design will naturally flow downstream of a properly normalized database.</p>
<p>Efficient data model design can help your database be a bedrock for your fellow team members to rely on.</p>
<h2 id="sqlalchemy">SQLAlchemy</h2>
<p><a href="https://www.sqlalchemy.org">SQLAlchemy</a> is a Python SQL toolkit and ORM (Object Relational Mapper) that allows Python developers to work with relational databases using the Python language. SQLAlchemy is indispensable if you&rsquo;re a Python developer looking to create a relational data model for your database. The two most significant components of SQLAlchemy are it&rsquo;s <strong>Object Relational Mapper (ORM)</strong> and <strong>SQLAlchemy Core</strong>.</p>
<ul>
<li>SQLAlchemy Core</li>
</ul>
<p>SQLALchemy Core is the foundation of SQLAlchemy, and is independent of the ORM. It provides the connectivity to the database, and by allowing for the construction of SQL expressions that can then be executed against a target database, enabling interaction with a database to in the form of queries.</p>
<ul>
<li>SQLAlchemy ORM</li>
</ul>
<p>The SQLAlchemy ORM (Object Relational Mapper) uses Core as a foundation. The ORM will be the bulk of what we discuss in this blog post. The ORM builds on Core to help create user-defined Python classes that can be mapped into database tables. The ORM is what allows SQLAlchemy to define a data model for a relational database.</p>
<figure class="align-center ">
    <img loading="lazy" src="https://docs.sqlalchemy.org/en/20/_images/sqla_arch_small.png#center"
         alt="The major components of SQLAlchemy."/> <figcaption>
            <p>The major components of SQLAlchemy.Taken from the <a href="https://docs.sqlalchemy.org/en/20/intro.html">SQLAlchemy documentation.</a></p>
        </figcaption>
</figure>

<h3 id="alembic">Alembic</h3>
<p><a href="https://alembic.sqlalchemy.org/en/latest/index.html">Alembic</a> is a database migration tool written by the author of SQLAlchemy. It helps create a version-controlled system of database migrations that can be used to upgrade a target database to any version of your data model. It uses SQLAlchemy as the underlying engine. By executing migrations in a sequential order, databases can be upgraded and downgraded across versions, even when you are building from scratch.</p>
<p>Alembic is a great way to test your migrations and schema design in a development environment before transferring these migrations to a production environment. An empty database environment can be converted into a database with a production ready schema with just a single command using Alembic.</p>
<h2 id="what-well-cover">What we&rsquo;ll cover</h2>
<p>In this blog post, we will cover how you can begin using SQLAlchemy and Alembic to start developing the first iteration of your data model.</p>
<p>We will do the following:</p>
<ul>
<li>Create a virtual environment with SQLAlchemy and Alembic installed</li>
<li>Initialize Alembic</li>
<li>Design a simple, rudimentary data model with three tables</li>
<li>Configure Alembic to connect to your database</li>
<li>Utilize Alembic to generate a migration script</li>
<li>Migrate your initial data model to the database for the first time</li>
</ul>
<p>In future posts, we will cover how you can use Alembic to generate database migrations and help grow your database within a version-controlled system, and how this data model can then be plugged into a FastAPI server as a module to help design a modern API service.</p>
<h1 id="prerequisites">Prerequisites</h1>
<p>The code in this blog post was written with Python 3.13.1, Alembic 1.14.0 and SQLAlchemy 2.0.36.</p>
<p>You will need the following prerequisites:</p>
<ul>
<li>
<p>A PostgreSQL server initialized and running, with a database that has been initialized but is otherwise empty. We will refer to this server as <code>development</code> from here.</p>
</li>
<li>
<p>An installation of <code>mamba</code>,<code>micromamba</code> or <code>conda</code> as a package manager to create a new environment. I like to use <code>micromamba</code>. <a href="https://mamba.readthedocs.io/en/latest/installation/micromamba-installation.html">You can find instructions on how to install <code>micromamba</code> here.</a></p>
</li>
</ul>
<p>We will create a new virtual environment using Python 3.13.1 as the pinned version. Create an environment named <code>data_model</code> as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>micromamba create <span style="color:#f92672">-</span>n data_model python<span style="color:#f92672">=</span><span style="color:#ae81ff">3.13.1</span> sqlalchemy<span style="color:#f92672">=</span><span style="color:#ae81ff">2.0.36</span> alembic<span style="color:#f92672">=</span><span style="color:#ae81ff">1.14.0</span> psycopg2<span style="color:#f92672">=</span><span style="color:#ae81ff">2.9.9</span> <span style="color:#f92672">-</span>y
</span></span></code></pre></div><p>This will create a micromamba environment with the pinned versions of Python and SQLAlchemy that can then be used to follow this tutorial. This environment can be activated with the command <code>micromamba activate data_model</code>.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>Using a virtual environment is a good way to keep your base environment uncluttered and free of broken dependencies.
It is good practice to begin any Python project with a virtual environment, even for development workflows.
Other options for virtual environment creation include <code>uv</code> and <code>Poetry</code>.</p>
      </div>
    </div><p>Lets now get into actually creating some data models!</p>
<h1 id="getting-started">Getting Started</h1>
<h2 id="setting-up-the-alembic-environment">Setting up the Alembic environment</h2>
<p>Before we can start working with our PostgreSQL database, we will first set up the migration environment using Alembic. The reason we set this up first is because we will use Alembic&rsquo;s config files to connect to our database first before we can begin creating our data model. Alembic uses SQLAlchemy core&rsquo;s <code>Engine</code> object as a dependency when connecting to a database.</p>
<p>In the root of your project directory, run:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>alembic init alembic
</span></span></code></pre></div><p>This will generate a directory of scripts. The root of your project directory will look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>.
</span></span><span style="display:flex;"><span>├── alembic
</span></span><span style="display:flex;"><span>│   ├── README
</span></span><span style="display:flex;"><span>│   ├── env.py
</span></span><span style="display:flex;"><span>│   ├── script.py.mako
</span></span><span style="display:flex;"><span>│   └── versions
</span></span><span style="display:flex;"><span>└── alembic.ini
</span></span></code></pre></div><p>The directory now includes these files of importance:</p>
<h3 id="alembicini"><code>alembic.ini</code></h3>
<p>This is Alembic&rsquo;s main configuration file. When Alembic is run, it looks in the directory for this file. This file is where you will add the SQLAlchemy URL to connect to your database. You can also define <strong>multiple</strong> environments to run migrations in.</p>
<p>For now we will go with the boilerplate file generated by the alembic init file. Here is what the default file should look like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#75715e"># A generic, single database configuration.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[alembic]</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># path to migration scripts</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Use forward slashes (/) also on windows to provide an os agnostic path</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">script_location</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">alembic</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># template used to generate migration file names; The default value is %%(rev)s_%%(slug)s</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Uncomment the line below if you want the files to be prepended with date and time</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># for all available tokens</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># sys.path path, will be prepended to sys.path if present.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># defaults to the current working directory.</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">prepend_sys_path</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># timezone to use when rendering the date within the migration file</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># as well as the filename.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># If specified, requires the python&gt;=3.9 or backports.zoneinfo library.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Any required deps can installed by adding `alembic[tz]` to the pip requirements</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># string value is passed to ZoneInfo()</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># leave blank for localtime</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># timezone =</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># max length of characters to apply to the &#34;slug&#34; field</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># truncate_slug_length = 40</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># set to &#39;true&#39; to run the environment during</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># the &#39;revision&#39; command, regardless of autogenerate</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># revision_environment = false</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># set to &#39;true&#39; to allow .pyc and .pyo files without</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># a source .py file to be detected as revisions in the</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># versions/ directory</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># sourceless = false</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># version location specification; This defaults</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># to alembic/versions.  When using multiple version</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># directories, initial revisions must be specified with --version-path.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># The path separator used here should be the separator specified by &#34;version_path_separator&#34; below.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># version_locations = %(here)s/bar:%(here)s/bat:alembic/versions</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># version path separator; As mentioned above, this is the character used to split</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># version_locations. The default within new alembic.ini files is &#34;os&#34;, which uses os.pathsep.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Valid values for version_path_separator are:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># version_path_separator = :</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># version_path_separator = ;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># version_path_separator = space</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># version_path_separator = newline</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">version_path_separator</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">os  # Use os.pathsep. Default configuration used for new projects.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># set to &#39;true&#39; to search source files recursively</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># in each &#34;version_locations&#34; directory</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># new in Alembic version 1.10</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># recursive_version_locations = false</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># the output encoding used when revision files</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># are written from script.py.mako</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># output_encoding = utf-8</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">sqlalchemy.url</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">driver://user:pass@localhost/dbname</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[post_write_hooks]</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># post_write_hooks defines scripts or Python functions that are run</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># on newly generated revision scripts.  See the documentation for further</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># detail and examples</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># format using &#34;black&#34; - use the console_scripts runner, against the &#34;black&#34; entrypoint</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># hooks = black</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># black.type = console_scripts</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># black.entrypoint = black</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># black.options = -l 79 REVISION_SCRIPT_FILENAME</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># lint with attempts to fix using &#34;ruff&#34; - use the exec runner, execute a binary</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># hooks = ruff</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ruff.type = exec</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ruff.executable = %(here)s/.venv/bin/ruff</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ruff.options = --fix REVISION_SCRIPT_FILENAME</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Logging configuration</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[loggers]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">keys</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">root,sqlalchemy,alembic</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[handlers]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">keys</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">console</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[formatters]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">keys</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">generic</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[logger_root]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">level</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">WARNING</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">handlers</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">console</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">qualname</span> <span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[logger_sqlalchemy]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">level</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">WARNING</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">handlers</span> <span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">qualname</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">sqlalchemy.engine</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[logger_alembic]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">level</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">INFO</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">handlers</span> <span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">qualname</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">alembic</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[handler_console]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">class</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">StreamHandler</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">args</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">(sys.stderr,)</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">level</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">NOTSET</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">formatter</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">generic</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[formatter_generic]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">format</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">%(levelname)-5.5s [%(name)s] %(message)s</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">datefmt</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">%H:%M:%S</span>
</span></span></code></pre></div><p>To begin with, we will substitute the <code>sqlalchemy.url</code> parameter for the <code>alembic</code> section with the connection string used for our database. As an example, the following is a dummy connection string.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#a6e22e">sqlalchemy.url</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">postgresql://nk_dev:placeholder_password@localhost/development</span>
</span></span></code></pre></div><p>In the above code snippet:</p>
<ul>
<li>The database driver is <code>postgresql</code></li>
<li>The database username is <code>nk_dev</code></li>
<li>The password of the user is <code>placeholder_password</code></li>
<li>The database host is <code>localhost</code></li>
<li>The name of the database is <code>development</code></li>
</ul>

    <div class="admonition warning">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"/></svg>
        <span>Warning</span>
      </div>
      <div class="admonition-content">
        <p>The above is an example to begin testing with.
This isn&rsquo;t the ideal way to store your PostgreSQL server connection details.
Ideally you manage them with a key vault or via injection of environment variables at the time of virtual environment activation.
Remember to <strong>never</strong> include the alembic.ini file within version control. Always have your config file (<code>alembic.ini</code>) added to <code>.gitignore</code>.</p>
<p>Here are the contents of the <code>.gitignore:</code> file:</p>
<pre tabindex="0"><code>alembic.ini
</code></pre>
      </div>
    </div><p>A more <a href="https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file">in-depth explanation of the <code>alembic.ini</code> file can be found in the official documentation here.</a></p>
<h3 id="the-alembic-directory">The <code>alembic</code> directory</h3>
<p>This directory is the home of the migration environment. The relevant contents of this directory include:</p>
<ul>
<li><strong><code>env.py</code></strong></li>
</ul>
<p>This is a Python script that can be run when the migration tool is invoked. You can do some cool stuff with this script, like registering entities like views, functions and triggers, and also generating filters to ignore tables that you don&rsquo;t want to migrate with Alembic.</p>
<ul>
<li><strong><code>versions/</code></strong></li>
</ul>
<p>This directory holds the individual version scripts. This directory will have the scripts in it run sequentially when upgrading to or downgrading to a particular version of your database. As you begin generating migrations and applying them, this folder will grow. Each migration will become it&rsquo;s own script, containing both an <code>upgrade</code> and a <code>downgrade</code> parameter for that particular version.</p>
<p>Now that all of this is finally sorted, we can get to creating basic data models!</p>
<h2 id="creating-a-file-for-our-data-models">Creating a file for our data models</h2>
<p>Create a file in the project directory called <code>models.py</code>. This file will be where we store our user-defined Python classes that will then be mapped into database tables in the database using Alembic to run the actual migrations. Let&rsquo;s start with a simple scenario where we have commerce data for customers, products and orders. Each customer can submit multiple orders, and each order can have several products in it.</p>
<p>As a basic assumption, this initial data model isn&rsquo;t bad, but there is plenty of room for improvement and refinement in the future. Let&rsquo;s start with defining these three tables as Python classes first.</p>
<p>We will build a one-to-many relationship between customers and orders, since every customer can have many orders, and each order belongs to just one customer. We will build a table to define our products as well.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>In future posts, we will expand on this rudimentary data model, building in connections between products and orders.
We will also incorporate additional features into our data model, such as views, functions and triggers.</p>
      </div>
    </div><p>This is what your <code>models.py</code> file will look like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>    Column,
</span></span><span style="display:flex;"><span>    Integer,
</span></span><span style="display:flex;"><span>    String,
</span></span><span style="display:flex;"><span>    Float,
</span></span><span style="display:flex;"><span>    ForeignKey,
</span></span><span style="display:flex;"><span>    Identity,
</span></span><span style="display:flex;"><span>    Text,
</span></span><span style="display:flex;"><span>    DateTime,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.orm <span style="color:#f92672">import</span> declarative_base, relationship
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.sql <span style="color:#f92672">import</span> func
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Base <span style="color:#f92672">=</span> declarative_base()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Customer</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;customers&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    first_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    last_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    email_address <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Order&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;customer&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Order</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;orders&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    order_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer,
</span></span><span style="display:flex;"><span>        ForeignKey(<span style="color:#e6db74">&#34;customers.customer_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex;"><span>        nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    order_date <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        DateTime(timezone<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>),
</span></span><span style="display:flex;"><span>        server_default<span style="color:#f92672">=</span>func<span style="color:#f92672">.</span>now(),
</span></span><span style="display:flex;"><span>        nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    customer <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Customer&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Product</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;products&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    product_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    product_name <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    description <span style="color:#f92672">=</span> Column(Text)
</span></span><span style="display:flex;"><span>    price <span style="color:#f92672">=</span> Column(Float, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span></code></pre></div><p>With these classes in <code>models.py</code>, We have created the following objects: <code>Customer</code>, <code>Order</code> and <code>Product</code>.</p>
<h3 id="customer"><code>Customer</code>:</h3>
<p>This object describes the attributes of the customers. The <code>customer_id</code> column is the primary key of the table, and we initialize it as a serialized integer. We have added some additional parameters to the column (which also apply to the primary keys of every other table).</p>
<ul>
<li><code>always=True</code> means the identity is always generated (even if a value is provided)</li>
</ul>
<p><code>first_name</code>,<code>last_name</code> and <code>email_address</code> are string columns that cannot be null, and need to be present in the data. This code snippet:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>    orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Order&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;customer&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span></code></pre></div><p>establishes a one-to-many relationship between <code>customers</code> and <code>orders</code>.</p>
<p>The <code>cascade</code> behavior (<code>&quot;all&quot;, &quot;delete-orphan&quot;</code>) means that when a instance of <code>customer</code> is deleted, all the associated <code>orders</code> (Orders that used the ID of the deleted customer as a foreign key) will also be deleted, and these changes will cascade downwards to any future child table of the <code>orders</code> table.</p>
<h3 id="order"><code>Order</code>:</h3>
<p>This object describes the attributes of each individual order. <code>order_id</code> is the primary key of this table. We designate the <code>customer_id</code> column as a foreign key from the <code>customers</code> table with this code snippet:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>  customer_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer,
</span></span><span style="display:flex;"><span>        ForeignKey(<span style="color:#e6db74">&#34;customers.customer_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex;"><span>        nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    )
</span></span></code></pre></div><p>The foreign key enforces referential integrity, while <code>nullable=False</code> ensures that each order must be associated with a customer. Orders that are associated with non-existent customers, or have no customer at all, will not be possible.</p>
<p>We also establish a many-to-one relationship between <code>orders</code> and <code>customers</code> using the following code snippet:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>  customer <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Customer&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</span>
</span></span><span style="display:flex;"><span>    )
</span></span></code></pre></div><h3 id="product"><code>Product</code>:</h3>
<p>This object describes the attributes of each individual product. <code>product_id</code> is the primary key of this table, and this table does not have any relationships with any other tables yet. This data model is currently incomplete, but it is a great starting point at which to begin the development and migration of our data model.</p>
<p>With this file complete, our rudimentary data model has been initialized. Without migrating these changes over to the database, however, these will only live as Python classes, and not as database objects.</p>
<h2 id="doing-our-first-database-migration">Doing our first database migration.</h2>
<p>The vast majority of database migration workflows with Alembic are done by <strong>auto-generating</strong> migration scripts. To begin auto-generating migration scripts, we will first need to connect Alembic to the database, and then point alembic towards the target metadata we would like to migrate the data model to.</p>
<p>Alembic can be configured to connect to the database (pointed to be the <code>sqlalchemy.url</code> parameter in the <code>alembic.ini</code> file). For Alembic to compare the state of the database <em>against</em> our target metadata, we will edit <code>alembic/env.py</code> so that Alembic can access the table metadata object that contains the target. Configuring Alembic therefore has two components:</p>
<ul>
<li>Connecting Alembic to the database, <strong>so that it can compare the state of the database against the target metadata.</strong></li>
<li>Importing the target metadata, <strong>so that Alembic knows what to compare the state of the database against.</strong></li>
</ul>
<p>Once <code>alembic/env.py</code> is configured, Alembic can then auto generate the “obvious” migrations based on a comparison. In this case, we want it to compare the empty database against our models, and then auto-generate migrations that we can then review and modify before applying.</p>
<p>We have already defined the database connection string in our configuration file. We will begin working with the <code>env.py</code> to access the table metadata object that contains the target.</p>
<h3 id="editing-envpy-to-import-our-models">Editing <code>env.py</code> to import our models</h3>
<p>The following is the default content of <code>alembic/env.py</code>. The sections that we will edit have been highlighted.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-python" data-lang="python"><span style="display:flex; background-color:#3c3d38"><span><span style="color:#f92672">from</span> logging.config <span style="color:#f92672">import</span> fileConfig
</span></span><span style="display:flex; background-color:#3c3d38"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> engine_from_config
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> pool
</span></span><span style="display:flex; background-color:#3c3d38"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> context
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># this is the Alembic Config object, which provides</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># access to the values within the .ini file in use.</span>
</span></span><span style="display:flex;"><span>config <span style="color:#f92672">=</span> context<span style="color:#f92672">.</span>config
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Interpret the config file for Python logging.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># This line sets up loggers basically.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> config<span style="color:#f92672">.</span>config_file_name <span style="color:#f92672">is</span> <span style="color:#f92672">not</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    fileConfig(config<span style="color:#f92672">.</span>config_file_name)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#75715e"># add your model&#39;s MetaData object here</span>
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#75715e"># for &#39;autogenerate&#39; support</span>
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#75715e"># from myapp import mymodel</span>
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#75715e"># target_metadata = mymodel.Base.metadata</span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>target_metadata <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># other values from the config, defined by the needs of env.py,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># can be acquired:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># my_important_option = config.get_main_option(&#34;my_important_option&#34;)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ... etc.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">run_migrations_offline</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Run migrations in &#39;offline&#39; mode.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    This configures the context with just a URL
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and not an Engine, though an Engine is acceptable
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    here as well.  By skipping the Engine creation
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    we don&#39;t even need a DBAPI to be available.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Calls to context.execute() here emit the given string to the
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    script output.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    url <span style="color:#f92672">=</span> config<span style="color:#f92672">.</span>get_main_option(<span style="color:#e6db74">&#34;sqlalchemy.url&#34;</span>)
</span></span><span style="display:flex;"><span>    context<span style="color:#f92672">.</span>configure(
</span></span><span style="display:flex;"><span>        url<span style="color:#f92672">=</span>url,
</span></span><span style="display:flex;"><span>        target_metadata<span style="color:#f92672">=</span>target_metadata,
</span></span><span style="display:flex;"><span>        literal_binds<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>        dialect_opts<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;paramstyle&#34;</span>: <span style="color:#e6db74">&#34;named&#34;</span>},
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> context<span style="color:#f92672">.</span>begin_transaction():
</span></span><span style="display:flex;"><span>        context<span style="color:#f92672">.</span>run_migrations()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">run_migrations_online</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Run migrations in &#39;online&#39; mode.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    In this scenario we need to create an Engine
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and associate a connection with the context.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    connectable <span style="color:#f92672">=</span> engine_from_config(
</span></span><span style="display:flex;"><span>        config<span style="color:#f92672">.</span>get_section(config<span style="color:#f92672">.</span>config_ini_section, {}),
</span></span><span style="display:flex;"><span>        prefix<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;sqlalchemy.&#34;</span>,
</span></span><span style="display:flex;"><span>        poolclass<span style="color:#f92672">=</span>pool<span style="color:#f92672">.</span>NullPool,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> connectable<span style="color:#f92672">.</span>connect() <span style="color:#66d9ef">as</span> connection:
</span></span><span style="display:flex;"><span>        context<span style="color:#f92672">.</span>configure(
</span></span><span style="display:flex;"><span>            connection<span style="color:#f92672">=</span>connection, target_metadata<span style="color:#f92672">=</span>target_metadata
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">with</span> context<span style="color:#f92672">.</span>begin_transaction():
</span></span><span style="display:flex;"><span>            context<span style="color:#f92672">.</span>run_migrations()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> context<span style="color:#f92672">.</span>is_offline_mode():
</span></span><span style="display:flex;"><span>    run_migrations_offline()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>    run_migrations_online()
</span></span></code></pre></div><p>First, we will import the <code>models.py</code> script. Edit the highlighted snippet to add an import statement.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> models <span style="color:#f92672">import</span> Base
</span></span></code></pre></div><p>Then we will replace the <code>target_metadata</code> value with the metadata of our data model. We will change the second highlighted section to:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>target_metadata <span style="color:#f92672">=</span> Base<span style="color:#f92672">.</span>metadata
</span></span></code></pre></div><p>Our <code>env.py</code> file should now look like the following, with our changes being highlighted.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> logging.config <span style="color:#f92672">import</span> fileConfig
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> engine_from_config
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> pool
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> context
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#f92672">from</span> models <span style="color:#f92672">import</span> Base
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># this is the Alembic Config object, which provides</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># access to the values within the .ini file in use.</span>
</span></span><span style="display:flex;"><span>config <span style="color:#f92672">=</span> context<span style="color:#f92672">.</span>config
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Interpret the config file for Python logging.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># This line sets up loggers basically.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> config<span style="color:#f92672">.</span>config_file_name <span style="color:#f92672">is</span> <span style="color:#f92672">not</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    fileConfig(config<span style="color:#f92672">.</span>config_file_name)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># add your model&#39;s MetaData object here</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># for &#39;autogenerate&#39; support</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># from myapp import mymodel</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># target_metadata = mymodel.Base.metadata</span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>target_metadata <span style="color:#f92672">=</span> Base<span style="color:#f92672">.</span>metadata
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># other values from the config, defined by the needs of env.py,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># can be acquired:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># my_important_option = config.get_main_option(&#34;my_important_option&#34;)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ... etc.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">run_migrations_offline</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Run migrations in &#39;offline&#39; mode.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    This configures the context with just a URL
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and not an Engine, though an Engine is acceptable
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    here as well.  By skipping the Engine creation
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    we don&#39;t even need a DBAPI to be available.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Calls to context.execute() here emit the given string to the
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    script output.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    url <span style="color:#f92672">=</span> config<span style="color:#f92672">.</span>get_main_option(<span style="color:#e6db74">&#34;sqlalchemy.url&#34;</span>)
</span></span><span style="display:flex;"><span>    context<span style="color:#f92672">.</span>configure(
</span></span><span style="display:flex;"><span>        url<span style="color:#f92672">=</span>url,
</span></span><span style="display:flex;"><span>        target_metadata<span style="color:#f92672">=</span>target_metadata,
</span></span><span style="display:flex;"><span>        literal_binds<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>        dialect_opts<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;paramstyle&#34;</span>: <span style="color:#e6db74">&#34;named&#34;</span>},
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> context<span style="color:#f92672">.</span>begin_transaction():
</span></span><span style="display:flex;"><span>        context<span style="color:#f92672">.</span>run_migrations()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">run_migrations_online</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Run migrations in &#39;online&#39; mode.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    In this scenario we need to create an Engine
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and associate a connection with the context.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    connectable <span style="color:#f92672">=</span> engine_from_config(
</span></span><span style="display:flex;"><span>        config<span style="color:#f92672">.</span>get_section(config<span style="color:#f92672">.</span>config_ini_section, {}),
</span></span><span style="display:flex;"><span>        prefix<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;sqlalchemy.&#34;</span>,
</span></span><span style="display:flex;"><span>        poolclass<span style="color:#f92672">=</span>pool<span style="color:#f92672">.</span>NullPool,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> connectable<span style="color:#f92672">.</span>connect() <span style="color:#66d9ef">as</span> connection:
</span></span><span style="display:flex;"><span>        context<span style="color:#f92672">.</span>configure(
</span></span><span style="display:flex;"><span>            connection<span style="color:#f92672">=</span>connection, target_metadata<span style="color:#f92672">=</span>target_metadata
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">with</span> context<span style="color:#f92672">.</span>begin_transaction():
</span></span><span style="display:flex;"><span>            context<span style="color:#f92672">.</span>run_migrations()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> context<span style="color:#f92672">.</span>is_offline_mode():
</span></span><span style="display:flex;"><span>    run_migrations_offline()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>    run_migrations_online()
</span></span></code></pre></div><h3 id="auto-generating-a-migration-script-with-alembic">Auto-generating a migration script with Alembic</h3>
<p>Now that <code>env.py</code> and <code>alembic.ini</code> have been configured, and we have created our first data model, we can auto-generate our first migration script! We will run the command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic revision --autogenerate -m <span style="color:#e6db74">&#34;Initial commit, created customers, orders and products&#34;</span>
</span></span></code></pre></div><p>This code does the following:</p>
<ul>
<li>Runs Alembic and creates a new revision.</li>
<li>Tells Alembic to auto-generate the migration script.</li>
<li>Creates a message string to use with the revision. In this case, this string is &ldquo;Initial commit, created customers, orders and products&rdquo;. This is analogous to a commit message when using git for version control.</li>
</ul>
<p>You should get output like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.runtime.migration<span style="color:#f92672">]</span> Context impl PostgresqlImpl.
</span></span><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.runtime.migration<span style="color:#f92672">]</span> Will assume transactional DDL.
</span></span><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.autogenerate.compare<span style="color:#f92672">]</span> Detected added table <span style="color:#e6db74">&#39;customers&#39;</span>
</span></span><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.autogenerate.compare<span style="color:#f92672">]</span> Detected added table <span style="color:#e6db74">&#39;products&#39;</span>
</span></span><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.autogenerate.compare<span style="color:#f92672">]</span> Detected added table <span style="color:#e6db74">&#39;orders&#39;</span>
</span></span><span style="display:flex;"><span>  Generating /path_to_your_project/alembic/versions/de5f32c11361_initial_commit_created_customers_orders_.py ...  <span style="color:#66d9ef">done</span>
</span></span></code></pre></div><p>This output means that Alembic has successfully generated a migration script, and the script is located at <code>/path_to_your_project/alembic/versions/de5f32c11361_initial_commit_created_customers_orders_.py</code>. The <code>versions</code> directory will have the following content:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic/versions/
</span></span><span style="display:flex;"><span>└── de5f32c11361_initial_commit_created_customers_orders_.py
</span></span></code></pre></div><p>The contents of this auto-generated script are:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;Initial commit, created customers, orders and products
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revision ID: de5f32c11361
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revises:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Create Date: 2026-01-15 16:10:01.002285
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Sequence, Union
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> op
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sqlalchemy <span style="color:#66d9ef">as</span> sa
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># revision identifiers, used by Alembic.</span>
</span></span><span style="display:flex;"><span>revision: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;de5f32c11361&#39;</span>
</span></span><span style="display:flex;"><span>down_revision: Union[str, <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>branch_labels: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>depends_on: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">upgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>create_table(<span style="color:#e6db74">&#39;customers&#39;</span>,
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;customer_id&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), sa<span style="color:#f92672">.</span>Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;first_name&#39;</span>, sa<span style="color:#f92672">.</span>String(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;last_name&#39;</span>, sa<span style="color:#f92672">.</span>String(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;email_address&#39;</span>, sa<span style="color:#f92672">.</span>String(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>PrimaryKeyConstraint(<span style="color:#e6db74">&#39;customer_id&#39;</span>)
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>create_table(<span style="color:#e6db74">&#39;products&#39;</span>,
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;product_id&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), sa<span style="color:#f92672">.</span>Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;product_name&#39;</span>, sa<span style="color:#f92672">.</span>String(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;description&#39;</span>, sa<span style="color:#f92672">.</span>Text(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;price&#39;</span>, sa<span style="color:#f92672">.</span>Float(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>PrimaryKeyConstraint(<span style="color:#e6db74">&#39;product_id&#39;</span>)
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>create_table(<span style="color:#e6db74">&#39;orders&#39;</span>,
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;order_id&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), sa<span style="color:#f92672">.</span>Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;customer_id&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;order_date&#39;</span>, sa<span style="color:#f92672">.</span>DateTime(timezone<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), server_default<span style="color:#f92672">=</span>sa<span style="color:#f92672">.</span>text(<span style="color:#e6db74">&#39;now()&#39;</span>), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>ForeignKeyConstraint([<span style="color:#e6db74">&#39;customer_id&#39;</span>], [<span style="color:#e6db74">&#39;customers.customer_id&#39;</span>], ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;CASCADE&#39;</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>PrimaryKeyConstraint(<span style="color:#e6db74">&#39;order_id&#39;</span>)
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">downgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_table(<span style="color:#e6db74">&#39;orders&#39;</span>)
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_table(<span style="color:#e6db74">&#39;products&#39;</span>)
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_table(<span style="color:#e6db74">&#39;customers&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</span>
</span></span></code></pre></div><p>We can see that a migration script has been generated, with an upgrade and a downgrade parameter. Alembic has compared our target metadata against the database, and has generated a migration script complete with the operations needed to upgrade our database to the state defined by our ORM.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>It is important to note that Alembic <strong>does not detect every change reliably</strong>. There are certain changes that it can always detect, but
it is <em>always</em> necessary to manually check the migration file to correct the auto-generated migrations.</p>
<p>From <a href="https://alembic.sqlalchemy.org/en/latest/autogenerate.html#what-does-autogenerate-detect-and-what-does-it-not-detect">Alembic&rsquo;s documentation on Autogenerate,</a></p>
<blockquote>
<p>It is critical to note that <strong>autogenerate is not intended to be perfect.</strong>
It is <strong>always</strong> necessary to manually review and correct the <strong>candidate migrations</strong> that autogenerate produces.</p>
</blockquote>
<p>Alembic&rsquo;s documentation has a <a href="https://alembic.sqlalchemy.org/en/latest/autogenerate.html#what-does-autogenerate-detect-and-what-does-it-not-detect">section covering what can and cannot be detected with autogeneration.</a></p>
<p>Given that our operations here are rudimentary, in this case, the changes needed to apply our data model
to the database have been correctly detected and applied, but it is always a good idea to
read through the migration script anyway.</p>
      </div>
    </div><h3 id="examining-the-auto-generated-migration-script">Examining the auto-generated migration script</h3>
<p>Going through the <code>upgrade</code> function generated by Alembic, we can see that the following migrations have been generated:</p>
<ul>
<li>
<p>The <code>customers</code>, <code>orders</code> and <code>products</code> tables have been created, with their primary keys being <code>customer_id</code>,<code>order_id</code> and <code>product_id</code> respectively.</p>
</li>
<li>
<p>The attributes defined by our <code>models.py</code> file have been correctly added to these newly created tables.</p>
</li>
<li>
<p>From this code snippet:</p>
<p><code>sa.ForeignKeyConstraint(['customer_id'], ['customers.customer_id'], ondelete='CASCADE'),</code>
We can see that the <code>customer_id</code> field in the <code>orders</code> table has been made a foreign key, connecting to the <code>customer_id</code> column in the <code>customers</code> table.</p>
</li>
</ul>
<p>The migration script looks good, and does not need further refinement. Let&rsquo;s now actually apply our first migration!</p>
<h3 id="applying-the-alembic-generated-migration-script">Applying the Alembic-generated migration script</h3>
<p>We will now apply the migration script with the following command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic upgrade head
</span></span></code></pre></div><p>This command tells Alembic to upgrade the database to the latest revision (the &ldquo;head&rdquo;). Depending on the state of the database, it will work through the scripts located in the <code>versions</code> directory to bring the database&rsquo;s metadata in line with the most recent migration script.</p>
<p>The output of the command is as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.runtime.migration<span style="color:#f92672">]</span> Context impl PostgresqlImpl.
</span></span><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.runtime.migration<span style="color:#f92672">]</span> Will assume transactional DDL.
</span></span><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.runtime.migration<span style="color:#f92672">]</span> Running upgrade  -&gt; de5f32c11361, Initial commit, created customers, orders and products
</span></span></code></pre></div><p>Since our database is empty and there is only a single migration script, only that script has been run. The last line confirms that migration <code>de5f32c11361</code> has been successfully applied.</p>
<h3 id="examining-the-database-to-verify-our-migration">Examining the database to verify our migration</h3>
<p>Connect to your database with the database management tool of your choice. <a href="https://dbeaver.io/">I use DBeaver.</a></p>
<p>The Entity Relationship Diagram for the <code>public</code> schema of your database should look like this.</p>
<figure class="align-center ">
    <img loading="lazy" src="basic_data_model_erd.png#center"
         alt="Our newly created data model!"/> <figcaption>
            <p>Our newly created data model!</p>
        </figcaption>
</figure>

<p>We&rsquo;ve successfully applied our first database migration! We&rsquo;ve created the <code>customers</code>, <code>orders</code> and <code>products</code> tables, and have built a foreign key relationship between <code>customers</code> and <code>orders</code>.</p>
<p>The <code>alembic_version</code> table has been newly created by Alembic. Alembic uses this table to identify the path to upgrade the database from its current state to the version requested by the <code>alembic upgrade</code> command. In our case, we requested the version <code>head</code>.</p>
<p>Alembic works through the migration scripts in the <code>versions</code> directory, starting from the version found in the database&rsquo;s <code>alembic_version</code>, up to the script that defined the version requested by us, invoking the <code>upgrade()</code> method in each file to get to the target revision. You can check this by examining the data in the <code>alembic_version</code> table. It should have a single entry, corresponding to the revision of the script we just applied.</p>
<h1 id="next-steps">Next Steps</h1>
<p>This is a great first step towards building a comprehensive data model, but there is a long way to go before we can consider migrating our data model from a development environment to a production environment! In subsequent blog posts, we will cover the following:</p>
<ul>
<li>Updating our data model with more tables and relationships to create a proper e-commerce database schema.</li>
<li>Using an Async engine for asynchronous database connections</li>
<li>Using the <code>alembic_utils</code> library to incorporate views, functions and triggers into the version control system.</li>
<li>Configuring Alembic to connect to a production environment, and seamlessly bring the production environment&rsquo;s schema up-to-date with our final revision.</li>
</ul>
<h1 id="references">References</h1>
<ul>
<li>
<p><a href="https://docs.sqlalchemy.org/en/20/intro.html">SQLAlchemy documentation</a>, most notably the <a href="https://docs.sqlalchemy.org/en/20/tutorial/index.html#unified-tutorial">SQLAlchemy Unified Tutorial Section.</a></p>
</li>
<li>
<p><a href="https://alembic.sqlalchemy.org/en/latest/index.html">Alembic&rsquo;s documentation</a>, and importantly, the <a href="https://alembic.sqlalchemy.org/en/latest/autogenerate.html">section covering Autogeneration.</a></p>
</li>
</ul>
]]></content:encoded></item><item><title>Secure SFTP Backups with pgBackRest for PostgreSQL: A Step-by-Step Guide</title><link>https://naveenkannan.dev/posts/pgbackrest_sftp_backups/</link><pubDate>Wed, 14 May 2025 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/pgbackrest_sftp_backups/</guid><description>A guide to configuring pgBackRest to securely perform SFTP based backups of your PostgreSQL server.</description><content:encoded><![CDATA[<h1 id="introduction">Introduction</h1>
<p>pgBackRest is an open source tool that allows you to perform automated backup and restore operations for a PostgreSQL server. It allows you to take both full and incremental file system backups of your data. With pgBackRest, you can set up multiple repositories for your backups, both local and remote. You can further customize the configuration to take backups at specific times of the day so that regular operations are not affected, and backup retention and rotation can be customized as needed.</p>
<p>Furthermore, it allows mirroring of backups to cloud-compatible object stores, including S3, Azure, and GCS compatible object stores.</p>
<figure class="align-center ">
    <img loading="lazy" src="pgbackrest_logo.png#center"
         alt="pgBackRest helps make taking database backups pretty straightforward."/> <figcaption>
            <p>pgBackRest helps make taking database backups pretty straightforward.</p>
        </figcaption>
</figure>

<p>Insecurely configured SFTP backups can expose critical database content to unauthorized access, yet many organizations rely on them for off-site storage. This guide addresses the security gap and will explain the process of setting up remote SFTP repositories in a secure manner. We will create a system user on the SFTP repository and limit their permissions, and perform scheduled backups exclusively via the limited user account.</p>
<p>A good database backup strategy uses multiple repositories of different types, alongside both physical and logical backups. Do not limit yourself to merely SFTP repositories. Remember, with database backups, redundancy is good. This tutorial is simply to explain how to <em>securely</em> configure SFTP backups.</p>
<h2 id="database-server-requirements">Database Server Requirements</h2>
<ul>
<li>
<p>PostgreSQL 12.0 or newer (this tutorial uses PostgreSQL 17.0)</p>
<ul>
<li>PostgreSQL 12.0+ is required because earlier versions lack WAL archiving features that enable point-in-time recovery when using pgBackRest.</li>
</ul>
</li>
<li>
<p>pgBackRest 2.41 or newer (this tutorial uses pgBackRest 2.55.1)</p>
<ul>
<li>Earlier versions may have limitations with certain authentication methods</li>
</ul>
</li>
<li>
<p>OpenSSH 8.0+ client for improved Ed25519 key support</p>
</li>
<li>
<p>libssh2 1.9.0+ for modern cryptographic algorithm support</p>
</li>
<li>
<p>Sufficient disk space for temporary backup files before transfer</p>
</li>
</ul>
<h2 id="sftp-host-requirements">SFTP Host Requirements</h2>
<ul>
<li>
<p>Linux server with SSH daemon supporting SFTP subsystem</p>
<ul>
<li>Recommended: OpenSSH 8.0+ for optimal security features</li>
</ul>
</li>
<li>
<p>Sufficient storage space for your backup retention needs. Plan for minimum 3x your database size (1x for full backup + 1x for transaction logs + 1x overhead and retention)</p>
</li>
<li>
<p>Root access for initial configuration</p>
</li>
<li>
<p>Ability to create dedicated system users and groups</p>
</li>
<li>
<p>Firewall configured to allow SFTP connections (typically port 22) from your database server IP</p>
</li>
</ul>
<h2 id="network-requirements">Network Requirements</h2>
<ul>
<li>
<p>Stable network connection between database and SFTP servers</p>
</li>
<li>
<p>Firewall rules allowing SFTP traffic (TCP port 22 by default)</p>
</li>
<li>
<p>Recommended: Internal network connectivity (avoid public internet exposure if possible)</p>
</li>
</ul>
<p>You can check your versions with these commands:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>postgres --version
</span></span><span style="display:flex;"><span>pgbackrest version
</span></span><span style="display:flex;"><span>ssh -V
</span></span></code></pre></div><h1 id="the-process">The Process</h1>
<p>For this process, this guide assumes both servers are on a private network (10.0.0.0/24) protected by a corporate firewall, with the following IP addresses:</p>
<ul>
<li>
<p>The IP of the database server is <code>10.0.0.1</code></p>
</li>
<li>
<p>The user that runs the code on the database server is <code>postgres</code>. <code>postgres</code> is a system user and cannot log in via SSH.</p>
</li>
<li>
<p>The IP of the SFTP repo is <code>10.0.0.2</code></p>
</li>
</ul>
<h2 id="on-the-database-host">On the database host</h2>
<ul>
<li>Create an SSH key pair in the <code>postgres</code> user&rsquo;s home directory.</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Adjust this path based on your system&#39;s postgres home</span>
</span></span><span style="display:flex;"><span>export PGHOME<span style="color:#f92672">=</span>/var/lib/postgresql
</span></span><span style="display:flex;"><span>ssh-keygen -f $PGHOME/.ssh/id_ed25519_sftp_key -t ed25519 -N <span style="color:#e6db74">&#34;&#34;</span>
</span></span></code></pre></div><p>Ensure that the <code>.ssh</code> folder is owned by the <code>postgres</code> user. We use Ed25519 keys because they provide stronger security with shorter keys compared to RSA, and are supported by modern OpenSSH implementations.</p>
<ul>
<li>Perform a key scan on the SFTP host to gather the public keys. Make sure you verify the legitimacy of the host server before you build your known hosts file.</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ssh-keyscan 10.0.0.2 &gt;&gt; $PGHOME/.ssh/known_hosts
</span></span></code></pre></div><p>You can validate the host keys via fingerprints by running:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ssh-keygen -lf $PGHOME/.ssh/known_hosts
</span></span></code></pre></div><h2 id="on-the-sftp-server-host">On the SFTP server host</h2>
<h3 id="create-a-directory-that-the-sftp-user-account-will-be-restricted-to">Create a directory that the SFTP user account will be restricted to.</h3>
<p>We create a dedicated /data directory separate from standard system paths to isolate backup data and simplify permission management.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Creating a folder in the root directory</span>
</span></span><span style="display:flex;"><span>mkdir /data
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Creating the home directory for the SFTP user</span>
</span></span><span style="display:flex;"><span>mkdir -p /data/sftpuser/upload
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Setting the appropriate permissions for this filesystem</span>
</span></span><span style="display:flex;"><span>chmod <span style="color:#ae81ff">701</span> /data
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## /data needs to have root:root permissions</span>
</span></span><span style="display:flex;"><span>chown root:root /data
</span></span></code></pre></div><h3 id="create-an-sftp-group-followed-by-an-sftp-user-with-minimal-permissions-and-as-a-system-user">Create an SFTP group, followed by an SFTP user with minimal permissions and as a system user.</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Creating an SFTP group</span>
</span></span><span style="display:flex;"><span>groupadd sftpusers
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Creating the SFTP user as a system user</span>
</span></span><span style="display:flex;"><span>useradd -g sftpusers -d /data/sftpuser/upload/ -s /usr/sbin/nologin -r sftpuser
</span></span></code></pre></div><p>This process creates a group called <code>sftpusers</code> and creates a system user in that group called <code>sftpuser</code>, with their home directory being <code>/data/sftpuser/upload</code>.</p>
<h3 id="change-the-permissions-for-the-sftp-directory">Change the permissions for the SFTP directory.</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">## Giving ownership to the sftpusers group for the sftpuser folder</span>
</span></span><span style="display:flex;"><span>chown root:sftpusers /data/sftpuser
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Giving the sftpuser system account access to the home directory we created</span>
</span></span><span style="display:flex;"><span>chown sftpuser:sftpusers /data/sftpuser/upload
</span></span></code></pre></div><p>It is very important that <code>/data/sftpuser/upload</code> must <em>not</em> be owned by root. This is a common cause of failure for chroot setups.</p>
<h3 id="updating-the-ssh-configuration-on-the-sftp-server">Updating the SSH configuration on the SFTP server.</h3>
<p>Add the following to the end <code>/etc/ssh/sshd_config</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># For sftp backups</span>
</span></span><span style="display:flex;"><span>Match Group sftpusers
</span></span><span style="display:flex;"><span>  ChrootDirectory /data/%u
</span></span><span style="display:flex;"><span>  ForceCommand internal-sftp
</span></span><span style="display:flex;"><span>  AllowTcpForwarding no
</span></span><span style="display:flex;"><span>  X11Forwarding no
</span></span></code></pre></div><p>Here&rsquo;s what this does:</p>
<ul>
<li>
<p>The first line <code>Match Group sftpusers</code> starts a conditional block that only applies to users in the <code>sftpusers</code> group.</p>
</li>
<li>
<p>The second line <code>ChrootDirectory /data/%u</code> (jails) the user to the directory <code>/data/username</code>, where <code>%u</code> is replaced with the <code>username</code>. This ensures the user is jailed to their upload directory and cannot access the broader file system.</p>
</li>
<li>
<p>The third line <code>ForceCommand internal-sftp</code> forces the SSH session to use the internal SFTP subsystem. The user cannot get a shell session, and only SFTP is allowed.</p>
</li>
<li>
<p>The fourth line <code>AllowTcpForwarding no</code> prevents the user using SSH port forwarding to avoid tunneling through SSH.</p>
</li>
<li>
<p>The fifth line <code>X11Forwarding no</code> disables X11 forwarding to prevent GUI application tunneling over SSH.</p>
</li>
</ul>
<h3 id="validate-your-ssh-config-and-then-restart-the-ssh-daemon">Validate your SSH config and then Restart the SSH daemon</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo sshd -T <span style="color:#f92672">&amp;&amp;</span> sudo systemctl restart sshd
</span></span></code></pre></div>
            <link rel="stylesheet" href="/css/vendors/admonitions.53cd9f8afa9d9a8ac09093f668df057bc6d0f4bbd0886f39991a7b99934a7432.css" integrity="sha256-U82fivqdmorAkJP2aN8Fe8bQ9LvQiG85mRp7mZNKdDI=" crossorigin="anonymous">
    <div class="admonition caution">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"/></svg>
        <span>Caution</span>
      </div>
      <div class="admonition-content">
        <p>Do not restart the SSH daemon without validating your SSH config with the command <code>sudo sshd -T</code>! If your config is wrong, the daemon will fail to initiate, and you may be locked out of being able to SSH into the server!</p>
      </div>
    </div><h3 id="create-the-ssh-directory-for-the-sftpuser-account-with-appropriate-permissions">Create the SSH directory for the sftpuser account with appropriate permissions</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Creating the .ssh directory</span>
</span></span><span style="display:flex;"><span>mkdir /data/sftpuser/upload/.ssh
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Creating the authorized_keys and known_hosts files</span>
</span></span><span style="display:flex;"><span>touch /data/sftpuser/upload/.ssh/authorized_keys
</span></span><span style="display:flex;"><span>touch /data/sftpuser/upload/.ssh/known_hosts
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Changing permissions for the newly created folders and files</span>
</span></span><span style="display:flex;"><span>chown -R sftpuser:sftpusers /data/sftpuser/upload/.ssh
</span></span><span style="display:flex;"><span>chmod <span style="color:#ae81ff">700</span> /data/sftpuser/upload/.ssh
</span></span><span style="display:flex;"><span>chmod <span style="color:#ae81ff">640</span> /data/sftpuser/upload/.ssh/authorized_keys
</span></span><span style="display:flex;"><span>chmod <span style="color:#ae81ff">644</span> /data/sftpuser/upload/.ssh/known_hosts
</span></span></code></pre></div><h3 id="append-the-contents-of-the-new-public-ssh-key-to-the-authorized_keys-file">Append the contents of the new public SSH key to the authorized_keys file</h3>
<p>On the database host, copy the contents of the public key we created (<code>$PGHOME/.ssh/id_ed25519_sftp_key.pub</code>) and append it to the authorized_keys file on the SFTP user&rsquo;s SSH directory (&rsquo;/data/sftpuser/upload/.ssh/authorized_keys&rsquo;).</p>

    <div class="admonition caution">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"/></svg>
        <span>Caution</span>
      </div>
      <div class="admonition-content">
        <p>Remember to <em>never share your private key</em> with any server, or to anyone else! Only copy the contents of the public key!</p>
      </div>
    </div><h1 id="testing">Testing</h1>
<p>From the database host, test if you can login to the SFTP server using the keys without needing a password.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sftp -i $PGHOME/.ssh/id_ed25519_sftp_key sftpuser@10.0.0.2
</span></span></code></pre></div><p>If your setup was correct, this will let you login without a password.</p>
<h1 id="deploying">Deploying</h1>
<p>Update your <code>pgbackrest.config</code> file with the following:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#75715e"># SFTP Backup Repo</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo2-bundle</span><span style="color:#f92672">=</span><span style="color:#e6db74">y</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo2-path</span><span style="color:#f92672">=</span><span style="color:#e6db74">/upload</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Host</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo2-sftp-host</span><span style="color:#f92672">=</span><span style="color:#e6db74">10.0.0.2</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo2-sftp-host-key-hash-type</span><span style="color:#f92672">=</span><span style="color:#e6db74">sha256</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># User</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo2-sftp-host-user</span><span style="color:#f92672">=</span><span style="color:#e6db74">sftpuser</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Key Location</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo2-sftp-private-key-file</span><span style="color:#f92672">=</span><span style="color:#e6db74">/var/lib/postgresql/.ssh/id_ed25519_sftp_key</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo2-sftp-public-key-file</span><span style="color:#f92672">=</span><span style="color:#e6db74">/var/lib/postgresql/.ssh/id_ed25519_sftp_key.pub</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo2-type</span><span style="color:#f92672">=</span><span style="color:#e6db74">sftp</span>
</span></span></code></pre></div><p>The above configuration file sets up the automation process for transferring backups to the SFTP host via the SFTP service, as the user <code>sftpuser</code>, who has restricted permissions.</p>
<p>An example of a full <code>pgbackrest.conf</code> file: (The default location is <code>/etc/pgbackrest/pgbackrest.conf</code>)</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[main]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">pg1-path</span><span style="color:#f92672">=</span><span style="color:#e6db74">/usr/local/pgsql/data</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">pg1-port</span><span style="color:#f92672">=</span><span style="color:#e6db74">5432</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">pg1-user</span><span style="color:#f92672">=</span><span style="color:#e6db74">postgres</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">pg1-database</span><span style="color:#f92672">=</span><span style="color:#e6db74">postgres</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[global]</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Local Backup Repo</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo1-cipher-pass</span><span style="color:#f92672">=</span><span style="color:#e6db74">xxx # Insert with your own</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo1-cipher-type</span><span style="color:#f92672">=</span><span style="color:#e6db74">aes-256-cbc</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo1-path</span><span style="color:#f92672">=</span><span style="color:#e6db74">/var/lib/pgbackrest</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo1-retention-full</span><span style="color:#f92672">=</span><span style="color:#e6db74">2</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># SFTP Backup Repo 1</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># This section handles the configuration for your SFTP repo</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo2-bundle</span><span style="color:#f92672">=</span><span style="color:#e6db74">y</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo2-path</span><span style="color:#f92672">=</span><span style="color:#e6db74">/upload</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo2-retention-full</span><span style="color:#f92672">=</span><span style="color:#e6db74">2</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Host</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo2-sftp-host</span><span style="color:#f92672">=</span><span style="color:#e6db74">10.0.0.2</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo2-sftp-host-key-hash-type</span><span style="color:#f92672">=</span><span style="color:#e6db74">sha256</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># User</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo2-sftp-host-user</span><span style="color:#f92672">=</span><span style="color:#e6db74">sftpuser</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Key Location</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo2-sftp-private-key-file</span><span style="color:#f92672">=</span><span style="color:#e6db74">/var/lib/postgresql/.ssh/id_ed25519_sftp_key</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">repo2-sftp-public-key-file</span><span style="color:#f92672">=</span><span style="color:#e6db74">/var/lib/postgresql/.ssh/id_ed25519_sftp_key.pub</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Logging Info</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">log-path</span><span style="color:#f92672">=</span><span style="color:#e6db74">/var/log/pgbackrest</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">log-level-console</span><span style="color:#f92672">=</span><span style="color:#e6db74">debug</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[global:archive-push]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">compress-level</span><span style="color:#f92672">=</span><span style="color:#e6db74">3</span>
</span></span></code></pre></div><p>Then create the stanza by running the following as the <code>postgres</code> user:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pgbackrest --stanza<span style="color:#f92672">=</span>main --log-level-console<span style="color:#f92672">=</span>info stanza-create
</span></span></code></pre></div><p>If your config is right, then this will succeed with the following output:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>P00   INFO: stanza-create command end: completed successfully
</span></span></code></pre></div><h1 id="addendum">Addendum</h1>
<h2 id="using-secure-key-algorithms-and-hash-functions">Using secure key algorithms and hash functions</h2>
<p>If you are using older versions of cryptography libraries like libssh2 and openssl, your configuration might be restricted to using older, more outdated key algorithms like RSA or DSS, along with older hash functions like SHA1. I recommend using more secure, newer key algorithms like Ed25519, along with more secure hashing functions like SHA256.</p>
<h3 id="check-the-key-algorithms-supported-by-your-installation-of-libssh2">Check the key algorithms supported by your installation of libssh2</h3>
<p>Run the following code to see what key types are supported by your installation of libssh2:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>strings /usr/lib/x86_64-linux-gnu/libssh2.so.1 | grep ssh-
</span></span></code></pre></div><p>Your output should return more modern key algorithms like <code>ssh-ed25519</code>. If you output only includes algorithms like <code>ssh-rsa</code> or <code>ssh-dss</code>, then you will need to update your installation of libssh2. Some older servers, like ones running outdated operating systems like Ubuntu 20.04, may not allow you to update your libssh2 library using package managers. In that case, you will have to manually build the newer versions from source.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">## Installing dependencies</span>
</span></span><span style="display:flex;"><span>sudo apt update
</span></span><span style="display:flex;"><span>sudo apt install build-essential cmake zlib1g-dev libssl-dev
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Downloading and building libssh2 from source</span>
</span></span><span style="display:flex;"><span>wget https://www.libssh2.org/download/libssh2-1.11.0.tar.gz
</span></span><span style="display:flex;"><span>tar -xzf libssh2-1.11.0.tar.gz
</span></span><span style="display:flex;"><span>cd libssh2-1.11.0
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Installing libssh2</span>
</span></span><span style="display:flex;"><span>./configure
</span></span><span style="display:flex;"><span>make -j<span style="color:#66d9ef">$(</span>nproc<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>sudo make install
</span></span><span style="display:flex;"><span>sudo ldconfig
</span></span></code></pre></div><p>The new libssh2 library will have it&rsquo;s binaries located at <code>/usr/local/lib/libssh2.so</code>. You can confirm if it was installed by running:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>strings /usr/local/lib/libssh2.so | grep ssh-
</span></span></code></pre></div><p>You should see the newer key algorithms like <code>ecdsa-sha2-nistp256</code>and <code>ssh-ed25519</code>.</p>
<p>If you built/installed pgBackRest <em>before</em> updating your libssh2 library, you will need to rebuild it from source so that it uses the newer library.</p>
<p>To check if pgBackRest is using the newly installed libssh2 as a dependency, run:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ldd <span style="color:#66d9ef">$(</span>which pgbackrest<span style="color:#66d9ef">)</span> | grep libssh2
</span></span></code></pre></div><p>Your output should look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>libssh2.so.1 <span style="color:#f92672">=</span>&gt; /usr/local/lib/libssh2.so.1 <span style="color:#f92672">(</span>0x00007f20882cf000<span style="color:#f92672">)</span>
</span></span></code></pre></div><p>This confirms that pgBackRest is using the manually installed libssh2 from <code>/usr/local/lib</code>, rather than the system-wide default at <code>/usr/lib/x86_64-linux-gnu</code>.</p>
<h1 id="references">References</h1>
<ul>
<li>
<p><a href="https://pgbackrest.org/user-guide.html">pgBackRest documentation</a></p>
</li>
<li>
<p><a href="https://libssh2.org/">libssh2</a></p>
</li>
<li>
<p><a href="https://www.postgresql.org/docs/current/backup.html">PostgreSQL backup documentation</a></p>
</li>
</ul>
]]></content:encoded></item><item><title>Integrating HDFS and PostgreSQL through Apache Spark.</title><link>https://naveenkannan.dev/posts/spark_postgres_hdfs/</link><pubDate>Mon, 18 Mar 2024 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/spark_postgres_hdfs/</guid><description>A guide to leveraging Spark&amp;#39;s `inferSchema` tool in conjunction with the HDFS to streamline PostgreSQL database schema/table creation.</description><content:encoded><![CDATA[<h1 id="introduction">Introduction</h1>
<p>The HDFS (Hadoop Distributed File System) and PostgreSQL databases are both powerful tools for data storage, queries and analyses. Each have their own unique strengths that make them well suited for specific tasks.</p>
<p>The HDFS, being distributed across several computing nodes, is robust and amenable to storing massive datasets, provided your computing infrastructure has the prerequisite width (the number of nodes in your cluster) and the depth(the available memory on each individual node). The HDFS is optimized for batch processing of massive datasets, making it suitable for big data applications like data warehousing, log processing, and large-scale data analytics. In fact, Spark, the HDFS&rsquo; natural companion, has it&rsquo;s own machine learning library <code>MLlib</code>, making large scale data analytics very much possible.</p>
<p>PostgreSQL databases, on the other hand, are robust RDBMS optimized for ACID (Atomicity, Consistency, Isolation, Durability) compliance. It provides robust data security and access control mechanisms, making it suitable for applications that require strict data protection. PostgreSQL supports SQL, a powerful query language that enables efficient querying and retrieval of data based on specific criteria. PostgreSQL shines at handling structured, transactional workloads that require data integrity, consistency, and complex querying capabilities.</p>
<figure class="align-center ">
    <img loading="lazy" src="post_hdfs.png#center"
         alt="Spark and PostgreSQL make for a uniquely powerful combination."/> <figcaption>
            <p>Spark and PostgreSQL make for a uniquely powerful combination.</p>
        </figcaption>
</figure>

<h1 id="why-connect-the-two">Why connect the two?</h1>
<p><strong>The ability to trivialize Data Ingestion.</strong></p>
<p>Spark has a tool called <code>inferSchema</code>. This is a function that allows for the input schema of data to be <strong>automatically inferred</strong>. It allows for more flexibility and easier data ingestion, especially when working with semi-structured or unstructured data formats. Provided your input data is cleaned up, the <code>schema</code> dictated by <code>inferSchema</code> is very reliable. It makes specifying the schema for a dataset with a large number of columns easy!
This eliminates the need for manually defining the schema, which can be a time-consuming and error-prone process, especially when dealing with datasets containing a large number of columns or complex data structures.</p>
<p>Once this data has it&rsquo;s schema specified, connecting a HDFS to a PostgreSQL database allows for the rapid, trivial loading of data into database tables.</p>
<p>This helps streamline the Data ingestion process as follows:</p>
<ul>
<li>
<p><strong>Data storage in HDFS</strong>: The raw data files (e.g., CSV, JSON) are stored in the HDFS, which provides distributed storage and fault tolerance.</p>
</li>
<li>
<p><strong>Schema inference with Spark</strong>: Spark reads the data from the HDFS and automatically creates the schema when using <code>inferSchema</code>. This process ensures that the schema accurately reflects the structure of the input data, especially if it has been cleaned up or transformed.</p>
</li>
</ul>
<p>Even if your data isn&rsquo;t clean to begin with, Spark allows for the data to be cleaned and then written back out into the HDFS, and <code>inferSchema</code> will ensure the schema of the table is accurate when this file is read back in.</p>
<ul>
<li><strong>Loading data into a PostgreSQL database</strong>: With the schema inferred by Spark, the data can be easily loaded into PostgreSQL database tables.</li>
</ul>
<p>And just like that, PostgreSQL gains the ability to ingest data from a wide range of semi-structured or unstructured data sources stored in a HDFS cluster quickly and reliably.</p>
<figure class="align-center ">
    <img loading="lazy" src="flowchart.png#center"/> 
</figure>

<h1 id="connecting-hdfs-to-a-postgresql-database">Connecting HDFS to a PostgreSQL database</h1>
<h2 id="the-prerequisites">The prerequisites</h2>
<p>This tutorial assumes you have the following prerequisites set up:</p>
<ul>
<li>
<p>Hadoop Distributed File System (HDFS): You have HDFS installed and running on your cluster. HDFS will serve as the distributed storage system for your data files.</p>
</li>
<li>
<p>Apache Spark: You have Apache Spark installed and configured to run on your cluster. Spark will act as the data processing engine to read data from HDFS and load it into PostgreSQL.</p>
</li>
<li>
<p>PostgreSQL Database: You have a PostgreSQL database instance up and running, either on the same cluster or a separate server. This is the target database where you want to load your data from HDFS.</p>
</li>
</ul>
<h2 id="downloading-the-postgresql-jdbc-driver">Downloading the PostgreSQL JDBC driver</h2>
<p>Find the PostgreSQL JDBC driver that is appropriate for your <code>Java</code> version and download it. The JDBC driver provides a standard API for Java applications (like Spark) to interact with relational databases. For example, if you&rsquo;re using PostgreSQL 14.x and Java 8, you might download the <code>postgresql-42.x.x.jar</code> file.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>wget https://jdbc.postgresql.org/download/postgresql-xx.x.x.jar
</span></span></code></pre></div><p>Once you have the <code>jar</code> file downloaded, place it in the <code>jars</code> folder of your Spark installation.</p>
<p>Edit your <code>spark-env.sh</code> file (which can be found in the <code>conf</code> folder) to have the following line:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>export SPARK_CLASSPATH<span style="color:#f92672">=</span>/path/to/spark-x.x.x-bin-hadoop3/jars/postgresql-xx.x.x.jar
</span></span></code></pre></div><p>Now restart your HDFS cluster, YARN, and MapReduce services to ensure the changes take effect. You can typically do this by running the <code>stop-all.sh</code> and <code>start-all.sh</code> scripts provided with your Hadoop installation.</p>
<h2 id="connecting-pyspark-to-postgresql">Connecting PySpark to PostgreSQL</h2>
<p>The following code chunk makes the following assumptions. Change them to suit your use case:</p>
<p>The host name of the server that runs the PostgreSQL database is <code>postgres.server</code>.</p>
<p>The port on which PostgreSQL is listening is <code>5432</code> (the default port).</p>
<p>The name of the database you want to load the tables into is <code>postgres</code>.</p>
<p>The name of the user who has connection and write privileges on that database is <code>user</code>.</p>
<p>The password for that user is <code>password</code>.</p>
<p>The raw data (that has been cleaned) is named <code>data_file</code>, and is a CSV file. This data type can be anything that&rsquo;s loaded on the HDFS, just remember to change the type of <code>reader</code> being used.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># Assumptions and configurations</span>
</span></span><span style="display:flex;"><span>data_file <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/path/to/data_file.csv&#34;</span>  <span style="color:#75715e"># Replace with the actual path to your data file on HDFS</span>
</span></span><span style="display:flex;"><span>table_name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;your_table_name&#34;</span>  <span style="color:#75715e"># Replace with the desired table name in PostgreSQL</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Read the data from HDFS</span>
</span></span><span style="display:flex;"><span>data <span style="color:#f92672">=</span> spark<span style="color:#f92672">.</span>read<span style="color:#f92672">.</span>csv(data_file,
</span></span><span style="display:flex;"><span>                      header<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>                      sep<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;,&#39;</span>,
</span></span><span style="display:flex;"><span>                      inferSchema<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)  <span style="color:#75715e"># inferSchema=True will automatically infer the schema</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># PostgreSQL database connection details</span>
</span></span><span style="display:flex;"><span>jdbc_url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;jdbc:postgresql://postgres.server:5432/postgres&#34;</span>
</span></span><span style="display:flex;"><span>properties <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;user&#34;</span>: <span style="color:#e6db74">&#34;user&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;password&#34;</span>: <span style="color:#e6db74">&#34;password&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;driver&#34;</span>: <span style="color:#e6db74">&#34;org.postgresql.Driver&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Write the data to the PostgreSQL table</span>
</span></span><span style="display:flex;"><span>data<span style="color:#f92672">.</span>write<span style="color:#f92672">.</span>jdbc(url<span style="color:#f92672">=</span>jdbc_url, table<span style="color:#f92672">=</span>table_name, mode<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;overwrite&#34;</span>, properties<span style="color:#f92672">=</span>properties)
</span></span></code></pre></div><p>This code chunk will:</p>
<ol>
<li>
<p>Read the data in, and automatically create a schema. The <code>spark.read.csv()</code> method is used to read the data from the specified CSV file located on HDFS. It assumes that the file has a header row, and the columns are separated by commas. The <code>inferSchema=True</code> option tells Spark to automatically infer the schema (column names and data types) from the data.</p>
</li>
<li>
<p>Set up a connection to your PostgreSQL database.  The <code>jdbc_url</code> variable specifies the JDBC connection string for the PostgreSQL database, including the hostname, port, and database name. The properties dictionary contains the user credentials (username and password) and the PostgreSQL JDBC driver class name.</p>
</li>
<li>
<p>Write the data to the database with the name <code>table_name</code>. The <code>data.write.jdbc()</code> method is used to write the Spark DataFrame (data) to the specified PostgreSQL table. The <code>url</code> parameter specifies the JDBC connection URL, the <code>table</code> parameter specifies the target table name, and the <code>mode=&quot;overwrite&quot;</code> option tells Spark to overwrite the table if it already exists. The <code>properties</code> parameter passes the connection properties, including the user credentials and driver class name.</p>
</li>
</ol>
<p>With this integration between HDFS, Apache Spark, and PostgreSQL, you can efficiently ingest and process large volumes of data stored in the HDFS and load it into your PostgreSQL database. Leveraging Spark&rsquo;s powerful data processing capabilities and its ability to automatically infer schemas streamlines the data ingestion process, making it more efficient and flexible. By combining the strengths of these two data storage systems, you can unlock new possibilities for storing and analyzing large datasets.</p>
]]></content:encoded></item><item><title>Mamba implementation in Scientific Pipelines.</title><link>https://naveenkannan.dev/posts/mamba_pipeline_implementation/</link><pubDate>Sat, 18 Nov 2023 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/mamba_pipeline_implementation/</guid><description>A guide to using Mamba (over Conda) for pipeline building.</description><content:encoded><![CDATA[<h1 id="what-is-mamba">What is Mamba?</h1>
<p><a href="https://github.com/mamba-org/mamba">Mamba</a> is intended to be a drop-in replacement for /reimplementation of <strong>conda</strong> (written in <strong>C++</strong>).</p>
<p>Mamba has been something that I have implemented into all of my pipelines, since it trivializes the package management process. I do almost all of my work within the context of containers/virtual environments, and mamba makes my work life so much easier.</p>
<p>Previously, I used to use Conda as my package manager of choice, relying on it to cut down the amount of time I would need to build an environment using <code>pip</code> as an installer. Once I discovered Mamba, however, I never went back to Conda.</p>
<h1 id="why-use-mamba-over-conda">Why use Mamba over Conda?</h1>
<p>Simple. Mamba is just faster.</p>
<p>Conda is implemented in Python, while Mamba utilizes C++ code for critical operations (instead of Python), optimizing it&rsquo;s performance.</p>
<p>At the same time, Mamba utilizes the same command line parser, package installation and deinstallation code and transaction verification routines as Conda to stay as compatible as possible.</p>
<p>This means that Mamba is a drop-in replacement for Conda. If you know how to use Conda, you know how to use Mamba!</p>
<p>Mamba&rsquo;s features include:</p>
<ul>
<li>
<p>multi-threaded parallel downloads of repo data and package files.</p>
</li>
<li>
<p><code>libsolv</code> for much faster dependency solving, which is also used in the RPM package manager of Red Hat, Fedora and OpenSUSE.</p>
</li>
<li>
<p>core parts of mamba are implemented in C++.</p>
</li>
<li>
<p>utilizes the same command line parser, package installation and deinstallation code and transaction verification routines as Conda to stay as compatible as possible.</p>
</li>
</ul>
<figure class="align-center ">
    <img loading="lazy" src="https://imgs.xkcd.com/comics/python_environment.png#center"
         alt="Maybe don&rsquo;t install everything in base."/> <figcaption>
            <p>Maybe don&rsquo;t install everything in base.<a href="https://imgs.xkcd.com/comics/python_environment.png">XKCD #1987</a></p>
        </figcaption>
</figure>

<p>Conda excels in creating <strong>virtual environments</strong> that help keep your base environment clutter-free. Initiating a Conda virtual environment helps you keep the dependencies for a particular project <em>limited to that environment.</em> No need to run the risk of nuking your workflow simply because you had to install Python 2.7 as a dependency for an old package that the maintainer never got around to updating.</p>

            <link rel="stylesheet" href="/css/vendors/admonitions.53cd9f8afa9d9a8ac09093f668df057bc6d0f4bbd0886f39991a7b99934a7432.css" integrity="sha256-U82fivqdmorAkJP2aN8Fe8bQ9LvQiG85mRp7mZNKdDI=" crossorigin="anonymous">
    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>Remember that <code>mamba</code> environments are essentially <code>conda</code> environments with <code>mamba</code> enabled. Functionally speaking, there is no difference between a <code>mamba</code> env and a <code>conda</code> env.</p>
      </div>
    </div><h1 id="implementing-mamba-into-your-workflow">Implementing Mamba into your workflow</h1>
<p>Implementing Mamba into your workflow is pretty straightforward. The potential options are:</p>
<ul>
<li>
<p><strong>Mambaforge:</strong> The mambaforge installer for conda (similar to conda-forge). This allows usage of the conda-forge channel by default.</p>
</li>
<li>
<p><strong>Micromamba:</strong> micromamba is a tiny executable version of the mamba package manager. It is a statically linked C++ executable with a separate command line interface. It does not need a base environment and does not come with a default version of Python.</p>
</li>
<li>
<p><strong>Miniforge:</strong> Similar to miniconda. As of August 2023, virtually similar to mambaforge, and is recommended over mambaforge since it now comes with mamba installed. Miniforge used to only ship with conda.</p>
</li>
</ul>
<p>As of July 2023, Miniforge then started to ship with <code>conda-libmamba-solver</code>. Since <code>conda-libmamba-solver</code> depends on <code>libmambapy</code>, the only difference between Miniforge and Mambaforge was the presence of the mamba Python package. As of August 2023, miniforge added <code>mamba</code> as well to minimize surprises.</p>
<ul>
<li><strong>Existing conda Install</strong> Simply installing mamba into a pre-existing conda environment. This is not recommended by the official documentation.</li>
</ul>
<h2 id="non-containerized-mamba">Non-containerized <code>mamba</code></h2>
<p>If you&rsquo;re intending on installing <code>mamba</code>, the official documentation says to ensure that <code>mamba</code> and <code>conda</code> are the only packages to be installed in the base environment.</p>
<p>Note that as of August 2023, <code>miniforge</code> and <code>mambaforge</code> are essentially the same thing.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>From the <a href="https://github.com/conda-forge/miniforge">official miniforge documentation</a>,</p>
<p>After the release of Miniforge 23.3.1 in August 2023, Miniforge and Mambaforge are essentially identical. The only difference is the name of the installer and subsequently the default installation path.</p>
<p>Before that release, Miniforge only shipped conda, while Mambaforge added mamba on top. Since Miniconda started shipping conda-libmamba-solver in July 2023, Miniforge followed suit and started shipping it too in August. At that point, since conda-libmamba-solver depends on libmambapy, the only difference between Miniforge and Mambaforge was the presence of the mamba Python package. To minimize surprises, we decided to add mamba to Miniforge too.</p>
      </div>
    </div><p>Given that the official documentation encourages the use of Miniforge over Mambaforge, the rest of this blog post will be looking at installing and using Miniforge.</p>
<h3 id="miniforge">Miniforge</h3>
<p>The <a href="https://github.com/conda-forge/miniforge">official miniforge documentation</a> has a list of installers for different operating systems and architectures.</p>
<p><img alt="A screencap of the docs." loading="lazy" src="/posts/mamba_pipeline_implementation/miniforge_installer.png">{fig-align=&ldquo;center&rdquo;}</p>
<p>The easiest way to do this is via a non-interactive method, using the following commands (for Unix-like platforms such as Mac OS &amp; Linux)</p>
<p>::: callout-important
This section assumes you already have <code>conda</code> installed and set up in the <code>base</code> environment.
:::</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">## Download the appropriate installer for your architecture and OS</span>
</span></span><span style="display:flex;"><span>wget <span style="color:#e6db74">&#34;https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-</span><span style="color:#66d9ef">$(</span>uname<span style="color:#66d9ef">)</span><span style="color:#e6db74">-</span><span style="color:#66d9ef">$(</span>uname -m<span style="color:#66d9ef">)</span><span style="color:#e6db74">.sh&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Run the script in non-interactive mode.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## -p is prefix option. A directory will be created at &#34;${HOME}/conda&#34;.</span>
</span></span><span style="display:flex;"><span>bash Miniforge3-<span style="color:#66d9ef">$(</span>uname<span style="color:#66d9ef">)</span>-<span style="color:#66d9ef">$(</span>uname -m<span style="color:#66d9ef">)</span>.sh -b -p <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>HOME<span style="color:#e6db74">}</span><span style="color:#e6db74">/conda&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">##  Create the path to conda</span>
</span></span><span style="display:flex;"><span>source <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>HOME<span style="color:#e6db74">}</span><span style="color:#e6db74">/conda/etc/profile.d/conda.sh&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## For mamba support also run the following command</span>
</span></span><span style="display:flex;"><span>source <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>HOME<span style="color:#e6db74">}</span><span style="color:#e6db74">/conda/etc/profile.d/mamba.sh&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Activate the base environment</span>
</span></span><span style="display:flex;"><span>conda activate
</span></span></code></pre></div><p>This above script installs <code>mamba</code> into the <code>base</code> conda environment and then activates the conda environment, enabling the usage of <code>mamba</code>. Now, using <code>mamba</code> is as simple as replacing all instances of <code>conda</code> with <code>mamba</code>!</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>Remember that the only packages you want installed in the <code>base</code> conda environment are <code>mamba</code> and <code>conda</code>, nothing else.</p>
      </div>
    </div><h3 id="micromamba">Micromamba</h3>
<p>The <a href="https://mamba.readthedocs.io/en/latest/installation/micromamba-installation.html">documentation for installation of <code>micromamba</code> can be found here.</a></p>
<p><code>micromamba</code> is a tiny executable version of the <code>mamba</code> package manager. It is a statically linked C++ executable with a separate command line interface. It comes with an empty base environment, and does not come with a default version of Python or <code>conda</code>.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>As a prerequisite, basic utilities (such as <code>curl</code>,<code>tar</code> and <code>bzip2</code>) need to be installed. Also you need a glibc based system like Ubuntu, Fedora or Centos (Alpine Linux does not work natively).</p>
      </div>
    </div><p>The following magic URL always returns the latest available version of <code>micromamba</code>, and the <code>bin/micromamba</code> part is automatically extracted using <code>tar</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Linux Intel (x86_64):</span>
</span></span><span style="display:flex;"><span>curl -Ls https://micro.mamba.pm/api/micromamba/linux-64/latest | tar -xvj bin/micromamba
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Linux ARM64:</span>
</span></span><span style="display:flex;"><span>curl -Ls https://micro.mamba.pm/api/micromamba/linux-aarch64/latest | tar -xvj bin/micromamba
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Linux Power:</span>
</span></span><span style="display:flex;"><span>curl -Ls https://micro.mamba.pm/api/micromamba/linux-ppc64le/latest | tar -xvj bin/micromamba
</span></span><span style="display:flex;"><span><span style="color:#75715e"># macOS Intel (x86_64):</span>
</span></span><span style="display:flex;"><span>curl -Ls https://micro.mamba.pm/api/micromamba/osx-64/latest | tar -xvj bin/micromamba
</span></span><span style="display:flex;"><span><span style="color:#75715e"># macOS Silicon/M1 (ARM64):</span>
</span></span><span style="display:flex;"><span>curl -Ls https://micro.mamba.pm/api/micromamba/osx-arm64/latest | tar -xvj bin/micromamba
</span></span></code></pre></div><p>After extraction is complete, the <code>micromamba</code> binary can be used.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Linux/bash:</span>
</span></span><span style="display:flex;"><span>./bin/micromamba shell init -s bash -p ~/micromamba  <span style="color:#75715e"># this writes to your .bashrc file</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># sourcing the bashrc file incorporates the changes into the running session.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># better yet, restart your terminal!</span>
</span></span><span style="display:flex;"><span>source ~/.bashrc
</span></span></code></pre></div><p>The above code chunk incorporates the path to the <code>micromamba</code> binary into the restarted terminal.</p>
<p>You can now activate the base environment, and build other environments.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>micromamba activate  <span style="color:#75715e"># this activates the base environment</span>
</span></span><span style="display:flex;"><span>micromamba install python<span style="color:#f92672">=</span>3.6 jupyter -c conda-forge
</span></span><span style="display:flex;"><span><span style="color:#75715e"># or</span>
</span></span><span style="display:flex;"><span>micromamba create -n env_name xtensor -c conda-forge
</span></span><span style="display:flex;"><span>micromamba activate env_name
</span></span></code></pre></div><h2 id="containerized-mamba-specifically-docker">Containerized <code>mamba</code> (specifically Docker)</h2>
<p>This is my recommended method of using <code>mamba</code> or <code>micromamba</code>. If you&rsquo;re using <code>mamba</code>, you&rsquo;re already encouraged to use <code>conda</code> environments. If you&rsquo;re using a <code>conda</code> environment, you should 100% consider incorporating the usage of containerization into your scientific workflow.</p>
<p>Containers make scientific pipelines easy to reproduce and easy to share, two concepts which form the bedrock of responsible science.</p>
<p><a href="https://naveenkannan.netlify.app/posts/containers_hpc/containers_HPC.html">I have a blog post about containers and why they rule. You can find it here.</a></p>
<h3 id="docker-based-miniforge">Docker-based Miniforge</h3>
<p>This is very simple. Simply pull from the <code>condaforge/miniforge3:latest</code> tag.</p>
<p>Start your <code>dockerfile</code> with the line:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>FROM condaforge/miniforge3:latest
</span></span></code></pre></div><p>This uses <code>miniforge</code> as a base image, and now you have a base environment with <code>mamba</code> and <code>conda</code> already installed and configured.</p>
<h3 id="docker-based-micromamba">Docker-based Micromamba</h3>
<p>This is similar to the above section. Start your <code>dockerfile</code> with the line:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>FROM mambaorg/micromamba:latest
</span></span></code></pre></div><h1 id="a-real-world-example-of-why-i-prefer-mamba-over-conda">A real-world example of why I prefer <code>mamba</code> over <code>conda</code>.</h1>
<p>Consider the following two <code>dockerfile</code>s:</p>
<h4 id="conda-based-dockerfile"><code>conda</code> based dockerfile</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>FROM condaforge/miniconda3:latest
</span></span><span style="display:flex;"><span>USER root
</span></span><span style="display:flex;"><span>RUN conda create -n test_env python<span style="color:#f92672">=</span>3.11 -y
</span></span><span style="display:flex;"><span>RUN conda install -n test_env <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-tidyverse&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-sessioninfo&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-bedr&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-dbplyr&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-rmariadb&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-seqinr&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-stringr&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-e1071&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-matrixeqtl&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;ucsc-blat&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-immunedeconv&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;bioconductor-ensembldb&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;bioconductor-bsgenome.hsapiens.ncbi.grch38&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;bioconductor-illuminahumanv4.db&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;plink&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;bioconductor-qvalue&#39;</span><span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-mashr&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;git&#39;</span><span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-vcfr&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-ashr&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-rstatix&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-ggpubr&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-glmnet&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-janitor&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-reticulate&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -c <span style="color:#e6db74">&#34;bioconda/label/cf201901&#34;</span> -c bioconda -c dnachun <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -c anaconda -c conda-forge -y <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#f92672">&amp;&amp;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    conda clean --all -f -y
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>RUN echo <span style="color:#e6db74">&#34;source activate test_env&#34;</span> &gt; ~/.bashrc
</span></span><span style="display:flex;"><span>ENV PATH /opt/conda/envs/test_env/bin:$PATH
</span></span></code></pre></div><h4 id="mamba-based-dockerfile"><code>mamba</code> based dockerfile</h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>FROM condaforge/miniforge3:latest
</span></span><span style="display:flex;"><span>USER root
</span></span><span style="display:flex;"><span>RUN mamba create -n test_env python<span style="color:#f92672">=</span>3.11 -y
</span></span><span style="display:flex;"><span>RUN mamba install -n test_env <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-tidyverse&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-sessioninfo&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-bedr&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-dbplyr&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-rmariadb&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-seqinr&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-stringr&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-e1071&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-matrixeqtl&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;ucsc-blat&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-immunedeconv&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;bioconductor-ensembldb&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;bioconductor-bsgenome.hsapiens.ncbi.grch38&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;bioconductor-illuminahumanv4.db&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;plink&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;bioconductor-qvalue&#39;</span><span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-mashr&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;git&#39;</span><span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-vcfr&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-ashr&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-rstatix&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-ggpubr&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-glmnet&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-janitor&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;r-reticulate&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -c <span style="color:#e6db74">&#34;bioconda/label/cf201901&#34;</span> -c bioconda -c dnachun <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    -c anaconda -c conda-forge -y <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    <span style="color:#f92672">&amp;&amp;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    mamba clean --all -f -y
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>RUN echo <span style="color:#e6db74">&#34;source activate test_env&#34;</span> &gt; ~/.bashrc
</span></span><span style="display:flex;"><span>ENV PATH /opt/conda/envs/test_env/bin:$PATH
</span></span></code></pre></div><p>Both <code>dockerfiles</code> are intended to accomplish the same goal. Create a <code>conda</code> env called <code>test_env</code>, install a list of packages in <code>test_env</code>, and then activate the <code>test_env</code>. However, while they accomplish the same goal, the way they both go about it is very different. I&rsquo;ve told you that the <code>mamba</code> based package installation is superior to using <code>conda</code> since it&rsquo;s faster, but now let&rsquo;s look at the output of a <code>docker build</code> command for both of these <code>dockerfiles</code>.</p>
<h4 id="mamba-based-docker-build-time"><code>mamba</code> based docker build time</h4>
<pre tabindex="0"><code class="language-lang-shell" data-lang="lang-shell">[+] Building 1250.0s (8/8) FINISHED
 =&gt; [internal] load build definition from dockerfile                                                                                                                                                        0.4s
 =&gt; =&gt; transferring dockerfile: 897B                                                                                                                                                                        0.0s
 =&gt; [internal] load .dockerignore                                                                                                                                                                           0.3s
 =&gt; =&gt; transferring context: 2B                                                                                                                                                                             0.0s
 =&gt; [internal] load metadata for docker.io/condaforge/miniforge3:latest                                                                                                                                     0.0s
 =&gt; CACHED [1/4] FROM docker.io/condaforge/miniforge3:latest                                                                                                                                                0.0s
 =&gt; [2/4] RUN mamba create -n test_env python=3.11 -y                                                                                                                                                      71.8s
 =&gt; [3/4] RUN mamba install -n test_env     &#39;r-tidyverse&#39;     &#39;r-sessioninfo&#39;     &#39;r-bedr&#39;     &#39;r-dbplyr&#39;     &#39;r-rmariadb&#39;     &#39;r-seqinr&#39;     &#39;r-stringr&#39;     &#39;r-e1071&#39;     &#39;r-matrixeqtl&#39;     &#39;ucsc-bl  1122.7s
 =&gt; [4/4] RUN echo &#34;source activate test_env&#34; &gt; ~/.bashrc                                                                                                                                                   1.5s
 =&gt; exporting to image                                                                                                                                                                                     53.4s
 =&gt; =&gt; exporting layers                                                                                                                                                                                    53.3s
 =&gt; =&gt; writing image sha256:94fbf957162e3735bcba17769deca5a2203f0ec6c7625d8f67254a085ac41531                                                                                                                0.0s
 =&gt; =&gt; naming to docker.io/library/mamba_test                                                                                                                                                               0.0s
</code></pre><p><code>mamba</code> (<code>miniforge3</code>, more specifically) took a total of 1250 seconds to accomplish this. (Approximately 21 minutes.)</p>
<h4 id="conda-based-build-time"><code>conda</code> based build time</h4>
<pre tabindex="0"><code class="language-lang-shell" data-lang="lang-shell">[+] Building 4180.1s (9/9) FINISHED
 =&gt; [internal] load build definition from dockerfile                                                                      0.4s
 =&gt; =&gt; transferring dockerfile: 875B                                                                                      0.0s
 =&gt; [internal] load .dockerignore                                                                                         0.3s
 =&gt; =&gt; transferring context: 2B                                                                                           0.0s
 =&gt; [internal] load metadata for docker.io/continuumio/miniconda3:latest                                                  0.7s
 =&gt; [auth] continuumio/miniconda3:pull token for registry-1.docker.io                                                     0.0s
 =&gt; CACHED [1/4] FROM docker.io/continuumio/miniconda3:latest@sha256:5ee0ed861bce8f67acb0a3e8a416891781db54302d93bf20a3c  0.0s
 =&gt; [2/4] RUN conda create -n test_env python=3.11 -y                                                                    30.9s
 =&gt; [3/4] RUN echo &#34;source activate test_env&#34; &gt; ~/.bashrc                                                                 1.4s
 =&gt; [4/4] RUN conda install     &#39;r-tidyverse&#39;     &#39;r-sessioninfo&#39;     &#39;r-bedr&#39;     &#39;r-dbplyr&#39;     &#39;r-rmariadb&#39;     &#39;r-  4073.2s
 =&gt; exporting to image                                                                                                   73.4s
 =&gt; =&gt; exporting layers                                                                                                  73.2s
 =&gt; =&gt; writing image sha256:30e994e7715109c9860dc45b40e2e57cac194c035a0239ad4b1a8246c59bf8f6                              0.1s
 =&gt; =&gt; naming to docker.io/library/conda_test                                                                             0.1s
</code></pre><p><code>conda</code> (<code>miniconda3</code>, more specifically) took a total of 4180 seconds to accomplish this. (Approximately 70 minutes.)</p>
<p><code>conda</code> took about 3.3 times the amount of time <code>mamba</code> did to build the same environment!</p>
<p><code>libsolv</code> makes <code>mamba</code> shine and helps it outperform regular <code>conda</code> for package management.</p>
<p>This is why I prefer using <code>mamba</code> over <code>conda</code> for environment based package management. Especially when I&rsquo;m adding stuff and rebuilding containers over and over, this extra saved time makes a big difference when it adds up.</p>
<p>Try <code>mamba</code> yourself! Remember, if you can use <code>conda</code>, you already know how to use <code>mamba</code>!</p>
<h1 id="references">References</h1>
<ul>
<li>
<p><a href="https://mamba.readthedocs.io/en/latest/installation/mamba-installation.html"><code>mamba</code> and <code>micromamba</code> installation documentation</a></p>
</li>
<li>
<p><a href="https://github.com/mamba-org/mamba"><code>mamba</code> Github repo</a></p>
</li>
<li>
<p><a href="https://github.com/conda-forge/miniforge"><code>miniforge</code> Github repo</a></p>
</li>
</ul>
]]></content:encoded></item><item><title>Moving Docker's Data directory to another location.</title><link>https://naveenkannan.dev/posts/docker_root_migration/</link><pubDate>Mon, 30 Oct 2023 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/docker_root_migration/</guid><description>A guide to transferring the Docker data storage to another location while preserving the existing images and containers.</description><content:encoded><![CDATA[<h1 id="introduction">Introduction</h1>
<p>Docker is a container service that we have discussed previously in my blog posts.</p>
<p>The default storage location for Docker is at <code>/var/lib/docker</code>. Speaking from experience, as images and containers are built over a period of time, especially if there are multiple users using the Docker service, the root filesystem can run into issues where the size of the Docker storage directory can cause potential out-of-space crises, and significantly deteriorate overall system performance.</p>
<p>In this blog post, we will look at transferring the Storage directory to another location. (In my case, I transferred it to a physical hard drive with expanded storage capacity).  It&rsquo;s an essential step to ensure that Docker operations continue to run smoothly and efficiently, particularly in production environments.</p>
<h1 id="the-process">The process</h1>
<p>The storage transfer process will have the following steps:</p>
<ul>
<li>
<p>Stopping the docker service.</p>
</li>
<li>
<p>Transferring the Docker storage files from <code>/var/lib/docker</code> to it&rsquo;s new location, while preserving the file&rsquo;s metadata.</p>
</li>
<li>
<p>Renaming the old storage so that Docker will not attempt to reuse it.</p>
</li>
<li>
<p>Creating a config file <code>daemon.json</code> at <code>/etc/docker</code> in order to point the Docker service to the new storage directory.</p>
</li>
<li>
<p>Modifying the systemctl service file in order for the Docker service to correctly identify the Docker daemon.<strong>(NOTE: This may or may not be necessary. Explained with greater detail in it&rsquo;s relevant section.)</strong></p>
</li>
<li>
<p>Reloading the daemons and restarting the Docker service.</p>
</li>
</ul>
<p>Note that these commands need to be run with elevated (superuser) privileges.</p>
<h2 id="stopping-the-docker-service">Stopping the Docker service.</h2>
<p>The Docker service can be stopped with the command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>systemctl stop docker
</span></span></code></pre></div><p>Systemctl is a Linux command used for controlling and managing system services, including starting, stopping, enabling, and disabling them, among other tasks.</p>
<h2 id="transferring-the-docker-storage-files-from-varlibdocker-to-its-new-location-while-preserving-the-files-metadata">Transferring the Docker storage files from <code>/var/lib/docker</code> to it&rsquo;s new location, while preserving the file&rsquo;s metadata.</h2>
<h3 id="creating-a-new-directory-for-the-storage">Creating a new directory for the storage</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkdir /path/to/new/storage
</span></span></code></pre></div><h3 id="copying-the-existing-storage-to-the-new-location">Copying the existing storage to the new location</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>rsync -aqxP /var/lib/docker /path/to/new/storage
</span></span></code></pre></div><p>The <code>rsync</code> command is used to synchronize and copy files and directories from one location to another. It stands for remote sync, and is a file synchronization tool.</p>
<p><code>-aqxP</code>:These are the options and flags used with rsync.</p>
<ul>
<li>
<p>-a: This option stands for &ldquo;archive&rdquo; and is used to perform a recursive copy while preserving file permissions, ownership, timestamps, and more.</p>
</li>
<li>
<p>-q: The &ldquo;quiet&rdquo; option suppresses non-error messages, making the output less verbose.</p>
</li>
<li>
<p>-x: This option ensures that rsync does not cross filesystem boundaries. It prevents rsync from copying data to a different filesystem.</p>
</li>
<li>
<p>-P: This combines two options: -P is equivalent to -rlptgD and &ndash;partial. It enables resumable copying and shows progress during the transfer.</p>
</li>
</ul>
<h2 id="renaming-the-old-docker-storage">Renaming the old Docker storage.</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mv /var/lib/docker /var/lib/docker.old
</span></span></code></pre></div><h2 id="creating-a-daemonjson-file-at-etcdocker">Creating a <code>daemon.json</code> file at <code>/etc/docker</code>.</h2>
<p>This file will help point the Docker service to the new storage location.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;{
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &#34;data-root&#34;: &#34;/path/to/new/storage&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">}&#39;</span> &gt; /etc/docker/daemon.json
</span></span></code></pre></div><h2 id="editing-the-systemctl-file-to-allow-docker-to-connect-to-the-docker-daemon">Editing the systemctl file to allow Docker to connect to the Docker Daemon</h2>
<p>When I attempted to perform the storage transfer on my own, I originally did not have this step included. When restarting the Docker service, I got the following error whenever I attempted any Docker commands.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
</span></span></code></pre></div><p>The Docker daemon was very clearly running (by running <code>systemctl status docker</code>), but the <code>docker</code> cli was unable to connect to the socket file.</p>
<p>This was because I had restarted the Docker daemon as a service using the <code>systemctl</code> command.</p>
<p>I found my service file path from the output of <code>systemctl status docker</code>.</p>
<p>The following is the first two lines of the output produced by this command.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>● docker.service - Docker Application Container Engine
</span></span><span style="display:flex;"><span>   Loaded: loaded <span style="color:#f92672">(</span>/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled<span style="color:#f92672">)</span>
</span></span></code></pre></div><p>Edit this file at the specified location.</p>
<p>The following was the content in the file on my system. Pay attention to the line that starts with <code>ExecStart=/usr/bin/dockerd</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f92672">[</span>Unit<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>Description<span style="color:#f92672">=</span>Docker Application Container Engine
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>Service<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>Type<span style="color:#f92672">=</span>notify
</span></span><span style="display:flex;"><span><span style="color:#75715e"># the default is not to use systemd for cgroups because the delegate issues still</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># exists and systemd currently does not support the cgroup feature set required</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># for containers run by docker</span>
</span></span><span style="display:flex;"><span>ExecStart<span style="color:#f92672">=</span>/usr/bin/dockerd -H fd:// --containerd<span style="color:#f92672">=</span>/run/containerd/containerd.sock
</span></span><span style="display:flex;"><span>...
</span></span></code></pre></div><p>The <code>-H fd://</code> means the daemon is using a file descriptor managed by systemctl, without pointing towards the socket file at <code>/var/run/docker.sock</code>.</p>
<p>Modify the line to look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ExecStart<span style="color:#f92672">=</span>/usr/bin/dockerd -H unix:///var/run/docker.sock --containerd<span style="color:#f92672">=</span>/run/containerd/containerd.sock
</span></span></code></pre></div><p>This command can be run to do it as part of a script.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sed -i <span style="color:#e6db74">&#39;s/^ExecStart=.*/ExecStart=\/usr\/bin\/dockerd -H unix:\/\/\/var\/run\/docker.sock --containerd=\/run\/containerd\/containerd.sock/&#39;</span> /lib/systemd/system/docker.service
</span></span></code></pre></div><h2 id="restarting-the-daemons-and-the-docker-service">Restarting the Daemons and the Docker service</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>systemctl daemon-reload
</span></span><span style="display:flex;"><span>systemctl start docker
</span></span></code></pre></div><h2 id="testing">Testing!</h2>
<ul>
<li>
<p>Run <code>docker image ls</code> and <code>docker ps -a</code> commands to see if the images and containers have been succesfully transferred, and Docker is able to see them at the new location.</p>
</li>
<li>
<p>Run <code>docker info</code> and look for the line that starts with <code>Docker Root Dir:</code>. Check if it&rsquo;s pointing towards the new storage location.</p>
</li>
</ul>
<h2 id="deleting-the-old-storage-directory">Deleting the old storage directory</h2>
<p>This is a risky step in my opinion. I have the original storage directory preserved at the <code>/var/lib</code> folder, only having renamed it to <code>docker.old</code>. If you are able to run the Docker service with the new storage location without any issues ( try creating new images and containers!) then it may be a good idea to delete it. (Definitely make a backup of it regardless.)</p>
<h1 id="bonus-automation">Bonus Automation!</h1>
<p>The following is a bash script that should automate this process for you while providing a way to check the time taken for each successful step.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">##################################################</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># The purpose of this script is to transfer the Docker storage (root) directory from the root directory (/var/lib/docker) to the hard drive (/path/to/new/storage).</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># The following are the steps of this process (in order):</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># - Stop the docker service</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># - Transfer the Docker storage files from /var/lib/docker to /path/to/new/storage.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># (Note: This transfer will need to preserve the metadata of the original storage files for Docker to identify it.)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># - Rename the old storage directory so that Docker can no longer identify it.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># - Once the storage has been copied, a new file named daemon.json needs to be placed in /etc/docker to point at the new storage location.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># - Once the storage has been successfully transfered to the new location, the systemctl service file path will need to be modified in order to allow</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Docker to identify the running Daemon.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># - The daemons need to be reloaded.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># - The Docker service needs to be restarted.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Note: This script needs to be run as a sudo user.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">###################################################</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Stopping the docker service</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>systemctl stop docker
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Docker service stopped at </span><span style="color:#66d9ef">$(</span>date<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Transferring the Docker storage files from /var/lib/docker to /path/to/new/storage.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Creating a new file at /path/to/new/storage</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>mkdir /path/to/new/storage
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;New storage directory created at /path/to/new/storage . Beginning cache transfer at </span><span style="color:#66d9ef">$(</span>date<span style="color:#66d9ef">)</span><span style="color:#e6db74">.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Copying the Docker storage files</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>rsync -aqxP /var/lib/docker /path/to/new/storage
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Docker storage cloned to /path/to/new/storage at </span><span style="color:#66d9ef">$(</span>date<span style="color:#66d9ef">)</span><span style="color:#e6db74">.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Renaming the old Docker storage</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>mv /var/lib/docker /var/lib/docker.old
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Old Docker storage renamed to /var/lib/docker.old at </span><span style="color:#66d9ef">$(</span>date<span style="color:#66d9ef">)</span><span style="color:#e6db74">.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Creating a daemon.json file at /etc/docker</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#39;{
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &#34;data-root&#34;: &#34;/path/to/new/storage&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">}&#39;</span> &gt; /etc/docker/daemon.json
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;daemon.json file created at /etc/docker/daemon.json at </span><span style="color:#66d9ef">$(</span>date<span style="color:#66d9ef">)</span><span style="color:#e6db74"> with the following content:&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>cat /etc/docker/daemon.json &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Editing the systemctl file</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># The following command will replace the line beginning with ExecStart with the appropriate line needed to actually have the systemctl comman connect to the Docker daemon.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>sed -i <span style="color:#e6db74">&#39;s/^ExecStart=.*/ExecStart=\/usr\/bin\/dockerd -H unix:\/\/\/var\/run\/docker.sock --containerd=\/run\/containerd\/containerd.sock/&#39;</span> /lib/systemd/system/docker.servi$
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;systemctl config file modified at </span><span style="color:#66d9ef">$(</span>date<span style="color:#66d9ef">)</span><span style="color:#e6db74">.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Restarting the daemons</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>systemctl daemon-reload
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;daemons reloaded at </span><span style="color:#66d9ef">$(</span>date<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Restarting the Docker service</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>systemctl start docker
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Docker service started at </span><span style="color:#66d9ef">$(</span>date<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span> &gt;&gt; docker_cache_transfer_progress.txt
</span></span></code></pre></div>]]></content:encoded></item><item><title>Installing and configuring the HIVE metastore with a MySQL backend.</title><link>https://naveenkannan.dev/posts/hive_mysql_metastore/</link><pubDate>Fri, 22 Sep 2023 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/hive_mysql_metastore/</guid><description>A guide to configuring Hive Metastore to use a MySQL server as the backend RDBMS for metadata storage, while enabling Spark to connect to the Metastore.</description><content:encoded><![CDATA[<h1 id="hive-metastore">Hive Metastore</h1>
<p>Hive, a powerful data warehousing system built on top of Hadoop, relies on a component known as the metastore to efficiently manage metadata about the data stored within it. This metadata is crucial for organizing, querying, and processing data in Hive. In this blog post, we&rsquo;ll explore the role of the Hive Metastore and the significance of selecting the right relational database management system (RDBMS) for it.</p>
<h2 id="metadata-management">Metadata Management</h2>
<p>The Hive Metastore serves as a central repository for storing and managing metadata related to datasets within Hive. This metadata includes essential information such as:</p>
<ul>
<li>
<p>Table schemas</p>
</li>
<li>
<p>Data types</p>
</li>
<li>
<p>Column names</p>
</li>
<li>
<p>Physical data file locations</p>
</li>
</ul>
<p>Without the metastore, Hive would struggle to efficiently keep track of these critical metadata elements.</p>
<h2 id="schema-definition">Schema Definition</h2>
<p>In Hive, tables are defined using a schema that specifies column names and their associated data types. This schema is fundamental for querying and processing data correctly. The metastore takes on the responsibility of storing and managing these table schemas, offering users the flexibility to:</p>
<ul>
<li>
<p>Create new tables</p>
</li>
<li>
<p>Modify existing schemas</p>
</li>
<li>
<p>Drop tables that are no longer needed</p>
</li>
</ul>
<h2 id="metadata-scaling">Metadata Scaling</h2>
<p>In large-scale data environments, metadata can become extensive. Fortunately, the Hive Metastore can scale horizontally to handle substantial volumes of metadata efficiently. Additionally, it can be configured for high availability, ensuring continuous access to metadata even in the face of hardware failures or other issues.</p>
<h1 id="metastore-and-the-relational-database-management-system-rdbms">Metastore and the Relational Database Management System (RDBMS)</h1>
<p>By default, Hive uses <strong>Apache Derby</strong> as the RDBMS for its metastore. Derby is a lightweight, open-source, Java-based RDBMS, and it serves as the default choice for small to medium-scale Hive installations, testing, and development purposes. Notably, Derby comes bundled with Hive, simplifying the initial setup</p>
<figure class="align-center ">
    <img loading="lazy" src="apache_derby_logo.png#center"
         alt="The Apache Derby Logo."/> <figcaption>
            <p>The Apache Derby Logo.</p>
        </figcaption>
</figure>

<p>While Derby is suitable for certain use cases, in this blog post, we will use MySQL as the RDBMS for Hive&rsquo;s metastore for various reasons, such as:</p>
<ul>
<li>
<p>Scalability: MySQL excels in handling larger and more complex workloads, making it ideal for scaling Hive in data-rich environments.</p>
</li>
<li>
<p>Performance: MySQL often delivers superior performance for complex queries and large datasets, making it well-suited for high-performance scenarios.</p>
</li>
<li>
<p>Backup and Recovery: Advanced backup and recovery options in MySQL enhance data integrity and reliability.</p>
</li>
<li>
<p>Enterprise Support: Organizations familiar with MySQL can leverage existing expertise for Hive&rsquo;s metadata management.</p>
</li>
<li>
<p>Integration: MySQL&rsquo;s compatibility with various tools simplifies data integration and management processes.</p>
</li>
<li>
<p>Community and Documentation: A large user community and extensive documentation resources make MySQL a dependable choice for Hive&rsquo;s metadata management.</p>
</li>
</ul>
<h1 id="prerequisites">Prerequisites</h1>
<p>The following prerequisites will be needed prior to installing MySQL and altering Hive&rsquo;s configuration.</p>
<ul>
<li>
<p>A properly configured Hadoop, YARN, Spark, and HDFS cluster.</p>
</li>
<li>
<p>An installation of Hive.</p>
</li>
</ul>
<h1 id="setting-up-mysql">Setting up MySQL</h1>
<p>My head node runs on RHEL 8. This guide will be for RHEL 8 Linux, but the general overview should be adaptable to other Unix-like systems.</p>
<h2 id="download-the-mysql-repo-onto-the-node">Download the MySQL Repo onto the node</h2>
<p>The yum repo for MySQL needs to be downloaded to the system. The appropriate repo for your system can be found <a href="https://dev.mysql.com/downloads/repo/yum/">here.</a></p>
<h2 id="install-the-repo-using-yum">Install the repo using <code>yum</code></h2>
<p>I downloaded the repo to <code>/opt/mysql80-community-release-el8-8.noarch.rpm</code> in my file directory.</p>
<p>Install the contents of the repo using</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>yum localinstall /opt/mysql80-community-release-el8-8.noarch.rpm -y
</span></span></code></pre></div><h2 id="disabling-the-local-mysql-module">Disabling the local <code>mysql</code> module</h2>
<p>Disable the local <code>mysql</code> module to prevent conflicts with the installation of the <code>mysql-community-server</code> module.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>yum -y module disable mysql
</span></span></code></pre></div><h2 id="installing-mysql-community-server">Installing <code>mysql-community-server</code></h2>
<p>Install the Community Server version of MySQL using:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>yum -y install mysql-community-server
</span></span></code></pre></div><h2 id="installing-mysql-connector-java">Installing <code>mysql-connector-java</code></h2>
<p><code>mysql-connector-java</code>, or the MySQL JDBC (Java Database Connectivity) driver, is a Java-based library that provides connectivity between Java applications and the MySQL database management system. It allows Java programs to interact with MySQL databases by facilitating the exchange of data and SQL queries.</p>
<p>Install it by running</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>yum -y install mysql-connector-java
</span></span></code></pre></div><h2 id="initializing-the-mysql-service">Initializing the <code>mysql</code> service</h2>
<p>Initialize MySQL by running:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>service mysqld start
</span></span></code></pre></div><p>This will initialize the MySQL service. However, this installation of MySQL needs to be secured!</p>
<h1 id="securing-mysql-and-initializing-the-server">Securing MySQL and initializing the server</h1>
<h2 id="running-mysql_secure_installation">Running <code>mysql_secure_installation</code></h2>
<p>MySQL version 8.0 or higher generates a temporary random password in <code>/var/log/mysqld.log</code> after installation.</p>
<p>Find your random password by running</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>grep <span style="color:#e6db74">&#39;A temporary password&#39;</span> /var/log/mysqld.log |tail -1 |awk <span style="color:#e6db74">&#39;{split($0,a,&#34;: &#34;); print a[2]}&#39;</span>
</span></span></code></pre></div><p>Once you have that, run <code>mysql_secure_installation</code>.</p>
<p>This script will guide you through setting up a root password, removing anonymous users, disallowing remote root login, and other security-related configurations.</p>
<h2 id="adding-new-users-and-setting-up-the-metastore">Adding new users and setting up the metastore</h2>
<p>Initialize MySQL by running</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mysql -u root -p
</span></span></code></pre></div><p>and enter your password.</p>
<p>Run the following:</p>
<pre tabindex="0"><code>mysql&gt; CREATE DATABASE metastore;
Query OK, 1 row affected (0.02 sec)
mysql&gt; use metastore;
Database changed
mysql&gt; create user &#39;hive&#39;@&#39;localhost&#39; identified by &#39;&lt;root_password&gt;&#39;;
Query OK, 0 rows affected (0.02 sec)
mysql&gt; grant all PRIVILEGES on *.* to &#39;hive&#39;@&#39;localhost&#39; with grant option;
Query OK, 0 rows affected (0.02 sec)
mysql&gt; create user &#39;hdfs&#39;@&#39;localhost&#39; identified by &#39;&lt;root_password&gt;&#39;;
Query OK, 0 rows affected (0.04 sec)
mysql&gt; grant all PRIVILEGES on *.* to &#39;hdfs&#39;@&#39;localhost&#39; with grant option;
Query OK, 0 rows affected (0.03 sec)
mysql&gt; FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.02 sec)
mysql&gt; exit
Bye
</code></pre><p>Replace <code>&lt;root_password&gt;</code> with your actual password. This will create the metastore and also create the users <code>hive</code> and <code>hdfs</code> and grant them all necessary privileges to work within the metastore and it&rsquo;s data.</p>
<h3 id="bonus-automate-this">Bonus! Automate this!</h3>
<p>If you&rsquo;ve been following my posts, you&rsquo;ll know that I&rsquo;ve been using Ansible to automate this process. The above command is interactive! How do we get around this? If you&rsquo;re deploying MySQL for production level systems, then this might be an unnecessary expenditure of time.</p>
<p>In this section, I&rsquo;ve added a bash script that automates the process of securing MySQL, adding new users and creating the metastore database.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Obtaining the  Temporary Password</span>
</span></span><span style="display:flex;"><span>root_temp_pass<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>grep <span style="color:#e6db74">&#39;A temporary password&#39;</span> /var/log/mysqld.log |tail -1 |awk <span style="color:#e6db74">&#39;{split($0,a,&#34;: &#34;); print a[2]}&#39;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;root_temp_pass:&#34;</span>$root_temp_pass
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Creating a mysql_secure_installation.sql script to secure MySQL</span>
</span></span><span style="display:flex;"><span>cat &gt; mysql_secure_installation.sql <span style="color:#e6db74">&lt;&lt; EOF
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Ensuring that the server cannot be accessed without a password
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># The password will be the cluster password
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Adding a user &#39;hdfs&#39; and a user &#39;hive&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">ALTER USER &#39;root&#39;@&#39;localhost&#39; IDENTIFIED BY &#39;root_password&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">CREATE USER &#39;hive&#39;@&#39;localhost&#39; IDENTIFIED BY &#39;root_password&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">CREATE USER &#39;hdfs&#39;@&#39;localhost&#39; IDENTIFIED BY &#39;root_password&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Removing the anonymous users
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">DELETE FROM mysql.user WHERE User=&#39;&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Disallowing remote login for root
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">DELETE FROM mysql.user WHERE User=&#39;root&#39; AND Host NOT IN (&#39;localhost&#39;, &#39;127.0.0.1&#39;, &#39;::1&#39;);
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Removing the demo database
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">DROP DATABASE IF EXISTS test;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">DELETE FROM mysql.db WHERE Db=&#39;test&#39; OR Db=&#39;test\\_%&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Make our changes take effect
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">FLUSH PRIVILEGES;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">EOF</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Running the SQL script to secure the installation of MySQL</span>
</span></span><span style="display:flex;"><span>mysql -uroot -p<span style="color:#e6db74">&#34;</span>$root_temp_pass<span style="color:#e6db74">&#34;</span> --connect-expired-password &lt;mysql_secure_installation.sql
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Creating another script to create the metastore and grant privileges to the hive, hdfs and root users for it</span>
</span></span><span style="display:flex;"><span>cat &gt; mysql_privilege_grant.sql <span style="color:#e6db74">&lt;&lt; EOF
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"># Granting privileges to the users hive, hdfs and root
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">CREATE DATABASE metastore;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">use metastore;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">grant all PRIVILEGES on *.* to &#34;hive&#34;@&#34;localhost&#34; with grant option;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">grant all PRIVILEGES on *.* to &#34;hdfs&#34;@&#34;localhost&#34; with grant option;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">grant all PRIVILEGES on *.* to &#34;root&#34;@&#34;localhost&#34; with grant option;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">FLUSH PRIVILEGES;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">EOF</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Running the SQL script to create the metastore and grant priviliges to the root, hive and hdfs users for the metastore</span>
</span></span><span style="display:flex;"><span>mysql -u root -p<span style="color:#e6db74">&#34;root_password&#34;</span> --connect-expired-password &lt;mysql_privilege_grant.sql
</span></span></code></pre></div><p>Replace <code>root_password</code> with your actual password and run this script. <strong>Remember to delete this script and the ones created by this script once it finishes running!</strong></p>
<h1 id="linking-mysql-and-hive-and-spark">Linking MySQL and Hive (and Spark!)</h1>
<h2 id="deleting-a-duplicate-library">Deleting a duplicate library</h2>
<p>We will need to remove the <code>log4j-slf4j-impl-2.17.1.jar</code> file since MySQL has the same library, and the presence of multiple duplicate libraries in the <code>CLASSPATH</code> may cause Hive to fail.</p>
<p>Change directories to your Hive installation, and then enter the <code>lib</code> directory, and delete the <code>log4j-slf4j-impl-2.17.1.jar</code> file.</p>
<h2 id="adding-the-mysql-jdbc-library-to-hive">Adding the MySQL JDBC library to Hive</h2>
<p>Copy the <code>mysql-connector-java.jar</code> file located at <code>/usr/share/java/mysql-connector-java.jar</code> to the <code>lib</code> directory of your Hive installation. This ensures that Hive can utilize the MySQL JDBC library to connect to MySQL databases seamlessly.</p>
<p>For example,</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cp /usr/share/java/mysql-connector-java.jar /opt/apache-hive-3.1.3-bin/lib
</span></span></code></pre></div><h2 id="adding-the-mysql-jdbc-library-to-spark">Adding the MySQL JDBC library to Spark</h2>
<p>Copy the <code>mysql-connector-java.jar</code> file located at <code>/usr/share/java/mysql-connector-java.jar</code> to the <code>jars</code> directory of your Spark installation.</p>
<p>This will enable Spark to also utilize the MySQL JDBC library to connect to MySQL databases seamlessly.</p>
<p>For example,</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cp /usr/share/java/mysql-connector-java.jar /opt/spark-3.3.2-bin-hadoop3/jars
</span></span></code></pre></div><h2 id="creating-a-hive-sitexml-file">Creating a <code>hive-site.xml</code> file</h2>
<p>Create a file named <code>hive-site.xml</code> in the <code>conf</code> directory of your Hive installation with the following content:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#f92672">&lt;configuration&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;name&gt;</span>javax.jdo.option.ConnectionURL<span style="color:#f92672">&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;value&gt;</span>jdbc:mysql://localhost/metastore?createDatabaseIfNotExist=true<span style="color:#f92672">&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;description&gt;</span>URL for establishing a connection to the Hive Metastore database.<span style="color:#f92672">&lt;/description&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;name&gt;</span>javax.jdo.option.ConnectionDriverName<span style="color:#f92672">&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;value&gt;</span>com.mysql.jdbc.Driver<span style="color:#f92672">&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;description&gt;</span>Fully qualified class name of the JDBC driver used for connecting Hive and MySQL.<span style="color:#f92672">&lt;/description&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;name&gt;</span>javax.jdo.option.ConnectionUserName<span style="color:#f92672">&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;value&gt;</span>hive<span style="color:#f92672">&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;description&gt;</span>User name used to connect to the MySQL database for Hive Metastore.<span style="color:#f92672">&lt;/description&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;name&gt;</span>javax.jdo.option.ConnectionPassword<span style="color:#f92672">&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;value&gt;</span>root_password<span style="color:#f92672">&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;description&gt;</span>Password for authenticating and connecting to the MySQL database.<span style="color:#f92672">&lt;/description&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;name&gt;</span>datanucleus.autoCreateSchema<span style="color:#f92672">&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;description&gt;</span>
</span></span><span style="display:flex;"><span>      When set to &#39;true,&#39; this option allows automatic creation of the schema (database structure)
</span></span><span style="display:flex;"><span>      if it doesn&#39;t already exist when Hive Metastore initializes.
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/description&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;value&gt;</span>true<span style="color:#f92672">&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;name&gt;</span>datanucleus.fixedDatastore<span style="color:#f92672">&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;value&gt;</span>true<span style="color:#f92672">&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;name&gt;</span>datanucleus.autoCreateTables<span style="color:#f92672">&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;description&gt;</span>
</span></span><span style="display:flex;"><span>      When set to &#39;true,&#39; it indicates that the datastore (MySQL database) structure is fixed
</span></span><span style="display:flex;"><span>      and should not be altered automatically by Hive Metastore.
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/description&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;value&gt;</span>True<span style="color:#f92672">&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/configuration&gt;</span>
</span></span></code></pre></div><h2 id="editing-the-spark-envsh-file">Editing the <code>spark-env.sh</code> file</h2>
<p>Add the following line to your <code>spark-env.sh</code> file located in the <code>conf</code> directory of your Spark installation.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>export SPARK_CLASSPATH<span style="color:#f92672">=</span>/opt/apache-hive-3.1.3-bin/jars/mysql-connector-java.jar
</span></span></code></pre></div><h2 id="editing-the-spark-defaultsconf-file">Editing the <code>spark-defaults.conf</code> file</h2>
<p>Add the following line to your <code>spark-defaults.conf</code> file located in the <code>conf</code> directory of your Spark installation.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>spark.sql.warehouse.dir<span style="color:#f92672">=</span>hdfs://localhost:9000/user/hive/warehouse
</span></span></code></pre></div><h2 id="restarting-the-mysql-service">Restarting the MySQL service</h2>
<p>Run the following commands to restart the MySQL service:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>service mysqld stop
</span></span><span style="display:flex;"><span>service mysqld start
</span></span></code></pre></div><h2 id="initializing-the-metastore">Initializing the Metastore</h2>
<p>Run the following commands:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>hdfs dfs -mkdir /tmp
</span></span><span style="display:flex;"><span>hdfs dfs -mkdir /user
</span></span><span style="display:flex;"><span>hdfs dfs -mkdir /user/hive
</span></span><span style="display:flex;"><span>hdfs dfs -mkdir /user/hive/warehouse
</span></span><span style="display:flex;"><span>hdfs dfs -chmod g+w /tmp
</span></span><span style="display:flex;"><span>hdfs dfs -chmod g+w /user/hive/warehouse
</span></span><span style="display:flex;"><span>schematool -dbType mysql -initSchema
</span></span></code></pre></div><p>This will initialize the Hive Metastore.</p>
<p>This should successfully have Hive configured to use the MySQL database as the backend RDBMS for metadata storage!</p>
<h1 id="testing">Testing</h1>
<p>Now we will load a table into Hive and see if it is present where it should be.</p>
<p>Download the data files from MovieLens 100k on the <a href="http://grouplens.org/datasets/movielens/">GroupLens datasets page</a> (which also has a README.txt file and index of unzipped files):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>wget http://files.grouplens.org/datasets/movielens/ml-100k.zip
</span></span></code></pre></div><p>Unzip the files</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>unzip ml-100k.zip
</span></span></code></pre></div><p>Initialize the Hive server.</p>
<p>Then, create a table by running the following:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> u_data (
</span></span><span style="display:flex;"><span>  userid INT,
</span></span><span style="display:flex;"><span>  movieid INT,
</span></span><span style="display:flex;"><span>  rating INT,
</span></span><span style="display:flex;"><span>  unixtime STRING)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ROW</span> FORMAT DELIMITED
</span></span><span style="display:flex;"><span>FIELDS TERMINATED <span style="color:#66d9ef">BY</span> <span style="color:#e6db74">&#39;\t&#39;</span>
</span></span><span style="display:flex;"><span>STORED <span style="color:#66d9ef">AS</span> TEXTFILE;
</span></span></code></pre></div><p>And load <code>u.data</code> into the table that was just created:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">LOAD</span> <span style="color:#66d9ef">DATA</span> <span style="color:#66d9ef">LOCAL</span> INPATH <span style="color:#e6db74">&#39;&lt;path&gt;/ml-100k//u.data&#39;</span>
</span></span><span style="display:flex;"><span>OVERWRITE <span style="color:#66d9ef">INTO</span> <span style="color:#66d9ef">TABLE</span> u_data;
</span></span></code></pre></div><p>Replace <code>&lt;path&gt;</code> with your path to the <code>ml-100k</code> folder.</p>
<p>Count the number of rows in table u_data.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">COUNT</span>(<span style="color:#f92672">*</span>) <span style="color:#66d9ef">FROM</span> u_data;
</span></span></code></pre></div><p>Enter the <code>mysql</code> shell.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mysql -u hive -p
</span></span></code></pre></div><p>and enter your password.</p>
<p>Run the following in the command line for MySQL:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span>use metastore;
</span></span></code></pre></div><p>Followed by</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">show</span> tables;
</span></span></code></pre></div><p>This will be your output:</p>
<pre tabindex="0"><code>+-------------------------------+
| Tables_in_metastore           |
+-------------------------------+
| AUX_TABLE                     |
| BUCKETING_COLS                |
| CDS                           |
| COLUMNS_V2                    |
| COMPACTION_QUEUE              |
| COMPLETED_COMPACTIONS         |
| COMPLETED_TXN_COMPONENTS      |
| CTLGS                         |
| DATABASE_PARAMS               |
| DBS                           |
| DB_PRIVS                      |
| DELEGATION_TOKENS             |
| FUNCS                         |
| FUNC_RU                       |
| GLOBAL_PRIVS                  |
| HIVE_LOCKS                    |
| IDXS                          |
| INDEX_PARAMS                  |
| I_SCHEMA                      |
| KEY_CONSTRAINTS               |
| MASTER_KEYS                   |
| MATERIALIZATION_REBUILD_LOCKS |
| METASTORE_DB_PROPERTIES       |
| MIN_HISTORY_LEVEL             |
| MV_CREATION_METADATA          |
| MV_TABLES_USED                |
| NEXT_COMPACTION_QUEUE_ID      |
| NEXT_LOCK_ID                  |
| NEXT_TXN_ID                   |
| NEXT_WRITE_ID                 |
| NOTIFICATION_LOG              |
| NOTIFICATION_SEQUENCE         |
| NUCLEUS_TABLES                |
| PARTITIONS                    |
| PARTITION_EVENTS              |
| PARTITION_KEYS                |
| PARTITION_KEY_VALS            |
| PARTITION_PARAMS              |
| PART_COL_PRIVS                |
| PART_COL_STATS                |
| PART_PRIVS                    |
| REPL_TXN_MAP                  |
| ROLES                         |
| ROLE_MAP                      |
| RUNTIME_STATS                 |
| SCHEMA_VERSION                |
| SDS                           |
| SD_PARAMS                     |
| SEQUENCE_TABLE                |
| SERDES                        |
| SERDE_PARAMS                  |
| SKEWED_COL_NAMES              |
| SKEWED_COL_VALUE_LOC_MAP      |
| SKEWED_STRING_LIST            |
| SKEWED_STRING_LIST_VALUES     |
| SKEWED_VALUES                 |
| SORT_COLS                     |
| TABLE_PARAMS                  |
| TAB_COL_STATS                 |
| TBLS                          |
| TBL_COL_PRIVS                 |
| TBL_PRIVS                     |
| TXNS                          |
| TXN_COMPONENTS                |
| TXN_TO_WRITE_ID               |
| TYPES                         |
| TYPE_FIELDS                   |
| VERSION                       |
| WM_MAPPING                    |
| WM_POOL                       |
| WM_POOL_TO_TRIGGER            |
| WM_RESOURCEPLAN               |
| WM_TRIGGER                    |
| WRITE_SET                     |
+-------------------------------+
74 rows in set (0.01 sec)
</code></pre><p>The created table should be located in <code>TBLS</code>.</p>
<p>Run the following command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">select</span> <span style="color:#f92672">*</span> <span style="color:#66d9ef">from</span> TBLS;
</span></span></code></pre></div><p>This is what you should see:</p>
<pre tabindex="0"><code>+--------+-------------+-------+------------------+-------+------------+-----------+-------+----------------------------------------+---------------+--------------------+--------------------+----------------------------------------+
| TBL_ID | CREATE_TIME | DB_ID | LAST_ACCESS_TIME | OWNER | OWNER_TYPE | RETENTION | SD_ID | TBL_NAME                               | TBL_TYPE      | VIEW_EXPANDED_TEXT | VIEW_ORIGINAL_TEXT | IS_REWRITE_ENABLED                     |
+--------+-------------+-------+------------------+-------+------------+-----------+-------+----------------------------------------+---------------+--------------------+--------------------+----------------------------------------+
|      1 |  1695343066 |     1 |                0 | root  | USER       |         0 |     1 | u_data                                 | MANAGED_TABLE | NULL               | NULL               | 0x00
+--------+-------------+-------+------------------+-------+------------+-----------+-------+----------------------------------------+---------------+--------------------+--------------------+----------------------------------------+
</code></pre><h1 id="further-reading">Further Reading</h1>
<ul>
<li><a href="https://cwiki.apache.org/confluence/display/Hive/Tutorial#Tutorial-Concepts">Hive Basics</a></li>
<li><a href="https://db.apache.org/derby/">Apache Derby</a></li>
<li><a href="https://www.mysql.com/">MySQL</a></li>
</ul>
]]></content:encoded></item><item><title>Using Ansible to install Hive on a Spark cluster.</title><link>https://naveenkannan.dev/posts/ansible_hive_spark/</link><pubDate>Mon, 04 Sep 2023 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/ansible_hive_spark/</guid><description>A detailed view of Ansible playbooks with a highly relevant example.</description><content:encoded><![CDATA[<h1 id="what-is-hive">What is Hive?</h1>
<p>Apache Hive is a distributed, fault-tolerant data warehouse system, built on top of Hadoop, designed to simplify and streamline the processing of large datasets.</p>
<p>Through Hive, a user can manage and analyze massive volumes of data by organizing it into tables, resembling a traditional relational database.</p>
<p>Hive uses the HiveQL (HQL) language, which is very similar to SQL. These SQL-like queries get translated into MapReduce tasks, leveraging the power of Hadoop&rsquo;s MapReduce functionalities while bypassing the need to know how to program MapReduce jobs.</p>
<p>Hive abstracts the complexity of Java-based utilization of Hadoop&rsquo;s file storage system.  It is best used for traditional data warehousing tasks.</p>
<p>In this blog post, we will look at installing Hive on a Hadoop cluster utilizing Ansible!</p>
<h1 id="what-is-ansible">What is Ansible?</h1>
<p>Ansible is an open-source IT automation tool that allows for automated management of remote systems.</p>
<figure class="align-center ">
    <img loading="lazy" src="ansible_logo.png#center"/> 
</figure>

<p>A basic Ansible environment has the following three components:</p>
<ul>
<li>
<p><strong>Control Node</strong>: This is a system on which Ansible is installed, and the system from which Ansible commands such as <code>ansible-inventory</code> are issued. This is also where Ansible playbooks and configuration files are stored.</p>
</li>
<li>
<p><strong>Managed node</strong>: This is a remote system that Ansible intends to manage and configure.</p>
</li>
<li>
<p><strong>Inventory</strong>: This is a list of managed nodes that are organized locally on the <strong>control node</strong>. This lists the IP addresses or the hostnames of the remote systems being managed along with any connection information needed.</p>
</li>
</ul>
<figure class="align-center ">
    <img loading="lazy" src="ansible_architecture.png#center"
         alt="A flowchart demonstrating the basic architecture of an Ansible environment."/> <figcaption>
            <p>A flowchart demonstrating the basic architecture of an Ansible environment.</p>
        </figcaption>
</figure>

<p>I have a blog post detailing the basics of Ansible and it&rsquo;s relevance in a computing cluster <a href="/posts/ansible_cluster_config/">here!</a>.</p>
<h1 id="prerequisites-for-this-blog-post">Prerequisites for this blog post</h1>
<ul>
<li>
<p>An installation of Ansible (In a <a href="/posts/ansible_cluster_config/">previous post</a>, I set up Ansible in a Docker container, minimizing the overhead on the actual cluster nodes).</p>
</li>
<li>
<p>Password-less SSH connections set up between each node and the node (or container) you are running Ansible from.</p>
</li>
<li>
<p>The inventory file, defined at <code>/etc/Ansible/hosts</code>.</p>
</li>
<li>
<p>An installation of Hadoop, with HDFS, YARN, MapReduce and Spark configured and running. <a href="/posts/hadoop_spark/">This post covers the basic overview of the installation and configuration of Hadoop and Spark.</a></p>
</li>
</ul>
<p>I used Ansible from a Docker container to configure a 4 node cluster. The cluster inventory file is in the <code>INI</code> format as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[cluster]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">XXX.XX.XXX.X</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">YY.Y.Y.2</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">YY.Y.Y.3</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">YY.Y.Y.4</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[head_node]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">XXX.XX.XXX.X</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[cluster_nodes]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">YY.Y.Y.2</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">YY.Y.Y.3</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">YY.Y.Y.4</span>
</span></span></code></pre></div><figure class="align-center ">
    <img loading="lazy" src="cluster_config.png#center"
         alt="The architecture used in this blog post."/> <figcaption>
            <p>The architecture used in this blog post.</p>
        </figcaption>
</figure>

<h1 id="what-are-playbooks">What are Playbooks?</h1>
<p>Ansible playbooks provide a simple and reusable method to define the configuration and management of multi-machine systems, and is well equipped to handling tasks that can range from simple ones like installing software packages to complex ones like configuring multiple servers with specific roles.</p>
<h1 id="playbook-syntax">Playbook Syntax</h1>
<p>YAML <em>(YAML Ain&rsquo;t Markup Language)</em> is a human-readable data serialization format that plays a pivotal role in Ansible playbooks.</p>
<p>YAML files use key-value pairs, lists, and nested structures to represent data. It eschews complex symbols, relying instead on whitespace and line breaks to delineate data structures, making it easy to grasp and write, even for those without extensive programming experience.</p>
<p>Playbooks are automation blueprints. Playbooks contain plays, which are a list of tasks that map to each managed node in the predefined inventory.</p>
<p>Tasks are a list of one or more modules that define the operations that Ansible can perform.</p>
<p>A Module is a unit of code that Ansible can run on managed nodes.</p>
<p>Each play defines two things:</p>
<ul>
<li>
<p>The managed nodes to target.</p>
</li>
<li>
<p>At least one task to execute.</p>
</li>
</ul>
<h1 id="an-example-playbook">An Example Playbook!</h1>
<p>Let&rsquo;s jump right in! The following is a playbook that installs Hive on a cluster with a head node and 3 cluster nodes.</p>
<p>The head node runs Rhel 8 and the 3 cluster nodes run Ubuntu.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Installing Apache Hive and its prerequisties on the RHEL 8 head node.</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hosts</span>: <span style="color:#ae81ff">head_node</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become_user</span>: <span style="color:#ae81ff">temp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become_method</span>: <span style="color:#ae81ff">sudo</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tasks</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Installing Maven on the RHEL 8 head node</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.get_url</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">url</span>: <span style="color:#ae81ff">https://dlcdn.apache.org/maven/maven-3/3.9.4/binaries/apache-maven-3.9.4-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/tmp/apache-maven-3.9.4-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">checksum</span>: <span style="color:#e6db74">&#34;sha512:https://downloads.apache.org/maven/maven-3/3.9.4/binaries/apache-maven-3.9.4-bin.tar.gz.sha512&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Decompressing the Maven tarball on the Rhel 8 head node</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.unarchive</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">src</span>: <span style="color:#ae81ff">/tmp/apache-maven-3.9.4-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/opt/</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">remote_src</span>: <span style="color:#66d9ef">yes</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Adding the Maven `bin` directory to the PATH</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.lineinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/home/temp/.bashrc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">line</span>: <span style="color:#ae81ff">export PATH=$PATH:/opt/apache-maven-3.9.4/bin</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Downloading Apache Hive for the RHEL 8 head node</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.get_url</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">url</span>: <span style="color:#ae81ff">https://dlcdn.apache.org/hive/hive-3.1.3/apache-hive-3.1.3-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/tmp/apache-hive-3.1.3-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">checksum</span>: <span style="color:#e6db74">&#34;sha256:https://dlcdn.apache.org/hive/hive-3.1.3/apache-hive-3.1.3-bin.tar.gz.sha256&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">timeout</span>: <span style="color:#ae81ff">60</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Decompressing the Apache Hive tarball for the RHEL 8 head node</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.unarchive</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">src</span>: <span style="color:#ae81ff">/tmp/apache-hive-3.1.3-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/opt/</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">remote_src</span>: <span style="color:#66d9ef">yes</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Adding the installation directory to the PATH</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.lineinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/home/temp/.bashrc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">line</span>: <span style="color:#ae81ff">export HIVE_HOME=/opt/apache-hive-3.1.3-bin</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Adding the installation directory to the PATH</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.lineinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/home/temp/.bashrc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">line</span>: <span style="color:#ae81ff">export PATH=$PATH:$HIVE_HOME/bin</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Editing the log4j.properties file</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.blockinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/opt/hadoop-3.3.6/etc/hadoop/log4j.properties</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">block</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        # Define an appender for the MRAppMaster logger
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        log4j.logger.org.apache.hadoop.mapreduce.v2.app.MRAppMaster=INFO, mrappmaster
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        # Appender for MRAppMaster logger
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        log4j.appender.mrappmaster=org.apache.log4j.ConsoleAppender
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        log4j.appender.mrappmaster.layout=org.apache.log4j.PatternLayout
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        log4j.appender.mrappmaster.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Reloading the shell</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.shell</span>: <span style="color:#ae81ff">source ~/.bashrc</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Configuring Hive on the RHEL 8 head node.</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hosts</span>: <span style="color:#ae81ff">head_node</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">remote_user</span>: <span style="color:#ae81ff">temp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become_user</span>: <span style="color:#ae81ff">temp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become_method</span>: <span style="color:#ae81ff">sudo</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tasks</span>:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Configuring the Hive Warehouse using hadoop</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.shell</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      source ~/.bashrc
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_HOME=/opt/hadoop-3.3.6
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export PATH=$PATH:$HADOOP_HOME/bin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export PATH=$PATH:$HADOOP_HOME/sbin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_MAPRED_HOME=${HADOOP_HOME}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_COMMON_HOME=${HADOOP_HOME}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_HDFS_HOME=${HADOOP_HOME}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_YARN_HOME=${HADOOP_HOME}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HIVE_HOME=/opt/apache-hive-3.1.3-bin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export PATH=$PATH:$HIVE_HOME/bin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -mkdir /tmp
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -mkdir /user
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -mkdir /user/hive
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -mkdir /user/hive/warehouse
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -chmod g+w /tmp
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -chmod g+w /user/hive/warehouse
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      schematool -dbType derby -initSchema</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">args</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">executable</span>: <span style="color:#ae81ff">/bin/bash</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Creating a hive-site.xml file</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">copy</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/opt/apache-hive-3.1.3-bin/conf/hive-site.xml</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">content</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;name&gt;javax.jdo.option.ConnectionURL&lt;/name&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;value&gt;jdbc:mysql://localhost/metastore&lt;/value&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;/property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;name&gt;javax.jdo.option.ConnectionDriverName&lt;/name&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;value&gt;com.mysql.jdbc.Driver&lt;/value&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;/property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;name&gt;javax.jdo.option.ConnectionUserName&lt;/name&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;value&gt;hive&lt;/value&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;/property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;name&gt;javax.jdo.option.ConnectionPassword&lt;/name&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;value&gt;hivepassword&lt;/value&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Configuring Spark to use the Hive Metastore</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.lineinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/opt/spark-3.3.2-bin-hadoop3/conf/spark-defaults.conf</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">line</span>: <span style="color:#ae81ff">spark.sql.warehouse.dir=hdfs://localhost:9000/user/hive/warehouse</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Installing Apache Hive and its prerequisties on the Ubuntu cluster nodes.</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hosts</span>: <span style="color:#ae81ff">cluster_nodes</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">remote_user</span>: <span style="color:#ae81ff">temp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become_method</span>: <span style="color:#ae81ff">sudo</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tasks</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Installing Maven on the RHEL 8 head node</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.get_url</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">url</span>: <span style="color:#ae81ff">https://dlcdn.apache.org/maven/maven-3/3.9.4/binaries/apache-maven-3.9.4-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/tmp/apache-maven-3.9.4-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">checksum</span>: <span style="color:#e6db74">&#34;sha512:https://downloads.apache.org/maven/maven-3/3.9.4/binaries/apache-maven-3.9.4-bin.tar.gz.sha512&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Decompressing the Maven tarball on the Rhel 8 head node</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.unarchive</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">src</span>: <span style="color:#ae81ff">/tmp/apache-maven-3.9.4-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/opt/</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">remote_src</span>: <span style="color:#66d9ef">yes</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Adding the Maven `bin` directory to the PATH</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.lineinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/home/temp/.bashrc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">line</span>: <span style="color:#ae81ff">export PATH=$PATH:/opt/apache-maven-3.9.4/bin</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Downloading Apache Hive for the RHEL 8 head node</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.get_url</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">url</span>: <span style="color:#ae81ff">https://dlcdn.apache.org/hive/hive-3.1.3/apache-hive-3.1.3-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/tmp/apache-hive-3.1.3-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">checksum</span>: <span style="color:#e6db74">&#34;sha256:https://dlcdn.apache.org/hive/hive-3.1.3/apache-hive-3.1.3-bin.tar.gz.sha256&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">timeout</span>: <span style="color:#ae81ff">60</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Decompressing the Apache Hive tarball for the RHEL 8 head node</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.unarchive</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">src</span>: <span style="color:#ae81ff">/tmp/apache-hive-3.1.3-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/opt/</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">remote_src</span>: <span style="color:#66d9ef">yes</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Adding the installation directory to the PATH</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.lineinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/home/temp/.bashrc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">line</span>: <span style="color:#ae81ff">export HIVE_HOME=/opt/apache-hive-3.1.3-bin</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Adding the installation directory to the PATH</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.lineinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/home/temp/.bashrc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">line</span>: <span style="color:#ae81ff">export PATH=$PATH:$HIVE_HOME/bin</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Editing the log4j.properties file</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.blockinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/opt/hadoop-3.3.6/etc/hadoop/log4j.properties</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">block</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        # Define an appender for the MRAppMaster logger
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        log4j.logger.org.apache.hadoop.mapreduce.v2.app.MRAppMaster=INFO, mrappmaster
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        # Appender for MRAppMaster logger
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        log4j.appender.mrappmaster=org.apache.log4j.ConsoleAppender
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        log4j.appender.mrappmaster.layout=org.apache.log4j.PatternLayout
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        log4j.appender.mrappmaster.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Reloading the environment variables</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.shell</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      . /home/temp/.bashrc</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">args</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">executable</span>: <span style="color:#ae81ff">/bin/bash</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Configuring the Hive Warehouse using hadoop</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.shell</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      . /home/temp/.bashrc
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_HOME=/opt/hadoop-3.3.6
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export PATH=$PATH:$HADOOP_HOME/bin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export PATH=$PATH:$HADOOP_HOME/sbin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_MAPRED_HOME=${HADOOP_HOME}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_COMMON_HOME=${HADOOP_HOME}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_HDFS_HOME=${HADOOP_HOME}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_YARN_HOME=${HADOOP_HOME}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HIVE_HOME=/opt/apache-hive-3.1.3-bin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export PATH=$PATH:$HIVE_HOME/bin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -mkdir /tmp
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -mkdir /user
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -mkdir /user/hive
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -mkdir /user/hive/warehouse
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -chmod g+w /tmp
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -chmod g+w /user/hive/warehouse
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      schematool -dbType derby -initSchema</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">args</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">executable</span>: <span style="color:#ae81ff">/bin/bash</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Creating a hive-site.xml file</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">copy</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/opt/apache-hive-3.1.3-bin/conf/hive-site.xml</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">content</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;name&gt;javax.jdo.option.ConnectionURL&lt;/name&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;value&gt;jdbc:mysql://localhost/metastore&lt;/value&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;/property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;name&gt;javax.jdo.option.ConnectionDriverName&lt;/name&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;value&gt;com.mysql.jdbc.Driver&lt;/value&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;/property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;name&gt;javax.jdo.option.ConnectionUserName&lt;/name&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;value&gt;hive&lt;/value&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;/property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;name&gt;javax.jdo.option.ConnectionPassword&lt;/name&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;value&gt;hivepassword&lt;/value&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Configuring Spark to use the Hive Metastore</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.lineinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/opt/spark-3.3.2-bin-hadoop3/conf/spark-defaults.conf</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">line</span>: <span style="color:#ae81ff">spark.sql.warehouse.dir=hdfs://localhost:9000/user/hive/warehouse</span>
</span></span></code></pre></div><h1 id="code-breakdown">Code Breakdown</h1>
<p>Let&rsquo;s look at the code.</p>
<h2 id="part-1-defining-a-play">Part 1: Defining a play</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Installing Apache Hive and its prerequisties on the RHEL 8 head node.</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hosts</span>: <span style="color:#ae81ff">head_node</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become_user</span>: <span style="color:#ae81ff">temp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become_method</span>: <span style="color:#ae81ff">sudo</span>
</span></span></code></pre></div><p>This section defines the play with the title &ldquo;Installing Apache Hive and its prerequisties on the RHEL 8 head node&rdquo;.</p>
<p><code>hosts</code> defines the managed node to run this playbook on (in this case, the Rhel 8 head node, named <code>head_node</code> in the inventory file).</p>
<p><code>become</code> tells Ansible that this play needs to be executed with elevated privileges.</p>
<p><code>become_user</code> defines the user to run the commands defined by Ansible as. (in this case, <code>temp</code> is the user.)</p>
<p><code>become_method</code> tells Ansible that the method to elevate the privilieges by is <code>sudo</code>.</p>
<h2 id="part-2-downloading-hive-with-its-prerequisites">Part 2: Downloading Hive (with it&rsquo;s prerequisites)</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>  <span style="color:#f92672">tasks</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Installing Maven on the RHEL 8 head node</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.get_url</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">url</span>: <span style="color:#ae81ff">https://dlcdn.apache.org/maven/maven-3/3.9.4/binaries/apache-maven-3.9.4-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/tmp/apache-maven-3.9.4-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">checksum</span>: <span style="color:#e6db74">&#34;sha512:https://downloads.apache.org/maven/maven-3/3.9.4/binaries/apache-maven-3.9.4-bin.tar.gz.sha512&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Decompressing the Maven tarball on the Rhel 8 head node</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.unarchive</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">src</span>: <span style="color:#ae81ff">/tmp/apache-maven-3.9.4-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/opt/</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">remote_src</span>: <span style="color:#66d9ef">yes</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Adding the Maven `bin` directory to the PATH</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.lineinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/home/temp/.bashrc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">line</span>: <span style="color:#ae81ff">export PATH=$PATH:/opt/apache-maven-3.9.4/bin</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Downloading Apache Hive for the RHEL 8 head node</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.get_url</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">url</span>: <span style="color:#ae81ff">https://dlcdn.apache.org/hive/hive-3.1.3/apache-hive-3.1.3-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/tmp/apache-hive-3.1.3-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">checksum</span>: <span style="color:#e6db74">&#34;sha256:https://dlcdn.apache.org/hive/hive-3.1.3/apache-hive-3.1.3-bin.tar.gz.sha256&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">timeout</span>: <span style="color:#ae81ff">60</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Decompressing the Apache Hive tarball for the RHEL 8 head node</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.unarchive</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">src</span>: <span style="color:#ae81ff">/tmp/apache-hive-3.1.3-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/opt/</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">remote_src</span>: <span style="color:#66d9ef">yes</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Adding the installation directory to the PATH</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.lineinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/home/temp/.bashrc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">line</span>: <span style="color:#ae81ff">export HIVE_HOME=/opt/apache-hive-3.1.3-bin</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Adding the installation directory to the PATH</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.lineinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/home/temp/.bashrc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">line</span>: <span style="color:#ae81ff">export PATH=$PATH:$HIVE_HOME/bin</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Editing the log4j.properties file</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.blockinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/opt/hadoop-3.3.6/etc/hadoop/log4j.properties</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">block</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        # Define an appender for the MRAppMaster logger
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        log4j.logger.org.apache.hadoop.mapreduce.v2.app.MRAppMaster=INFO, mrappmaster
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        # Appender for MRAppMaster logger
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        log4j.appender.mrappmaster=org.apache.log4j.ConsoleAppender
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        log4j.appender.mrappmaster.layout=org.apache.log4j.PatternLayout
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        log4j.appender.mrappmaster.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Reloading the shell</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.shell</span>: <span style="color:#ae81ff">source ~/.bashrc</span>
</span></span></code></pre></div><p>This code of chunk downloads Maven (a software project management and comprehension tool), and installs it, and then proceeds to download and install Hive, and then updates the <code>.bashrc</code> file to add the installed binaries.</p>
<p>Commands like <code>ansible.builtin.blockinfile</code> and <code>ansible.builtin.get_url</code> are modules that are built-in by Ansible to perform particular functions, and are part of each named task, which are themselves a part of each named play.</p>
<h2 id="part-3-configuring-hive">Part 3: Configuring Hive</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Configuring Hive on the RHEL 8 head node.</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hosts</span>: <span style="color:#ae81ff">head_node</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">remote_user</span>: <span style="color:#ae81ff">temp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become_user</span>: <span style="color:#ae81ff">temp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become_method</span>: <span style="color:#ae81ff">sudo</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tasks</span>:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Configuring the Hive Warehouse using hadoop</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.shell</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      source ~/.bashrc
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_HOME=/opt/hadoop-3.3.6
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export PATH=$PATH:$HADOOP_HOME/bin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export PATH=$PATH:$HADOOP_HOME/sbin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_MAPRED_HOME=${HADOOP_HOME}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_COMMON_HOME=${HADOOP_HOME}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_HDFS_HOME=${HADOOP_HOME}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_YARN_HOME=${HADOOP_HOME}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HIVE_HOME=/opt/apache-hive-3.1.3-bin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export PATH=$PATH:$HIVE_HOME/bin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -mkdir /tmp
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -mkdir /user
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -mkdir /user/hive
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -mkdir /user/hive/warehouse
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -chmod g+w /tmp
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -chmod g+w /user/hive/warehouse
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      schematool -dbType derby -initSchema</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">args</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">executable</span>: <span style="color:#ae81ff">/bin/bash</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Creating a hive-site.xml file</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">copy</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/opt/apache-hive-3.1.3-bin/conf/hive-site.xml</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">content</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;name&gt;javax.jdo.option.ConnectionURL&lt;/name&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;value&gt;jdbc:mysql://localhost/metastore&lt;/value&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;/property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;name&gt;javax.jdo.option.ConnectionDriverName&lt;/name&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;value&gt;com.mysql.jdbc.Driver&lt;/value&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;/property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;name&gt;javax.jdo.option.ConnectionUserName&lt;/name&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;value&gt;hive&lt;/value&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;/property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;name&gt;javax.jdo.option.ConnectionPassword&lt;/name&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;value&gt;hivepassword&lt;/value&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Configuring Spark to use the Hive Metastore</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.lineinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/opt/spark-3.3.2-bin-hadoop3/conf/spark-defaults.conf</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">line</span>: <span style="color:#ae81ff">spark.sql.warehouse.dir=hdfs://localhost:9000/user/hive/warehouse</span>
</span></span></code></pre></div><p>Let&rsquo;s reiterate. The first few lines define the play, which is to configure Hive. What follows that are tasks, whice each have a name and run a module. This above chunk initiates the Hive warehouse and creates a hive-site.xml file to use so that Spark can detect and use the Hive Metastore. Modules are the commands like <code>ansible.builtin.shell</code> and <code>ansible.builtin.lineinfile</code>, and they execute specific functions.</p>
<h2 id="part-4-downloading-and-configuring-hive-for-the-cluster-nodes">Part 4: Downloading and configuring Hive for the cluster nodes</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Installing Apache Hive and its prerequisties on the Ubuntu cluster nodes.</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hosts</span>: <span style="color:#ae81ff">cluster_nodes</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">remote_user</span>: <span style="color:#ae81ff">temp</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">become_method</span>: <span style="color:#ae81ff">sudo</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tasks</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Installing Maven on the RHEL 8 head node</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.get_url</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">url</span>: <span style="color:#ae81ff">https://dlcdn.apache.org/maven/maven-3/3.9.4/binaries/apache-maven-3.9.4-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/tmp/apache-maven-3.9.4-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">checksum</span>: <span style="color:#e6db74">&#34;sha512:https://downloads.apache.org/maven/maven-3/3.9.4/binaries/apache-maven-3.9.4-bin.tar.gz.sha512&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Decompressing the Maven tarball on the Rhel 8 head node</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.unarchive</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">src</span>: <span style="color:#ae81ff">/tmp/apache-maven-3.9.4-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/opt/</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">remote_src</span>: <span style="color:#66d9ef">yes</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Adding the Maven `bin` directory to the PATH</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.lineinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/home/temp/.bashrc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">line</span>: <span style="color:#ae81ff">export PATH=$PATH:/opt/apache-maven-3.9.4/bin</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Downloading Apache Hive for the RHEL 8 head node</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.get_url</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">url</span>: <span style="color:#ae81ff">https://dlcdn.apache.org/hive/hive-3.1.3/apache-hive-3.1.3-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/tmp/apache-hive-3.1.3-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">checksum</span>: <span style="color:#e6db74">&#34;sha256:https://dlcdn.apache.org/hive/hive-3.1.3/apache-hive-3.1.3-bin.tar.gz.sha256&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">timeout</span>: <span style="color:#ae81ff">60</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Decompressing the Apache Hive tarball for the RHEL 8 head node</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.unarchive</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">src</span>: <span style="color:#ae81ff">/tmp/apache-hive-3.1.3-bin.tar.gz</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/opt/</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">remote_src</span>: <span style="color:#66d9ef">yes</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Adding the installation directory to the PATH</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.lineinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/home/temp/.bashrc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">line</span>: <span style="color:#ae81ff">export HIVE_HOME=/opt/apache-hive-3.1.3-bin</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Adding the installation directory to the PATH</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.lineinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/home/temp/.bashrc</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">line</span>: <span style="color:#ae81ff">export PATH=$PATH:$HIVE_HOME/bin</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Editing the log4j.properties file</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.blockinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/opt/hadoop-3.3.6/etc/hadoop/log4j.properties</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">block</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        # Define an appender for the MRAppMaster logger
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        log4j.logger.org.apache.hadoop.mapreduce.v2.app.MRAppMaster=INFO, mrappmaster
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        # Appender for MRAppMaster logger
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        log4j.appender.mrappmaster=org.apache.log4j.ConsoleAppender
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        log4j.appender.mrappmaster.layout=org.apache.log4j.PatternLayout
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        log4j.appender.mrappmaster.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Reloading the environment variables</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.shell</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      . /home/temp/.bashrc</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">args</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">executable</span>: <span style="color:#ae81ff">/bin/bash</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Configuring the Hive Warehouse using hadoop</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.shell</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      . /home/temp/.bashrc
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_HOME=/opt/hadoop-3.3.6
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export PATH=$PATH:$HADOOP_HOME/bin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export PATH=$PATH:$HADOOP_HOME/sbin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_MAPRED_HOME=${HADOOP_HOME}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_COMMON_HOME=${HADOOP_HOME}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_HDFS_HOME=${HADOOP_HOME}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HADOOP_YARN_HOME=${HADOOP_HOME}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export HIVE_HOME=/opt/apache-hive-3.1.3-bin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      export PATH=$PATH:$HIVE_HOME/bin
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -mkdir /tmp
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -mkdir /user
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -mkdir /user/hive
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -mkdir /user/hive/warehouse
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -chmod g+w /tmp
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      hadoop fs -chmod g+w /user/hive/warehouse
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      schematool -dbType derby -initSchema</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">args</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">executable</span>: <span style="color:#ae81ff">/bin/bash</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Creating a hive-site.xml file</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">copy</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">dest</span>: <span style="color:#ae81ff">/opt/apache-hive-3.1.3-bin/conf/hive-site.xml</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">content</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;name&gt;javax.jdo.option.ConnectionURL&lt;/name&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;value&gt;jdbc:mysql://localhost/metastore&lt;/value&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;/property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;name&gt;javax.jdo.option.ConnectionDriverName&lt;/name&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;value&gt;com.mysql.jdbc.Driver&lt;/value&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;/property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;name&gt;javax.jdo.option.ConnectionUserName&lt;/name&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;value&gt;hive&lt;/value&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;/property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;property&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;name&gt;javax.jdo.option.ConnectionPassword&lt;/name&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            &lt;value&gt;hivepassword&lt;/value&gt;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        &lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Configuring Spark to use the Hive Metastore</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ansible.builtin.lineinfile</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/opt/spark-3.3.2-bin-hadoop3/conf/spark-defaults.conf</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insertafter</span>: <span style="color:#ae81ff">EOF</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">line</span>: <span style="color:#ae81ff">spark.sql.warehouse.dir=hdfs://localhost:9000/user/hive/warehouse</span>
</span></span></code></pre></div><p>This chunk of code performs the same tasks that the first three did, but on the cluster nodes! The <code>hosts</code> defined here are the <code>cluster_nodes</code>, which include the three cluster nodes, as seen from the inventory file.</p>
<h1 id="further-reading">Further Reading</h1>
<ul>
<li><a href="https://cwiki.apache.org/confluence/display/Hive/Tutorial#Tutorial-Concepts">Hive Basics</a></li>
<li><a href="https://docs.ansible.com/ansible/latest/plugins/module.html">Ansible Modules</a></li>
<li><a href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_intro.html">Ansible Playbooks</a></li>
</ul>
]]></content:encoded></item><item><title>Installing and configuring Hadoop and Spark on a 4 node cluster.</title><link>https://naveenkannan.dev/posts/hadoop_spark/</link><pubDate>Mon, 21 Aug 2023 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/hadoop_spark/</guid><description>A guide to installing Hadoop and Spark on a 4 node cluster, while configuring and setting up HDFS, YARN and MapReduce.</description><content:encoded><![CDATA[<h1 id="a-brief-introduction-to-hadoop">A brief introduction to Hadoop</h1>
<p>Apache Hadoop is an open source software library that allows for the distributed processing of large datasets across a computing cluster.</p>
<p>Hadoop shines at processing enormously massive amounts of data that is too big to fit on a single computing system.</p>
<h1 id="hadoops-main-components">Hadoop&rsquo;s Main Components</h1>
<p>Hadoop has components integral to its functioning. These include:</p>
<p><strong>The HDFS (Hadoop Distributed File System)</strong></p>
<p>HDFS is a distributed file system that provides high-throughput access to application data, and is used to scale a single Apache Hadoop cluster to hundreds (and even thousands) of nodes.</p>
<p>HDFS takes massive data files and splits them into smaller chunks called &ldquo;blocks.&rdquo; These blocks are typically quite large (often 128 MB or 256 MB), which helps in efficient data management and processing.</p>
<p>These blocks are spread out and replicated across multiple machines in the cluster, ensuring that if one machine fails, the stored data is still safe because there are replicated copies of each block spread across different machines.</p>
<p>HDFS has two components: the NameNode and the DataNode.</p>
<p>The NameNode keeps track of where each block of data is located in the cluster.</p>
<p>The DataNode(s) store the actual data blocks and are spread across the cluster.</p>
<p><strong>YARN (Yet Another Resource Negotiator)</strong></p>
<p>YARN, which stands for Yet Another Resource Negotiator, plays a pivotal role in managing resources and scheduling tasks within the Hadoop architecture. It consists of two primary components: the ResourceManager (RM) and the NodeManager (NM).</p>
<p>YARN effectively distributes computing resources and manages job scheduling across various applications running on the Hadoop cluster.</p>
<p>The ResourceManager is the central decision-maker for resource allocation and is responsible for fairly distributing resources among all the applications running on the cluster.</p>
<p>The NodeManager is responsible for managing resources on a per-machine basis.</p>
<p><strong>MapReduce</strong></p>
<p>MapReduce is a programming model and processing framework that divides big data tasks into smaller sub-tasks, processes them in parallel across a cluster, and then combines the results.</p>
<p>Mapping:</p>
<ul>
<li>Data is split into chunks and assigned to &ldquo;mappers.&rdquo;</li>
<li>Mappers process data and produce intermediate key-value pairs.</li>
</ul>
<p>Shuffling and Sorting:</p>
<ul>
<li>Intermediate pairs are sorted and grouped by keys.</li>
<li>Data with the same key is grouped together.</li>
</ul>
<p>Reducing:</p>
<ul>
<li>Grouped data is sent to &ldquo;reducers.&rdquo;</li>
<li>Reducers process the data, performing operations and producing final results.</li>
</ul>
<p><strong>Hadoop Common</strong></p>
<p>Hadoop Common provides essential fundamental libraries and utilities for different components within the Hadoop architecture. It serves as a shared resource that supports the entire Hadoop framework.</p>
<figure class="align-center ">
    <img loading="lazy" src="hadoop_architecture.png#center"
         alt="A diagrammatic representation of the Hadoop architecture across a computing cluster with a Master Node and 3 Slave Nodes."/> <figcaption>
            <p>A diagrammatic representation of the Hadoop architecture across a computing cluster with a Master Node and 3 Slave Nodes.</p>
        </figcaption>
</figure>

<h1 id="installing-hadoop-on-a-4-node-cluster">Installing Hadoop on a 4 node cluster</h1>
<h2 id="prerequisites">Prerequisites:</h2>
<p>To begin with, the 4 clusters require a Unix-like operating system.</p>
<p>Furthermore, while not essential, using Ansible enormously reduces the time needed to configure and install HDFS with the aforementioned components.</p>
<p>In my previous blog post, I described how to use Ansible (deployed via a Docker container) to remotely configure a computing cluster. This obviates the need for Ansible itself to be installed on the head node. For details on how to use Ansible to configure a cluster, please refer my previous blog post.</p>
<p>I used Ansible to perform all of the tasks detailed in this blog post. While this post describes the general steps needed for installing Hadoop, the next blog post will have the Ansible playbooks posted so as to describe each step of the process in more detail.</p>
<h3 id="jdk-8">JDK 8</h3>
<p>Ensure that each cluster node has OpenJDK installed.</p>
<p>Note that if this is being installed using the <code>yum</code> package manager, the EPEL (Extra Packages for Enterprise Linux) repository needs to be imported and activated.</p>
<h3 id="ssh-secure-shell-and-pdsh">SSH (Secure Shell) and PDSH</h3>
<p>Hadoop requires SSH and PDSH to be installed on each node of the cluster. Communication between nodes is done via Hadoop.</p>
<p>Also, ensure that each node can SSH into every other node without a password prompt. This will require performing an SSH Key exchange between every node of the cluster and every other node. (Refer to my blog post about using Ansible to remotely configure a cluster for more details on SSH key exchanges.)</p>
<p>Pro-tip! On my RHEL 8 head node, I ran into a lot of erros from using <code>pdsh</code> since it didn&rsquo;t come with SSH incorporated. I used <code>dnf</code> to download <code>pdsh-rcmd-ssh</code> after installing <code>pdsh</code> using <code>yum.</code></p>
<p>Update your <code>.bashrc</code> or <code>.bash_profile</code> file with the following:</p>
<pre tabindex="0"><code>export PDSH_RCMD_TYPE=ssh
</code></pre><h3 id="python-39">Python 3.9</h3>
<p>While not a hard requirement for just Hadoop, we intend to install Spark, and use Python to interact with the Scala API, using Jupyter Notebook as the driver.</p>
<h3 id="jupyter-notebook">Jupyter Notebook</h3>
<p>Once Python is installed, run <code>python3 -m pip install --upgrade pip</code> in order to upgrade pip, and then install Jupyter using <code>pip install jupyter</code>.</p>
<h2 id="installing-hadoop-and-spark">Installing Hadoop and Spark</h2>
<h3 id="downloading-and-unzipping-hadoop">Downloading and Unzipping Hadoop</h3>
<p>Download a recent stable release from one of the <a href="https://www.apache.org/dyn/closer.cgi/hadoop/common/">Apache Download Mirrors for Hadoop.</a> (I downloaded Hadoop 3.3.6.)</p>
<p>Unzip the downloaded tarball.</p>
<h3 id="downloading-and-unzipping-spark">Downloading and Unzipping Spark</h3>
<p>Download a recent stable release from one of the <a href="https://www.apache.org/dyn/closer.lua/spark/spark-3.4.1/spark-3.4.1-bin-hadoop3.tgz">Apache Download Mirrors for Spark.</a> (I downloaded Spark 3.3.2 to be compatiable with Hail and Glow.)</p>
<p>Unzip the downloaded tarball.</p>
<h3 id="configuring-hadoop-and-spark">Configuring Hadoop and Spark</h3>
<p>In your <code>.bashrc</code> or <code>bash_profile</code>, update the locations of Java, Hadoop and Spark to your PATH.</p>
<p>Update the locations of HDFS, MapReduce and Common.</p>
<p>Reload the terminal.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>export HADOOP_HOME<span style="color:#f92672">=</span>/opt/hadoop-3.3.6
</span></span><span style="display:flex;"><span>export PATH<span style="color:#f92672">=</span>$PATH:$HADOOP_HOME/bin
</span></span><span style="display:flex;"><span>export PATH<span style="color:#f92672">=</span>$PATH:$HADOOP_HOME/sbin
</span></span><span style="display:flex;"><span>export HADOOP_MAPRED_HOME<span style="color:#f92672">=</span><span style="color:#e6db74">${</span>HADOOP_HOME<span style="color:#e6db74">}</span>
</span></span><span style="display:flex;"><span>export HADOOP_COMMON_HOME<span style="color:#f92672">=</span><span style="color:#e6db74">${</span>HADOOP_HOME<span style="color:#e6db74">}</span>
</span></span><span style="display:flex;"><span>export HADOOP_HDFS_HOME<span style="color:#f92672">=</span><span style="color:#e6db74">${</span>HADOOP_HOME<span style="color:#e6db74">}</span>
</span></span><span style="display:flex;"><span>export HADOOP_YARN_HOME<span style="color:#f92672">=</span><span style="color:#e6db74">${</span>HADOOP_HOME<span style="color:#e6db74">}</span>
</span></span><span style="display:flex;"><span>export JAVA_HOME<span style="color:#f92672">=</span>/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.382.b05-2.el8.x86_64
</span></span><span style="display:flex;"><span>export LANG<span style="color:#f92672">=</span>en_US.UTF-8
</span></span><span style="display:flex;"><span>export TZ<span style="color:#f92672">=</span>UTC
</span></span><span style="display:flex;"><span>export CLASSPATH_PREPEND_DISTCACHE<span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span>export SPARK_HOME<span style="color:#f92672">=</span>/opt/spark-3.3.2-bin-hadoop3
</span></span><span style="display:flex;"><span>export PATH<span style="color:#f92672">=</span>$PATH:$SPARK_HOME/bin
</span></span><span style="display:flex;"><span>export HADOOP_CONF_DIR<span style="color:#f92672">=</span>/opt/hadoop-3.3.6/etc/hadoop
</span></span><span style="display:flex;"><span>export LD_LIBRARY_PATH<span style="color:#f92672">=</span>/opt/hadoop-3.3.6/lib/native:$LD_LIBRARY_PATH
</span></span><span style="display:flex;"><span>export PYSPARK_DRIVER_PYTHON<span style="color:#f92672">=</span>jupyter
</span></span><span style="display:flex;"><span>export PYSPARK_DRIVER_PYTHON_OPTS<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;notebook --NotebookApp.open_browser=False --NotebookApp.ip=&#39;localhost&#39; &#34;</span>
</span></span><span style="display:flex;"><span>export PATH<span style="color:#f92672">=</span>$PATH:/usr/local/bin
</span></span></code></pre></div><h3 id="updating-the-hadoop-configuration-files">Updating the Hadoop configuration files</h3>
<p>The following configuration files need to be edited across the cluster.</p>
<ul>
<li>hadoop-env.sh</li>
</ul>
<p>This file can be found in your installation directory. Assuming the name of the directory hadoop was unzipped to is /hadoop, the file can be found at /hadoop/etc/hadoop/hadoop-env.sh.</p>
<p>Add the following line to the file in order to add the location of the Java installation to Hadoop:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>export JAVA_HOME<span style="color:#f92672">=</span>/usr/lib/jvm/java-8-openjdk-amd64/
</span></span></code></pre></div><p>Note that the name/location of the Java installation can change depending on your system.</p>
<ul>
<li>workers</li>
</ul>
<p>Edit the <code>workers</code> file to add the names of your cluster. Note that this requires the names of the nodes with their IP addresses to be present in the /etc/hosts file.</p>
<ul>
<li>core-site.xml</li>
</ul>
<p>This file can be found in your installation directory. Assuming the name of the directory hadoop was unzipped to is /hadoop, the file can be found at /hadoop/etc/hadoop/core-site.xml.</p>
<p>Insert this code block between <code>&lt;configuration&gt;</code> and <code>&lt;/configuration&gt;</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ae81ff">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;name&gt;fs.defaultFS&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;value&gt;hdfs://namenode:9000&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;/property&gt;</span>
</span></span></code></pre></div><ul>
<li>hdfs-site.xml</li>
</ul>
<p>This file can be found in your installation directory. Assuming the name of the directory hadoop was unzipped to is /hadoop, the file can be found at /hadoop/etc/hadoop/hdfs-site.xml.</p>
<p>Insert this code block between <code>&lt;configuration&gt;</code> and <code>&lt;/configuration&gt;</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ae81ff">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;name&gt;dfs.namenode.name.dir&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;value&gt;/home/temp/data/nameNode&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;name&gt;dfs.datanode.data.dir&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;value&gt;/home/temp/data/dataNode&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;name&gt;dfs.replication&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;value&gt;4&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;/property&gt;</span>
</span></span></code></pre></div><ul>
<li>yarn-site.xml</li>
</ul>
<p>This file can be found in your installation directory. Assuming the name of the directory hadoop was unzipped to is /hadoop, the file can be found at /hadoop/etc/hadoop/yarn-site.xml.</p>
<p>Insert this code block between <code>&lt;configuration&gt;</code> and <code>&lt;/configuration&gt;</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ae81ff">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;name&gt;yarn.nodemanager.aux-services&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;value&gt;mapreduce_shuffle&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;name&gt;yarn.nodemanager.env-whitelist&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;value&gt;JAVA_HOME,HADOOP_COMMON_HOME,HADOOP_HDFS_HOME,HADOOP_CONF_DIR,CLASSPATH_PREPEND_DISTCACHE,HADOOP_YARN_HOME,HADOOP_HOME,PATH,LANG,TZ,HADOOP_MAPRED_HOME&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;name&gt;yarn.resourcemanager.hostname&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;value&gt;(namenode IP Address)&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;name&gt;yarn.acl.enable&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;value&gt;0&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;/property&gt;</span>
</span></span></code></pre></div><ul>
<li>mapred-site.xml</li>
</ul>
<p>This file can be found in your installation directory. Assuming the name of the directory hadoop was unzipped to is /hadoop, the file can be found at /hadoop/etc/hadoop/mapred-site.xml.</p>
<p>Insert this code block between <code>&lt;configuration&gt;</code> and <code>&lt;/configuration&gt;</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ae81ff">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;name&gt;mapreduce.framework.name&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;value&gt;yarn&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;name&gt;yarn.app.mapreduce.am.env&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;value&gt;HADOOP_MAPRED_HOME=$HADOOP_HOME&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;name&gt;mapreduce.map.env&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;value&gt;HADOOP_MAPRED_HOME=$HADOOP_HOME&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;name&gt;mapreduce.reduce.env&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;value&gt;HADOOP_MAPRED_HOME=$HADOOP_HOME&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;/property&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;property&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;name&gt;mapreduce.application.classpath&lt;/name&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">&lt;value&gt;$HADOOP_MAPRED_HOME/share/hadoop/mapreduce/*,$HADOOP_MAPRED_HOME/share/hadoop/mapreduce/lib/*,$HADOOP_MAPRED_HOME/share/hadoop/common/*,$HADOOP_MAPRED_HOME/share/hadoop/common/lib/*,$HADOOP_MAPRED_HOME/share/hadoop/yarn/*,$HADOOP_MAPRED_HOME/share/hadoop/yarn/lib/*,$HADOOP_MAPRED_HOME/share/hadoop/hdfs/*,$HADOOP_MAPRED_HOME/share/hadoop/hdfs/lib/*&lt;/value&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">&lt;/property&gt;</span>
</span></span></code></pre></div><h3 id="updating-the-spark-configuration-files">Updating the Spark configuration files</h3>
<p>Rename spark-defaults.conf.template to spark-defaults.conf. The file can be found at spark-3.3.2-bin-hadoop3/conf/spark-defaults.conf.template.</p>
<p>Replace</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ae81ff">spark.master   spark://master:7077</span>
</span></span></code></pre></div><p>with</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#ae81ff">spark.master   yarn</span>
</span></span></code></pre></div><h3 id="starting-the-hdfs-and-yarn-services">Starting the HDFS and YARN services</h3>
<p>Once the configuration files are edited and the PATH is updated, the HDFS namenode can be formatted by running <code>hdfs namenode -format</code>.</p>
<p>The HDFS node services can be initiated by running <code>start-dfs.sh</code>.</p>
<p>YARN can be initiated by running <code>start-yarn.sh</code>.</p>
<p>Ensure that these services are running by running the <code>jps</code> command.</p>
<p>The head node will have the services <code>NameNode</code> and <code>ResourceManager</code> running. Remember that these services run only on the head node.</p>
<p>If used as a data node, it will also have the services <code>DataNode</code> and <code>NodeManager</code> running on it.</p>
<p>The slave nodes will have <code>DataNode</code> and <code>NodeManager</code> running on it.</p>
<h1 id="monitoring-the-services">Monitoring the services</h1>
<p>Initiate the history server by running <code>$SPARK_HOME/sbin/start-history-server.sh</code>.</p>
<p>This can be accessed at <code>http://localhost:8088</code>.</p>
]]></content:encoded></item><item><title>Using Ansible to remotely configure a cluster.</title><link>https://naveenkannan.dev/posts/ansible_cluster_config/</link><pubDate>Sat, 24 Jun 2023 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/ansible_cluster_config/</guid><description>Using a containerized instance of Ansible to remotely connect to a cluster and perform a simple ping task to confirm connection.</description><content:encoded><![CDATA[<h1 id="what-is-ansible">What is Ansible?</h1>
<p>Ansible is an open-source IT automation tool that allows for automated management of remote systems.</p>
<p>A basic Ansible environment has the following three components:</p>
<ul>
<li>
<p><strong>Control Node</strong>: This is a system on which Ansible is installed, and the system from which Ansible commands such as <code>ansible-inventory</code> are issued. This is also where Ansible playbooks and configuration files are stored.</p>
</li>
<li>
<p><strong>Managed node</strong>: This is a remote system that Ansible intends to manage and configure.</p>
</li>
<li>
<p><strong>Inventory</strong>: This is a list of managed nodes that are organized locally on the <strong>control node</strong>. This lists the IP addresses or the hostnames of the remote systems being managed along with any connection information needed.</p>
</li>
</ul>
<figure class="align-center ">
    <img loading="lazy" src="ansible_architecture.png#center"
         alt="A flowchart demonstrating the basic architecture of an Ansible environment."/> <figcaption>
            <p>A flowchart demonstrating the basic architecture of an Ansible environment.</p>
        </figcaption>
</figure>

<h1 id="what-does-ansible-do">What does Ansible do?</h1>
<p>Ansible uses a declarative language in the YAML format to describe the <em>desired end state</em> of the managed nodes, and it can connect to these managed nodes via standard protocols like SSH and handle the execution of the tasks required to acheive this end state. These are done via YAML-based files called <strong>Playbooks</strong>.</p>
<p><strong>Playbooks</strong> are automation blueprints, in the <code>YAML</code> format. Playbooks contain <strong>plays</strong>, which are a list of tasks that map to each managed node in the predefined inventory.</p>
<p><strong>Tasks</strong> are a list of one or more modules that define the operations that Ansible can perform.</p>
<p>A <strong>Module</strong> is a unit of code that Ansible can run on managed nodes.</p>
<figure class="align-center ">
    <img loading="lazy" src="ansible_playbook_structure.png#center"/> 
</figure>

<p>The key features of Ansible that make it deal to perform automated configuration management of remote systems are as follows:</p>
<ul>
<li>
<p><strong>Agentless Architecture</strong>: Ansible only needs to be installed on the Control Node, and the Managed nodes do not require Ansible to be installed.</p>
</li>
<li>
<p><strong>Idempotent Execution</strong>: Ansible ensures that no matter how many time the same Playbook is run, the end state of the managed nodes will be the same, regardless of the initial state of the managed node.</p>
</li>
</ul>
<h1 id="potential-use-case-scenario">Potential Use Case Scenario</h1>
<p>Now, let&rsquo;s get into the meat of the post, which is about using Ansible to connect to a cluster and configure each node of the cluster.</p>
<p>Let&rsquo;s assume that we&rsquo;ve been given access to a sparkling new cluster for computing purposes.</p>
<p>The cluster has a head node, and several individual cluster nodes.</p>
<p>Now, the head node has both a public and a private IP address.</p>
<p>The cluster nodes, however, only have private IP addresses, and can only be connected to via the head node.</p>
<p>We intend to use Ansible to automate and configure the cluster and it&rsquo;s nodes.</p>
<p>Instead of downloading and installing Ansible on the head node of the cluster, we intend to use a dockerized version of Ansible to connect to the cluster.</p>
<p>Let&rsquo;s assume that the head node has the hypothetical IP address XXX.XX.XXX.X.</p>
<p>Let&rsquo;s also assume that there are three cluster nodes, each with private IP addresses in the format of:</p>
<ul>
<li>YY.Y.Y.2</li>
<li>YY.Y.Y.3</li>
<li>YY.Y.Y.4</li>
</ul>
<figure class="align-center ">
    <img loading="lazy" src="cluster_config.png#center"
         alt="The example use case for Ansible."/> <figcaption>
            <p>The example use case for Ansible.</p>
        </figcaption>
</figure>

<h1 id="the-ansible-dockerfile">The Ansible Dockerfile</h1>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Loading from a miniconda3 image</span>
</span></span><span style="display:flex;"><span>FROM continuumio/miniconda3
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Installing Mamba using Conda.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># I find Mamba to be much faster for package installation compared to Conda.</span>
</span></span><span style="display:flex;"><span>RUN conda install -c conda-forge mamba -y
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Creating a Mamba ENV called `ansible_env`.</span>
</span></span><span style="display:flex;"><span>RUN mamba create -y -n ansible_env
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Setting the new environment as the default</span>
</span></span><span style="display:flex;"><span>RUN echo <span style="color:#e6db74">&#34;conda activate ansible_env&#34;</span> &gt;&gt; ~/.bashrc
</span></span><span style="display:flex;"><span>SHELL <span style="color:#f92672">[</span><span style="color:#e6db74">&#34;/bin/bash&#34;</span>, <span style="color:#e6db74">&#34;--login&#34;</span>, <span style="color:#e6db74">&#34;-c&#34;</span><span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#Installing Linux based dependencies</span>
</span></span><span style="display:flex;"><span>RUN apt-get update <span style="color:#f92672">&amp;&amp;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    apt-get install -y git nano software-properties-common gcc wget build-essential ssh less screen <span style="color:#f92672">&amp;&amp;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    apt-get clean <span style="color:#f92672">&amp;&amp;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>    rm -rf /var/lib/apt/lists/*
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#Installing ansible using mamba</span>
</span></span><span style="display:flex;"><span>RUN mamba install -y -c conda-forge ansible
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Generating a configuration file for Ansible.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># This will need to placed in a particular directory.</span>
</span></span><span style="display:flex;"><span>WORKDIR /etc
</span></span><span style="display:flex;"><span>RUN ansible-config init --disabled -t all &gt; ansible.cfg
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Creating an ansible folder in /etc/ to mount the hosts file</span>
</span></span><span style="display:flex;"><span>WORKDIR ../
</span></span><span style="display:flex;"><span>RUN mkdir /etc/ansible
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Creating the entrypoint</span>
</span></span><span style="display:flex;"><span>ENTRYPOINT <span style="color:#f92672">[</span><span style="color:#e6db74">&#34;/bin/bash&#34;</span><span style="color:#f92672">]</span>
</span></span></code></pre></div><p>The above Docker image will require you to create a file called <code>hosts</code> to be mounted to <code>/etc/ansible</code> inside the container. This <code>hosts</code> file will be the inventory.</p>
<p>By default, Ansible expects the inventory file to be located at <code>/etc/ansible/hosts</code>.</p>
<p>The inventory is a list of the managed nodes you intend to connect to, and this is what we will be using to assess if the SSH connection has been set up properly.</p>
<p>The <code>hosts</code> file can be in <code>INI</code> or <code>YAML</code> formats. I went with <code>INI</code>, as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[cluster]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">XXX.XX.XXX.X</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">YY.Y.Y.2</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">YY.Y.Y.3</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">YY.Y.Y.4</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[head_node]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">XXX.XX.XXX.X</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[cluster_nodes]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">YY.Y.Y.2</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">YY.Y.Y.3</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">YY.Y.Y.4</span>
</span></span></code></pre></div><p>I first defined the cluster using all of it&rsquo;s nodes, both the head node and the cluster nodes. I then defined the head node and cluster node seperately, forming groups, making running potential tasks on individual node types easier.</p>
<p>I created a folder called <code>ansible_mount</code> on the host directory, and created the <code>hosts</code> file there. I will be using <code>ansible_mount</code> to mount all the directories we will need in this tutorial.</p>
<h1 id="setting-up-the-ssh-connections">Setting up the SSH connections</h1>
<p>Ansible will need to be able to SSH directly into all the managed nodes in order to be able to perform tasks on each of them.</p>
<p>However, in our example set-up, it is not possible to directly SSH into the cluster nodes using their private IP addresses. (At least, not without some configuration.) This requires us to use the head node as a jump server and edit the SSH configuration file.</p>
<p>For the sake of making this process easier to understand, follow this flow chart for a break down of the individual steps.</p>
<figure class="align-center ">
    <img loading="lazy" src="ssh_flowchart.png#center"/> 
</figure>

<h2 id="generating-an-ssh-key">Generating an SSH key</h2>
<p>I created a folder inside the <code>ansible_mount</code> directory called <code>.ssh</code>.</p>
<p>I mounted <code>/path/to/ansible_mount/.ssh</code> to <code>/root/.ssh</code> in the Docker container.</p>
<p>Then, once within the Docker instance, I ran <code>ssh-keygen -t ed25519</code> to generate a SSH key inside <code>/root/.ssh</code>.</p>
<p>Since the folder is mounted, <em>this key persists, and is not deleted</em> after termination of the container.</p>
<p>This generates two things. <code>id_ed25519</code>, and <code>id_ed25519.pub</code>.</p>
<p><code>id_ed25519</code> is the private key and cannot be shared under any circumstances.</p>
<p><code>id_ed25519.pub</code> is our public key, and this is what we shall use to connect to the 4 nodes.</p>
<h2 id="editing-the-config-file-in-ssh-to-set-up-a-jump-host">Editing the <code>config</code> file in <code>.ssh</code> to set up a jump host</h2>
<p>Now, in order to be able to SSH into the clusters of the nodes in one line of code, I created a config file <code>config</code> at <code>path/to/ansible_mount/.ssh/config</code>.</p>
<p>Since the entirety of <code>path/to/ansible_mount/.ssh</code> is being mounted into the container at <code>/root/.ssh</code>, this config file will persist.</p>
<p>To reiterate, <code>path/to/ansible_mount/.ssh</code> contains both the keys and the <code>config</code> file.</p>
<p>This config file has the following text:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>Host XXX.XX.XXX.X
</span></span><span style="display:flex;"><span>    HostName abc.host.domain
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Host YY.Y.Y.2
</span></span><span style="display:flex;"><span>    ProxyJump sudo_user@XXX.XX.XXX.X
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Host YY.Y.Y.3
</span></span><span style="display:flex;"><span>    ProxyJump sudo_user@XXX.XX.XXX.X
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Host YY.Y.Y.4
</span></span><span style="display:flex;"><span>    ProxyJump sudo_user@XXX.XX.XXX.X
</span></span></code></pre></div><p>This sets up the head node as a jump server <em>through which</em> we can SSH into the cluster nodes using the private IP.</p>
<p>This allows for using XXX.XX.XXX.X(the head node of the cluster) as an intermediate server from which one can jump directly into the clusters.</p>
<p>Here, the term <code>sudo_user</code> refers to the user(s) who have sudo access to the head node.</p>
<h2 id="exchanging-ssh-keys-with-the-head-node">Exchanging SSH keys with the head node</h2>
<p>However, first I needed to set up a key exchange between the Docker container and the head node, so that I could <strong>directly SSH into the head node without a need for a password.</strong></p>
<p>Connecting to a server for the first time through SSH requires input from the user to establish the authenticity of the host. Manually SSH into the head node if you&rsquo;re doing this for the first time to confirm the authenticity of the host.</p>
<p>You will see something like this if you&rsquo;re connecting to the head node for the first time:</p>
<pre tabindex="0"><code>The authenticity of host &#39;YY.Y.Y.4 (&lt;no hostip for proxy command&gt;)&#39; can&#39;t be established.
ECDSA key fingerprint is xxx.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
</code></pre><p>Make sure to type in <code>yes</code> and accept this process.</p>
<p>From the docker container, I ran:</p>
<p><code>ssh-copy-id -i /root/.ssh/id_ed25519.pub sudo_user@abc.host.domain</code></p>
<p>You will see something like:</p>
<pre tabindex="0"><code>/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: &#34;/root/.ssh/id_ed25519.pub&#34;
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
</code></pre><p>Enter your password when prompted and you should see:</p>
<pre tabindex="0"><code>Number of key(s) added: 1

Now try logging into the machine, with:   &#34;ssh &#39;sudo_user@abc.host.domain&#39;&#34;
and check to make sure that only the key(s) you wanted were added.
</code></pre><p>This has copied the public key from the docker container into the head node at <code>/.ssh/authorized_keys</code>.</p>
<p>This made the docker container trusted by the head node, so when SSH&rsquo;ing into the head node, there is no need for a password.</p>
<p>Test this out by running <code>ssh sudo_user@abc.host.domain</code> and see if it gets you into the head node without prompting you for a password.</p>
<h2 id="exchanging-ssh-keys-with-the-cluster-nodes">Exchanging SSH keys with the cluster nodes</h2>
<p>As said before, connecting to a server for the first time through SSH requires input from the user to establish the authenticity of the host. However, this is a bit more complicated for the cluster nodes. This will require you to do the following:</p>
<p>Once in the container, run:</p>
<p><code>ssh -J sudo_user@abc.host.domain sudo_user@Y.Y.Y.2</code></p>
<p>To connect to the cluster node using the head node as a jump node. This uses the head node as a jump server between the container and the cluster node.</p>
<p>Accept the connection process as before and enter the password for the <code>sudo_user</code> on the cluster node.</p>
<p>Do the same for all the nodes.</p>
<p><em>Make sure you accept the authenticity of the host by performing the above ssh command before proceeding!</em></p>
<p>Now, running these commands:</p>
<p><code>ssh sudo_user@XX.X.X.2</code></p>
<p><code>ssh sudo_user@XX.X.X.3</code></p>
<p><code>ssh sudo_user@XX.X.X.4</code></p>
<p>Allowed me to directly SSH into the cluster nodes as the user <code>sudo_temp</code> <em>from the container</em>, since we set up the <code>ProxyJump</code> function using the XXX.XX.XXX.X IP address of the head node!</p>
<p>Note that running these commands before completing the fingerprinting process will fail. You will need to manually connect to the clusters individually first.</p>
<p>However, this would still require me to enter the password for <code>sudo_user@XXX.XX.XXX.X</code>, since the cluster nodes did not have the keys in the docker container exchanged with it.</p>
<p>From the container, I ran:</p>
<p><code>ssh-copy-id -i /root/.ssh/id_ed25519.pub sudo_user@&lt;private_IP_address_for_cluster&gt;</code></p>
<p>For every single cluster. Enter the password when prompted. Again, like before, you should see something like:</p>
<pre tabindex="0"><code>/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: &#34;/root/.ssh/id_ed25519.pub&#34;
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
</code></pre><p>Enter your password for the <code>sudo_user</code> on the cluster nodes. Again, you should see something like:</p>
<pre tabindex="0"><code>Number of key(s) added: 1

Now try logging into the machine, with:   &#34;ssh &#39;sudo_user@YY.Y.Y.2&#39;&#34;
and check to make sure that only the key(s) you wanted were added.
</code></pre><p>Test the key exchange process by running:</p>
<p><code>ssh sudo_user@YY.Y.Y.2</code>.</p>
<p>Do the same for all the other cluster nodes.</p>
<p>This copied the public key of the docker container into the authorized_keys for the 3 servers.</p>
<h2 id="direct-ssh-connections-into-the-head-node-and-the-cluster-nodes">Direct SSH connections into the head node and the cluster nodes</h2>
<p>Now it was possible to SSH into all 4 servers, <strong>without needing to enter a password</strong>.</p>
<p>For the head node, it was with the command:</p>
<p><code>ssh sudo_user@abc.host.domain</code></p>
<p>For the cluster nodes, it was with the command:</p>
<p><code>ssh &lt;private_IP_address_for_cluster&gt; -l sudo_user</code></p>
<p>This is essential to streamline the process by which Ansible can connect to the cluster nodes, since otherwise it would need the inventory files and the configuration files edited to enable ProxyJump and SSH password entry.</p>
<p>Ansible allows for using of a -u flag for user, and -u will always be <code>sudo_user</code>, since the keys are authorized for user temp.</p>
<h1 id="testing">Testing!</h1>
<p>Initiate the docker container in interactive mode, making sure to mount:</p>
<ul>
<li><code>/path/to/ansible_mount/.ssh</code> from the host directory to <code>/root/.ssh</code> in the container directory.</li>
<li><code>/path/to/ansible_mount/hosts</code> from the host directory to <code>/etc/ansible/hosts</code> in the container directory.</li>
</ul>
<p>Once inside, run</p>
<p><code>ansible all -m ping -u &lt;sudo_user&gt;</code></p>
<p>Replacing <code>&lt;sudo_user&gt;</code> with the user who has SSH access to the nodes.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>XXX.XX.XXX.X | SUCCESS <span style="color:#f92672">=</span>&gt; <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;ansible_facts&#34;</span>: <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;discovered_interpreter_python&#34;</span>: <span style="color:#e6db74">&#34;/usr/libexec/platform-python&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">}</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;changed&#34;</span>: false,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;ping&#34;</span>: <span style="color:#e6db74">&#34;pong&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>YY.Y.Y.4 | SUCCESS <span style="color:#f92672">=</span>&gt; <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;ansible_facts&#34;</span>: <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;discovered_interpreter_python&#34;</span>: <span style="color:#e6db74">&#34;/usr/bin/python3&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">}</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;changed&#34;</span>: false,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;ping&#34;</span>: <span style="color:#e6db74">&#34;pong&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>YY.Y.Y.2 | SUCCESS <span style="color:#f92672">=</span>&gt; <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;ansible_facts&#34;</span>: <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;discovered_interpreter_python&#34;</span>: <span style="color:#e6db74">&#34;/usr/bin/python3&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">}</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;changed&#34;</span>: false,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;ping&#34;</span>: <span style="color:#e6db74">&#34;pong&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span><span style="display:flex;"><span>YY.Y.Y.3 | SUCCESS <span style="color:#f92672">=</span>&gt; <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;ansible_facts&#34;</span>: <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;discovered_interpreter_python&#34;</span>: <span style="color:#e6db74">&#34;/usr/bin/python3&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">}</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;changed&#34;</span>: false,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;ping&#34;</span>: <span style="color:#e6db74">&#34;pong&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>should be the successful output.</p>
<h1 id="things-to-remember">Things to Remember</h1>
<p>Remember that the keys generated on the host directory pose a security risk if the private keys are shared.</p>
<p>Also remember that the <code>docker run</code> command may become large and unwieldly depending on the folders needed to be mounted.</p>
<p>The example dockerfile shared here was used to build an image that can be located on Dockerhub at <code>bush/ansible:latest</code>.</p>
]]></content:encoded></item><item><title>Docker, Singularity, and HPC.</title><link>https://naveenkannan.dev/posts/containers_hpc/</link><pubDate>Sat, 17 Jun 2023 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/containers_hpc/</guid><description>A brief rundown of Docker and Singularity, and their relevance in HPC environments.</description><content:encoded><![CDATA[<h1 id="containers">Containers</h1>
<p>Containers are environments that are intended to be lightweight and standalone software, with isolation from the host machine, which ensures that containers work uniformly across different staging and development instances.</p>
<p>Containers <em>share</em> the host OS&rsquo;s kernel and do not require an OS per application, which is a key difference between containers and virtual machines, which otherwise share a lot of similarities.</p>
<figure class="align-center ">
    <img loading="lazy" src="docker_architecture.png#center"
         alt="A very basic overview of container architecture."/> <figcaption>
            <p>A very basic overview of container architecture.</p>
        </figcaption>
</figure>

<p>Containers can be built using specifications (most commonly, a <strong>dockerfile</strong> for Docker containers, and a <strong>definition(def)</strong> file for Singularity.)</p>
<p>These specification files are simple text files that are merely an aggregation of instructions to the container client to set up the container image.</p>
<p>The packages installed according to the specifications could be wildly different from the host OS packages and software, and this won&rsquo;t result in any conflicts between the container and the host system since they are isolated from each other, merely sharing the host OS kernel.</p>
<p><em>Containers are not interchangeable with images!</em></p>
<p>Images and containers are two different entities. Containers are instances of images, and depending on the container client used, they may or may not be immutable.</p>
<p>When a container is removed, any changes to its state that are not stored in persistent storage disappear.</p>
<p>In this post, I will be going over two common platforms that can be used to run containers, and this will be purely in the context of use in HPC environments. These are <strong>Docker</strong> and <strong>Singularity</strong>.</p>
<h1 id="container-mounting">Container Mounting</h1>
<p>Containers and the changes made to the container do not persist after the container instance is killed.</p>
<figure class="align-center ">
    <img loading="lazy" src="cat_bubble.jpg#center"
         alt="Think of containers as a bubble. Once they pop, its like they were never there at all!"/> <figcaption>
            <p>Think of containers as a bubble. Once they pop, its like they were never there at all!</p>
        </figcaption>
</figure>

<p>It is possible to &ldquo;mount&rdquo; data directories on the host machine into the container instance, allowing for the changes made to the host system directory to persist after the container instance is killed.</p>
<figure class="align-center ">
    <img loading="lazy" src="container_mounting.png#center"
         alt="Mounting a directory allows for the changes made by the container within the directory to persist after the container is killed."/> <figcaption>
            <p>Mounting a directory allows for the changes made by the container within the directory to persist after the container is killed.</p>
        </figcaption>
</figure>

<h2 id="container-lingo">Container Lingo</h2>
<p>Containers are built from <strong>images</strong>.</p>
<p>Images are built from a series of read-only &ldquo;layers&rdquo;, and each one of these layers are instructions from the specification file(most commonly, a <strong>dockerfile</strong> for Docker containers, and a <strong>definition(def)</strong> file for Singularity.) that was used to build an image.</p>
<p>Think of images as the template from which containers are run.</p>
<p>A container is a running instance of an image, and they are isolated from the host system.</p>
<figure class="align-center ">
    <img loading="lazy" src="container_lingo.png#center"/> 
</figure>

<h1 id="docker-containers">Docker Containers</h1>
<p>Docker is an open platform that allows for separation of development tools and processes from your native infrastructure. It provides the ability to run processes in an semi-isolated environment called a <strong>container</strong>.</p>
<figure class="align-center ">
    <img loading="lazy" src="docker_logo.jpg#center"
         alt="The Docker Logo."/> <figcaption>
            <p>The Docker Logo.</p>
        </figcaption>
</figure>

<p>Generally speaking, containers are isolated from the host machine, and depending on the commands used, the changes made within the container will not persist on the host machine. This ensures that containers work uniformly across different staging and development instances. Docker (unless specifically instructed to do otherwise) is isolated from the host system&rsquo;s directories.</p>
<p>Docker is specifically intended for:</p>
<ul>
<li>Application Development.</li>
<li>Distributing and testing of an application.</li>
<li>Deploying applications in a production environment for further testing.</li>
<li>Sharing builds across teams and collaborators.</li>
</ul>
<p>An important feature to note about Docker is that it runs the container with <strong>root privileges</strong>. Allowing a user to use Docker is basically allowing them to have full root access. This is by design; running containers (and applications) with Docker implies running the Docker daemon, and the Docker daemon requires root privileges.</p>
<p>This, in conjunction with the ability to mount folders and files from the host system allows any user who is allowed to run Docker containers and commands to basically have root access!</p>
<p>Docker Containers are built from Docker images, which are compiled from dockerfiles.</p>
<h1 id="singularity">Singularity</h1>
<p>Singularity is an open source tool that was created to run complex applications specifically on HPC clusters.</p>
<figure class="align-center ">
    <img loading="lazy" src="singularity_logo.png#center"
         alt="The Singularity Logo."/> <figcaption>
            <p>The Singularity Logo.</p>
        </figcaption>
</figure>

<p>Singularity is focused on integration over isolation, and the SIF container format is lightweight and easy to transport and share.</p>
<p>Singularity containers retain the permissions of the user launching the container instance, ensuring that users are not given permissions that they did not have in the host system.</p>
<p>Singularity supports integration with Batch Scheduling and Resource Managers commonly used in HPC environments, like SLURM and PBS, and the synergy of the lightweight nature of containers with the resource efficient batch submission capabilities of SLURM provide a fantastic working relationship in the context of HPC clusters.</p>
<p>Furthermore, Singularity containers can be built to be immutable (production build). This means that the Singularity image file is <em>read-only</em>. For example, if you were to create a container that has data and performs an analysis on that data, and you wanted to share the container and the data without allowing for it to be edited in any manner, production build Singularity containers would be perfectly suited for that.</p>
<p>Although, if you did want a Singularity image to be writable, or to have a writable folder, there are options for that!</p>
<ul>
<li>The <code>--sandbox</code> option allows for building the image into a writable directory.</li>
<li>The <code>--writable</code> option allows for building into an ext3 image file, that has writable permissions. (Note that the production build Singularity containers are in the read-only squashfs format.)</li>
</ul>
<p>Note that usage of these two options will require <strong>sudo privileges</strong>, futher making it well suited for use in a HPC cluster.</p>
<p>The Singularity flow consists of two environments.</p>
<ul>
<li>A <strong>build</strong> environment (user needs to have sudo privileges) for testing purposes, allowing for the image or a directory within the image to be writable, using the above options.</li>
<li>A <strong>production</strong> environment, where the image is in the immutable, read-only squashfs format.</li>
</ul>
<p>The combination of the above factors means that Singularity is particularly well suited for use in HPC environments, allowing for:</p>
<ul>
<li><strong>Content integrity</strong>(if using the immutable squashfs format)</li>
<li><strong>Reproducible environments</strong>, which facilitate simplicity in sharing scientific workflows</li>
<li><strong>Highly portable Image formats</strong>( The SIF file format is lightweight and super easy to share across networks.)</li>
<li><strong>Ensuring preservation of user permissions within the container</strong></li>
<li><strong>Integration of the host system with the container file system</strong>, allowing for host documents to be integrated with the containers applications (depending on the permissions of the user.) While Singularity also allows for binding of host directories to the container directories (like Docker!), in addition to allowing for binding directories, by default, Singularity has static system-defined bind points, which are <code>$HOME</code>, <code>/tmp</code>, <code>/proc</code>, <code>/sys</code> and <code>/dev</code>. The permissions of the user in the container are the same as the permissions of the user in the host system, ensuring that the containers are not given unnecessary permissions and access to files they shouldn&rsquo;t have.</li>
</ul>
<h1 id="docker-vs-singularity-in-hpc-clusters">Docker vs Singularity in HPC clusters</h1>
<p>Singularity is much better suited for use in HPC clusters compared to Docker containers.</p>
<table>
  <thead>
      <tr>
          <th>Feature</th>
          <th>Docker</th>
          <th>Singularity</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Integration with batch submission and resource arbitration tools like SLURM and PBS</td>
          <td>Requires custom scripting and configuration. SLURM container implementation requires that containers must run in unprivileged (rootless) invocation, and the oci.conf file on each node must be configured to allow Docker to run in rootless mode.</td>
          <td>Specifically designed for integration with HPC clusters and use alongside SLURM. Pretty easy to integrate them.</td>
      </tr>
      <tr>
          <td>Permissions and security</td>
          <td>Docker provides the user in the container with root access, which is a big issue in HPC cluster usage.</td>
          <td>Singularity preserves permissions across the host and container, and depending on the image format, the image can be immutable as well (read-only).</td>
      </tr>
      <tr>
          <td>Isolation from host system</td>
          <td>Docker favors isolation over integration, only allowing for directories from the host system to be accessed if mounted, with complete isolation from the host system otherwise.</td>
          <td>Singularity favors integration over isolation. While directory mounting is allowed, by default, Singularity has system-defined bind paths that allow for the integration of the host directories with the container, and the preservation of permission ensures the container user cannot perform actions on these files that they cannot do in the host system.</td>
      </tr>
      <tr>
          <td>Portability</td>
          <td>Local transport of Docker images involves either the creation of cumbersome .tar files, or the uploading of the image to a registry.</td>
          <td>Images are in a lightweight, easy to transport SIF format, which greatly enhances reproducibility of scientific environments, especially when using production builds.</td>
      </tr>
  </tbody>
</table>
<h1 id="further-reading-and-references">Further Reading and References</h1>
<p>This post is only meant to cover the bare necessities of Docker containers and Singularity containers in the context of HPC computation. While I&rsquo;ve done my best to be as descriptive as possible, it is always a good idea to peruse official documentation for your own enlightenment!</p>
<p>A lot of the information here comes from these links:</p>
<p><a href="https://docs.docker.com/">Docker Documentation</a></p>
<p><a href="https://singularity-userdoc.readthedocs.io/en/latest/index.html">Singularity documentation</a></p>
<p><a href="https://slurm.schedmd.com/containers.html">SLURM documentation regarding container integration</a></p>
]]></content:encoded></item><item><title>SLURM and HPC.</title><link>https://naveenkannan.dev/posts/slurm_blog/</link><pubDate>Thu, 15 Jun 2023 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/slurm_blog/</guid><description>An introduction to SLURM in the context of HPC clusters.</description><content:encoded><![CDATA[<h1 id="slurm-workload-manager">SLURM Workload Manager</h1>
<p>SLURM (formerly known as Simple Linux Utility for Resource Management) is an open-source job scheduling system for Linux clusters.</p>
<p>It does not require kernel modification and is relatively self contained. It has three key functions:</p>
<ul>
<li>
<p>Allocation of access to resources (compute nodes) to users for a defined period of time.</p>
</li>
<li>
<p>Providing a framework that allows for starting and executing jobs, including parallel computing processes.</p>
</li>
<li>
<p>Queue management to arbitrate resource contention.</p>
</li>
</ul>
<h1 id="slurm-and-hpc-clusters">SLURM and HPC clusters</h1>
<p>SLURM is uniquely suited for use in HPC clusters, due to its ability to facilitate <em>efficient utilization</em> of the resources available for an HPC cluster.</p>
<p>SLURM places jobs/tasks in a queue, and access to resources is allowed based on the processes that are already running at the time, which is very well suited for an HPC cluster, where resources are under heavy usage.</p>
<figure class="align-center ">
    <img loading="lazy" src="slurm-diag.png#center"/> <figcaption>
            <p>SLURM architecture. Slurm has a centralized manager, slurmctld, to monitor resources and work. Each compute server (node) has a slurmd daemon, which can be compared to a remote shell: it waits for work, executes that work, returns status, and waits for more work.</p>
        </figcaption>
</figure>

<p>SLURM makes it super easy to simplify and streamline the job submission process, without having to rely on complicated Python or Bash scripting. It also allows for proper resource allocation depending on the job&rsquo;s requirements, which is essential in an HPC environment, <em>especially with multiple people using it</em>.</p>
<h1 id="slurm-directives">SLURM Directives</h1>
<p>SLURM allows for specifying commands in the SLURM scripts that allow it to arbitrate job allocation and resource management. These commands are present at the top of a SLURM script, preceeded by <code>#SBATCH</code>.</p>
<p>These <code>SBATCH</code> commands and how specific they can get are the lynchpin of SLURM&rsquo;s ability to perform resource and task arbitration. There are a <a href="https://slurm.schedmd.com/cpu_management.html">large number of <code>#SBATCH</code> commands.</a></p>
<p>Of these commands, the most relevant are:</p>
<table>
  <thead>
      <tr>
          <th>Command</th>
          <th>Utility</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>-N</strong> or <strong>--nodes</strong>=minnodes[-maxnodes]</td>
          <td>Request that a minimum of N nodes be allocated</td>
      </tr>
      <tr>
          <td><strong>-n</strong> or <strong>--ntasks</strong>=number</td>
          <td>Request sufficient resources to launch a maximum of number tasks</td>
      </tr>
      <tr>
          <td><strong>--mem-per-node</strong>=#MB or <strong>--mem-per-cpu</strong>=#MB</td>
          <td>Specify minimum memory requirement per node or per CPU</td>
      </tr>
      <tr>
          <td><strong>--ntasks-per-node</strong>=ntasks</td>
          <td>Request the maximum ntasks be invoked on each node</td>
      </tr>
      <tr>
          <td><strong>&ndash;mail-user</strong>=email_adress</td>
          <td>Send emails to the specified address for job-related events</td>
      </tr>
  </tbody>
</table>
<p>Based on these request parameters, SLURM can queue and allocate jobs to run on the available compute nodes. Each job that SLURM runs has it&rsquo;s own job ID.</p>
<p>Submitted jobs by the user can be accessed by the command <code>squeue -u &lt;USERNAME&gt;</code>. These resulting output shows the job ID associated with each job.</p>
<h1 id="slurm-job-arrays">SLURM job arrays</h1>
<p>If the same job needs to be run several times for different files, or if the same script needs to be run with several times with different parameters, SLURM job arrays allow for each individual job to be run in parallel without the need to submit an external script, or to run an iterative loop.</p>
<p>Here, the <code>#SBATCH</code> command <code>--array</code> is used along with other SLURM directives. For example, <code>#SBATCH --array=1-20</code> instructs the SLURM central manager to run the script 20 times. 20 independent tasks are created, that will run in parallel. These tasks are all assigned their own unique task ID, in addition to a common job ID (since these tasks are all submitted as part of a single job).</p>
<p>SLURM uses an environmental variable that is unique for each task in the array. This variable is called <code>$SLURM_ARRAY_TASK_ID</code>.</p>
<h1 id="slurm-scripts">SLURM Scripts</h1>
<p>The following is an example of a simple SLURM script. A folder named <code>text_files</code> was created, and that contained text files in the format chr{x}.txt, where x could be from 1 to 22, as well as X, Y and M. The purpose of this script is to read the absolute location of each text file, and print that in an output file called <code>output.txt</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#SBATCH --mail-user=XXX@gmail.com</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#SBATCH --mail-type=ALL</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#SBATCH --job-name=array_test</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#SBATCH --nodes 1</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#SBATCH --ntasks 25</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#SBATCH --mem 24g</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#SBATCH --array=1-25</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#Specifying path to array testing folder</span>
</span></span><span style="display:flex;"><span>config<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;/home/nxk562/slurm_array_testing/text_files&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#Setting the array testing folder as the working directory</span>
</span></span><span style="display:flex;"><span>cd <span style="color:#e6db74">&#34;</span>$config<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#Using the index to iterate through the text files.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#There are 25 text files.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#This command uses &#34;ls -1&#34; to obtain a list of files.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#sed -n &#34;${SLURM_ARRAY_TASK_ID}p&#34; uses the array task id as sort of a numerical index. It selects the line of the</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#ls output associated with the array ID.</span>
</span></span><span style="display:flex;"><span>text_file<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>ls | sed -n <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>SLURM_ARRAY_TASK_ID<span style="color:#e6db74">}</span><span style="color:#e6db74">p&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#Obtaining the absolute location of the text files</span>
</span></span><span style="display:flex;"><span>text_file_location<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>config<span style="color:#e6db74">}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">${</span>text_file<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;</span>$text_file_location<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#Printing output</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;</span>$text_file_location<span style="color:#e6db74">&#34;</span> &gt;&gt; /home/nxk562/slurm_array_testing/output.txt
</span></span></code></pre></div><p>Let&rsquo;s look at this code. The <code>#SBATCH</code> options here are:</p>
<ul>
<li><code>--mail-user=XXX@gmail.com</code>, which directs SLURM to send requested emails to the specified email address.</li>
<li><code>--mail-type=ALL</code>, which directs SLURM to notify user by email when certain event types occur. These can include NONE, BEGIN, END, FAIL, ARRAY_TASKS.</li>
<li><code>--job-name=array_test</code>, which provides a name for the job to go along with the job ID.</li>
<li><code>--nodes=1</code>, which directs SLURM to request a single compute node.</li>
<li><code>--ntasks=25</code>, which directs SLURM to request a node which can handle running a minimum of 25 tasks simultaneously.</li>
<li><code>--mem 24g</code>, which specifies the real memory required per node.</li>
<li><code>--array=1-25</code>, which specifies that there are 25 jobs to be executed with identical parameters.</li>
</ul>
<p>The rest of the code uses <code>$SLURM_ARRAY_TASK_ID</code> as a numerical variable to sort through the files in the <code>text_files</code> folder using the <code>ls</code> command piped to a <code>sed</code> command. <code>sed -n &quot;${SLURM_ARRAY_TASK_ID}p&quot;</code> allows for selection of the line of output from <code>ls</code> associated with the <code>$SLURM_ARRAY_TASK_ID</code> variable.</p>
<p>For example, when <code>$SLURM_ARRAY_TASK_ID</code> is equal to 1, the SLURM script executes <code>ls | sed -n &quot;1p&quot;</code> , which takes the first line of output from the <code>ls</code> command, and prints it to <code>output.txt</code>.</p>
<h1 id="slurm-output-files">SLURM Output Files</h1>
<p>By default, SLURM produces output files in the format <code>slurm-&lt;job_ID&gt;.out</code>.</p>
<p>For task arrays within a job, each task in the array will be associated with its own output file in the format <code>slurm-&lt;job_ID&gt;_&lt;task_ID&gt;.out</code>.</p>
<h1 id="submitting-viewing-and-cancelling-slurm-jobs">Submitting, viewing and cancelling SLURM jobs</h1>
<p>SLURM jobs are submitted using the <code>sbatch</code> command followed by the script.</p>
<p>SLURM jobs that have been submitted can be viewed by the command <code>squeue -u &lt;username&gt;</code>.</p>
<p>SLURM jobs can also be cancelled using the <code>scancel &lt;job_ID&gt;</code> command.</p>
<h1 id="references">References</h1>
<p>SLURM documentation can be found with great detail <a href="https://slurm.schedmd.com/overview.html">here.</a> This post is intended to be a very basic example of SLURM, and is meant to motivate further interest in SLURM and how SLURM job arrays are specifically suited to a HPC environment with multiple users.</p>
]]></content:encoded></item><item><title>p values, Statistical Significance, and the magic number.</title><link>https://naveenkannan.dev/posts/p_value_exploration/</link><pubDate>Wed, 22 Mar 2023 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/p_value_exploration/</guid><description>A brief exploration on p-values.</description><content:encoded><![CDATA[<p>Ever since I&rsquo;ve been in academia, the term <strong>(&lsquo;p &lt;0.05&rsquo;)</strong> has always been a constant.</p>
<p>From the days of medical school, where my classmates and I learned to read and understand scientific journals and articles, all the way to grad school (where I currently am), for better or for worse, p values have been an eternal constant.</p>
<p>We accept reality in the way it is presented to us. In much the same fashion, I accepted the notion of p values and the effect it had on <strong>statistical significance</strong> as a fundamental part of basic science.</p>
<p>And why wouldn&rsquo;t I?</p>
<p>P values are everywhere. In the scientific articles of most sciences related to healthcare, even tangentially, p values seem not just important, but essential to back up the validity of an observation, or of a statistical test.</p>
<h1 id="the-dichotomization-of-science">The Dichotomization of Science</h1>
<p>Why have p values become so inextricably linked with scientific investigation? Millions of articles use them in conjunction with the term <strong>statistical significance.</strong> Is it really so simple as a bright line in the sand being the only metric that separates meaningless noise from meaningful findings?</p>
<p>To be more precise,</p>
<p>Is a p value of 0.051 so different from a p value of 0.049? Is one really so much more significant than the other?</p>
<p>First, what does statistically significant even mean to begin with?</p>
<p>The term&rsquo;s original use <a href="https://www.jstor.org/stable/25163974">(all the way back in 1885!)</a> was to be a simple tool to identify if a particular  observation or inference merited further exploration. It was never meant to be a yardstick for what was scientifically important, and what wasn&rsquo;t. Are p values such infallible arbiters of something being, for example,</p>
<p><strong>statistically significant</strong> versus <strong>clinically significant?</strong></p>
<h1 id="where-do-we-go-from-here">Where do we go from here?</h1>
<p>Okay, I think you get the point. I&rsquo;m hardly the first person to bring this up.</p>
<p>But where do we go from here? How do we navigate the uncertainty that is inherent to data?</p>
<p>Do we replace p values with something else? Perhaps confidence intervals that include or exclude the null hypothesis?</p>
<p>A Bonferroni correction?</p>
<p>Much like how there is no clear line between noise and signal, there is no one-size-fits-all solution to this problem. As long as statistical tests exist, the urge to use them to explain patterns and give meaning to uncertainity will also exist.</p>
<p>We cannot just simply swap one statistical test for another.</p>
<p>To me, atleast, the way forward is what has been recommended by the ASA.</p>
<p>Honest and open communication of results and inferences, such as simply providing the p value as a continuous value, and providing point and interval estimates would go a long way in fostering a thoughtful, open environment.</p>
<p>This would be more conducive for replicablity and reproducible research, one of the cornerstones of responsible research!</p>
<p>This is a learning process, and I&rsquo;m still working on it too. But hopefully, the learning pain of moving into a world that has moved past <strong>p&lt;0.05</strong> will ease up with thoughtful research and open communication!</p>
]]></content:encoded></item><item><title>About Me</title><link>https://naveenkannan.dev/about/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/about/</guid><description>&lt;p&gt;I&amp;rsquo;m a Data Engineer by trade. In my spare time I enjoy painting miniatures, swimming, hiking and cooking.&lt;/p&gt;
&lt;p&gt;This is my personal little nerd cave. I like to share some of the cool stuff I work on via my blog. I hope you find something useful here!&lt;/p&gt;
&lt;h2 id="education"&gt;Education&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;MSc in Biomedical and Health Informatics&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Case Western Reserve University | Cleveland, Ohio&lt;/p&gt;
&lt;p&gt;January 2022 - August 2023&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MBBS (Bachelor of Medicine and Bachelor of Surgery)&lt;/strong&gt;&lt;/p&gt;</description><content:encoded><![CDATA[<p>I&rsquo;m a Data Engineer by trade. In my spare time I enjoy painting miniatures, swimming, hiking and cooking.</p>
<p>This is my personal little nerd cave. I like to share some of the cool stuff I work on via my blog. I hope you find something useful here!</p>
<h2 id="education">Education</h2>
<p><strong>MSc in Biomedical and Health Informatics</strong></p>
<p>Case Western Reserve University | Cleveland, Ohio</p>
<p>January 2022 - August 2023</p>
<p><strong>MBBS (Bachelor of Medicine and Bachelor of Surgery)</strong></p>
<p>Madras Medical College | Chennai, India</p>
<p>September 2016 - March 2020</p>
<hr>
<h2 id="experience">Experience</h2>
<p><strong>Intermediate Data Engineer</strong></p>
<p>Progressive Insurance, Mayfield Village, Ohio</p>
<p>May 2026 - Present</p>
<hr>
<p><strong>Database Engineer</strong></p>
<p>Trailhead Biosystems, Cleveland, Ohio</p>
<p>July 2024 - April 2026</p>
<hr>
<p><strong>Research Associate</strong></p>
<p>Department of Population and Quantitative Health Sciences, Case Western Reserve University</p>
<p>May 2023 - July 2024</p>
<hr>
<p><strong>Graduate Research Assistant</strong></p>
<p>Department of Population and Quantitative Health Sciences, Case Western Reserve University</p>
<p>May 2022 - May 2023</p>
<hr>
<p><strong>Junior Resident Doctor</strong></p>
<p>Department of Psychiatry, Saveetha Medical College, India</p>
<p>July 2020 - September 2020</p>
<hr>
<p><strong>Intern Doctor</strong></p>
<p>Madras Medical College, Chennai, India</p>
<p>March 2019 - March 2020</p>
<figure class="align-center "><a href="https://en.uesp.net/wiki/Lore:CHIM">
    <img loading="lazy" src="../images/avatar.png#center"
         alt="The tri-angled truth."/> </a><figcaption>
            <p>The tri-angled truth.</p>
        </figcaption>
</figure>

]]></content:encoded></item><item><title>Publications</title><link>https://naveenkannan.dev/publications/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/publications/</guid><description>&lt;hr&gt;
&lt;p&gt;Ajayakumar, J., Curtis, A. J., Maisha, F. M., Bempah, S., Ali, A., &lt;strong&gt;Kannan, N.&lt;/strong&gt;, Armstrong, G., &amp;amp; Morris, J. G. (2024). &lt;a href="https://doi.org/10.1186/s12942-024-00382-7"&gt;Using spatial video and deep learning for automated mapping of ground-level context in relief camps. International Journal of Health Geographics, 23(1), 23.&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Ruksakulpiwat, S., Thongking, W., &lt;strong&gt;Kannan, N.&lt;/strong&gt;, Wright, E., Niyomyart, A., Benjasirisan, C., Chiaranai, C., Smothers, C., Aldossary, H. M., &amp;amp; Still, C. H. (9900). &lt;a href="https://journals.lww.com/jcnjournal/fulltext/9900/understanding_the_relationship_between.224.aspx"&gt;Understanding the Relationship Between Comorbidities, Medication Nonadherence, Activities of Daily Living, and Heart Condition Status Among Older Adults in the United States: A Regression Analysis and Machine Learning Approach. Journal of Cardiovascular Nursing.&lt;/a&gt;&lt;/p&gt;</description><content:encoded><![CDATA[<hr>
<p>Ajayakumar, J., Curtis, A. J., Maisha, F. M., Bempah, S., Ali, A., <strong>Kannan, N.</strong>, Armstrong, G., &amp; Morris, J. G. (2024). <a href="https://doi.org/10.1186/s12942-024-00382-7">Using spatial video and deep learning for automated mapping of ground-level context in relief camps. International Journal of Health Geographics, 23(1), 23.</a></p>
<hr>
<p>Ruksakulpiwat, S., Thongking, W., <strong>Kannan, N.</strong>, Wright, E., Niyomyart, A., Benjasirisan, C., Chiaranai, C., Smothers, C., Aldossary, H. M., &amp; Still, C. H. (9900). <a href="https://journals.lww.com/jcnjournal/fulltext/9900/understanding_the_relationship_between.224.aspx">Understanding the Relationship Between Comorbidities, Medication Nonadherence, Activities of Daily Living, and Heart Condition Status Among Older Adults in the United States: A Regression Analysis and Machine Learning Approach. Journal of Cardiovascular Nursing.</a></p>
<hr>
<p>Annotation and scoring of the deleteriousness of individual genetic variants in the 4th release of the Alzheimer&rsquo;s Disease Sequencing Project. <strong>(PB4451)</strong></p>
<p><strong>Naveen Kannan</strong><sup>1</sup>, Nicholas Wheeler<sup>1</sup>, Genome Center for Alzheimer&rsquo;s Disease, Li-San Wang<sup>2</sup>, Yuk Yee Leung<sup>2</sup>, William S. Bush<sup>1</sup></p>
<ol>
<li>Cleveland Institute for Computational Biology, Department for Population and Quantitative Health Sciences, Case Western Reserve University, Cleveland, Ohio 44106, USA.</li>
<li>Department of Pathology and Laboratory Medicine, Penn Neurodegeneration Genomics Center, Perelman School of Medicine, University of Pennsylvania, Philadelphia, Pennsylvania 19104, USA.</li>
</ol>
<p>Presented at the Annual Meeting of The American Society of Human Genetics, November 3, 2023 in Washington DC.</p>
<hr>
<p>Segamalai, D., Abdul Jameel, A. R., <strong>Kannan, N</strong>., Anbalagan, A., Duraisamy, B., Raju, P., &amp; Devy Gounder, K. (2017). <a href="https://downloads.hindawi.com/archive/2017/5247626.pdf">Mediastinal pseudocyst: varied presentations and management&mdash;experience from a tertiary referral care centre in India.</a> <em>HPB Surgery</em>, <em>2017</em>.</p>
<hr>
<p>Vellaisamy, R., <strong>Kannan, N</strong>., Anbalagan, A., Raju, P., Duraisamy, B., Murugesan, C. S., &amp; Gounder, K. D. (2016). <a href="https://hpbonline.org/article/S1365-182X%2816%2931428-9/fulltext">Endoscopic access to hepatic duct through duodenum during follow up&ndash;after primary surgery for hepatolithiasis</a>. <em>HPB</em>, <em>18</em>, e530.</p>
<hr>
<p>Ramasamy, V., Vellaisamy, R., <strong>Kannan, N</strong>., &amp; Gounder, K. D. (2016). <a href="https://hpbonline.org/article/S1365-182X%2816%2931598-2/abstract">Refined technique of access loop in hepatobiliary surgery.</a> <em>HPB</em>, <em>18</em>, e593-e594.</p>
<hr>
<p><strong>Kannan, N</strong>., Vellaisamy, R., Govindarajan, M., &amp; Gounder, K. D. (2016). <a href="https://www.hpbonline.org/article/S1365-182X%2816%2931013-9/fulltext">Pellagra following pancreaticoduodenectomy for malignant pancreatic carcinoid with pluripotent hormonal potential</a>. <em>HPB</em>, <em>18</em>, e381-e382.</p>
]]></content:encoded></item></channel></rss>