<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Emre Baykal</title>
    <description>The latest articles on DEV Community by Emre Baykal (@emre_baykal_a4a7a479d48c5).</description>
    <link>https://dev.to/emre_baykal_a4a7a479d48c5</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3847401%2F80842041-66f0-4ae5-bdf1-1d471dd9b006.jpg</url>
      <title>DEV Community: Emre Baykal</title>
      <link>https://dev.to/emre_baykal_a4a7a479d48c5</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/emre_baykal_a4a7a479d48c5"/>
    <language>en</language>
    <item>
      <title>Building a Morpheus Plugin A Practical Walkthrough</title>
      <dc:creator>Emre Baykal</dc:creator>
      <pubDate>Wed, 29 Apr 2026 20:10:27 +0000</pubDate>
      <link>https://dev.to/emre_baykal_a4a7a479d48c5/building-a-morpheus-plugin-a-practical-walkthrough-3gbh</link>
      <guid>https://dev.to/emre_baykal_a4a7a479d48c5/building-a-morpheus-plugin-a-practical-walkthrough-3gbh</guid>
      <description>&lt;p&gt;Morpheus is a hybrid cloud management platform with a plugin SDK that lets you extend almost everything  cloud integrations, custom reports, IPAM and DNS providers, backup integrations, UI tabs, global UI hooks. The SDK is documented in pieces, but the day-to-day workflow of going from "I have an idea" to "shadow JAR is running on my appliance" isn't laid out anywhere. This post walks through that workflow.&lt;/p&gt;

&lt;p&gt;I'll use a plugin we recently built as the running example: a small enforcement utility that forces every Morpheus user to enable 2FA. If MFA isn't your problem, the example is incidental. What matters is the path.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can extend
&lt;/h2&gt;

&lt;p&gt;The SDK exposes more than fifty provider types. A provider is your hook into Morpheus; Morpheus calls back into your code at well defined moments, and you pick the type that matches what you want to do.&lt;/p&gt;

&lt;p&gt;A non exhaustive sample, mostly to give a feel for the catalog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CloudProvider&lt;/code&gt;, &lt;code&gt;ProvisionProvider&lt;/code&gt; — add a new cloud integration&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BackupProvider&lt;/code&gt; — register a backup integration&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ReportProvider&lt;/code&gt; — add a custom report type&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AppTabProvider&lt;/code&gt;, &lt;code&gt;ServerTabProvider&lt;/code&gt;, &lt;code&gt;ClusterTabProvider&lt;/code&gt;, &lt;code&gt;InstanceTabProvider&lt;/code&gt;, &lt;code&gt;NetworkTabProvider&lt;/code&gt; — add a tab to a detail page&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GlobalUIComponentProvider&lt;/code&gt; — inject HTML into every authenticated page render&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TaskProvider&lt;/code&gt; — register a task type for the automation engine&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CypherProvider&lt;/code&gt; — extend the secure-vault subsystem&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GuidanceRecommendationProvider&lt;/code&gt;, &lt;code&gt;AnalyticsProvider&lt;/code&gt; — feed dashboard widgets&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GenericProvider&lt;/code&gt; — generic integration for things that don't fit elsewhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full list lives at &lt;code&gt;morpheus-plugin-core/morpheus-plugin-api/src/main/java/com/morpheusdata/core/providers/&lt;/code&gt;. Picking the right one is the most important early decision; everything else flows from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up your reference material first
&lt;/h2&gt;

&lt;p&gt;Before writing any code, clone these three repositories locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="nt"&gt;--branch&lt;/span&gt; v1.2.x &lt;span class="se"&gt;\&lt;/span&gt;
    https://github.com/HewlettPackard/morpheus-plugin-core.git
git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
    https://github.com/HewlettPackard/morpheus-docs.git
git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
    https://github.com/HewlettPackard/morpheus-openapi.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;morpheus-plugin-core&lt;/code&gt; is the source of the &lt;code&gt;com.morpheusdata:morpheus-plugin-api&lt;/code&gt; library you'll compile against. Provider interfaces, abstract classes, model classes  everything is here. When you want to know what a method returns or which fields a model exposes, grep this.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;morpheus-docs&lt;/code&gt; is the source of the official Morpheus product documentation, currently published as &lt;a href="https://support.hpe.com/hpesc/public/docDisplay?docId=sd00007621en_us" rel="noopener noreferrer"&gt;HPE Morpheus VM Essentials&lt;/a&gt; on the HPE support portal. When you need to understand a feature from the user's perspective  what does Morpheus call this concept, how does the UI organize it  this is the reference.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;morpheus-openapi&lt;/code&gt; is the OpenAPI 3.1 spec for Morpheus's REST API: 600+ endpoint files, 650+ schemas. If your plugin needs to call Morpheus over HTTP, or you need the canonical shape of a data model, this is faster than the live docs site.&lt;/p&gt;

&lt;p&gt;Grep is fast; docs search is slow. A local clone pays off within the first hour.&lt;/p&gt;

&lt;p&gt;A few web resources are worth bookmarking alongside the local clones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.morpheusdata.com" rel="noopener noreferrer"&gt;developer.morpheusdata.com&lt;/a&gt; — Morpheus developer portal landing page; starter guides and links into the rest of the developer materials.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.morpheusdata.com/docs" rel="noopener noreferrer"&gt;developer.morpheusdata.com/docs&lt;/a&gt; — plugin development handbook, organized by provider type and concept (Reports, Cloud, Tasks, UI Extensions, etc.).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.morpheusdata.com/api/index.html?overview-summary.html" rel="noopener noreferrer"&gt;developer.morpheusdata.com/api&lt;/a&gt; — plugin API Javadoc, useful when you want a browsable class hierarchy view instead of grepping the source.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use them when the local clones don't have what you need; the local clones answer most questions faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick the right provider type
&lt;/h2&gt;

&lt;p&gt;The methodology is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write down what you want Morpheus to do.&lt;/li&gt;
&lt;li&gt;Translate that into a question: when does Morpheus need to call my code?&lt;/li&gt;
&lt;li&gt;Find the provider whose method signatures answer that question.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the MFA enforcer, the chain went:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Force users without 2FA to enable it before they can use Morpheus."&lt;/p&gt;

&lt;p&gt;"We need to be invoked on every page render, with the current user, so we can decide whether to render a blocking overlay."&lt;/p&gt;

&lt;p&gt;Match: &lt;code&gt;GlobalUIComponentProvider&lt;/code&gt;, which defines &lt;code&gt;Boolean show(User, Account)&lt;/code&gt; and &lt;code&gt;HTMLResponse renderTemplate(User, Account)&lt;/code&gt;. Morpheus invokes both on every authenticated page render.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If we'd wanted a custom report, we'd have picked &lt;code&gt;ReportProvider&lt;/code&gt;. If we'd wanted a tab on the instance detail page, &lt;code&gt;InstanceTabProvider&lt;/code&gt;. The question-to-provider mapping becomes mechanical once you know the catalog.&lt;/p&gt;

&lt;p&gt;One thing worth knowing up front: not every hook you might want exists. There is no &lt;code&gt;LoginProvider&lt;/code&gt; and no &lt;code&gt;AuthenticationProvider&lt;/code&gt; — you can't intercept the login flow itself. For the MFA enforcer that meant settling for &lt;code&gt;GlobalUIComponentProvider&lt;/code&gt;, which runs &lt;em&gt;after&lt;/em&gt; login. The SDK won't always give you exactly what you want; pick the closest match and design around its constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build environment
&lt;/h2&gt;

&lt;p&gt;The plugin API targets Java 8, but Gradle 7.x needs JDK 11 to build. The shadow plugin used by older Morpheus samples (&lt;code&gt;com.bmuschko.gradle-plugin-shadow 2.0.3&lt;/code&gt;) is Gradle 6.x only — use the maintained &lt;code&gt;com.github.johnrengelman.shadow 7.1.2&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;On Linux:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="s2"&gt;"https://get.sdkman.io"&lt;/span&gt; | bash
&lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.sdkman/bin/sdkman-init.sh"&lt;/span&gt;

sdk &lt;span class="nb"&gt;install &lt;/span&gt;java 11.0.22-tem
sdk &lt;span class="nb"&gt;install &lt;/span&gt;gradle 7.6.4

&lt;span class="nb"&gt;cd &lt;/span&gt;my-plugin
gradle wrapper &lt;span class="nt"&gt;--gradle-version&lt;/span&gt; 7.6.4 &lt;span class="nt"&gt;--distribution-type&lt;/span&gt; bin
./gradlew shadowJar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A minimal &lt;code&gt;build.gradle&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="s1"&gt;'java'&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="s1"&gt;'groovy'&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="s1"&gt;'com.github.johnrengelman.shadow'&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="s1"&gt;'7.1.2'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;group&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'com.morpheusdata'&lt;/span&gt;
&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'1.0.0'&lt;/span&gt;

&lt;span class="n"&gt;sourceCompatibility&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'11'&lt;/span&gt;
&lt;span class="n"&gt;targetCompatibility&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'11'&lt;/span&gt;

&lt;span class="n"&gt;repositories&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;mavenCentral&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;compileOnly&lt;/span&gt; &lt;span class="s1"&gt;'com.morpheusdata:morpheus-plugin-api:0.15.4'&lt;/span&gt;
    &lt;span class="n"&gt;compileOnly&lt;/span&gt; &lt;span class="s1"&gt;'org.codehaus.groovy:groovy-all:3.0.11'&lt;/span&gt;
    &lt;span class="n"&gt;compileOnly&lt;/span&gt; &lt;span class="s1"&gt;'io.reactivex.rxjava3:rxjava:3.1.5'&lt;/span&gt;
    &lt;span class="n"&gt;compileOnly&lt;/span&gt; &lt;span class="s1"&gt;'org.slf4j:slf4j-api:1.7.26'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;shadowJar&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;manifest&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'Plugin-Class'&lt;/span&gt;  &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'com.example.myplugin.MyPlugin'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'Plugin-Version'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;archiveVersion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;compileOnly&lt;/code&gt; is not optional. Morpheus's runtime already provides these libraries; bundling them into your shadow JAR causes classpath conflicts that surface as &lt;code&gt;NoSuchMethodError&lt;/code&gt; at load time.&lt;/p&gt;

&lt;p&gt;Bumping the &lt;code&gt;version&lt;/code&gt; field on every revision is worth doing — the version becomes part of the JAR filename, so you can confirm which build is loaded by glancing at the plugin list in the Morpheus UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anatomy of a plugin
&lt;/h2&gt;

&lt;p&gt;Three pieces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Plugin class&lt;/strong&gt; is your entry point, registered in the JAR manifest as &lt;code&gt;Plugin-Class&lt;/code&gt;. It registers your providers in &lt;code&gt;initialize()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MfaEnforcerPlugin&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Plugin&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt; &lt;span class="n"&gt;getCode&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'morpheus-mfa-enforcer-plugin'&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;initialize&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;setName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"MFA Enforcer"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;MfaEnforcerProvider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;morpheus&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;pluginProviders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Provider class&lt;/strong&gt; is where the work lives. Always extend the matching &lt;code&gt;Abstract*Provider&lt;/code&gt; instead of implementing the interface directly. The abstract classes set up the renderer with the correct classpath prefix, register handlebars helpers (asset, nonce, i18n), and wire up things you don't want to wire up yourself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Slf4j&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MfaEnforcerProvider&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;AbstractGlobalUIComponentProvider&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Plugin&lt;/span&gt; &lt;span class="n"&gt;plugin&lt;/span&gt;
    &lt;span class="n"&gt;MorpheusContext&lt;/span&gt; &lt;span class="n"&gt;morpheusContext&lt;/span&gt;

    &lt;span class="nf"&gt;MfaEnforcerProvider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Plugin&lt;/span&gt; &lt;span class="n"&gt;plugin&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MorpheusContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;plugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plugin&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;morpheusContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt; &lt;span class="n"&gt;Boolean&lt;/span&gt; &lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isMasterTenantAdmin&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isUsing2FA&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt; &lt;span class="n"&gt;HTMLResponse&lt;/span&gt; &lt;span class="n"&gt;renderTemplate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Account&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ViewModel&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;()&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nl"&gt;userSettingsUrl:&lt;/span&gt; &lt;span class="s1"&gt;'/user-settings'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;getRenderer&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;renderTemplate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"hbs/mfaEnforcer"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt; &lt;span class="n"&gt;MorpheusContext&lt;/span&gt; &lt;span class="n"&gt;getMorpheus&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;morpheusContext&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt; &lt;span class="n"&gt;Plugin&lt;/span&gt; &lt;span class="n"&gt;getPlugin&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;plugin&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;getMorpheus()&lt;/code&gt; and &lt;code&gt;getPlugin()&lt;/code&gt; overrides are required by &lt;code&gt;PluginProvider&lt;/code&gt; and need to be explicit because Groovy's auto-generated getters use the field name (&lt;code&gt;getMorpheusContext&lt;/code&gt;, not &lt;code&gt;getMorpheus&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt; live under &lt;code&gt;src/main/resources/&lt;/code&gt;. Handlebars templates go in &lt;code&gt;renderer/hbs/&lt;/code&gt;. The renderer's classpath prefix is &lt;code&gt;renderer&lt;/code&gt;, so &lt;code&gt;getRenderer().renderTemplate("hbs/mfaEnforcer", model)&lt;/code&gt; resolves to &lt;code&gt;src/main/resources/renderer/hbs/mfaEnforcer.hbs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For inline &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags, use the &lt;code&gt;{{nonce}}&lt;/code&gt; helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"mfa-shield"&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"dialog"&lt;/span&gt; &lt;span class="na"&gt;aria-modal=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;style &lt;/span&gt;&lt;span class="na"&gt;nonce=&lt;/span&gt;&lt;span class="s"&gt;"{{nonce}}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nf"&gt;#mfa-shield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2147483647&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c"&gt;/* ... */&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Two-Factor Authentication Required&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cta"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{{userSettingsUrl}}"&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"_top"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            Go to User Settings → Enable 2FA
        &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the nonce, Morpheus's Content-Security-Policy header blocks the styles and the script silently. There's no console error, no log entry, no visible failure mode — the page just renders without your component. Easy hours to lose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The example, end-to-end
&lt;/h2&gt;

&lt;p&gt;A quick note on why this plugin exists, for the curious. Morpheus supports two-factor authentication out of the box, but only as an opt-in toggle on each user's profile  there is no appliance wide setting to require it. The product documentation says so explicitly: &lt;em&gt;"There is not currently a way for an administrator to enforce the use of two-factor authentication appliance wide."&lt;/em&gt; As an MSP we couldn't rely on every sub-tenant user enabling 2FA on their own, so we wrote a small plugin to close the gap.&lt;/p&gt;

&lt;p&gt;MFA enforcer's full source is at &lt;a href="https://github.com/emrbaykal/morpheus-plugin-test/tree/main/morpheus-mfa-enforcer" rel="noopener noreferrer"&gt;emrbaykal/morpheus-plugin-test/morpheus-mfa-enforcer&lt;/a&gt;. It's roughly 200 lines split across four files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;morpheus-mfa-enforcer/
├── build.gradle                                          # project config
└── src/main/
    ├── groovy/com/morpheusdata/mfaenforcer/
    │   ├── MfaEnforcerPlugin.groovy                      # entry point, registers provider
    │   └── MfaEnforcerProvider.groovy                    # show() + renderTemplate() + state lookup
    └── resources/renderer/hbs/
        └── mfaEnforcer.hbs                               # overlay HTML + CSS + small JS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The decision logic in &lt;code&gt;show()&lt;/code&gt; reads the user's 2FA flag from the Morpheus database through &lt;code&gt;MorpheusReportService.getReadOnlyDatabaseConnection()&lt;/code&gt;. The result is cached in a static &lt;code&gt;ConcurrentHashMap&lt;/code&gt; with a short TTL — &lt;code&gt;show()&lt;/code&gt; is hot, called on every authenticated page render, and uncached SELECTs would generate noticeable load and contention against Morpheus's own writes.&lt;/p&gt;

&lt;p&gt;The visible result, in five steps:&lt;/p&gt;

&lt;p&gt;A user logs in:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe1uas1awjy3lg9wr1heo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe1uas1awjy3lg9wr1heo.png" alt="Login screen" width="794" height="461"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The dashboard loads, but the overlay covers it immediately:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvj8xj9estgba3nt6f3ag.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvj8xj9estgba3nt6f3ag.png" alt="Dashboard with MFA overlay" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The CTA hard-navigates to &lt;code&gt;/user-settings&lt;/code&gt;, where the standard Morpheus 2FA enable flow runs (password confirmation, QR code, authenticator app):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6s3fb2zc59djw6buicq4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6s3fb2zc59djw6buicq4.png" alt="Enable 2FA — confirm password" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After enabling, the user-settings page reflects the new state:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fraljrk9wa4hup3aur89a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fraljrk9wa4hup3aur89a.png" alt="User settings showing Disable 2FA / Get 2FA Code" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On the next login, Morpheus prompts for the 2FA code from the authenticator:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd3ztfzhlxsnvle9o1ems.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd3ztfzhlxsnvle9o1ems.png" alt="2FA code prompt at login" width="794" height="461"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the dashboard loads cleanly  the overlay is gone, because &lt;code&gt;show()&lt;/code&gt; now reads &lt;code&gt;is_using2fa = true&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftb2yax3flt7acaxu5iht.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftb2yax3flt7acaxu5iht.png" alt="Clean dashboard, no overlay" width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Build, package, deploy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew shadowJar
&lt;span class="c"&gt;# build/libs/morpheus-mfa-enforcer-1.0.10-all.jar&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in the Morpheus UI: &lt;strong&gt;Administration → Integrations → Plugins → Choose File&lt;/strong&gt;. Upload the shadow JAR. The plugin loads on every appliance node, no daemon restart required.&lt;/p&gt;

