Installation

This guide covers deploying the X2A Backstage plugin on OpenShift using Red Hat Developer Hub.

Prerequisites

  • OpenShift cluster access (CRC or production cluster)
  • Cluster-admin rights (for operator installation)
  • oc CLI tool installed and configured(documentation)
  • AWS credentials with access to Bedrock (for LLM functionality)
  • Ansible Automation Platform instance (optional, for publishing roles)

Quick Start

Deploy to any namespace with these simple commands:

# 1. Git clone current x2a-ansible code
git clone https://github.com/x2ansible/x2ansible/
cd x2a-convertor

# 2. Install operator (cluster-scoped, one-time installation)
oc apply -f deploy/operator.yaml

# 3. Create your namespace (or use existing)
oc create namespace <your-namespace>

# 4. Configure and apply secrets
cp deploy/secrets.yaml.template deploy/secrets.yaml

# 5. Deploy application resources
oc apply -n <your-namespace> -f deploy/app.yaml

# Edit deploy/secrets.yaml with your actual credentials
oc apply -n <your-namespace> -f deploy/secrets.yaml

# 6. Get the application URL
oc get route developer-hub -n <your-namespace> -o jsonpath='https://{.spec.host}{"\n"}'

Installation Files

All deployment files are located in the deploy/ directory at the root of the repository.

1. Operator Installation

File: deploy/operator.yaml

This installs the Red Hat Developer Hub operator. This is cluster-scoped and only needs to be installed once.

---
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
  name: developer-hub-operator-subscription
  namespace: openshift-operators
spec:
  channel: "fast-1.9"
  installPlanApproval: Automatic
  name: rhdh
  source: redhat-operators
  sourceNamespace: openshift-marketplace
---
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
  name: devspaces
  namespace: openshift-operators
spec:
  channel: stable
  installPlanApproval: Automatic
  name: devspaces
  source: redhat-operators
  sourceNamespace: openshift-marketplace

Wait for the operator to be ready:

oc get csv -n openshift-operators | grep rhdh

2. Application Deployment

File: deploy/app.yaml

This file contains all the application resources: ConfigMaps, PersistentVolumeClaim, and the Backstage Custom Resource.