&lt;p&gt;The plugin list in that section shows the version from the JAR filename. Combined with version-bump-on-every-revision, that's all the deployment tracking you need for a small plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few practical notes
&lt;/h2&gt;

&lt;p&gt;A handful of things that aren't obvious from reading the SDK:&lt;/p&gt;

&lt;p&gt;Always extend the matching &lt;code&gt;Abstract*Provider&lt;/code&gt;. The abstract classes do non-trivial work — registering handlebars helpers, configuring the renderer's classpath prefix, wiring up locale support. Implementing the bare interface is reinventing this work, and the omissions tend to manifest as silent failures (template not found, CSS blocked by CSP) that are time-consuming to diagnose.&lt;/p&gt;

&lt;p&gt;Be careful with hot-path DB access. Any provider invoked per page render needs caching. Without it you add latency to every authenticated request and, in the worst case, contend with Morpheus's own writes via InnoDB share locks. A small &lt;code&gt;ConcurrentHashMap&lt;/code&gt; with a short TTL plus an explicit &lt;code&gt;sql.commit()&lt;/code&gt; after each SELECT keeps the plugin a good citizen.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/api/*&lt;/code&gt; endpoints require an OAuth2 bearer token. Browser session cookies aren't accepted. If your plugin tries to fetch them from inline JavaScript in a template, you'll get 401. For state lookups, do them server-side via the database or via plugin-side service calls.&lt;/p&gt;

&lt;p&gt;When something doesn't work, check the Morpheus daemon log first. Browser console second. Plugin failures often produce no console output at all — the symptoms are silent CSP blocks, silent SPA click intercepts, silent fail-open paths. The daemon log is more honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;The plugin SDK is more capable than the documentation makes obvious. Once you have the three reference repositories cloned, the build environment set up, and the right &lt;code&gt;Abstract*Provider&lt;/code&gt; class identified, the actual work moves quickly.&lt;/p&gt;

&lt;p&gt;The same pattern applies to password complexity rules, IP-based access restrictions, role-aware session timeouts, and other guardrails the platform doesn't ship with: pick the provider that matches your hook, extend the abstract class, ship the shadow JAR.&lt;/p&gt;

&lt;p&gt;If you've built something interesting with the SDK, drop a link in the comments.&lt;/p&gt;

</description>
      <category>morpheus</category>
      <category>plugin</category>
      <category>groovy</category>
      <category>devops</category>
    </item>
    <item>
      <title>Setting Up Disaster Recovery for HPE Morpheus Enterprise with MySQL InnoDB ClusterSet Step by Step</title>
      <dc:creator>Emre Baykal</dc:creator>
      <pubDate>Mon, 20 Apr 2026 18:34:42 +0000</pubDate>
      <link>https://dev.to/emre_baykal_a4a7a479d48c5/morpheus-enterprise-disaster-recovery-with-mysql-innodb-clusterset-a-step-by-step-guide-11ni</link>
      <guid>https://dev.to/emre_baykal_a4a7a479d48c5/morpheus-enterprise-disaster-recovery-with-mysql-innodb-clusterset-a-step-by-step-guide-11ni</guid>
      <description>&lt;h1&gt;
  
  
  HPE Morpheus Enterprise Disaster Recovery with MySQL InnoDB ClusterSet: A Production Grade Guide
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;In a previous build out we deployed two separate MySQL InnoDB Clusters, one on the Master site and one on the DR site, as the backing database for our &lt;strong&gt;Morpheus Enterprise&lt;/strong&gt; platform. This guide is the disaster recovery follow up: we merge those two clusters into a single &lt;strong&gt;InnoDB ClusterSet&lt;/strong&gt; so Morpheus Enterprise keeps serving traffic even if an entire site is lost. It covers TLS encrypted async replication between the sites, re bootstrapping MySQL Router so the Morpheus app layer doesn't need any connection string changes, and both planned switchovers and emergency failovers, including the nasty errant GTID cleanup that most guides skip. Every step is executed &lt;strong&gt;without interrupting live Morpheus traffic&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Introduction: Why InnoDB ClusterSet?
&lt;/h2&gt;