All resources intentionally omit the namespace field - specify your desired namespace using the -n flag when applying.

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: x2a-sa
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: x2a-role
rules:
  - apiGroups: ["batch"]
    resources: ["jobs"]
    verbs: ["create", "get", "list", "watch", "delete"]
  - apiGroups: [""]
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["create", "get", "delete", "put"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: x2a-rolebinding
subjects:
  - kind: ServiceAccount
    name: x2a-sa
roleRef:
  kind: Role
  name: x2a-role
  apiGroup: rbac.authorization.k8s.io
---
# ClusterRole/ClusterRoleBinding for x2a-sa to pass the plugin's connection test
# which hardcodes a pod list call against the 'default' namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: x2a-cluster-role
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: x2a-cluster-rolebinding
subjects:
  - kind: ServiceAccount
    name: x2a-sa
    namespace: x2ansible
roleRef:
  kind: ClusterRole
  name: x2a-cluster-role
  apiGroup: rbac.authorization.k8s.io
---
# X2A Backstage Plugin Application Resources
# Apply to any namespace using: oc apply -n <your-namespace> -f app.yaml
#
# ConfigMap: Dynamic Plugins Configuration
kind: ConfigMap
apiVersion: v1
metadata:
  name: dynamic-plugins
  # namespace field intentionally omitted - use -n flag when applying
data:
  dynamic-plugins.yaml: |
    includes:
      - dynamic-plugins.default.yaml
    plugins:
      # The plugin-x2a-dcr is for RHDH 1.9 only. To be replaced by backstage-plugin-auth in 1.10:
      - package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-x2a-dcr:bs_1.49.4__0.2.0"

      # We need https://github.com/backstage/backstage/blob/master/plugins/mcp-actions-backend/CHANGELOG.md#0112-next2 or later
      # which contains required fix: https://github.com/backstage/backstage/issues/33062
      # To match RHDH 1.10 (will be based on Backstage 1.49.4 compatible with mcp-actions-backend:0.1.11), following build is based on mcp-actions-backend:0.1.11 patched for the issues/33062 fix.
      # This image works well in both RHDH 1.9 (Backstage 1.45.3) and the upcoming RHDH 1.10.
      # We should not need this custom build in a RHDH 1.10 releases (will be with mcp-actions-backend:0.1.12 or later).
      - package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/backstage-plugin-mcp-actions-backend:pr_2236__0.1.11"

      # All X2A plugins
      - package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-x2a:bs_1.49.4__1.2.1"
      - package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-x2a-backend:bs_1.49.4__1.4.0"
      - package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-x2a-mcp-extras:bs_1.49.4__0.2.0"
      - package: "oci://ghcr.io/redhat-developer/rhdh-plugin-export-overlays/red-hat-developer-hub-backstage-plugin-scaffolder-backend-module-x2a:bs_1.49.4__0.3.1"

      # In addition, the backstage-plugin-auth-backend 0.25.6 or later is required, should be met since RHDH 1.9. Keeping this note here for a reference in case of issues.

---
# ConfigMap: Application Configuration
kind: ConfigMap
apiVersion: v1
metadata:
  name: app-config-rhdh
data:
  app-config-rhdh.yaml: |

    backend:
      cors:
        origin:
          # for @modelcontextprotocol/inspector, remove if not needed
          - http://localhost:6274
        credentials: true
      actions:
        pluginSources:
          - 'x2a-mcp-extras'
      reading:
        allow:
          - host: raw.githubusercontent.com

    # Disable namespaced tool names for MCP actions
    # Workaround for recent MCP server returning `undefined` for tool namespaces
    # Will be solved by later @backstage/backend-defaults.
    mcpActions:
      namespacedToolNames: false

    integrations:
      github:
      - host: github.com
      # CUSTOMIZE:
      # gitlab:
      # - host: gitlab.com
      # - host: gitlab.internal.io # Self-hosted GitLab
      #   apiBaseUrl: https://gitlab.internal.io/api/v4
      # bitbucketCloud:
      # - host: bitbucket.org

    auth:
      experimentalDynamicClientRegistration:
        # enable the feature
        enabled: true
        allowedRedirectUriPatterns:
          - cursor://*
          # CUSTOMIZE:
          - https://*
          - http://*

      # see https://backstage.io/docs/auth/ to learn about auth providers
      environment: development
      providers:
        # See https://backstage.io/docs/auth/guest/provider
        # guest: {}

        # CUSTOMIZE:
        github:
          development:
            clientId: ${AUTH_GITHUB_CLIENT_ID}
            clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
            signIn:
              resolvers:
                - resolver: usernameMatchingUserEntityName
                  dangerouslyAllowSignInWithoutUserInCatalog: true
        # Bitbucket example for authentication
        # bitbucket:
        #   development:
        #     clientId: ${AUTH_BITBUCKET_CLIENT_ID:-}
        #     clientSecret: ${AUTH_BITBUCKET_CLIENT_SECRET:-}
        #     signIn:
        #       resolvers:
        #         - resolver: usernameMatchingUserEntityAnnotation
        #           dangerouslyAllowSignInWithoutUserInCatalog: true

        # Gitlab example for authentication
        # gitlab:
        #   development:
        #     clientId: ${AUTH_GITLAB_CLIENT_ID}
        #     clientSecret: ${AUTH_GITLAB_CLIENT_SECRET}
        #     signIn:
        #       resolvers:
        #         - resolver: emailMatchingUserEntityProfileEmail
        #           dangerouslyAllowSignInWithoutUserInCatalog: true
    signInPage:
      - github
      # - gitlab
      # - bitbucket
    dynamicPlugins:
      frontend:
        red-hat-developer-hub.backstage-plugin-x2a-dcr:
          dynamicRoutes:
            - path: /oauth2/*
              importName: DcrConsentPage

        red-hat-developer-hub.backstage-plugin-x2a:
          scaffolderFieldExtensions:
            - importName: RepoAuthenticationExtension
          appIcons:
            - name: x2aIcon
              importName: X2AIcon
          dynamicRoutes:
            - path: /x2a/
              importName: X2APage
              menuItem:
                text: Conversion Hub
                icon: x2aIcon

    catalog:
      locations:
        # x2a Create Project template
        - type: file
          target: /opt/app-root/src/dynamic-plugins-root/red-hat-developer-hub-backstage-plugin-scaffolder-backend-module-x2a/templates/conversion-project-template.yaml
          rules:
            - allow: [Template]
    x2a:
      callbackBaseUrl: http://${SERVICE_NAME}.${NAMESPACE}.svc.cluster.local/api/x2a
      kubernetes:
        image: ${X2A_KUBERNETES_IMAGE:-quay.io/x2ansible/x2a-convertor}
        imageTag: ${X2A_KUBERNETES_IMAGE_TAG:-latest}
        ttlSecondsAfterFinished: 86400
        resources:
          requests:
            cpu: 500m
            memory: 1Gi
          limits:
            cpu: 2000m
            memory: 4Gi
      credentials:
        llm:
          LLM_MODEL: ${LLM_MODEL:-anthropic.claude-v2}
          # Example for AWS Bedrock with bearer token:
          AWS_REGION: ${AWS_REGION}
          AWS_BEARER_TOKEN_BEDROCK: ${AWS_BEARER_TOKEN_BEDROCK}
        aap:
          url: ${AAP_URL:-https://aap.example.com}
          orgName: ${AAP_ORG_NAME:-MyOrganization}
          oauthToken: ${AAP_OAUTH_TOKEN:-your-oauth-token}
          skipSSLVerification: ${AAP_SKIP_SSL_VERIFICATION:-false}
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: dynamic-plugins-root
spec:
  accessModes:
    - ReadWriteOnce
  # CUSTOMIZATION: Change storage class for your environment
  # Common values:
  #   - OpenShift CRC: crc-csi-hostpath-provisioner
  #   - AWS EBS: gp2 or gp3
  #   - Azure: managed-premium
  #   - GCP: standard or standard-rwo
  # List available storage classes: oc get storageclass
  # storageClassName: kubevirt-csi-infra-default
  storageClassName: crc-csi-hostpath-provisioner
  resources:
    requests:
      storage: 2Gi
---
apiVersion: rhdh.redhat.com/v1alpha4
kind: Backstage
metadata:
  name: developer-hub
spec:
  deployment:
    patch:
      spec:
        template:
          spec:
            serviceAccountName: x2a-sa
            automountServiceAccountToken: true
            containers:
              - name: backstage-backend
                env:
                  - name: NODE_EXTRA_CA_CERTS
                    value: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
                  - name: NAMESPACE
                    valueFrom:
                      fieldRef:
                        fieldPath: metadata.namespace
                  - name: SERVICE_NAME
                    valueFrom:
                      fieldRef:
                        fieldPath: metadata.labels['rhdh.redhat.com/app']
  application:
    dynamicPluginsConfigMapName: dynamic-plugins
    appConfig:
      mountPath: /opt/app-root/src
      configMaps:
        - name: app-config-rhdh
    extraFiles:
      mountPath: /opt/app-root/src
    extraEnvs:
      secrets:
        - name: x2a-credentials
    route:
      enabled: true

Apply to your namespace:

oc apply -n <your-namespace> -f deploy/app.yaml

3. Secrets Configuration

SECURITY WARNING: Never commit real credentials to git!

File: deploy/secrets.yaml.template

This is a template file with placeholder values. You must create your own secrets.yaml from this template.

# ============================================================================
# WARNING: This is a TEMPLATE file. DO NOT commit actual secrets to git!
# ============================================================================
#
# INSTRUCTIONS:
# 1. Copy this file to secrets.yaml:
#    cp secrets.yaml.template secrets.yaml
#
# 2. Edit secrets.yaml with your actual credentials:
#    vi secrets.yaml
#    (Replace all REPLACE-WITH-YOUR-* placeholders)
#
# 3. Apply to your namespace:
#    oc apply -n <your-namespace> -f secrets.yaml
#
# 4. IMPORTANT: The secrets.yaml file is git-ignored and will NOT be committed
#
# PRODUCTION SECURITY NOTES:
# - For production environments, consider using:
#   * External Secrets Operator (https://external-secrets.io/)
#   * HashiCorp Vault integration
#   * OpenShift Sealed Secrets
#   * Your organization's secret management solution
#
# - Never commit real credentials to version control
# - Rotate credentials regularly
# - Use minimal permissions for AWS IAM and AAP OAuth tokens
# ============================================================================
---
kind: Secret
apiVersion: v1
metadata:
  name: x2a-credentials
type: Opaque
stringData:
  # Model to use - see AWS Bedrock documentation for available models
  # Examples:
  #   - anthropic.claude-3-7-sonnet-20250219-v1:0 (recommended)
  #   - anthropic.claude-3-5-sonnet-20241022-v2:0
  LLM_MODEL: "anthropic.claude-3-7-sonnet-20250219-v1:0"

  # AWS Region where Bedrock is available
  # Common values: us-east-1, us-west-2, eu-west-1
  AWS_REGION: "us-east-1"

  # AWS IAM Credentials with Bedrock access
  # Required permissions: bedrock:InvokeModel
  AWS_ACCESS_KEY_ID: "REPLACE-WITH-YOUR-AWS-ACCESS-KEY"
  AWS_SECRET_ACCESS_KEY: "REPLACE-WITH-YOUR-AWS-SECRET-KEY"

  # ========================================
  # Ansible Automation Platform (Optional)
  # ========================================
  # Only required if:
  # - You're using the "publish" feature to push roles to AAP
  # - You want to take advantage of deduplication on the migrate phase.
  # If not using AAP, you can leave these as placeholders
  AAP_URL: "https://your-aap-instance.com"
  AAP_ORG_NAME: "your-org-name"
  AAP_OAUTH_TOKEN: "REPLACE-WITH-YOUR-AAP-TOKEN"


  # GitHub auth:
  # Docs: https://x2ansible.github.io/ui/authentication.html#github
  # Example Config:
  # AUTH_GITHUB_CLIENT_ID: ""
  # AUTH_GITHUB_CLIENT_SECRET: ""

  # Bitbucket auth:
  # Docs: https://x2ansible.github.io/ui/authentication.html#bitbucket
  # Example Config:
  # AUTH_BITBUCKET_CLIENT_ID: ""
  # AUTH_BITBUCKET_CLIENT_SECRET: ""
  
  # Gitlab auth:
  # Docs: https://x2ansible.github.io/ui/authentication.html#gitlab
  # Example config:
  # AUTH_GITLAB_CLIENT_ID: ""
  # AUTH_GITLAB_CLIENT_SECRET: ""

Steps to configure secrets:

  1. Copy the template:
    cp deploy/secrets.yaml.template deploy/secrets.yaml
    
  2. Edit with your credentials:
    vi deploy/secrets.yaml
    

    Replace all REPLACE-WITH-YOUR-* placeholders with your actual credentials.

  3. Apply to your namespace:
    oc apply -n <your-namespace> -f deploy/secrets.yaml
    
  4. Restart the Backstage pod to pick up the new secrets:
    oc delete pod -n <your-namespace> -l app.kubernetes.io/name=developer-hub
    

The secrets.yaml file is git-ignored and will not be committed to version control.

For production environments, consider using:

  • External Secrets Operator (https://external-secrets.io/)
  • HashiCorp Vault integration
  • OpenShift Sealed Secrets
  • Your organization’s secret management solution

Customization Options

The deployment files include clear comments (marked with # CUSTOMIZATION:) for common customization points.

Namespace Selection

No file editing required - just specify the namespace when applying:

oc apply -n my-custom-namespace -f deploy/app.yaml

Plugin Versions

To use different plugin versions, update the OCI image references in the dynamic-plugins ConfigMap section of deploy/app.yaml. The default manifest also includes the MCP server, X2A MCP extras, and DCR consent UI packages alongside the Conversion Hub plugins.

MCP tools

The default deploy/app.yaml wires up Model Context Protocol (MCP) so assistants can call X2A MCP tools against your RHDH X2A route.

Use MCP tools for the tool list and permissions description.

Optional tweaks in app-config

Most teams can leave the bundled app-config-rhdh fragment as-is. Edit it when you need something different from the sample - for example:

  • auth.experimentalDynamicClientRegistration - tighten allowedRedirectUriPatterns in production (the sample uses broad patterns suitable for labs).
  • backend.cors - add or remove origins if you use browser-based MCP clients or the Inspector from a host that is not already listed.

YAML examples and behavior notes for those keys live on the MCP tools page.

After any change to deploy/app.yaml, re-apply and restart the RHDH pod so configuration and dynamic plugins reload:

oc apply -n <your-namespace> -f deploy/app.yaml
oc delete pod -n <your-namespace> -l app.kubernetes.io/name=developer-hub

Access the Application

Get the RHDH URL:

oc get route developer-hub -n <your-namespace> -o jsonpath='https://{.spec.host}{"\n"}'

Open the URL in your browser and navigate to the X2A menu item to start using the migration tool.

Troubleshooting

Check operator installation

oc get csv -n openshift-operators | grep rhdh
oc get pods -n openshift-operators

Check Backstage deployment

oc get backstage -n <your-namespace>
oc get pods -n <your-namespace>
oc logs -n <your-namespace> deployment/backstage-developer-hub

Verify secrets are loaded

oc get secret x2a-credentials -n <your-namespace>
oc describe secret x2a-credentials -n <your-namespace>

Common Issues

Issue: Backstage pod fails to start

Solution: Check secrets are properly configured and applied:

oc get secret x2a-credentials -n <your-namespace>
oc logs -n <your-namespace> deployment/backstage-developer-hub

Back to top