&lt;p&gt;MySQL InnoDB Cluster gives you solid &lt;strong&gt;high availability&lt;/strong&gt; inside a single data center. Group Replication handles node failures transparently. What it doesn't give you is protection against a site level disaster: if the DC goes dark, a fiber gets cut, or a regional outage hits, your cluster vanishes with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;InnoDB ClusterSet&lt;/strong&gt; fills that gap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Primary Cluster&lt;/strong&gt; serves write traffic (Master Site).&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Replica Cluster&lt;/strong&gt; is fed via async replication (DR Site).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL Router&lt;/strong&gt; picks up topology changes automatically, so there are no application side connection string changes after failover.&lt;/li&gt;
&lt;li&gt;Planned switchovers give you &lt;strong&gt;zero data loss&lt;/strong&gt;; emergency failovers keep RPO in the single digit seconds range.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article walks through the exact steps I run in production, with the error messages you will hit, the security posture you should aim for, and the operational tips most tutorials skip. It is specifically written to layer ClusterSet on top of two existing InnoDB Clusters rather than a greenfield install.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One caveat worth stating up front: ClusterSet is not a replacement for backups. Logical corruption (&lt;code&gt;DROP TABLE&lt;/code&gt;, a bad migration, an application bug) replicates to DR just fine. You still need MySQL Enterprise Backup or Percona XtraBackup on an independent schedule.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Starting Point and Target Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Current State
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Master Site&lt;/th&gt;
&lt;th&gt;DR Site&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;InnoDB Cluster&lt;/td&gt;
&lt;td&gt;Installed and running&lt;/td&gt;
&lt;td&gt;Installed (will be dropped)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL Router&lt;/td&gt;
&lt;td&gt;Registered and active&lt;/td&gt;
&lt;td&gt;Registered (will be re bootstrapped)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Preserved (source of truth)&lt;/td&gt;
&lt;td&gt;Doesn't matter (will be overwritten by clone)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Requirements and Pre-Flight Checks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Software Versions
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MySQL Server&lt;/td&gt;
&lt;td&gt;8.0.27+ (8.4 LTS recommended)&lt;/td&gt;
&lt;td&gt;Identical version across every node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL Shell&lt;/td&gt;
&lt;td&gt;Same major as server&lt;/td&gt;
&lt;td&gt;Required on every node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL Router&lt;/td&gt;
&lt;td&gt;8.0.x (8.2+ for R/W splitting)&lt;/td&gt;
&lt;td&gt;At least one instance per site&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Firewall Port List
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cross-site (Master site ↔ DR site, both directions):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3306&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;MySQL Classic&lt;/td&gt;
&lt;td&gt;ClusterSet async replication channel (&lt;code&gt;clusterset_replication&lt;/code&gt;) + AdminAPI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;ClusterSet uses standard async replication on port &lt;strong&gt;3306&lt;/strong&gt; between the primary and replica clusters. Group Replication's internal port (33061) is intra cluster only and does not carry cross site traffic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Intra cluster (within the same site, between GR members):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3306&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;MySQL Classic&lt;/td&gt;
&lt;td&gt;Client + recovery donor connections&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;33061&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;Group Replication&lt;/td&gt;
&lt;td&gt;Inter-node GR &lt;code&gt;local_address&lt;/code&gt; (configurable via &lt;code&gt;group_replication_local_address&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;33062&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;XCom (recommended free)&lt;/td&gt;
&lt;td&gt;Used when &lt;code&gt;communicationStack: 'XCOM'&lt;/code&gt;. On the default &lt;code&gt;MYSQL&lt;/code&gt; stack it isn't required, but reserving it avoids surprises if you ever switch stacks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Operator workstation → any MySQL node:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3306&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mysqlsh&lt;/code&gt; classic connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;33060&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mysqlsh&lt;/code&gt; X Protocol (default for newer Shells)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;A common mistake: port 33060 is not required between MySQL nodes (cross site or intra cluster). It is only needed from the operator/DBA workstation. Leaving it open cross site is unnecessary attack surface.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Application → MySQL Router:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6446&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;Classic R/W, routes to primary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6447&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;Classic R/O, load-balanced across secondaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6448 / 6449&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;X Protocol R/W and R/O (optional)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6450&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;R/W splitting endpoint (MySQL Router 8.2+, optional)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Verify connectivity before you start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Cross-site: only 3306 is required&lt;/span&gt;
nc &lt;span class="nt"&gt;-zv&lt;/span&gt; &amp;lt;remote_site_node_ip&amp;gt; 3306

&lt;span class="c"&gt;# Intra-cluster (within same site): 3306 and 33061&lt;/span&gt;
nc &lt;span class="nt"&gt;-zv&lt;/span&gt; &amp;lt;local_peer_ip&amp;gt; 3306
nc &lt;span class="nt"&gt;-zv&lt;/span&gt; &amp;lt;local_peer_ip&amp;gt; 33061
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Server-Level Consistency Checks (often forgotten)
&lt;/h3&gt;

&lt;p&gt;Any of the following drifting between Master and DR will eventually break replication or corrupt query semantics. Run on one node per cluster and diff the output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Character set and collation&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;VARIABLES&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'character_set_server'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;VARIABLES&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'collation_server'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Timezone (critical for TIMESTAMP columns)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="k"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time_zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="k"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system_time_zone&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- SQL mode&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="k"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sql_mode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Lower case table names (cannot be changed after initialization)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="k"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower_case_table_names&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Clone plugin availability (required for recoveryMethod: 'clone')&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;PLUGINS&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'clone'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Expected: Status = ACTIVE. If missing:&lt;/span&gt;
&lt;span class="c1"&gt;-- INSTALL PLUGIN clone SONAME 'mysql_clone.so';&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Watch out for &lt;code&gt;lower_case_table_names&lt;/code&gt;. If Master and DR were initialized with different values, replication will break on any mixed case table name. The value is set at &lt;code&gt;mysqld --initialize&lt;/code&gt; time and cannot be changed afterwards, so the only fix is a full re init of the divergent side.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 1: Configure &lt;code&gt;server_id&lt;/code&gt; and &lt;code&gt;report_host&lt;/code&gt; on the Existing Cluster
&lt;/h2&gt;

&lt;p&gt;ClusterSet requires every MySQL node to have a unique &lt;code&gt;server_id&lt;/code&gt; (across &lt;em&gt;both&lt;/em&gt; sites) and a &lt;code&gt;report_host&lt;/code&gt; that advertises its real address. These parameters don't take effect until MySQL restarts, so on a live cluster you perform a rolling restart. Never shut all nodes down at once.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Critical: restart secondaries first and the primary last. Otherwise you risk losing quorum.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  1.1 Inspect Current Values
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; clusterAdmin &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SELECT @@server_id, @@report_host, @@report_port, @@hostname;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Target values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Node&lt;/th&gt;
&lt;th&gt;server_id&lt;/th&gt;
&lt;th&gt;report_host&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;master_node1 (Primary)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;master_node1 IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;master_node2 (Secondary)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;master_node2 IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;master_node3 (Secondary)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;master_node3 IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dr_node1&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;dr_node1 IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dr_node2&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;dr_node2 IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dr_node3&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;dr_node3 IP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Why this matters: &lt;code&gt;server_id&lt;/code&gt; must be unique across the entire ClusterSet, not just per cluster. Two nodes sharing an id will drop replication events or create a silent data divergence.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  1.2 Edit &lt;code&gt;my.cnf&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[mysqld]&lt;/span&gt;
&lt;span class="c"&gt;# ── Required for ClusterSet ──────────────────────────────────────
&lt;/span&gt;&lt;span class="py"&gt;server_id&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1             # Must be UNIQUE across BOTH sites&lt;/span&gt;
&lt;span class="py"&gt;report_host&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.10.1.11    # Real IP of this node&lt;/span&gt;
&lt;span class="py"&gt;report_port&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3306&lt;/span&gt;

&lt;span class="c"&gt;# ── Should already be present on an InnoDB Cluster ───────────────
&lt;/span&gt;&lt;span class="py"&gt;gtid_mode&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ON&lt;/span&gt;
&lt;span class="py"&gt;enforce_gtid_consistency&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ON&lt;/span&gt;
&lt;span class="py"&gt;binlog_format&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ROW&lt;/span&gt;
&lt;span class="py"&gt;log_replica_updates&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ON            # Renamed from log_slave_updates in 8.0.26&lt;/span&gt;
&lt;span class="py"&gt;plugin_load_add&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;group_replication.so&lt;/span&gt;

&lt;span class="c"&gt;# ── Auto-rejoin after restart (optional but recommended) ─────────
&lt;/span&gt;&lt;span class="py"&gt;loose-group_replication_start_on_boot&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ON&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;A note on hostnames: use IP addresses for &lt;code&gt;report_host&lt;/code&gt;. Hostname based setups frequently hit &lt;code&gt;Server address configuration error&lt;/code&gt; when DNS is inconsistent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  1.3 Rolling Restart
&lt;/h3&gt;

&lt;p&gt;The sub steps below follow the safe order: identify primary, restart each secondary in turn, restart primary last, move the primary role back to &lt;code&gt;master_node1&lt;/code&gt;, then verify Router.&lt;/p&gt;

&lt;h4&gt;
  
  
  1.3.1 Identify the Primary
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;mysqlsh&lt;/span&gt; &lt;span class="nx"&gt;clusterAdmin&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;master_node1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3306&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;cluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dba&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCluster&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The node with &lt;code&gt;memberRole: "PRIMARY"&lt;/code&gt; is restarted last.&lt;/p&gt;

&lt;h4&gt;
  
  
  1.3.2 Restart Secondaries One at a Time
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# A) (Optional) Gracefully remove from cluster&lt;/span&gt;
mysqlsh clusterAdmin@&amp;lt;secondary_node&amp;gt;:3306
&lt;span class="c"&gt;# &amp;gt;&amp;gt; dba.getCluster().removeInstance('&amp;lt;secondary_node&amp;gt;:3306', {force: false})&lt;/span&gt;

&lt;span class="c"&gt;# B) Restart MySQL&lt;/span&gt;
systemctl restart mysqld

&lt;span class="c"&gt;# C) Verify new parameters&lt;/span&gt;
mysql &lt;span class="nt"&gt;-u&lt;/span&gt; clusterAdmin &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SELECT @@server_id, @@report_host;"&lt;/span&gt;

&lt;span class="c"&gt;# D) Re-add&lt;/span&gt;
mysqlsh clusterAdmin@master_node1:3306
&lt;span class="c"&gt;# &amp;gt;&amp;gt; dba.getCluster().addInstance('&amp;lt;secondary_node&amp;gt;:3306', {recoveryMethod: 'incremental'})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Shortcut: with &lt;code&gt;loose group_replication_start_on_boot = ON&lt;/code&gt;, a plain &lt;code&gt;systemctl restart mysqld&lt;/code&gt; is enough. The node rejoins on its own within 30 to 60 seconds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  1.3.3 Restart the Primary Last
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Confirm every secondary is ONLINE first&lt;/span&gt;
systemctl restart mysqld
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;mysqld&lt;/code&gt; exits, Group Replication performs a view change and promotes a healthy secondary (5–15 seconds).&lt;/p&gt;

&lt;h4&gt;
  
  
  1.3.4 Move the Primary Role Back
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;mysqlsh&lt;/span&gt; &lt;span class="nx"&gt;clusterAdmin&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;any_node&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3306&lt;/span&gt;
&lt;span class="nx"&gt;dba&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCluster&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;setPrimaryInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;master_node1:3306&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a coordinated primary handover, not an election. The current primary drains in flight transactions, sets the target node writable, and transfers the role. Router picks up the change via its metadata TTL (5 to 15 seconds in practice).&lt;/p&gt;

&lt;h4&gt;
  
  
  1.3.5 Verify Router Routing
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-h&lt;/span&gt; &amp;lt;router_ip&amp;gt; &lt;span class="nt"&gt;-P&lt;/span&gt; 6446 &lt;span class="nt"&gt;-u&lt;/span&gt; clusterAdmin &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SELECT @@hostname, @@report_host;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output should show &lt;code&gt;master_node1&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.4 Full Verification
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Expected&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MEMBER_STATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ONLINE&lt;/code&gt; for every node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@@server_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unique (1–6)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@@report_host&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Real IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cluster.status()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;statusText: "Cluster is ONLINE and can tolerate up to ONE failure"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step 2: Verify Server Certificates Are Present (Pre-Flight for TLS)
&lt;/h2&gt;

&lt;p&gt;Cross-site async replication without TLS is a serious exposure in production. Before you create the ClusterSet, confirm that every node already has working server certificates so the replication channel can negotiate TLS as soon as it's created in §4.1. The actual enforcement option (&lt;code&gt;clusterSetReplicationSslMode&lt;/code&gt;) is set at ClusterSet creation time, not here.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1 Confirm Certificates Exist
&lt;/h3&gt;

&lt;p&gt;MySQL auto-generates self signed certs under &lt;code&gt;datadir&lt;/code&gt; on first init. For production, replace them with certs issued by your internal CA. Verify they exist on every node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;VARIABLES&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'ssl_%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- ssl_ca, ssl_cert, ssl_key should all point to valid files&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test TLS is actually offered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; clusterAdmin &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; master_node1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ssl-mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;REQUIRED &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SHOW STATUS LIKE 'Ssl_cipher';"&lt;/span&gt;
&lt;span class="c"&gt;# Ssl_cipher should be non-empty (e.g., TLS_AES_256_GCM_SHA384)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2.2 How TLS Is Enforced (Preview of §4.1)
&lt;/h3&gt;

&lt;p&gt;TLS on the ClusterSet replication channel is controlled by the &lt;code&gt;clusterSetReplicationSslMode&lt;/code&gt; option, set at ClusterSet creation time via &lt;code&gt;cluster.createClusterSet()&lt;/code&gt; (see §4.1). Possible values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;AUTO&lt;/code&gt; (default)&lt;/td&gt;
&lt;td&gt;Enables encryption when the server supports it, disables otherwise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;REQUIRED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enables encryption for all ClusterSet replication channels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DISABLED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Disables encryption. Avoid in production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;VERIFY_CA&lt;/code&gt; (Shell 8.0.33+)&lt;/td&gt;
&lt;td&gt;Like &lt;code&gt;REQUIRED&lt;/code&gt;, plus verifies the server certificate against the CA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;VERIFY_IDENTITY&lt;/code&gt; (Shell 8.0.33+)&lt;/td&gt;
&lt;td&gt;Like &lt;code&gt;VERIFY_CA&lt;/code&gt;, plus verifies the certificate identity matches the server hostname&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The internal replication user (typically &lt;code&gt;mysql_innodb_cs_&amp;lt;hex&amp;gt;&lt;/code&gt;) is created by AdminAPI and the channel is configured to match this option. Optionally you can also restrict the account server-side post-creation as defense-in-depth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Defense in depth; the channel is already configured per clusterSetReplicationSslMode&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;Host&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;user&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;User&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'mysql_innodb_cs_%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'mysql_innodb_cs_xxxx'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;scoped_host&amp;gt;'&lt;/span&gt; &lt;span class="n"&gt;REQUIRE&lt;/span&gt; &lt;span class="n"&gt;SSL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;TLS version note: MySQL 8.0 negotiates TLSv1.2 and TLSv1.3 by default. Pinning to &lt;code&gt;tls_version = TLSv1.3&lt;/code&gt; is good hardening only when every client supports it; keeping &lt;code&gt;TLSv1.2,TLSv1.3&lt;/code&gt; is the safer default.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 3: Remove the Existing DR Cluster
&lt;/h2&gt;

&lt;p&gt;The legacy InnoDB Cluster on DR cannot be added to a ClusterSet as a replica. It has to be fully dismantled first.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Data loss warning: this wipes DR. Clone re-seeds from Master afterwards. Confirm nothing on DR needs to be preserved before continuing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3.1 Method A: &lt;code&gt;dissolve()&lt;/code&gt; (Recommended)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;mysqlsh&lt;/span&gt; &lt;span class="nx"&gt;clusterAdmin&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;dr_node1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3306&lt;/span&gt;

&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;oldCluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dba&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCluster&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;oldCluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;oldCluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dissolve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;force&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;dissolve()&lt;/code&gt; connects to every node, stops Group Replication, and drops the &lt;code&gt;mysql_innodb_cluster_metadata&lt;/code&gt; schema. &lt;code&gt;force: true&lt;/code&gt; lets it proceed even with OFFLINE nodes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify on every DR node:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="n"&gt;group_replication_group_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                  &lt;span class="c1"&gt;-- empty&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'group_replication_primary_member'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;-- empty&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;dba&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCluster&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// should throw&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3.2 Method B: &lt;code&gt;dropMetadataSchema()&lt;/code&gt; (Fallback)
&lt;/h3&gt;

&lt;p&gt;If &lt;code&gt;dissolve()&lt;/code&gt; fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;mysqlsh&lt;/span&gt; &lt;span class="nx"&gt;clusterAdmin&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;dr_node1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3306&lt;/span&gt;

&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;
&lt;span class="nx"&gt;STOP&lt;/span&gt; &lt;span class="nx"&gt;GROUP_REPLICATION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="nx"&gt;RESET&lt;/span&gt; &lt;span class="nx"&gt;REPLICA&lt;/span&gt; &lt;span class="nx"&gt;ALL&lt;/span&gt; &lt;span class="nx"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;no&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;op&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;pure&lt;/span&gt; &lt;span class="nx"&gt;InnoDB&lt;/span&gt; &lt;span class="nc"&gt;Cluster &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;GR&lt;/span&gt; &lt;span class="nx"&gt;uses&lt;/span&gt; &lt;span class="nx"&gt;its&lt;/span&gt; &lt;span class="nx"&gt;own&lt;/span&gt;
&lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="nx"&gt;group_replication_recovery&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;SQL&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt; &lt;span class="nx"&gt;replication&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt; &lt;span class="nx"&gt;It&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s only
-- useful here if the node was *previously* a ClusterSet replica or had some
-- manual async channel configured. Skip unless `SHOW REPLICA STATUS&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;G` shows
-- a channel. Running it on a clean node is harmless but unnecessary.
-- RESET REPLICA ALL;
&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;js

dba.dropMetadataSchema({force: true})
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repeat on &lt;code&gt;dr_node2&lt;/code&gt; and &lt;code&gt;dr_node3&lt;/code&gt;. If you still see errant GTIDs blocking clone later, a controlled reset is sometimes necessary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Use only if you're 100% sure DR data is disposable&lt;/span&gt;
&lt;span class="k"&gt;RESET&lt;/span&gt; &lt;span class="n"&gt;MASTER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                        &lt;span class="c1"&gt;-- MySQL 8.0.x&lt;/span&gt;
&lt;span class="c1"&gt;-- RESET BINARY LOGS AND GTIDS;      -- MySQL 8.4+ (new spelling; RESET MASTER is deprecated there)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3.3 Clean Up the DR Router
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl stop mysqlrouter
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /etc/mysqlrouter/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/mysqlrouter/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3.4 Confirm DR Nodes Are Standalone
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;mysqlsh&lt;/span&gt; &lt;span class="nx"&gt;clusterAdmin&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;dr_nodeX&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3306&lt;/span&gt;
&lt;span class="nx"&gt;dba&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCluster&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                                               &lt;span class="c1"&gt;// ERROR expected&lt;/span&gt;
&lt;span class="nx"&gt;dba&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkInstanceConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clusterAdmin@&amp;lt;dr_nodeX&amp;gt;:3306&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// "ok"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: Create the ClusterSet and Join DR
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 Create ClusterSet from Master (TLS hardened)
&lt;/h3&gt;

&lt;p&gt;Both TLS enforcement and the replication user host scope are configured at ClusterSet creation time. They belong on &lt;code&gt;cluster.createClusterSet()&lt;/code&gt;, not on &lt;code&gt;createReplicaCluster()&lt;/code&gt; later. Setting them here means every replica cluster you add inherits the secure defaults.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Assumed naming convention used throughout this guide (substitute your own names if different):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Primary cluster name (created earlier when you ran &lt;code&gt;dba.createCluster('morpheus cluster')&lt;/code&gt; on Master): &lt;code&gt;morpheus cluster&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;ClusterSet name (created in this step): &lt;code&gt;MorpheusClusterSet&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Replica cluster name (created in §4.2): &lt;code&gt;MorphDrCluster&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can confirm the primary cluster name with &lt;code&gt;dba.getCluster().getName()&lt;/code&gt; before proceeding.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;mysqlsh&lt;/span&gt; &lt;span class="nx"&gt;clusterAdmin&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;master_node1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3306&lt;/span&gt;

&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;cluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dba&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCluster&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;// Sanity check: this should return 'morpheus cluster' in this guide&lt;/span&gt;
&lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;clusterSet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createClusterSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MorpheusClusterSet&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;clusterSetReplicationSslMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;REQUIRED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// Force TLS on the ClusterSet replication channel&lt;/span&gt;
  &lt;span class="na"&gt;replicationAllowedHost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;10.0.0.0/8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;             &lt;span class="c1"&gt;// MUST cover BOTH primary and replica subnets (see note below)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;clusterSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Version requirements: &lt;code&gt;clusterSetReplicationSslMode&lt;/code&gt; is available in MySQL Shell 8.0.27 and later. The values &lt;code&gt;VERIFY_CA&lt;/code&gt; and &lt;code&gt;VERIFY_IDENTITY&lt;/code&gt; (stronger than &lt;code&gt;REQUIRED&lt;/code&gt;) were added in 8.0.33. The &lt;code&gt;replicationAllowedHost&lt;/code&gt; option requires MySQL Shell 8.0.28 or later.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;code&gt;replicationAllowedHost&lt;/code&gt; is bidirectional. Per the official docs, the host pattern you set must be reachable from nodes in both the primary and replica clusters. The internal replication user (&lt;code&gt;mysql_innodb_cs_*&lt;/code&gt;) is granted with this host pattern and will be used by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The replica cluster when it connects to the primary (normal-state replication)&lt;/li&gt;
&lt;li&gt;The original primary's nodes if they later need to connect to a new primary after failover or switchover&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scoping the pattern to only the DR subnet (for example &lt;code&gt;10.20.0.0/24&lt;/code&gt;) will work as long as the primary never changes, but &lt;code&gt;setPrimaryCluster()&lt;/code&gt; or &lt;code&gt;forcePrimaryCluster()&lt;/code&gt; will break replication for the old primary's nodes because they can't satisfy the host grant. Use a CIDR or wildcard that covers every MySQL node in every cluster, for example &lt;code&gt;10.0.0.0/8&lt;/code&gt; covering both sites, or a pair of wildcards like &lt;code&gt;10.10.%.%&lt;/code&gt; combined with &lt;code&gt;10.20.%.%&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Host pattern syntax: per the official docs, &lt;code&gt;replicationAllowedHost&lt;/code&gt; accepts CIDR notation (for example &lt;code&gt;192.0.2.0/24&lt;/code&gt;). The MySQL &lt;code&gt;Host&lt;/code&gt; column also accepts wildcards like &lt;code&gt;10.%.%.%&lt;/code&gt; and netmask form &lt;code&gt;10.20.0.0/255.255.0.0&lt;/code&gt; if your network requires those. Avoid &lt;code&gt;'%'&lt;/code&gt; in production, since it lets the replication user connect from any IP.&lt;/p&gt;

&lt;p&gt;Already created? If you see &lt;code&gt;MYSQLSH 51601 — function is not available … already belongs to a ClusterSet&lt;/code&gt;, fetch the existing ClusterSet:&lt;/p&gt;


&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;clusterSet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dba&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClusterSet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;// To change the host scope after the fact:&lt;/span&gt;
&lt;span class="nx"&gt;clusterSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setOption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;replicationAllowedHost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;10.0.0.0/8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4.2 Add DR as Replica Cluster
&lt;/h3&gt;

&lt;p&gt;TLS and host scope were already set at the ClusterSet level in §4.1, so the replica cluster inherits them automatically. Here you only need the provisioning options that belong on &lt;code&gt;createReplicaCluster()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;clusterSet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dba&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClusterSet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;replicaCluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;clusterSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createReplicaCluster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clusterAdmin@dr_node1:3306&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MorphDrCluster&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;recoveryMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clone&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// AUTO | CLONE | INCREMENTAL&lt;/span&gt;
    &lt;span class="na"&gt;recoveryProgress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// 0 = silent, 1 = static, 2 = progress bars&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;                 &lt;span class="c1"&gt;// Seconds to wait for post-provisioning sync&lt;/span&gt;
    &lt;span class="c1"&gt;// communicationStack: 'MYSQL'   // Default on 8.0.27+; set only to pin explicitly&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;What's actually happening under the hood, per the official docs: &lt;code&gt;createReplicaCluster()&lt;/code&gt; creates the &lt;code&gt;clusterset_replication&lt;/code&gt; asynchronous channel, generates an internal replication user with a random password, and configures encryption on the channel according to the &lt;code&gt;clusterSetReplicationSslMode&lt;/code&gt; you set on the ClusterSet in §4.1. It also sets &lt;code&gt;skip_replica_start = ON&lt;/code&gt; and &lt;code&gt;super_read_only = ON&lt;/code&gt;, and enables &lt;code&gt;mysql_start_failover_channels_if_primary&lt;/code&gt; for async failover on the channel.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Add remaining DR nodes:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;replicaCluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clusterAdmin@dr_node2:3306&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;recoveryMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clone&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;replicaCluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clusterAdmin@dr_node3:3306&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;recoveryMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clone&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note on &lt;code&gt;cloneDonor&lt;/code&gt;: by default AdminAPI picks a secondary from the primary cluster as clone donor (and falls back to the primary if no secondary is available). For cross-WAN clones of large datasets, consider pinning an explicit donor with &lt;code&gt;cloneDonor: 'host:port'&lt;/code&gt; to control which node sources the snapshot, and therefore which link the traffic flows through.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4.3 Common Errors
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Server address configuration error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;report_host&lt;/code&gt; missing/wrong&lt;/td&gt;
&lt;td&gt;Add to &lt;code&gt;my.cnf&lt;/code&gt;, restart mysqld&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Target instance already part of an InnoDB Cluster&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DR not cleaned up&lt;/td&gt;
&lt;td&gt;Re-run Step 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Errant GTIDs detected&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Old transactions on DR&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;recoveryMethod: 'clone'&lt;/code&gt; (overwrites)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Can't connect to primary cluster&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Firewall / &lt;code&gt;report_host&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Test with &lt;code&gt;nc -zv&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clone plugin not installed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Missing plugin&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;INSTALL PLUGIN clone SONAME 'mysql_clone.so';&lt;/code&gt; on both sides&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Access denied for user 'mysql_innodb_cs_*'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TLS mismatch or host restriction&lt;/td&gt;
&lt;td&gt;Verify &lt;code&gt;replicationAllowedHost&lt;/code&gt; matches donor IP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step 5: Re-bootstrap MySQL Router
&lt;/h2&gt;

&lt;p&gt;DR's metadata was rebuilt, so Router on both sites must re-learn the new topology.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1 DR Router
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Always bootstrap against a Master site node. Router reads metadata from the primary.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl stop mysqlrouter
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /etc/mysqlrouter/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/mysqlrouter/&lt;span class="k"&gt;*&lt;/span&gt;

mysqlrouter &lt;span class="nt"&gt;--bootstrap&lt;/span&gt; clusterAdmin@master_node1:3306 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--account&lt;/span&gt; routeruser &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mysqlrouter &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf-use-gr-notifications&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;span class="c"&gt;# Note: consider dropping --disable-rest if you want metrics/health endpoints&lt;/span&gt;

systemctl start mysqlrouter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.2 Master Router
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl stop mysqlrouter

mysqlrouter &lt;span class="nt"&gt;--bootstrap&lt;/span&gt; clusterAdmin@master_node1:3306 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--account&lt;/span&gt; routeruser &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mysqlrouter &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--conf-use-gr-notifications&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--force&lt;/span&gt;

systemctl start mysqlrouter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--conf-use-gr-notifications&lt;/code&gt; makes Router react to GR state changes without waiting for a metadata TTL, which meaningfully shortens failover detection.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3 Keyring Error (&lt;code&gt;Keyring decryption failed&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl stop mysqlrouter

find / &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'*.keyring'&lt;/span&gt; 2&amp;gt;/dev/null
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/lib/mysqlrouter/&lt;span class="k"&gt;*&lt;/span&gt;.keyring
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/lib/mysqlrouter/keyring&lt;span class="k"&gt;*&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /etc/mysqlrouter/&lt;span class="k"&gt;*&lt;/span&gt;.key

mysqlrouter &lt;span class="nt"&gt;--bootstrap&lt;/span&gt; clusterAdmin@master_node1:3306 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--account&lt;/span&gt; routeruser &lt;span class="nt"&gt;--user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mysqlrouter &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.4 &lt;code&gt;mysql_native_password&lt;/code&gt; Warning
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Failed changing the authentication plugin: mysql_native_password is deprecated&lt;/code&gt; is a warning, not an error. In MySQL 8.4 the plugin is disabled by default. Upgrade the account cleanly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'routeruser'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;caching_sha2_password&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;password&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 6: Verification and Monitoring
&lt;/h2&gt;

&lt;h3&gt;
  
  
  6.1 ClusterSet Health
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;mysqlsh&lt;/span&gt; &lt;span class="nx"&gt;clusterAdmin&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;master_node1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3306&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;clusterSet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dba&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClusterSet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;clusterSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;extended&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"primaryCluster"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"morpheus-cluster"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HEALTHY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"replicaClusters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"MorphDrCluster"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"clusterRole"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REPLICA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"clusterSetReplicationStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"transactionSetConsistencyStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OK"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;transactionSetConsistencyStatus&lt;/code&gt; is anything other than &lt;code&gt;OK&lt;/code&gt; (especially &lt;code&gt;INCONSISTENT&lt;/code&gt;), stop and investigate before letting traffic continue.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.2 Replication Channel Queries
&lt;/h3&gt;

&lt;p&gt;A handful of queries cover the vast majority of troubleshooting on a running ClusterSet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1) ClusterSet replication channel status (run on DR primary)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;CHANNEL_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SERVICE_STATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LAST_ERROR_NUMBER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LAST_ERROR_MESSAGE&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;performance_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replication_connection_status&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;CHANNEL_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'clusterset_replication'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 2) Applier worker status (look for LAST_ERROR on any worker)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;CHANNEL_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WORKER_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SERVICE_STATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LAST_ERROR_NUMBER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LAST_ERROR_MESSAGE&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;performance_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replication_applier_status_by_worker&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;CHANNEL_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'clusterset_replication'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 3) GTID gap check: the DR executed set should be a subset of Master's&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="k"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gtid_executed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;-- run on both, compare&lt;/span&gt;

&lt;span class="c1"&gt;-- 4) Classic lag view (still useful for a quick eyeball)&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;REPLICA&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="k"&gt;G&lt;/span&gt;
&lt;span class="c1"&gt;-- Seconds_Behind_Source (8.0.22+) / Seconds_Behind_Master (older)&lt;/span&gt;

&lt;span class="c1"&gt;-- 5) Group Replication member view&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;MEMBER_HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MEMBER_ROLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MEMBER_STATE&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;performance_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replication_group_members&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;MEMBER_ROLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.3 Router Registration and Ports
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;clusterSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listRouters&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Router&lt;/th&gt;
&lt;th&gt;Hostname&lt;/th&gt;
&lt;th&gt;R/W Port&lt;/th&gt;
&lt;th&gt;R/O Port&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Master Router&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;master_server&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6446&lt;/td&gt;
&lt;td&gt;6447&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DR Router&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;dr_server&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6446&lt;/td&gt;
&lt;td&gt;6447&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ss &lt;span class="nt"&gt;-tlnp&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;mysqlrouter
&lt;span class="c"&gt;# LISTEN  0  70  0.0.0.0:6446   # R/W&lt;/span&gt;
&lt;span class="c"&gt;# LISTEN  0  70  0.0.0.0:6447   # R/O&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.4 Metrics to Pipe Into Prometheus
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Exporter version caveat: the metric names below are &lt;em&gt;indicative&lt;/em&gt; and follow &lt;code&gt;prometheus/mysqld_exporter&lt;/code&gt; historical naming. Metric names have changed across versions (e.g. &lt;code&gt;slave&lt;/code&gt; → &lt;code&gt;replica&lt;/code&gt; in newer releases, and some Group Replication metrics moved between collector flags). Before copy-pasting into your alerts, verify the exact names against the exporter version you run. Query your Prometheus with &lt;code&gt;{__name__=~"mysql_.*(replica|slave).*"}&lt;/code&gt; and &lt;code&gt;{__name__=~"mysql_.*group_replication.*"}&lt;/code&gt; and adjust. You may also need to enable collectors like &lt;code&gt;--collect.slave_status&lt;/code&gt; / &lt;code&gt;--collect.replica_status&lt;/code&gt; and &lt;code&gt;--collect.perf_schema.replication_group_member_stats&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric (indicative name; verify against your exporter)&lt;/th&gt;
&lt;th&gt;Threshold&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;mysql_slave_status_seconds_behind_master&lt;/code&gt; &lt;em&gt;(or &lt;code&gt;mysql_replica_status_seconds_behind_source&lt;/code&gt; on newer exporters)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;&amp;gt; 10 s sustained&lt;/td&gt;
&lt;td&gt;Check WAN bandwidth, DR apply throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;mysql_perf_schema_replication_group_member_info{member_role="PRIMARY"}&lt;/code&gt; or equivalent&lt;/td&gt;
&lt;td&gt;Must match expected primary&lt;/td&gt;
&lt;td&gt;Alert on unexpected primary change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;mysql_up&lt;/code&gt; on any node&lt;/td&gt;
&lt;td&gt;= 0 for &amp;gt; 60 s&lt;/td&gt;
&lt;td&gt;Page on-call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transactionSetConsistencyStatus&lt;/code&gt; (via custom exporter scraping &lt;code&gt;clusterSet.status()&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;!= &lt;code&gt;OK&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Freeze writes, investigate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row does not have a built-in exporter. A common pattern is a small sidecar that periodically runs &lt;code&gt;mysqlsh --js -e "print(JSON.stringify(dba.getClusterSet().status({extended:1})))"&lt;/code&gt; and exposes the parsed fields as a Prometheus textfile.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Failover Procedures
&lt;/h2&gt;

&lt;h3&gt;
  
  
  7.1 Planned Switchover
&lt;/h3&gt;

&lt;p&gt;For maintenance windows, DC migrations, and version upgrades. Zero data loss.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;cs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dba&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClusterSet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Promote DR&lt;/span&gt;
&lt;span class="nx"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setPrimaryCluster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MorphDrCluster&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Switch back&lt;/span&gt;
&lt;span class="nx"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setPrimaryCluster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;morpheus-cluster&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Realistic RTO is roughly 30 seconds to 2 minutes, dominated by Router metadata refresh and the in-flight transaction drain. With &lt;code&gt;--conf-use-gr-notifications&lt;/code&gt; enabled, the Router side is typically under 10 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.2 Emergency Failover (&lt;code&gt;forcePrimaryCluster&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Use this when Master is completely unreachable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Warning: &lt;code&gt;forcePrimaryCluster()&lt;/code&gt; skips consistency checks. Transactions committed on Master but not yet replicated to DR will be lost. Expect a small RPO measured in seconds, not zero.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;mysqlsh&lt;/span&gt; &lt;span class="nx"&gt;clusterAdmin&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;dr_node1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3306&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;cs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dba&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClusterSet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forcePrimaryCluster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MorphDrCluster&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Critical: Fence the Old Master
&lt;/h4&gt;

&lt;p&gt;Before or immediately after the force failover, make sure the old Master cannot accept writes when it comes back up. Otherwise the application may briefly hit the old Master through its Router and create divergent data. The preferred approach is to use the AdminAPI fencing commands (MySQL Shell 8.0.27 and later). They coordinate the fence across every node in the old Master cluster and instruct Router to stop routing to it, rather than relying on each DBA to toggle &lt;code&gt;super_read_only&lt;/code&gt; by hand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Connect to ANY reachable node of the old (ex-primary) cluster&lt;/span&gt;
&lt;span class="nx"&gt;mysqlsh&lt;/span&gt; &lt;span class="nx"&gt;clusterAdmin&lt;/span&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;master_node1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3306&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;cluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dba&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCluster&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Hard fence: blocks BOTH writes and reads via Router. Use when you want&lt;/span&gt;
&lt;span class="c1"&gt;// the old cluster completely out of traffic while you investigate.&lt;/span&gt;
&lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fenceAllTraffic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Softer alternative: only blocks writes, keeps the cluster available for R/O.&lt;/span&gt;
&lt;span class="c1"&gt;// Useful if the old Master can safely keep serving reads from stale data.&lt;/span&gt;
&lt;span class="c1"&gt;// cluster.fenceWrites()&lt;/span&gt;

&lt;span class="c1"&gt;// After you've repaired the old Master and are ready to rejoin it as a REPLICA:&lt;/span&gt;
&lt;span class="c1"&gt;// cluster.unfenceWrites()   // only needed if you used fenceWrites()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the AdminAPI fencing actually does, per the official Shell docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fenceAllTraffic()&lt;/code&gt; sets &lt;code&gt;super_read_only = ON&lt;/code&gt; on every member, stops Group Replication on every member, and updates the metadata so Router stops routing any traffic to the cluster.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fenceWrites()&lt;/code&gt; sets &lt;code&gt;super_read_only = ON&lt;/code&gt; on the primary and updates metadata so Router stops sending R/W traffic but still allows R/O. The cluster keeps replicating from the new primary through ClusterSet.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;unfenceWrites()&lt;/code&gt; reverses &lt;code&gt;fenceWrites()&lt;/code&gt;, so the cluster comes back online as a healthy replica.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If AdminAPI is unreachable (for example the old Master cluster is completely partitioned away), fall back in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Remove the old Master's VIP, DNS entry, or load balancer registration so the application can't reach it.&lt;/li&gt;
&lt;li&gt;Firewall-block ports 3306 and 6446 at the old site from the application subnet.&lt;/li&gt;
&lt;li&gt;As a last resort, &lt;code&gt;SET GLOBAL super_read_only = ON;&lt;/code&gt; manually on each old Master node. This is a per-node toggle, easy to forget on one host, and it does not update ClusterSet metadata.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Skipping fencing is how you end up with a split-brain when the old Master briefly reconnects to the network.&lt;/p&gt;

&lt;h4&gt;
  
  
  After Master Recovers, Rejoin It
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Run from the DR primary (now the ClusterSet primary)&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;cs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dba&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClusterSet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;cs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rejoinCluster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;morpheus-cluster&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7.3 The Most Common Rejoin Failure: Errant GTIDs on the Old Master
&lt;/h3&gt;

&lt;p&gt;If the old Master committed transactions during the outage that never made it to DR, &lt;code&gt;rejoinCluster()&lt;/code&gt; will fail with an errant-GTID error. In practice this is by far the most common post-failover pain point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diagnose:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- On old Master primary&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="k"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gtid_executed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- On new primary (DR)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;@@&lt;/span&gt;&lt;span class="k"&gt;global&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gtid_executed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Compute the diff:&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;GTID_SUBTRACT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'&amp;lt;old_master_executed&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'&amp;lt;dr_executed&amp;gt;'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;errant_gtids&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any non-empty &lt;code&gt;errant_gtids&lt;/code&gt; means the old Master has transactions DR never saw. You have three options, ordered from safest to fastest:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Extract and replay manually.&lt;/strong&gt; Use &lt;code&gt;mysqlbinlog&lt;/code&gt; on the old Master to dump only the errant GTIDs and then replay them against the new primary (DR). The slowest option, but no data loss.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="c"&gt;# Step 1: On the OLD Master, find the binlog files covering the outage window&lt;/span&gt;
   &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lh&lt;/span&gt; /var/lib/mysql/binlog.&lt;span class="k"&gt;*&lt;/span&gt;

   &lt;span class="c"&gt;# Step 2: Dump only the errant GTIDs to a SQL file.&lt;/span&gt;
   &lt;span class="c"&gt;#         --include-gtids takes the exact GTID set printed by GTID_SUBTRACT above.&lt;/span&gt;
   mysqlbinlog &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;--read-from-remote-server&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;--host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;old_master_ip&amp;gt; &lt;span class="nt"&gt;--port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3306 &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;--user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;clusterAdmin &lt;span class="nt"&gt;--password&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;--include-gtids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;errant_gtids_from_GTID_SUBTRACT&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;--skip-gtids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     binlog.000123 binlog.000124 binlog.000125 &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; errant.sql

   &lt;span class="c"&gt;# Step 3: Inspect errant.sql. Confirm the statements are safe to re-apply&lt;/span&gt;
   &lt;span class="c"&gt;#         (no DROP/TRUNCATE you didn't expect, no migrations already run on DR).&lt;/span&gt;
   less errant.sql

   &lt;span class="c"&gt;# Step 4: On the NEW primary (DR), replay as clusterAdmin&lt;/span&gt;
   mysql &lt;span class="nt"&gt;-h&lt;/span&gt; dr_node1 &lt;span class="nt"&gt;-u&lt;/span&gt; clusterAdmin &lt;span class="nt"&gt;-p&lt;/span&gt; &amp;lt; errant.sql

   &lt;span class="c"&gt;# Step 5: Re-run the GTID diff from §7.3. errant_gtids should now be empty.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Why &lt;code&gt;--skip-gtids=false&lt;/code&gt;: it preserves the original GTIDs so the new primary records them as executed. Otherwise a later &lt;code&gt;rejoinCluster()&lt;/code&gt; of the old Master will still see them as errant.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Accept the loss and re-clone.&lt;/strong&gt; Treat the old Master cluster as disposable, &lt;code&gt;dissolve()&lt;/code&gt; it, and re-add it as a new replica cluster against the current primary (DR). The fastest option, but loses the errant transactions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forcibly reset and rejoin.&lt;/strong&gt; On each old Master node:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;   &lt;span class="n"&gt;STOP&lt;/span&gt; &lt;span class="n"&gt;GROUP_REPLICATION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;RESET&lt;/span&gt; &lt;span class="n"&gt;MASTER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                      &lt;span class="c1"&gt;-- MySQL 8.0.x; on 8.4+ use RESET BINARY LOGS AND GTIDS&lt;/span&gt;
   &lt;span class="c1"&gt;-- Then rejoinCluster() and let clone re-seed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only safe if you have the original data preserved and audited elsewhere. This wipes the entire GTID history on the target node.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is why backups are non-negotiable. Option 1 depends on binary log retention that covers the outage window. Set &lt;code&gt;binlog_expire_logs_seconds&lt;/code&gt; accordingly. Seven days is a reasonable floor for most shops.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  7.4 Router Behavior After Failover
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Master Router&lt;/th&gt;
&lt;th&gt;DR Router&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Normal&lt;/td&gt;
&lt;td&gt;Master Cluster → R/W (6446)&lt;/td&gt;
&lt;td&gt;Master Cluster → R/W (WAN)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After failover&lt;/td&gt;
&lt;td&gt;DR Cluster → R/W (auto)&lt;/td&gt;
&lt;td&gt;DR Cluster → R/W (local)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No application connection string change is needed. Router reacts to ClusterSet topology changes via GR notifications and its metadata cache.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Master Site Role&lt;/th&gt;
&lt;th&gt;DR Site Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cluster type&lt;/td&gt;
&lt;td&gt;Primary (R/W)&lt;/td&gt;
&lt;td&gt;Replica (R/O until failover)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intra-cluster replication&lt;/td&gt;
&lt;td&gt;Group Replication (sync)&lt;/td&gt;
&lt;td&gt;Group Replication (sync)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-site replication&lt;/td&gt;
&lt;td&gt;Source (donor)&lt;/td&gt;
&lt;td&gt;Async + TLS from Master&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failover method&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;setPrimaryCluster()&lt;/code&gt; (planned)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;forcePrimaryCluster()&lt;/code&gt; (emergency)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Router behavior&lt;/td&gt;
&lt;td&gt;Always routes to the primary&lt;/td&gt;
&lt;td&gt;Auto-updates after failover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Realistic RTO&lt;/td&gt;
&lt;td&gt;30 s to 2 min (planned)&lt;/td&gt;
&lt;td&gt;2 to 5 min (forced, includes fence + rejoin)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RPO&lt;/td&gt;
&lt;td&gt;Zero (planned)&lt;/td&gt;
&lt;td&gt;Near-zero; non-zero on force (typically 1 to 5 s)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Operational Tips and Best Practices
&lt;/h2&gt;

&lt;p&gt;A switchover drill every month is the single highest-leverage habit. Failover should never be tested for the first time during a real disaster; a monthly &lt;code&gt;setPrimaryCluster()&lt;/code&gt; round-trip proves the procedure still works and keeps on-call muscle memory sharp.&lt;/p&gt;

&lt;p&gt;Log &lt;code&gt;clusterSet.status({extended: 1})&lt;/code&gt; every 60 seconds to a file. A simple cron-scraped JSON log often beats "enterprise" monitoring when you are trying to reconstruct what happened during an incident.&lt;/p&gt;

&lt;p&gt;Put Routers behind a load balancer. A single Router IP is a single point of failure. At minimum, run two Routers per site behind HAProxy, Keepalived, or a cloud load balancer.&lt;/p&gt;

&lt;p&gt;Back up independently of the cluster. ClusterSet protects against infrastructure failure, not logical failure. A &lt;code&gt;DROP TABLE&lt;/code&gt; replicates. Use MySQL Enterprise Backup or Percona XtraBackup on an independent schedule, ideally to a third location that is neither Master nor DR.&lt;/p&gt;

&lt;p&gt;Keep the rolling approach for MySQL version upgrades: upgrade DR first, observe for a week, switch over, upgrade the old Master, then switch back.&lt;/p&gt;

&lt;p&gt;Watch cross-site RTT. Async replication tolerates latency well, but GTID apply throughput drops meaningfully above roughly 50 ms RTT for write-heavy workloads. Benchmark before assuming it will work for your traffic shape.&lt;/p&gt;

&lt;p&gt;Rotate the internal &lt;code&gt;mysql_innodb_cs_*&lt;/code&gt; replication account credentials on a schedule. AdminAPI creates them with strong random passwords but never rotates them on its own. Build a runbook for the rotation.&lt;/p&gt;

&lt;p&gt;Keep &lt;code&gt;binlog_expire_logs_seconds&lt;/code&gt; generous. Seven days is a reasonable floor for most production environments. It is the difference between manual recovery (option 1 in §7.3) and data loss (option 2).&lt;/p&gt;

&lt;p&gt;Don't share credentials between &lt;code&gt;clusterAdmin&lt;/code&gt; and &lt;code&gt;routeruser&lt;/code&gt;. They have different privilege needs, and scoping them tightly limits blast radius.&lt;/p&gt;

&lt;p&gt;Document your fencing procedure before you need it. When a real emergency failover happens, you do not want to be figuring out "how do we stop the old Master from accepting writes" on the fly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrap-Up
&lt;/h2&gt;

&lt;p&gt;InnoDB ClusterSet is a production-grade multi-site DR solution that can be introduced into a live environment without downtime, provided you respect three disciplines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Rolling restart discipline, so you never break quorum.&lt;/li&gt;
&lt;li&gt;Clean DR teardown followed by a TLS-hardened ClusterSet creation. Use &lt;code&gt;dissolve()&lt;/code&gt; (or fallback &lt;code&gt;dropMetadataSchema()&lt;/code&gt;) to clean DR, then create the ClusterSet with &lt;code&gt;clusterSetReplicationSslMode: 'REQUIRED'&lt;/code&gt; (or &lt;code&gt;VERIFY_IDENTITY&lt;/code&gt; on Shell 8.0.33+) and a scoped &lt;code&gt;replicationAllowedHost&lt;/code&gt;. Never use &lt;code&gt;%&lt;/code&gt; in production.&lt;/li&gt;
&lt;li&gt;Failover realism. Plan for errant GTIDs, old-Master fencing, and backup-dependent recovery paths before you need them, not after.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Get those three right and your application keeps talking to the same Router address while your infrastructure is ready for the worst case: a full data center loss.&lt;/p&gt;

</description>
      <category>mysql</category>
      <category>database</category>
      <category>highavailability</category>
      <category>morpheus</category>
    </item>
    <item>
      <title>Automating MySQL InnoDB Cluster Deployment for HPE Morpheus Enterprise HA</title>
      <dc:creator>Emre Baykal</dc:creator>
      <pubDate>Sun, 19 Apr 2026 17:02:48 +0000</pubDate>
      <link>https://dev.to/emre_baykal_a4a7a479d48c5/automating-mysql-innodb-cluster-deployment-for-hpe-morpheus-enterprise-ha-51b0</link>
      <guid>https://dev.to/emre_baykal_a4a7a479d48c5/automating-mysql-innodb-cluster-deployment-for-hpe-morpheus-enterprise-ha-51b0</guid>
      <description>&lt;h1&gt;
  
  
  Automating MySQL InnoDB Cluster Deployment for HPE Morpheus Enterprise HA Environments
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;A single Python script that turns a painful, multi-hour manual process into a guided 10-minute deployment.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔗 &lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/emrbaykal/morpheus-innodb-cluster" rel="noopener noreferrer"&gt;emrbaykal/morpheus-innodb-cluster&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👤 &lt;strong&gt;Author:&lt;/strong&gt; Emre Baykal&lt;/p&gt;

&lt;p&gt;📜 &lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;If you deploy &lt;strong&gt;HPE Morpheus Enterprise&lt;/strong&gt; in a 3-Node HA configuration, the installer automatically clusters the Application Tier, OpenSearch, and RabbitMQ — but leaves the &lt;strong&gt;Transactional Database Tier (MySQL)&lt;/strong&gt; entirely in your hands. You must stand up a resilient, production-grade MySQL InnoDB Cluster &lt;em&gt;before&lt;/em&gt; you can even start the Morpheus HA installation.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/emrbaykal/morpheus-innodb-cluster" rel="noopener noreferrer"&gt;&lt;strong&gt;morpheus-innodb-cluster&lt;/strong&gt;&lt;/a&gt; project solves this with a single interactive Python script backed by modular Ansible roles — it turns a multi-day manual task into a &lt;strong&gt;10-minute guided wizard&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;If you've ever deployed HPE Morpheus Enterprise in a High Availability (HA) configuration, you know the drill: the application tier, messaging tier, and non-transactional database tier all install and cluster automatically across your three nodes — but the &lt;strong&gt;Transactional Database Tier&lt;/strong&gt; is left entirely in your hands. That tier is MySQL, and for a production HA environment, it needs to be a properly configured, redundant, multi-node cluster.&lt;/p&gt;

&lt;p&gt;As the &lt;a href="https://support.hpe.com/hpesc/public/docDisplay?docId=sd00007510en_us&amp;amp;page=GUID-C1061ACC-BCAF-4F7C-A413-2219EAFB7983.html" rel="noopener noreferrer"&gt;HPE Morpheus Enterprise v8.1.0 HA Installation Overview&lt;/a&gt; clearly states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"In this architecture, all tiers are deployed on three machines by HPE Morpheus Enterprise during the installation, with the exception of the Transactional Database Tier. This provides HA not just for the HPE Morpheus Enterprise Application Tier but all underlying tiers that support HPE Morpheus Enterprise. The Transactional Database Tier will remain external, either as a separate cluster or PaaS, following the supported services. An external MySQL cluster must still be set up outside of the HPE Morpheus Enterprise app nodes."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means &lt;strong&gt;you&lt;/strong&gt; are responsible for standing up a resilient, properly tuned MySQL cluster &lt;strong&gt;before&lt;/strong&gt; you can even begin the Morpheus HA installation. There is no embedded option — Morpheus disables its internal MySQL (&lt;code&gt;mysql['enable'] = false&lt;/code&gt;) and expects you to provide an external endpoint.&lt;/p&gt;

&lt;p&gt;This is where the &lt;strong&gt;morpheus-innodb-cluster&lt;/strong&gt; project comes in. It is a single interactive Python script, backed by modular Ansible roles, that automates the entire process of deploying a production-ready 3-node MySQL InnoDB Cluster on Ubuntu or RHEL-based systems.&lt;/p&gt;

&lt;p&gt;In this post, I'll walk you through why this tool exists, what problems it solves, and how to use it from start to finish.&lt;/p&gt;




&lt;h2&gt;
  
  
  Understanding HPE Morpheus Enterprise HA Architecture
&lt;/h2&gt;

&lt;p&gt;HPE Morpheus Enterprise is a unified hybrid cloud management platform that provides provisioning, orchestration, monitoring, and governance across private and public clouds. It can start as a simple single-machine instance, or it can be split into individual services per machine and configured in a high availability (HA) configuration.&lt;/p&gt;

&lt;p&gt;According to the official HPE documentation (v8.1.0, February 2026), there are &lt;strong&gt;four primary tiers&lt;/strong&gt; of services within the Morpheus appliance:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Application Tier
&lt;/h3&gt;

&lt;p&gt;The stateless services layer — Nginx and Tomcat. These can be installed across all regions and placed behind a central load balancer or geo-based load balancer. Shared storage (NFS, Amazon S3, or OpenStack Swift) is required for deployment archives, virtual image catalogs, and backups.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Transactional Database Tier (MySQL) 🎯
&lt;/h3&gt;

&lt;p&gt;This is the focus of this article. The HPE documentation states: &lt;em&gt;"The Transactional Database tier consists of a MySQL compatible database. It is recommended that a lockable clustered configuration be used, such as a Galera cluster, which can also provide high availability."&lt;/em&gt; For the recommended 3-Node HA architecture, this tier &lt;strong&gt;must be external&lt;/strong&gt; — it is not deployed by the Morpheus installer.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Non-Transactional Database Tier (OpenSearch)
&lt;/h3&gt;

&lt;p&gt;Used for log aggregation, stats, metrics, and temporal data. Morpheus clusters OpenSearch &lt;strong&gt;automatically&lt;/strong&gt; during installation — no manual setup required.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Messaging Tier (RabbitMQ)
&lt;/h3&gt;

&lt;p&gt;An AMQP-based tier with STOMP protocol for agent communication. RabbitMQ needs at least 3 instances for HA. While RabbitMQ is installed automatically during Morpheus setup, it &lt;strong&gt;does require manual clustering&lt;/strong&gt; afterward.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Architecture Diagram
&lt;/h3&gt;

&lt;p&gt;The recommended 3-Node HA deployment looks like this: three Morpheus application nodes sit behind a load balancer, each running embedded RabbitMQ and OpenSearch. On the side, a &lt;strong&gt;separate 3-node MySQL Database Cluster&lt;/strong&gt; provides the transactional database tier, connected via port 3306. Shared storage connects to all application nodes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1fvmum910zbt8liy5uon.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1fvmum910zbt8liy5uon.png" alt="HPE Morpheus 3-Node HA Architecture Diagram" width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;HPE Morpheus Enterprise 3-Node HA Architecture — the external MySQL cluster on the right is exactly what this tool automates.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  MySQL Requirements for Morpheus HA
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://support.hpe.com/hpesc/public/docDisplay?docId=sd00007510en_us&amp;amp;page=GUID-2D8A0A86-2231-4239-AB44-5475B4AE0827.html" rel="noopener noreferrer"&gt;HPE 3-Node HA Install documentation&lt;/a&gt; specifies the following MySQL requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MySQL version v8.0.x&lt;/strong&gt; (minimum of v8.0.72)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL cluster with at least 3 nodes&lt;/strong&gt; for redundancy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Morpheus application nodes must have connectivity&lt;/strong&gt; to the MySQL cluster&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also an important note: &lt;em&gt;"Morpheus does not create primary keys on all tables. If you use a clustering technology that requires primary keys, you will need to leverage the invisible primary key option in MySQL 8."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once the MySQL cluster is up, you must create the Morpheus database and user &lt;strong&gt;before&lt;/strong&gt; installing Morpheus itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create the Morpheus database&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;morpheus&lt;/span&gt; &lt;span class="nb"&gt;CHARACTER&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;utf8mb4&lt;/span&gt; &lt;span class="k"&gt;COLLATE&lt;/span&gt; &lt;span class="n"&gt;utf8mb4_general_ci&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Create the Morpheus database user&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'morpheus'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'morpheusDbUserPassword'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Grant required permissions&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;morpheus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'morpheus'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;OPTION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PROCESS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;DATABASES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RELOAD&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'morpheus'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;FLUSH&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in each Morpheus app node's &lt;code&gt;/etc/morpheus/morpheus.rb&lt;/code&gt;, you configure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'enable'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
&lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'host'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'127.0.0.1'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;6446&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;# MySQL Router local endpoint&lt;/span&gt;
&lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'morpheus_db'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'morpheus'&lt;/span&gt;
&lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'morpheus_db_user'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'morpheus'&lt;/span&gt;
&lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'morpheus_password'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'morpheusDbUserPassword'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;mysql['enable'] = false&lt;/code&gt; — this tells Morpheus to skip its embedded MySQL and use your external cluster instead. The host points to &lt;code&gt;127.0.0.1:6446&lt;/code&gt;, which is the &lt;strong&gt;MySQL Router&lt;/strong&gt; read-write endpoint running locally on each app node.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pain of Manual MySQL InnoDB Cluster Setup
&lt;/h2&gt;

&lt;p&gt;The HPE documentation tells you that you need an external MySQL cluster, but it doesn't deploy one for you. You're on your own. Here's what you're typically facing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Repetitive Per-Node Configuration&lt;/strong&gt; — OS tuning, kernel parameters, firewall rules for ports 3306, 33060, and 33061, NTP sync, locale settings, and MySQL installation on every node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. MySQL Installation Complexity&lt;/strong&gt; — Repository management, correct stream/version selection, package locks, and systemd overrides. On RHEL, you also deal with AppStream module conflicts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. InnoDB-Specific Tuning&lt;/strong&gt; — Enable GTID mode, enforce GTID consistency, set unique &lt;code&gt;server_id&lt;/code&gt;, tune &lt;code&gt;innodb_buffer_pool_size&lt;/code&gt; to RAM, configure binary log expiration, and bind addresses correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Cluster Bootstrap Choreography&lt;/strong&gt; — &lt;code&gt;dba.configureInstance()&lt;/code&gt; on every node, &lt;code&gt;dba.createCluster()&lt;/code&gt; on the primary, &lt;code&gt;cluster.addInstance()&lt;/code&gt; for each secondary with the correct recovery method. Order matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Security Considerations&lt;/strong&gt; — MySQL root passwords, cluster admin credentials, SSH keys, and sudo escalation — all of which need to be managed securely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. OS-Specific Differences&lt;/strong&gt; — Ubuntu/Debian vs. RHEL differ in repository management, package names, services, paths, and security frameworks (AppArmor vs. SELinux).&lt;/p&gt;

&lt;p&gt;Put it all together and you're looking at multiple hours — or days — of careful, error-prone work before you can even &lt;strong&gt;begin&lt;/strong&gt; the Morpheus installation itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Introducing morpheus-innodb-cluster
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;morpheus-innodb-cluster&lt;/strong&gt; project eliminates all of this manual toil. It is a single Python script (&lt;code&gt;innodb_cluster_setup.py&lt;/code&gt;) that orchestrates the entire deployment through an interactive 11-step wizard, backed by four modular Ansible roles that handle the actual configuration work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture at a Glance
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You (on master node)
  └── innodb_cluster_setup.py  (Python orchestrator)
        ├── Step 1-9:  Interactive wizard (collect &amp;amp; validate)
        └── Step 10:   Ansible playbook execution
              ├── Role 01: OS Pre-configuration
              ├── Role 02: MySQL Installation
              ├── Role 03: InnoDB Cluster Pre-configuration
              └── Role 04: Cluster Creation (master only)
        └── Step 11:   Setup Report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script is designed to run &lt;strong&gt;from the master node&lt;/strong&gt; of the cluster. It installs its own dependencies (Ansible, sshpass, community.mysql collection), connects to all three nodes via SSH, and executes the Ansible playbook that does the heavy lifting. You don't need to pre-install anything other than Python 3.6+ and sudo access.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step-by-Step Walkthrough
&lt;/h2&gt;

&lt;p&gt;Let's walk through the entire deployment process, using screenshots from a real deployment on RHEL 9 nodes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting Started
&lt;/h3&gt;

&lt;p&gt;Clone the repository and run the script on the node you want to be the &lt;strong&gt;primary (master)&lt;/strong&gt; node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/emrbaykal/morpheus-innodb-cluster.git
&lt;span class="nb"&gt;cd &lt;/span&gt;morpheus-innodb-cluster
&lt;span class="nb"&gt;sudo &lt;/span&gt;python3 innodb_cluster_setup.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 1/11 — Environment Setup
&lt;/h3&gt;

&lt;p&gt;The script begins by detecting your operating system family and automatically installing all required prerequisites. On RHEL systems, it checks for the Ansible Automation Platform repository in subscription-manager; if it's not enabled, the script gracefully falls back to installing Ansible via pip3.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcv27r168zuexg8jy6fe2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcv27r168zuexg8jy6fe2.png" alt="Step 1 - Environment Setup" width="800" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the screenshot above, you can see the tool detecting RHEL 9, installing &lt;code&gt;sshpass&lt;/code&gt; and &lt;code&gt;python3-pip&lt;/code&gt; via the OS package manager, falling back to pip for Ansible (since the AAP repository wasn't enabled in subscription-manager), and installing the &lt;code&gt;community.mysql&lt;/code&gt; Ansible collection. The entire environment is ready in seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2/11 — Cluster Configuration
&lt;/h3&gt;

&lt;p&gt;This is the heart of the wizard. It collects all the information needed in five organized sections:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section 1/5 — Cluster Nodes:&lt;/strong&gt; You provide the hostname and IP address for each of the three cluster nodes. The first node is automatically designated as the master (primary), and the other two become secondaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section 2/5 — SSH Connection:&lt;/strong&gt; The script asks for the SSH user, key file (or password), privilege escalation method (sudo or dzdo), and sudo password.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbyyjj93ei9gbegkuxked.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbyyjj93ei9gbegkuxked.png" alt=" " width="800" height="606"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section 3/5 — MySQL Credentials:&lt;/strong&gt; You set the MySQL root password and the InnoDB Cluster admin credentials. The cluster admin user (default: &lt;code&gt;clusterAdmin&lt;/code&gt;) is the account that will manage the InnoDB Cluster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section 4/5 — Cluster Settings:&lt;/strong&gt; You name your cluster (in our case, &lt;code&gt;morpheus-cluster&lt;/code&gt;) and set a password for the MySQL Router user account (&lt;code&gt;routeruser&lt;/code&gt;) that will be created for application connectivity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section 5/5 — System Settings:&lt;/strong&gt; NTP server configuration for time synchronization across all nodes — &lt;strong&gt;critical&lt;/strong&gt; for Group Replication to function correctly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjxwmnvhulibjuhgmgiau.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjxwmnvhulibjuhgmgiau.png" alt="Step 2 - MySQL Credentials, Cluster &amp;amp; NTP Settings" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration Summary
&lt;/h3&gt;

&lt;p&gt;Before anything is written to disk or executed, the script presents a complete summary table of everything you've entered — cluster nodes with IPs, SSH connection details, MySQL configuration, and system settings. You review and confirm with &lt;code&gt;y&lt;/code&gt; before proceeding.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fun140hwqj2r8n6kgzdjw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fun140hwqj2r8n6kgzdjw.png" alt="Configuration Summary" width="800" height="485"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a safety net. All passwords are masked, and the configuration is saved to &lt;code&gt;cluster_config.json&lt;/code&gt; with mode &lt;code&gt;0600&lt;/code&gt; so it can be reused on subsequent runs without re-entering everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Steps 3–4 — Inventory Generation &amp;amp; SSH Connectivity Test
&lt;/h3&gt;

&lt;p&gt;The script generates an Ansible inventory file from your inputs and then tests SSH connectivity to every node. This catches authentication problems, firewall issues, or unreachable hosts &lt;strong&gt;before&lt;/strong&gt; the deployment begins.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu4hed3dyd2xpadk9rzwr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu4hed3dyd2xpadk9rzwr.png" alt="Steps 3-4 - Inventory Generation &amp;amp; SSH Connectivity Test" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In our deployment, all three nodes — &lt;code&gt;innodb-rhel-1&lt;/code&gt; (192.168.42.100), &lt;code&gt;innodb-rhel-2&lt;/code&gt; (192.168.42.102), and &lt;code&gt;innodb-rhel-3&lt;/code&gt; (192.168.42.101) — came back as reachable. The inventory uses IP addresses throughout, making the deployment DNS-independent. Note the &lt;code&gt;ansible_ssh_common_args='-o StrictHostKeyChecking=no'&lt;/code&gt; setting, which prevents SSH from blocking first-time connections.&lt;/p&gt;

&lt;h3&gt;
  
  
  Steps 5–7 — Pre-Flight Checks
&lt;/h3&gt;

&lt;p&gt;The script performs three validation checks before deployment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step 5 — RHEL Repository Check:&lt;/strong&gt; Verifies that &lt;code&gt;rhel-9-for-x86_64-baseos-rpms&lt;/code&gt; and &lt;code&gt;rhel-9-for-x86_64-appstream-rpms&lt;/code&gt; are enabled on all nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 6 — Pre-Flight MySQL Package Check:&lt;/strong&gt; Scans all nodes for pre-existing &lt;code&gt;mysql-server&lt;/code&gt; and &lt;code&gt;mysql-shell&lt;/code&gt; installations that could cause conflicts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 7 — Internet Connectivity Check:&lt;/strong&gt; Tests that all nodes can reach &lt;code&gt;dev.mysql.com&lt;/code&gt; to download MySQL packages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjm7e1sw0ob8a6i1o722s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjm7e1sw0ob8a6i1o722s.png" alt="Steps 5-7 - Pre-Flight Checks" width="800" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8/11 — MySQL Version Selection
&lt;/h3&gt;

&lt;p&gt;On RHEL systems, the script presents the available MySQL AppStream streams (8.0 and 8.4) and then lists the specific versions within your chosen stream. In our deployment, we selected &lt;strong&gt;MySQL 8.4.7&lt;/strong&gt; from the 8.4 LTS stream — well above the HPE minimum requirement of v8.0.72.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcg3wjgv1bp2df5rhx7w4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcg3wjgv1bp2df5rhx7w4.png" alt="Step 8 - MySQL Version Selection" width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 9/11 — Deployment Confirmation
&lt;/h3&gt;

&lt;p&gt;A final confirmation screen shows exactly what will be deployed: the cluster name (&lt;code&gt;morpheus-cluster&lt;/code&gt;), admin user (&lt;code&gt;clusterAdmin&lt;/code&gt;), and all three nodes with their roles. The master node is marked with a star.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fupa2c6s2mzvok82rtijj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fupa2c6s2mzvok82rtijj.png" alt="Step 9 - Deployment Confirmation" width="800" height="237"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 10/11 — Ansible Playbook Execution
&lt;/h3&gt;

&lt;p&gt;Once you confirm, the Ansible playbook executes with real-time streaming output. You can watch every task as it runs across all three nodes. The playbook applies five roles in sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;pre_tasks&lt;/strong&gt; — Populates &lt;code&gt;/etc/hosts&lt;/code&gt; with cluster node entries and sets hostnames on all nodes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;01-os-preconfigure&lt;/strong&gt; — Deploys SSH banners, disables SELinux/AppArmor, configures firewall rules (ports 3306, 33060, 33061), sets locale, installs and configures NTP (chrony/systemd-timesyncd), tunes kernel parameters for database workloads, disables Transparent Huge Pages, and sets MySQL-specific system limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;02-mysql-install&lt;/strong&gt; — Adds the MySQL repository, selects the AppStream stream/version, installs MySQL Server and MySQL Shell, configures systemd overrides with NUMA interleaving, and sets the root password&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;03-mysql-innodb-cluster&lt;/strong&gt; — Creates the cluster admin user, removes anonymous users, drops the test database, writes InnoDB-optimized &lt;code&gt;my.cnf&lt;/code&gt; configuration (with buffer pool auto-sized to 80% of RAM), enables GTID mode, and restarts MySQL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;04-mysql-create-innodb-cluster&lt;/strong&gt; — Runs on the master node only: configures instances via &lt;code&gt;dba.configureInstance()&lt;/code&gt; in MySQL Shell, creates the cluster with &lt;code&gt;dba.createCluster()&lt;/code&gt;, adds secondary nodes with clone-based recovery, verifies cluster status, and creates the &lt;code&gt;routeruser&lt;/code&gt; account&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxr223n9mviq6hn9zbxzf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxr223n9mviq6hn9zbxzf.png" alt="Step 10 - Ansible Playbook Execution" width="800" height="671"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In our deployment, the entire Ansible playbook completed in &lt;strong&gt;4 minutes and 31 seconds&lt;/strong&gt; with zero failures across all three nodes. ⚡&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 11/11 — Setup Report
&lt;/h3&gt;

&lt;p&gt;After completion, the script generates a comprehensive setup report that includes everything you need to verify and manage your new cluster.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0i8u1pseo7er817ue3qn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0i8u1pseo7er817ue3qn.png" alt="Step 11 - Setup Report Header" width="800" height="703"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;NODE STATUS&lt;/strong&gt; section shows the Ansible PLAY RECAP for each node — in our deployment, the master node had 70 OK / 32 Changed tasks, while each secondary had 61 OK / 26 Changed, with zero unreachable or failed across the board.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;APPLIED ROLES&lt;/strong&gt; section shows what each role did, and the &lt;strong&gt;NEXT STEPS&lt;/strong&gt; section provides ready-to-use commands:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fggzky2d0gigvrgpnsaxp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fggzky2d0gigvrgpnsaxp.png" alt="Step 11 - Node Status &amp;amp; Next Steps" width="800" height="683"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Quick cluster status check&lt;/span&gt;
mysqlsh clusterAdmin@192.168.42.100 &lt;span class="nt"&gt;--&lt;/span&gt; cluster status

&lt;span class="c"&gt;# Interactive cluster management&lt;/span&gt;
mysqlsh clusterAdmin@192.168.42.100
var cluster &lt;span class="o"&gt;=&lt;/span&gt; dba.getCluster&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
cluster.status&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;# Bootstrap MySQL Router (run on each Morpheus app node)&lt;/span&gt;
mysqlrouter &lt;span class="nt"&gt;--bootstrap&lt;/span&gt; routeruser@192.168.42.100:3306 &lt;span class="nt"&gt;--user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mysqlrouter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What Makes This Project Different
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✅ Truly Idempotent
&lt;/h3&gt;

&lt;p&gt;The entire workflow is safe to re-run. The script saves your configuration to &lt;code&gt;cluster_config.json&lt;/code&gt; and offers to reuse it on subsequent runs. Ansible tasks check the current state before making changes. MySQL root password handling tries socket authentication first, then falls back to the existing password.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ OS-Aware Automation
&lt;/h3&gt;

&lt;p&gt;The Ansible roles automatically detect &lt;code&gt;ansible_os_family&lt;/code&gt; and execute the appropriate tasks for each distribution. Whether you're running Ubuntu 22.04, Ubuntu 24.04, RHEL 8, or RHEL 9, the same script handles everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ Production-Grade Tuning
&lt;/h3&gt;

&lt;p&gt;This isn't just a "get MySQL running" script. It applies production-grade OS and database tuning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kernel parameters:&lt;/strong&gt; TCP backlog, connection queue, TIME_WAIT reuse, swap minimization, dirty page ratios, file descriptor limits, and async I/O limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL limits:&lt;/strong&gt; 65,535 open files, 65,535 processes, unlimited memory lock&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NUMA interleaving:&lt;/strong&gt; MySQL runs with &lt;code&gt;numactl --interleave=all&lt;/code&gt; for optimal memory access on multi-socket servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;InnoDB buffer pool:&lt;/strong&gt; Automatically sized to 80% of total RAM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GTID-based replication:&lt;/strong&gt; Required for InnoDB Cluster and enabled automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binary log management:&lt;/strong&gt; Automatic purge after 7 days&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ✅ Security by Default
&lt;/h3&gt;

&lt;p&gt;All generated configuration files are created with mode &lt;code&gt;0600&lt;/code&gt;. Passwords are never logged (Ansible &lt;code&gt;no_log: true&lt;/code&gt;). Terminal input is masked for all password prompts. Temporary credential files in &lt;code&gt;/tmp/&lt;/code&gt; are cleaned up after cluster creation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Connecting Morpheus to Your New Cluster
&lt;/h2&gt;

&lt;p&gt;Once the InnoDB Cluster is running, you need to bridge it with your Morpheus application nodes. Here's the complete workflow:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create the Morpheus Database
&lt;/h3&gt;

&lt;p&gt;Log into your new cluster's primary node and create the database and user that Morpheus expects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;mysql&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;morpheus&lt;/span&gt; &lt;span class="nb"&gt;CHARACTER&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;utf8mb4&lt;/span&gt; &lt;span class="k"&gt;COLLATE&lt;/span&gt; &lt;span class="n"&gt;utf8mb4_general_ci&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'morpheus'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'morpheusDbUserPassword'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;morpheus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'morpheus'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;OPTION&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PROCESS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;DATABASES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RELOAD&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'morpheus'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;FLUSH&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Bootstrap MySQL Router on Each App Node
&lt;/h3&gt;

&lt;p&gt;Install MySQL Router on each Morpheus application node and bootstrap it against the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysqlrouter &lt;span class="nt"&gt;--bootstrap&lt;/span&gt; routeruser@192.168.42.100:3306 &lt;span class="nt"&gt;--user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mysqlrouter
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;mysqlrouter &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; systemctl start mysqlrouter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MySQL Router will automatically discover all cluster members and create local read-write (port 6446) and read-only (port 6447) endpoints.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Configure Morpheus
&lt;/h3&gt;

&lt;p&gt;Edit &lt;code&gt;/etc/morpheus/morpheus.rb&lt;/code&gt; on each app node, pointing MySQL to the local router endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'enable'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
&lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'host'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'127.0.0.1'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;6446&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'morpheus_db'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'morpheus'&lt;/span&gt;
&lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'morpheus_db_user'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'morpheus'&lt;/span&gt;
&lt;span class="n"&gt;mysql&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'morpheus_password'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'morpheusDbUserPassword'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reconfigure and proceed with the rest of the Morpheus HA installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;morpheus-ctl reconfigure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this setup, &lt;strong&gt;MySQL Router handles automatic failover&lt;/strong&gt;. If the primary node goes down, Group Replication elects a new primary, and MySQL Router transparently redirects traffic — all without any Morpheus downtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Network Requirements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  MySQL InnoDB Cluster Ports (between DB nodes)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3306&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;MySQL Classic Protocol&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;33060&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;MySQL X Protocol&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;33061&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;Group Replication&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Morpheus App Node to MySQL Cluster
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3306&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;MySQL connection (or via Router 6446/6447)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Additional Morpheus HA Ports (for reference)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;443&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;HTTPS (inbound from users/agents)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4369&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;RabbitMQ EPMD (inter-node discovery)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5671/5672&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;RabbitMQ (TLS/non-TLS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9200&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;OpenSearch API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9300&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;OpenSearch inter-node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25672&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;RabbitMQ inter-node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;61613/61614&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;STOMP (non-TLS/TLS)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  PaaS Alternatives
&lt;/h2&gt;

&lt;p&gt;The HPE documentation also lists supported PaaS offerings as alternatives to self-managed MySQL clusters:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cloud&lt;/th&gt;
&lt;th&gt;Database (MySQL)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;td&gt;Amazon Aurora&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GCP&lt;/td&gt;
&lt;td&gt;MySQL Instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OCI&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alibaba&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As you can see, PaaS support for MySQL is limited to AWS and GCP. For &lt;strong&gt;on-premises deployments, private cloud, or any other environment&lt;/strong&gt;, a self-managed MySQL cluster is your only option — which is exactly what this tool provides.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Setting up a MySQL InnoDB Cluster for HPE Morpheus Enterprise HA shouldn't be a multi-day project requiring deep MySQL expertise. The &lt;strong&gt;morpheus-innodb-cluster&lt;/strong&gt; tool reduces it to a single command and a 10-minute guided wizard. It handles OS detection, prerequisite installation, interactive configuration, pre-flight validation, Ansible-driven deployment, and post-deployment reporting — all in one cohesive workflow.&lt;/p&gt;

&lt;p&gt;While HPE Morpheus Enterprise excels at automatically clustering OpenSearch and providing the framework for RabbitMQ clustering, the Transactional Database Tier remains the one piece that administrators must provision themselves. This tool fills that gap, giving you a consistent, repeatable, production-ready MySQL InnoDB Cluster every time.&lt;/p&gt;

&lt;p&gt;Whether you're deploying Morpheus HA for the first time or rebuilding your database tier after a migration, you're three commands away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/emrbaykal/morpheus-innodb-cluster.git
&lt;span class="nb"&gt;cd &lt;/span&gt;morpheus-innodb-cluster
&lt;span class="nb"&gt;sudo &lt;/span&gt;python3 innodb_cluster_setup.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;If you found this useful, give the &lt;a href="https://github.com/emrbaykal/morpheus-innodb-cluster" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; a ⭐ and feel free to open issues or contribute.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://support.hpe.com/hpesc/public/docDisplay?docId=sd00007510en_us&amp;amp;page=GUID-C1061ACC-BCAF-4F7C-A413-2219EAFB7983.html" rel="noopener noreferrer"&gt;HPE Morpheus Enterprise v8.1.0 — HA Installation Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://support.hpe.com/hpesc/public/docDisplay?docId=sd00007510en_us&amp;amp;page=GUID-2D8A0A86-2231-4239-AB44-5475B4AE0827.html" rel="noopener noreferrer"&gt;HPE Morpheus Enterprise v8.1.0 — 3-Node HA Install Example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/refman/8.4/en/mysql-innodb-cluster-introduction.html" rel="noopener noreferrer"&gt;MySQL InnoDB Cluster Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/mysql-shell/8.4/en/deploying-production-innodb-cluster.html" rel="noopener noreferrer"&gt;MySQL Shell — Deploying Production InnoDB Cluster&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mysql</category>
      <category>ansible</category>
      <category>devops</category>
      <category>python</category>
    </item>
    <item>
      <title>HPE Morpheus Enterprise &amp; VM Essentials SAML Integration with Keycloak: A Complete Technical Guide</title>
      <dc:creator>Emre Baykal</dc:creator>
      <pubDate>Sat, 28 Mar 2026 17:26:07 +0000</pubDate>
      <link>https://dev.to/emre_baykal_a4a7a479d48c5/hpe-morpheus-enterprise-vm-essentials-saml-integration-with-keycloak-a-complete-technical-guide-7a8</link>
      <guid>https://dev.to/emre_baykal_a4a7a479d48c5/hpe-morpheus-enterprise-vm-essentials-saml-integration-with-keycloak-a-complete-technical-guide-7a8</guid>
      <description>&lt;h2&gt;
  
  
  1. Introduction
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1.1 What is SAML 2.0?
&lt;/h3&gt;

&lt;p&gt;SAML (Security Assertion Markup Language) 2.0 is an XML-based open standard for exchanging authentication and authorization data between two parties: an &lt;strong&gt;Identity Provider (IdP)&lt;/strong&gt; that authenticates users, and a &lt;strong&gt;Service Provider (SP)&lt;/strong&gt; that hosts the application. Instead of every application managing its own username/password database, SAML lets you delegate authentication to a central IdP. When a user logs in once at the IdP, they get access to all connected SPs without entering credentials again — this is &lt;strong&gt;Single Sign-On (SSO)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In practical terms: the user clicks "Login with SSO" on the application, gets redirected to the IdP login page, authenticates there, and is sent back to the application with a cryptographically signed XML document (the "SAML assertion") that proves who they are and what groups they belong to.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.2 Why Keycloak?
&lt;/h3&gt;

&lt;p&gt;There are several IdP options available (Okta, Azure AD, ADFS, Ping Identity, etc.), so why Keycloak?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open-source and free&lt;/strong&gt; — No per-user licensing costs, which matters at scale&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted&lt;/strong&gt; — Full control over your identity infrastructure; no dependency on external SaaS providers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protocol versatility&lt;/strong&gt; — Supports SAML 2.0, OpenID Connect, and OAuth 2.0 in a single platform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LDAP/AD federation&lt;/strong&gt; — Connects directly to Active Directory without migrating users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes-native&lt;/strong&gt; — Runs well as a containerized deployment with built-in clustering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CNCF project&lt;/strong&gt; — Active community, regular releases, and long-term sustainability&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1.3 What We Will Build
&lt;/h3&gt;

&lt;p&gt;Enterprise environments demand centralized identity management. When you operate HPE Morpheus Enterprise as your cloud management platform, integrating it with a dedicated Identity Provider (IdP) via SAML 2.0 eliminates password sprawl and gives you single sign-on (SSO) across the entire infrastructure stack.&lt;/p&gt;

&lt;p&gt;In this post, we walk through the entire journey: understanding how Morpheus handles SAML under the hood, deploying Keycloak on Kubernetes as your IdP, wiring the two together, and diagnosing issues when things do not go as planned. Every configuration value and YAML snippet comes from a real lab deployment, so you can replicate it in your own environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lab environment:&lt;/strong&gt; Kubernetes (single-node / Minikube compatible), Keycloak 26.x, Morpheus Enterprise 8.x, Active Directory for user federation.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Morpheus SAML Architecture
&lt;/h2&gt;

&lt;p&gt;HPE Morpheus Enterprise supports SAML 2.0 as an Identity Source type within its Administration panel. Understanding how Morpheus participates in the SAML exchange is essential before configuring anything on the Keycloak side.&lt;/p&gt;

&lt;p&gt;📸 IMAGE: Morpheus &amp;gt; Login Screen&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frpkhxjfj01eoynggqsyg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frpkhxjfj01eoynggqsyg.png" alt=" " width="695" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📸 IMAGE: Morpheus &amp;gt; Forward Keycloak Login Screen&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs8v0yna8llvpcbe0imcc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs8v0yna8llvpcbe0imcc.png" alt=" " width="695" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1 Morpheus as a SAML Service Provider (SP)
&lt;/h3&gt;

&lt;p&gt;Morpheus acts exclusively as a &lt;strong&gt;SAML Service Provider&lt;/strong&gt;. It does not function as an Identity Provider itself. When a user attempts to log in via SSO, Morpheus generates a SAML AuthnRequest, redirects the browser to the configured IdP, and then consumes the SAML Response (assertion) returned by the IdP.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2 Key SAML Endpoints in Morpheus
&lt;/h3&gt;

&lt;p&gt;When you create a SAML Identity Source in Morpheus, the platform automatically generates two critical values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SP Entity ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A unique identifier for Morpheus as a Service Provider. Auto-generated from the hostname, e.g. &lt;code&gt;https://morpheus-url/saml/&amp;lt;uniqueID&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SP ACS URL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The callback URL where the IdP posts the SAML Response after authentication, e.g. &lt;code&gt;https://morpheus-url/externalLogin/callback/&amp;lt;uniqueID&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Login Redirect URL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The IdP's SAML SSO endpoint where Morpheus sends the AuthnRequest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SAML Logout Redirect URL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The IdP's SAML SLO endpoint for single logout&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The SP Entity ID and ACS URL are generated only after you save the Identity Source for the first time. You must save first, then copy these values to configure the IdP.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2.3 SAML Request and Response Configuration
&lt;/h3&gt;

&lt;p&gt;Morpheus provides granular control over how SAML requests are signed and responses are validated:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Options &amp;amp; Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SAML Request&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No Signature / Self Signed / Custom RSA Signature — Controls whether AuthnRequest messages are signed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SAML Response&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Do Not Validate / Validate Assertion Signature — Controls signature validation on the IdP's assertion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;POST Binding Mode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ON/OFF — Uses HTTP-POST binding instead of HTTP-Redirect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Includes SAML Request Parameter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes/No — Whether the SAML request is included in the redirect&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  2.4 Assertion Attribute Mappings
&lt;/h3&gt;

&lt;p&gt;Morpheus maps SAML assertion attributes to internal user fields:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Morpheus Field&lt;/th&gt;
&lt;th&gt;Expected SAML Attribute&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Given Name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;firstName&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surname&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lastName&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;email&lt;/code&gt; (or NameID)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  2.5 Role Mapping Mechanism
&lt;/h3&gt;

&lt;p&gt;Morpheus supports role-based access control through SAML group assertions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role Mapping Field&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Default Role&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The role assigned to all authenticated users (e.g., Standard User)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Role Attribute Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The SAML attribute containing group/role info (e.g., &lt;code&gt;groups&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Required Role Attribute Value&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A group name the user must belong to for authorization (e.g., &lt;code&gt;mspusers&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;📸 IMAGE: Morpheus &amp;gt; Identity Sources &amp;gt; Keycloak&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4d8izve0axdxvfqj0fu7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4d8izve0axdxvfqj0fu7.png" alt="Morpheus SAML SSO Configuration" width="722" height="868"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Keycloak-SAML Ecosystem: How It Works
&lt;/h2&gt;

&lt;p&gt;Keycloak is an open-source Identity and Access Management (IAM) solution maintained by the CNCF. It supports OpenID Connect, OAuth 2.0, and SAML 2.0 protocols natively. In our setup, Keycloak serves as the SAML Identity Provider (IdP) that authenticates users against an Active Directory backend via LDAP federation.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 Core Keycloak Concepts
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Realm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A tenant-level isolation boundary. Each realm has its own users, clients, roles, and identity providers. Our realm: &lt;code&gt;morpheus-lab&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Client&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;An application that delegates authentication to Keycloak. Morpheus is registered as a SAML client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User Federation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Allows Keycloak to pull users from LDAP/Active Directory without duplicating credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Protocol Mappers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Transform user attributes and group memberships into SAML assertions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Roles &amp;amp; Groups&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Realm-level and client-level roles. AD groups can be synced and mapped into assertions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  3.2 SAML 2.0 Authentication Flow (SP-Initiated SSO)
&lt;/h3&gt;

&lt;p&gt;The diagram below illustrates the SP-Initiated SAML SSO flow between Morpheus and Keycloak:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdz38hluaexap97meqmwe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdz38hluaexap97meqmwe.png" alt="SAML 2.0 SP-Initiated SSO Flow Diagram" width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step-by-step:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user navigates to the Morpheus login page and clicks the SSO login button.&lt;/li&gt;
&lt;li&gt;Morpheus generates a SAML AuthnRequest and redirects the user's browser to Keycloak's SAML endpoint: &lt;code&gt;https://keycloak-server:30443/realms/morpheus-lab/protocol/saml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Keycloak presents the login form. The user enters their AD credentials.&lt;/li&gt;
&lt;li&gt;Keycloak authenticates the user against the federated LDAP/AD backend.&lt;/li&gt;
&lt;li&gt;On successful authentication, Keycloak constructs a &lt;strong&gt;SAML Response&lt;/strong&gt; containing signed assertions with user attributes (&lt;code&gt;firstName&lt;/code&gt;, &lt;code&gt;lastName&lt;/code&gt;) and group memberships (&lt;code&gt;groups&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Keycloak POSTs the SAML Response to the Morpheus ACS URL.&lt;/li&gt;
&lt;li&gt;Morpheus validates the assertion signature, maps attributes and roles, creates or updates the user session, and grants access.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3.3 Authentication vs Authorization
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Authentication (Who are you?)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keycloak acts as the authentication broker&lt;/strong&gt;, sitting between the application (Morpheus) and the identity store (Active Directory).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User Federation (LDAP)&lt;/strong&gt; allows Keycloak to verify credentials against AD without storing passwords locally. Keycloak performs LDAP BIND operations to authenticate users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MFA&lt;/strong&gt; can be layered on top through Keycloak's authentication flow configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session Management:&lt;/strong&gt; Once authenticated, Keycloak creates a session. Subsequent SAML requests within the session's lifetime do not require re-authentication (SSO behavior).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvef5xllize3ka1tvqyt6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvef5xllize3ka1tvqyt6.png" alt="Keycloak authentication flow diagram" width="800" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Authorization (What can you do?)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Keycloak syncs AD groups via the &lt;strong&gt;LDAP Group Mapper&lt;/strong&gt; (&lt;code&gt;mspusers&lt;/code&gt;, &lt;code&gt;apparchitech&lt;/code&gt;, &lt;code&gt;selfservice&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;SAML Group List Mapper&lt;/strong&gt; serializes group memberships into the SAML assertion as a &lt;code&gt;groups&lt;/code&gt; attribute.&lt;/li&gt;
&lt;li&gt;Morpheus reads the &lt;code&gt;groups&lt;/code&gt; attribute and maps it to internal roles:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mspusers&lt;/code&gt; → authorized user (Required Role)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apparchitech&lt;/code&gt; → Application Architect role&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;selfservice&lt;/code&gt; → Self-Service User role&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Users not in the required group are &lt;strong&gt;denied access&lt;/strong&gt; even if authentication succeeds.&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.4 Single Logout (SLO) Flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User clicks Logout → Morpheus sends LogoutRequest → Keycloak terminates session
→ Keycloak POSTs LogoutResponse to /login/auth → User lands on login page
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; The Logout Service POST Binding URL must be set to &lt;code&gt;/login/auth&lt;/code&gt; (the login page), NOT the ACS callback URL. Morpheus's ACS handler cannot process LogoutResponse objects and throws a &lt;code&gt;GroovyCastException&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  4. Deploying Keycloak on Kubernetes
&lt;/h2&gt;

&lt;p&gt;This section walks through deploying a production-grade Keycloak cluster on Kubernetes using a single YAML manifest.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The complete Kubernetes manifest used in this guide is available on GitHub:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/emrbaykal/morpheus-k8/blob/main/sso-k8s/keycloak/keycloak.yaml" rel="noopener noreferrer"&gt;keycloak.yaml on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4.1 Architecture Overview
&lt;/h3&gt;

&lt;p&gt;The deployment stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keycloak StatefulSet&lt;/strong&gt; (2 replicas) with Infinispan clustering for HA&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL Deployment&lt;/strong&gt; with PersistentVolumeClaim (10Gi)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes Secret&lt;/strong&gt; for admin and database passwords&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-signed TLS certificates&lt;/strong&gt; generated by init containers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NodePort Service&lt;/strong&gt; for external HTTPS access on port 30443&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headless Service&lt;/strong&gt; for Infinispan/JGroups cluster discovery&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4.2 Prerequisites
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A running Kubernetes cluster or Minikube instance&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kubectl&lt;/code&gt; configured and connected to your cluster&lt;/li&gt;
&lt;li&gt;At least &lt;strong&gt;4GB RAM&lt;/strong&gt; and &lt;strong&gt;2 CPU cores&lt;/strong&gt; available for the Keycloak + PostgreSQL pods&lt;/li&gt;
&lt;li&gt;A storage provisioner (default StorageClass or Rook-Ceph). For Minikube, enable it with:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;minikube addons &lt;span class="nb"&gt;enable &lt;/span&gt;default-storageclass
minikube addons &lt;span class="nb"&gt;enable &lt;/span&gt;storage-provisioner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Minikube users:&lt;/strong&gt; Start Minikube with sufficient resources:&lt;br&gt;
&lt;code&gt;minikube start --cpus=4 --memory=8192 --driver=docker&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Network &amp;amp; DNS requirements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Kubernetes node IP must be reachable from the Morpheus server (for SAML redirects)&lt;/li&gt;
&lt;li&gt;The Morpheus server hostname (e.g., &lt;code&gt;morpheus-server&lt;/code&gt;) must be resolvable from both the user's browser &lt;strong&gt;and&lt;/strong&gt; the Keycloak pods. If you're using a local domain, add entries to &lt;code&gt;/etc/hosts&lt;/code&gt; on the machines or configure your internal DNS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firewall rules:&lt;/strong&gt; Ensure these ports are open between the components:&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Destination&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;User Browser&lt;/td&gt;
&lt;td&gt;Morpheus Server&lt;/td&gt;
&lt;td&gt;443&lt;/td&gt;
&lt;td&gt;HTTPS&lt;/td&gt;
&lt;td&gt;Access Morpheus UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User Browser&lt;/td&gt;
&lt;td&gt;K8s Node&lt;/td&gt;
&lt;td&gt;30443&lt;/td&gt;
&lt;td&gt;HTTPS&lt;/td&gt;
&lt;td&gt;Keycloak login page (SAML redirect)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Morpheus Server&lt;/td&gt;
&lt;td&gt;K8s Node&lt;/td&gt;
&lt;td&gt;30443&lt;/td&gt;
&lt;td&gt;HTTPS&lt;/td&gt;
&lt;td&gt;SAML backchannel (POST binding)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;K8s Pod Network&lt;/td&gt;
&lt;td&gt;AD Domain Controller&lt;/td&gt;
&lt;td&gt;389&lt;/td&gt;
&lt;td&gt;LDAP&lt;/td&gt;
&lt;td&gt;User federation / authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Active Directory requirements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;dedicated service account&lt;/strong&gt; for Keycloak LDAP binding (e.g., &lt;code&gt;svc-keycloak&lt;/code&gt;). This account needs &lt;strong&gt;read-only access&lt;/strong&gt; to the Users container (&lt;code&gt;CN=Users,DC=yourdomain,DC=local&lt;/code&gt;). It does not need Domain Admin privileges — basic "Read all user information" permission is sufficient&lt;/li&gt;
&lt;li&gt;AD groups that will map to Morpheus roles (e.g., &lt;code&gt;mspusers&lt;/code&gt;, &lt;code&gt;apparchitech&lt;/code&gt;, &lt;code&gt;selfservice&lt;/code&gt;) must exist and users must be members of the appropriate groups&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4.3 Step 1: Prepare Secrets
&lt;/h3&gt;

&lt;p&gt;The YAML uses a Kubernetes Secret to store sensitive credentials. The passwords are Base64-encoded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Encode your passwords&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'YourAdminPassword!'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt;
&lt;span class="c"&gt;# Output: WW91*****UGFzc3***cmQh&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'YourDBPassword!'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt;
&lt;span class="c"&gt;# Output: WW9************mQh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Secret resource in the YAML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keycloak-secret&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keycloak&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Opaque&lt;/span&gt;
&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;admin-password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;base64-encoded-admin-password&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;db-password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;base64-encoded-db-password&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Never commit plain-text passwords to version control. Use a secrets manager (Vault, Sealed Secrets) in production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4.4 Step 2: Namespace and Storage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Namespace&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keycloak&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app.kubernetes.io/part-of&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sso-stack&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PersistentVolumeClaim&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres-pvc&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keycloak&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;accessModes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ReadWriteOnce&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.5 Step 3: PostgreSQL Database
&lt;/h3&gt;

&lt;p&gt;Keycloak requires an external database in production mode. Key points from our manifest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keycloak&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD&lt;/span&gt;
        &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;secretKeyRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keycloak-secret&lt;/span&gt;
            &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db-password&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keycloak&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PGDATA&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/lib/postgresql/data/pgdata&lt;/span&gt;
    &lt;span class="na"&gt;readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;exec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-U"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keycloak"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
      &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256Mi&lt;/span&gt;
        &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;100m&lt;/span&gt;
      &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512Mi&lt;/span&gt;
        &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;500m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.6 Step 4: Keycloak StatefulSet
&lt;/h3&gt;

&lt;p&gt;The Keycloak deployment uses a &lt;strong&gt;StatefulSet with 2 replicas&lt;/strong&gt; for high availability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Init Containers:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;wait-for-postgres:&lt;/strong&gt; Polls PostgreSQL port 5432 before Keycloak starts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;generate-tls-cert:&lt;/strong&gt; Generates a self-signed TLS certificate (10 years validity)
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;initContainers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;generate-tls-cert&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alpine:3.19&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;apk add --no-cache openssl&lt;/span&gt;
        &lt;span class="s"&gt;openssl req -x509 -nodes -days 3650 \&lt;/span&gt;
          &lt;span class="s"&gt;-newkey rsa:2048 \&lt;/span&gt;
          &lt;span class="s"&gt;-keyout /certs/tls.key \&lt;/span&gt;
          &lt;span class="s"&gt;-out /certs/tls.crt \&lt;/span&gt;
          &lt;span class="s"&gt;-subj "/CN=keycloak-morpheus-lab/O=Lab/C=TR"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Environment Variables:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KC_BOOTSTRAP_ADMIN_USERNAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Initial admin username (&lt;code&gt;admin&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KC_BOOTSTRAP_ADMIN_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Admin password from Secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;KC_DB&lt;/code&gt; / &lt;code&gt;KC_DB_URL_HOST&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Database type and host (&lt;code&gt;postgres&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KC_HTTPS_CERTIFICATE_FILE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Path to TLS certificate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KC_HTTPS_CERTIFICATE_KEY_FILE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Path to TLS key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KC_CACHE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clustering mode (&lt;code&gt;ispn&lt;/code&gt; = Infinispan)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KC_HOSTNAME_STRICT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Disabled for NodePort/self-signed setups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KC_FEATURES&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Additional features (&lt;code&gt;token-exchange&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KC_HEALTH_ENABLED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enables &lt;code&gt;/health/*&lt;/code&gt; endpoints for probes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Health Probes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;startupProbe:&lt;/strong&gt; &lt;code&gt;/health/started&lt;/code&gt; — 1s interval, 600 retries (10-minute startup window)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;readinessProbe:&lt;/strong&gt; &lt;code&gt;/health/ready&lt;/code&gt; — Every 10s, 3 failures to mark unready&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;livenessProbe:&lt;/strong&gt; &lt;code&gt;/health/live&lt;/code&gt; — Every 10s, 3 failures to restart pod&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4.7 Step 5: Services
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Type &amp;amp; Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;keycloak&lt;/code&gt; (ClusterIP)&lt;/td&gt;
&lt;td&gt;Internal access: ports 8080 (HTTP) and 8443 (HTTPS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;keycloak-discovery&lt;/code&gt; (Headless)&lt;/td&gt;
&lt;td&gt;JGroups cluster member discovery on port 7800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;keycloak-nodeport&lt;/code&gt; (NodePort)&lt;/td&gt;
&lt;td&gt;External access: port &lt;strong&gt;30080&lt;/strong&gt; (HTTP) and &lt;strong&gt;30443&lt;/strong&gt; (HTTPS)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  4.8 Step 6: Deploy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Apply the complete manifest&lt;/span&gt;
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; keycloak.yaml

&lt;span class="c"&gt;# Watch the rollout&lt;/span&gt;
kubectl rollout status statefulset/keycloak &lt;span class="nt"&gt;-n&lt;/span&gt; keycloak &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10m

&lt;span class="c"&gt;# Verify all pods are running&lt;/span&gt;
kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; keycloak

&lt;span class="c"&gt;# Access the admin console&lt;/span&gt;
&lt;span class="c"&gt;# https://&amp;lt;node-ip&amp;gt;:30443/admin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; For Minikube, use &lt;code&gt;minikube ip&lt;/code&gt; to get the node IP. For a multi-node cluster, any node's IP will work with NodePort.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;📸 IMAGE: Terminal output of kubectl get pods -n keycloak showing all pods running&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg7ezfolhb43tk9nfz9rz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg7ezfolhb43tk9nfz9rz.png" alt="Terminal output of kubectl get pods -n keycloak showing all pods running" width="800" height="211"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Keycloak-Morpheus SAML Integration Guide
&lt;/h2&gt;

&lt;p&gt;With Keycloak deployed and running, we can now configure the SAML integration. The configuration involves both the Keycloak side (IdP) and the Morpheus side (SP), and the &lt;strong&gt;order matters&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important — Configuration Order:&lt;/strong&gt;&lt;br&gt;
There is a chicken-and-egg situation here. Keycloak needs the &lt;strong&gt;SP Entity ID&lt;/strong&gt; and &lt;strong&gt;ACS URL&lt;/strong&gt; to create the SAML client, but these values are auto-generated by Morpheus only after you save an Identity Source. The correct order is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create the Keycloak Realm and LDAP Federation first (Steps 1-2)&lt;/li&gt;
&lt;li&gt;Create a &lt;strong&gt;preliminary&lt;/strong&gt; Identity Source in Morpheus to obtain the SP Entity ID and ACS URL (Step 3)&lt;/li&gt;
&lt;li&gt;Use those values to create the SAML Client in Keycloak (Steps 4-7)&lt;/li&gt;
&lt;li&gt;Come back to Morpheus and complete the Identity Source configuration (Step 8)&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  5.1 Step 1: Create the Keycloak Realm
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Log in to Keycloak Admin Console at &lt;code&gt;https://keycloak-server:30443/admin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click the realm dropdown (top-left) and select "Create Realm"&lt;/li&gt;
&lt;li&gt;Set Realm Name to: &lt;code&gt;morpheus-lab&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click Create&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;📸 IMAGE: Keycloak Admin Console &amp;gt; Manage Realm &amp;gt; Create Realm&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv5c3rfxj98koe6lad6ng.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv5c3rfxj98koe6lad6ng.png" alt="Keycloak Admin Console &amp;gt; Create Realm dialog" width="800" height="564"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 Step 2: Configure LDAP User Federation
&lt;/h3&gt;

&lt;p&gt;Navigate to &lt;strong&gt;morpheus-lab &amp;gt; User Federation &amp;gt; Add New provider &amp;gt; LDAP&lt;/strong&gt; and configure:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LAB AD&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vendor&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Active Directory&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connection URL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ldap://active-directory:389&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bind Type&lt;/td&gt;
&lt;td&gt;&lt;code&gt;simple&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bind DN&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CN=svc-keycloak,CN=Users,DC=domain,DC=domain&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Users DN&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CN=Users,DC=domain,DC=domain&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Username LDAP Attribute&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sAMAccountName&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edit Mode&lt;/td&gt;
&lt;td&gt;&lt;code&gt;READ_ONLY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Import Users&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ON&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Add Group Mapper&lt;/strong&gt; (Mappers &amp;gt; Add mapper):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mapper Type&lt;/td&gt;
&lt;td&gt;&lt;code&gt;group-ldap-mapper&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groups DN&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CN=Users,DC=domain,DC=domian&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Group Name LDAP Attribute&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cn&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Membership LDAP Attribute&lt;/td&gt;
&lt;td&gt;&lt;code&gt;member&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mode&lt;/td&gt;
&lt;td&gt;&lt;code&gt;READ_ONLY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User Roles Retrieve Strategy&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LOAD_GROUPS_BY_MEMBER_ATTRIBUTE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; After saving, click &lt;strong&gt;'Sync all users'&lt;/strong&gt; and &lt;strong&gt;'Sync LDAP groups to Keycloak'&lt;/strong&gt; to import users and groups from Active Directory.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;📸 IMAGE: Keycloak &amp;gt; User Federation &amp;gt; LAB AD settings&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fotbn2eukf4wo109g7s45.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fotbn2eukf4wo109g7s45.png" alt="LDAP 1" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fifwk7n772eavobu2vhax.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fifwk7n772eavobu2vhax.png" alt="LDAP 2" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3 Step 3: Create Preliminary Identity Source in Morpheus (Get SP Entity ID)
&lt;/h3&gt;

&lt;p&gt;Before creating the SAML client in Keycloak, we need to obtain the SP Entity ID and ACS URL from Morpheus. These values are auto-generated and unique to your Morpheus instance.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in to Morpheus at &lt;code&gt;https://morpheus-server&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Administration &amp;gt; Identity Sources&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;+ Add Identity Source&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;Type&lt;/strong&gt; to &lt;code&gt;SAML SSO&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;Name&lt;/strong&gt; to &lt;code&gt;Keycloak-SSO&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;For now, enter any placeholder URL in &lt;strong&gt;Login Redirect URL&lt;/strong&gt; (e.g., &lt;code&gt;https://placeholder.local&lt;/code&gt;) — we will update this later&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save Changes&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After saving, Morpheus generates and displays two critical values in the Identity Source list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SP Entity ID&lt;/strong&gt; — e.g., &lt;code&gt;https://morpheus-server/saml/N3h***B***O&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SP ACS URL&lt;/strong&gt; — e.g., &lt;code&gt;https://morpheus-server/externalLogin/callback/N***3***O&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Copy both of these values!&lt;/strong&gt; You will need them in the next step to configure the SAML client in Keycloak. The &lt;code&gt;N3****BO&lt;/code&gt; part is a unique identifier generated by your Morpheus instance — yours will be different.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;📸 IMAGE: Morpheus &amp;gt; Identity Sources list showing the auto-generated SP Entity ID and ACS URL&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F01w6rcum5xhjfzkd2ona.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F01w6rcum5xhjfzkd2ona.png" alt=" " width="731" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5.4 Step 4: Create the SAML Client in Keycloak
&lt;/h3&gt;

&lt;p&gt;Now go back to the Keycloak Admin Console and create the SAML client using the values from the previous step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;morpheus-lab &amp;gt; Clients &amp;gt; Create Client&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Set Client Type to &lt;strong&gt;SAML&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;Client ID&lt;/strong&gt; to the SP Entity ID you copied from Morpheus: &lt;code&gt;https://morpheus-server/saml/N3h**K**5**&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set Name to: &lt;code&gt;Morpheus Enterprise&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Access Settings:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Root URL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://morpheus-server&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Valid Redirect URIs (ACS URL)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://morpheus-server/externalLogin/callback/N3h**K**5**&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Master SAML Processing URL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://morpheus-server/externalLogin/callback/N3h**K**5**&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IDP-Initiated SSO URL Name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;morpheus&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;SAML Capabilities:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Name ID Format&lt;/td&gt;
&lt;td&gt;&lt;code&gt;username&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Force POST Binding&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ON&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Include AuthnStatement&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ON&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Signature &amp;amp; Encryption:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sign Documents&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ON&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sign Assertions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ON&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signature Algorithm&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RSA_SHA256&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Client Signature Required&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;OFF&lt;/code&gt; (CRITICAL!)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encrypt Assertions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OFF&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt; &lt;code&gt;Client Signature Required&lt;/code&gt; must be &lt;strong&gt;OFF&lt;/strong&gt;. Morpheus signs requests with a self-signed certificate that does not match the certificate registered in Keycloak. If this is ON, every SAML request from Morpheus will be rejected.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;📸 IMAGE: Keycloak &amp;gt; Clients &amp;gt; Morpheus Enterprise &amp;gt; Settings&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi0q4zq6rljlnm1qyut85.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi0q4zq6rljlnm1qyut85.png" alt=" " width="800" height="656"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5.5 Step 5: Configure Logout (Advanced Settings)
&lt;/h3&gt;

&lt;p&gt;Navigate to &lt;strong&gt;Advanced tab &amp;gt; Fine Grain SAML Endpoint Configuration:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Logout Service POST Binding URL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://morpheus-server/login/auth&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This is crucial!&lt;/strong&gt; Setting this URL to &lt;code&gt;/login/auth&lt;/code&gt; prevents the &lt;code&gt;GroovyCastException&lt;/code&gt; bug. The Morpheus ACS callback handler cannot process SAML LogoutResponse objects.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  5.6 Step 6: Configure SAML Mappers
&lt;/h3&gt;

&lt;p&gt;Navigate to &lt;strong&gt;Client Scopes &amp;gt; dedicated &amp;gt; Mappers&lt;/strong&gt; and add:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mapper 1: Groups (Group List)&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mapper Type&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Group list&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SAML Attribute Name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;groups&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SAML Attribute NameFormat&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Basic&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single Group Attribute&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OFF&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full Group Path&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OFF&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Mapper 2: firstName (User Attribute)&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mapper Type&lt;/td&gt;
&lt;td&gt;&lt;code&gt;User Attribute&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User Attribute&lt;/td&gt;
&lt;td&gt;&lt;code&gt;firstName&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SAML Attribute Name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;firstName&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Mapper 3: lastName (User Attribute)&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mapper Type&lt;/td&gt;
&lt;td&gt;&lt;code&gt;User Attribute&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User Attribute&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lastName&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SAML Attribute Name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lastName&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;📸 IMAGE: Keycloak &amp;gt; Client Scopes &amp;gt; dedicated &amp;gt; Mappers list&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgjq2jo6p96otyqp3i2xb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgjq2jo6p96otyqp3i2xb.png" alt=" " width="800" height="143"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5.7 Step 7: Copy the Realm Certificate
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;morpheus-lab &amp;gt; Realm Settings &amp;gt; Keys&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Find the &lt;strong&gt;RS256&lt;/strong&gt; key row and click the &lt;strong&gt;Certificate&lt;/strong&gt; button&lt;/li&gt;
&lt;li&gt;Copy the entire certificate string&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;📸 IMAGE: Keycloak &amp;gt; Realm Settings &amp;gt; Keys &amp;gt; RS256 certificate&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnlg74czikm8u6dks5cpv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnlg74czikm8u6dks5cpv.png" alt=" " width="800" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5.8 Step 8: Complete the Morpheus Identity Source Configuration
&lt;/h3&gt;

&lt;p&gt;Now go back to the &lt;strong&gt;preliminary Identity Source&lt;/strong&gt; you created in Step 3 and update it with the real values. Navigate to &lt;strong&gt;Administration &amp;gt; Identity Sources &amp;gt; Keycloak-SSO &amp;gt; Edit&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Login Redirect URL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://keycloak-server:30443/realms/morpheus-lab/protocol/saml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SAML Logout Redirect URL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://keycloak-server:30443/realms/morpheus-lab/protocol/saml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Includes SAML Request Parameter&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Yes&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST Binding Mode&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ON&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SAML Request&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Self Signed&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SAML Response&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Validate Assertion Signature&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SAML Response Public Key&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(paste RS256 certificate from Keycloak)&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Assertion Attribute Mappings:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Morpheus Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Given Name Attribute Name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;firstName&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surname Attribute Name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lastName&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Role Mappings:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Morpheus Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default Role&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Standard User&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Role Attribute Name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;groups&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required Role Attribute Value&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mspusers&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application Architect Role&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apparchitech&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self Service User Role&lt;/td&gt;
&lt;td&gt;&lt;code&gt;selfservice&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Click &lt;strong&gt;Save Changes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;📸 IMAGE: Morpheus &amp;gt; Identity Sources &amp;gt; Keycloak-SSO configuration&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3zd2fdz1gjw1za4ofo7t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3zd2fdz1gjw1za4ofo7t.png" alt=" " width="718" height="855"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5.9 Step 9: Test the Integration
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open a new browser / incognito window&lt;/li&gt;
&lt;li&gt;Navigate to &lt;code&gt;https://morpheus-server&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click the SSO login option (Keycloak-SSO should appear)&lt;/li&gt;
&lt;li&gt;You will be redirected to Keycloak's login page&lt;/li&gt;
&lt;li&gt;Enter an AD username (e.g., &lt;code&gt;emre.baykal&lt;/code&gt;) and password&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  6. On success, you will be redirected back to Morpheus and logged in
&lt;/h2&gt;

&lt;h2&gt;
  
  
  6. Troubleshooting SAML Integration
&lt;/h2&gt;

&lt;p&gt;SAML integrations can fail silently or produce cryptic errors. This section covers the most common issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.1 Diagnostic Tools
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Usage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;SAML Tracer&lt;/strong&gt; (Browser Extension)&lt;/td&gt;
&lt;td&gt;Captures SAML requests/responses in real-time. Available for Firefox and Chrome&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Keycloak Events&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enable in Realm Settings &amp;gt; Events. Shows auth attempts and errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Keycloak Logs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;kubectl logs statefulset/keycloak -n keycloak -f&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Morpheus Logs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/var/log/morpheus/morpheus-ui/current&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Base64 Decoder&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;`echo '' \&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  6.2 Common Issues and Solutions
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Issue 1: 'Invalid Requester' Error
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Symptom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Keycloak returns 'Invalid requester' or 'Client not found'&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cause&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SP Entity ID in Morpheus doesn't match Client ID in Keycloak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Copy the exact SP Entity ID from Morpheus Identity Source and use it as Client ID in Keycloak&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Issue 2: Signature Validation Failure
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Symptom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;'Signature validation failed' or 'Invalid signature'&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cause&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SAML Response Public Key in Morpheus doesn't match Keycloak's RS256 certificate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Copy fresh certificate from Keycloak &amp;gt; Realm Settings &amp;gt; Keys &amp;gt; RS256 &amp;gt; Certificate. &lt;strong&gt;Must be updated after every Keycloak redeployment&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Issue 3: User Authenticated but Access Denied
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Symptom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;User authenticates at Keycloak but gets 'Access Denied' in Morpheus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cause&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;User not in the required group, or Group List mapper missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1) Verify user is in {% raw %}&lt;code&gt;mspusers&lt;/code&gt; AD group. 2) Check Group List mapper in client scopes. 3) Use SAML Tracer to verify &lt;code&gt;groups&lt;/code&gt; attribute in assertion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Issue 4: GroovyCastException on Logout
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Symptom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;500 error on logout with &lt;code&gt;GroovyCastException&lt;/code&gt; in logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cause&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LogoutResponse sent to ACS URL instead of login page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Set Logout Service POST Binding URL to &lt;code&gt;https://morpheus-server/login/auth&lt;/code&gt; in Keycloak Advanced settings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Issue 5: Login Loop / Redirect Loop
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Symptom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser keeps redirecting between Morpheus and Keycloak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cause&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mixed HTTP/HTTPS, clock skew, or invalid ACS URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1) Ensure both use HTTPS. 2) Verify NTP sync (SAML assertions are time-sensitive). 3) Check Valid Redirect URIs matches exactly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Issue 6: Attributes Not Mapped
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Symptom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;firstName/lastName are empty, roles not assigned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cause&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SAML mappers missing or attribute names don't match&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1) Add User Attribute mappers for &lt;code&gt;firstName&lt;/code&gt; and &lt;code&gt;lastName&lt;/code&gt;. 2) Names are &lt;strong&gt;case-sensitive&lt;/strong&gt;. 3) Use SAML Tracer to verify attributes in assertion XML&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Issue 7: LDAP Users Not Appearing
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Symptom&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No users appear after LDAP federation setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cause&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Wrong Bind DN, Users DN, or network connectivity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Solution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1) Test connectivity from pod. 2) Verify Bind DN has read access. 3) Click 'Sync all users'. 4) Check Keycloak logs for LDAP errors&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  6.3 SAML Assertion Debugging Checklist
&lt;/h3&gt;

&lt;p&gt;When debugging, use SAML Tracer to capture the assertion and verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NameID:&lt;/strong&gt; Present and in expected format (&lt;code&gt;username&lt;/code&gt;)?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issuer:&lt;/strong&gt; Matches the Keycloak realm URL?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AudienceRestriction:&lt;/strong&gt; Audience matches Morpheus SP Entity ID?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditions/NotBefore/NotOnOrAfter:&lt;/strong&gt; Valid timestamps? Check for clock skew&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AuthnStatement:&lt;/strong&gt; Present? (Required by Morpheus)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AttributeStatement:&lt;/strong&gt; &lt;code&gt;firstName&lt;/code&gt;, &lt;code&gt;lastName&lt;/code&gt;, &lt;code&gt;groups&lt;/code&gt; attributes present with correct values?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signature:&lt;/strong&gt; Assertion signed? Certificate matches?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6.4 Useful Debug Commands
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check Keycloak pod logs&lt;/span&gt;
kubectl logs &lt;span class="nt"&gt;-f&lt;/span&gt; statefulset/keycloak &lt;span class="nt"&gt;-n&lt;/span&gt; keycloak

&lt;span class="c"&gt;# Decode a SAML Response from browser&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;base64-saml-response&amp;gt;'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; | xmllint &lt;span class="nt"&gt;--format&lt;/span&gt; -

&lt;span class="c"&gt;# Test LDAP connectivity from inside the cluster&lt;/span&gt;
kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; keycloak-0 &lt;span class="nt"&gt;-n&lt;/span&gt; keycloak &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'nc -zv domain-controller 389'&lt;/span&gt;

&lt;span class="c"&gt;# Check Keycloak health&lt;/span&gt;
kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; keycloak-0 &lt;span class="nt"&gt;-n&lt;/span&gt; keycloak &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  curl &lt;span class="nt"&gt;-sk&lt;/span&gt; https://localhost:8443/health/ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7. Conclusion
&lt;/h2&gt;

&lt;p&gt;Integrating Morpheus Enterprise with Keycloak via SAML provides a robust, centralized authentication solution for enterprise cloud management. Key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Morpheus acts as a SAML SP;&lt;/strong&gt; Keycloak acts as the SAML IdP with AD backend&lt;/li&gt;
&lt;li&gt;The Kubernetes deployment uses a &lt;strong&gt;StatefulSet with Infinispan clustering&lt;/strong&gt; for HA&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-signed TLS certificates&lt;/strong&gt; are generated by init containers — no external cert-manager required for lab environments&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Client Signature Required&lt;/code&gt; must be &lt;strong&gt;OFF&lt;/strong&gt; in Keycloak for Morpheus compatibility&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Logout Service POST Binding URL&lt;/strong&gt; must point to &lt;code&gt;/login/auth&lt;/code&gt; to avoid the &lt;code&gt;GroovyCastException&lt;/code&gt; bug&lt;/li&gt;
&lt;li&gt;Always &lt;strong&gt;update the SAML Response Public Key&lt;/strong&gt; in Morpheus after redeploying Keycloak&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have questions or ran into a different issue? Drop a comment below!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by Emre Baykal — March 2026&lt;/em&gt;&lt;/p&gt;

</description>
      <category>keycloak</category>
      <category>sso</category>
      <category>kubernetes</category>
      <category>morpheus</category>
    </item>
  </channel>
</rss>
