Installation on

Steps to install an OpenShift 4 cluster on

These steps follow the Installing a cluster on bare metal docs to set up a user provisioned installation (UPI). Terraform is used to provision the cloud infrastructure.

This guide describes the installation with Cilium as the network provider. Cilium is an optional add-on and marked sections should be skipped for clusters which don’t require Cilium. Optional sections are marked with Cilium Optional.

The commands are idempotent and can be retried if any of the steps fail.

The certificates created during bootstrap are only valid for 24h. So make sure you complete these steps within 24h.

This how-to guide is still a work in progress and will change. It’s currently very specific to VSHN and needs further changes to be more generic.

Starting situation

  • You already have a Tenant and its git repository

  • You have a CCSP Red Hat login and are logged into Red Hat Openshift Cluster Manager

  • You want to register a new cluster in Lieutenant and are about to install Openshift 4 on Cloudscale


Make sure the version of openshift-install and the rhcos image is the same, otherwise ignition will fail.

Cluster Installation

Register the new OpenShift 4 cluster in Lieutenant.

Lieutenant API endpoint

Use the following endpoint for Lieutenant:

Set up LDAP service

  1. Create an LDAP service

    Use to create a service. The name must contain the customer and the cluster name. And then put the LDAP service ID in the following variable:

    export LDAP_ID="Your_LDAP_ID_here"
    export LDAP_PASSWORD="Your_LDAP_pw_here"

Configure input

Access to cloud API
export CLOUDSCALE_TOKEN=<cloudscale-api-token>
export TF_VAR_lb_cloudscale_api_secret=<cloudscale-api-token-for-Floaty>
Access to various API
# From, "api" scope is sufficient
export GITLAB_TOKEN=<gitlab-api-token>
export GITLAB_USER=<gitlab-user-name>

# For example:
# IMPORTANT: do NOT add a trailing `/`. Commands below will fail.
export COMMODORE_API_URL=<lieutenant-api-endpoint>

# Set Project Syn cluster and tenant ID
export CLUSTER_ID=<lieutenant-cluster-id> # Looks like: c-<something>
export TENANT_ID=$(curl -sH "Authorization: Bearer $(commodore fetch-token)" ${COMMODORE_API_URL}/clusters/${CLUSTER_ID} | jq -r .tenant)
Configuration for hieradata commits
export GIT_AUTHOR_NAME=$(git config --global
export GIT_AUTHOR_EMAIL=$(git config --global
export TF_VAR_control_vshn_net_token=<control-vshn-net-token>
OpenShift configuration
export BASE_DOMAIN=<your-base-domain>
export PULL_SECRET='<redhat-pull-secret>' # As copied from "Copy pull secret". value must be inside quotes.

For BASE_DOMAIN explanation, see DNS Scheme.

Set up S3 bucket for cluster bootstrap

  1. Create S3 bucket

    1. If a bucket user already exists for this cluster:

      # Use already existing bucket user
      response=$(curl -sH "Authorization: Bearer ${CLOUDSCALE_TOKEN}" \ | \
        jq -e ".[] | select(.display_name == \"${CLUSTER_ID}\")")
    2. To create a new bucket user:

      # Create a new user
      response=$(curl -sH "Authorization: Bearer ${CLOUDSCALE_TOKEN}" \
        -F display_name=${CLUSTER_ID} \
  2. Configure the Minio client

    export REGION=$(curl -sH "Authorization: Bearer $(commodore fetch-token)" ${COMMODORE_API_URL}/clusters/${CLUSTER_ID} | jq -r .facts.region)
    mc config host add \
      "${CLUSTER_ID}" "https://objects.${REGION}" \
      $(echo $response | jq -r '.keys[0].access_key') \
      $(echo $response | jq -r '.keys[0].secret_key')
    mc mb --ignore-existing \

Upload Red Hat CoreOS image

  1. Export the Authorization header for the Cloudscale API.

    export AUTH_HEADER="Authorization: Bearer ${CLOUDSCALE_TOKEN}"

    The variable CLOUDSCALE_TOKEN could be used directly. Exporting the variable AUTH_HEADER is done to be compatible with the Cloudscale API documentation.

  2. Check if image already exists in the correct zone

    curl -sH "$AUTH_HEADER" | jq -r '.[] | select(.slug == "rhcos-4.10") | .zones[].slug'

    If a URL is printed to the output, you can skip the next steps and directly jump to the next section.

  3. Fetch and convert the latest Red Hat CoreOS image

    curl -L | gzip -d > rhcos-4.10.qcow
    qemu-img convert rhcos-4.10.qcow rhcos-4.10.raw
  4. Upload the image to S3 and make it public

    mc cp rhcos-4.10.raw "${CLUSTER_ID}/${CLUSTER_ID}-bootstrap-ignition/"
    mc policy set download "${CLUSTER_ID}/${CLUSTER_ID}-bootstrap-ignition/rhcos-4.10.raw"

    You can check that the download policy is applied successfully with

    mc policy get "${CLUSTER_ID}/${CLUSTER_ID}-bootstrap-ignition/rhcos-4.10.raw"

    The output should be

    `Access permission for `[…]-bootstrap-ignition/rhcos-4.10.raw` is `download``
  5. Import the image to Cloudscale

    curl -i -H "$AUTH_HEADER" \
      -F url="$(mc share download --json "${CLUSTER_ID}/${CLUSTER_ID}-bootstrap-ignition/rhcos-4.10.raw" | jq -r .url)" \
      -F name='RHCOS 4.10' \
      -F zones="${REGION}1" \
      -F slug=rhcos-4.10 \
      -F source_format=raw \
      -F user_data_handling=pass-through \

Set secrets in Vault

Connect with Vault
export VAULT_ADDR=
vault login -method=oidc
Store various secrets in Vault
# Set the access secrets
vault kv put clusters/kv/${TENANT_ID}/${CLUSTER_ID}/cloudscale \
  s3_access_key=$(mc config host ls ${CLUSTER_ID} -json | jq -r .accessKey) \
  s3_secret_key=$(mc config host ls ${CLUSTER_ID} -json | jq -r .secretKey)

# Put LB API key in Vault
vault kv put clusters/kv/${TENANT_ID}/${CLUSTER_ID}/floaty \

# Generate an HTTP secret for the registry
vault kv put clusters/kv/${TENANT_ID}/${CLUSTER_ID}/registry \
  httpSecret=$(LC_ALL=C tr -cd "A-Za-z0-9" </dev/urandom | head -c 128)

# Set the LDAP password
vault kv put clusters/kv/${TENANT_ID}/${CLUSTER_ID}/vshn-ldap \

# Generate a master password for K8up backups
vault kv put clusters/kv/${TENANT_ID}/${CLUSTER_ID}/global-backup \
  password=$(LC_ALL=C tr -cd "A-Za-z0-9" </dev/urandom | head -c 32)

# Generate a password for the cluster object backups
vault kv put clusters/kv/${TENANT_ID}/${CLUSTER_ID}/cluster-backup \
  password=$(LC_ALL=C tr -cd "A-Za-z0-9" </dev/urandom | head -c 32)

# Copy the Dagobert OpenShift Node Collector Credentials
vault kv get -format=json "clusters/kv/template/dagobert" | jq '' \
  | vault kv put -cas=0 "clusters/kv/${TENANT_ID}/${CLUSTER_ID}/dagobert" -

# Copy the VSHN acme-dns registration password
vault kv get -format=json "clusters/kv/template/cert-manager" | jq '' \
  | vault kv put -cas=0 "clusters/kv/${TENANT_ID}/${CLUSTER_ID}/cert-manager" -
Grab the LB hieradata repo token from Vault
export HIERADATA_REPO_SECRET=$(vault kv get \
  -format=json "clusters/kv/lbaas/hieradata_repo_token" | jq '')
export HIERADATA_REPO_USER=$(echo "${HIERADATA_REPO_SECRET}" | jq -r '.user')
export HIERADATA_REPO_TOKEN=$(echo "${HIERADATA_REPO_SECRET}" | jq -r '.token')

Prepare Cluster Repository

For the following steps, change into a clean directory (for example a directory in your home).

Check Running Commodore for details on how to run commodore.

  1. Prepare Commodore inventory.

    mkdir -p inventory/classes/
    git clone $(curl -sH"Authorization: Bearer $(commodore fetch-token)" "${COMMODORE_API_URL}/tenants/${TENANT_ID}" | jq -r '.gitRepo.url') inventory/classes/${TENANT_ID}

Cilium Optional: Prepare Cilium Configuration

  1. Add Cilium to cluster configuration

    pushd "inventory/classes/${TENANT_ID}/"
    yq eval -i '.applications += ["cilium"]' ${CLUSTER_ID}.yml
    yq eval -i '.parameters.networkpolicy.networkPlugin = "cilium"' ${CLUSTER_ID}.yml
    yq eval -i '.parameters.networkpolicy.ignoredNamespaces = ["openshift-oauth-apiserver"]' ${CLUSTER_ID}.yml
    yq eval -i '.parameters.openshift.infraID = "TO_BE_DEFINED"' ${CLUSTER_ID}.yml
    yq eval -i '.parameters.openshift.clusterID = "TO_BE_DEFINED"' ${CLUSTER_ID}.yml
    git commit -a -m "Add Cilium addon to ${CLUSTER_ID}"
    git push
  2. Compile catalog

    commodore catalog compile ${CLUSTER_ID} --push -i

Configure the OpenShift Installer

Starting with this section, we recommend that you change into a clean directory (for example a directory in your home).

  1. Generate SSH key

    We generate a unique SSH key pair for the cluster as this gives us troubleshooting access.

    export SSH_PUBLIC_KEY="${SSH_PRIVATE_KEY}.pub"
    ssh-keygen -C "vault@$CLUSTER_ID" -t ed25519 -f $SSH_PRIVATE_KEY -N ''
    if [[ "$OSTYPE" == "linux"* ]]; then
      BASE64_NO_WRAP='base64 --wrap 0'
    vault kv put clusters/kv/${TENANT_ID}/${CLUSTER_ID}/cloudscale/ssh \
      private_key=$(cat $SSH_PRIVATE_KEY | eval "$BASE64_NO_WRAP")
    ssh-add $SSH_PRIVATE_KEY
  2. Prepare install-config.yaml

    You can add more options to the install-config.yaml file. Have a look at the config example for more information.

    For example, you could change the SDN from a default value to something a customer requests due to some network requirements.

    export INSTALLER_DIR="$(pwd)/target"
    mkdir -p "${INSTALLER_DIR}"
    cat > "${INSTALLER_DIR}/install-config.yaml" <<EOF
    apiVersion: v1
      name: ${CLUSTER_ID}
    baseDomain: ${BASE_DOMAIN}
      none: {}
      networkType: OVNKubernetes
    pullSecret: |
    sshKey: "$(cat $SSH_PUBLIC_KEY)"
  3. Cilium Optional: Add Cilium

    yq eval -i '.networking.networkType = "Cilium"' "${INSTALLER_DIR}/install-config.yaml"
    If setting custom CIDR for the OpenShift networking, the corresponding values should be updated in your Commodore cluster definitions. See Cilium Component Defaults and Parameter Reference. Verify with less catalog/manifests/cilium/olm/*ciliumconfig.yaml.

Run the OpenShift Installer

The steps in this section aren’t idempotent and have to be completed uninterrupted in one go. If you have to recreate the install config or any of the generated manifests you need to rerun all of the subsequent steps.
  1. Render install manifests (this will consume the install-config.yaml)

    openshift-install --dir "${INSTALLER_DIR}" \
      create manifests
    1. If you want to change the default "apps" domain for the cluster:

      yq w -i "${INSTALLER_DIR}/manifests/cluster-ingress-02-config.yml" \
  2. Cilium Optional: Copy pre-rendered Cilium manifests

    cp catalog/manifests/cilium/olm/* target/manifests/
  3. Extract the cluster domain from the generated manifests

    export CLUSTER_DOMAIN=$(yq e '.spec.baseDomain' \
  4. Prepare install manifests and ignition config

    openshift-install --dir "${INSTALLER_DIR}" \
      create ignition-configs
  5. Upload ignition config

    mc cp "${INSTALLER_DIR}/bootstrap.ign" "${CLUSTER_ID}/${CLUSTER_ID}-bootstrap-ignition/"
    export TF_VAR_ignition_bootstrap=$(mc share download \
      --json --expire=4h \
      "${CLUSTER_ID}/${CLUSTER_ID}-bootstrap-ignition/bootstrap.ign" | jq -r '.share')

Terraform Cluster Config

  1. Set team responsible for handling Icinga alerts

    # use lower case for team name.
    # e.g. TEAM=tarazed
  2. Prepare Terraform cluster config

    CA_CERT=$(jq -r '[0].source' \
      "${INSTALLER_DIR}/master.ign" | \
      awk -F ',' '{ print $2 }' | \
      base64 --decode)
    pushd "inventory/classes/${TENANT_ID}/"
    yq eval -i '.applications += ["openshift4-terraform"]' ${CLUSTER_ID}.yml
    yq eval -i ".parameters.openshift.infraID = \"$(jq -r .infraID "${INSTALLER_DIR}/metadata.json")\"" \
    yq eval -i ".parameters.openshift.clusterID = \"$(jq -r .clusterID "${INSTALLER_DIR}/metadata.json")\"" \
    yq eval -i ".parameters.openshift.baseDomain = \"${CLUSTER_DOMAIN}\"" \
    yq eval -i '.parameters.openshift.appsDomain = "apps.${openshift:baseDomain}"' \
    yq eval -i ".parameters.openshift4_terraform.terraform_variables.base_domain = \"${BASE_DOMAIN}\"" \
    yq eval -i ".parameters.openshift4_terraform.terraform_variables.ignition_ca = \"${CA_CERT}\"" \
    yq eval -i ".parameters.openshift4_terraform.terraform_variables.ssh_keys = [\"$(cat ${SSH_PUBLIC_KEY})\"]" \
    yq eval -i ".parameters.openshift4_terraform.terraform_variables.hieradata_repo_user = \"${HIERADATA_REPO_USER}\"" \
    yq eval -i " = \"${TEAM}\"" \
    # Enable proxy protocol on the load-balancer and ingress controller to forward the
    # source IP to workloads running on the cluster
    yq eval -i ".parameters.openshift4_terraform.terraform_variables.lb_enable_proxy_protocol = true" \
    yq eval -i ".parameters.openshift4_ingress.ingressControllers.default.endpointPublishingStrategy.type = \"HostNetwork\"" \
    yq eval -i ".parameters.openshift4_ingress.ingressControllers.default.endpointPublishingStrategy.hostNetwork.protocol = \"PROXY\"" \
    # Configure the OpenShift update channel
    yq eval -i " = \"stable-4.10\"" \
    # Configure default ingress controller with 3 replicas, so that the
    # VSHN-managed LB HAproxy health check isn't complaining about a missing backend
    yq eval -i ".parameters.openshift4_ingress.ingressControllers.default.replicas = 3" \
    yq eval -i ".parameters.vshnLdap.serviceId = \"${LDAP_ID}\"" \
    # Configure Git author information for the CI pipeline
    yq eval -i ".parameters.openshift4_terraform.gitlab_ci.git.username = \"GitLab CI\"" \
    yq eval -i " = \"tech+${CLUSTER_ID}\"" \
  3. Bootstrap dynamic facts

    Some components rely on dynamic facts fetched from the cluster at runtime. Those facts aren’t available before Steward is installed on the cluster.

    yq eval -i ".parameters.dynamic_facts.kubernetesVersion.major = \"$(echo "1.23" | awk -F. '{print $1}')\"" \
    yq eval -i ".parameters.dynamic_facts.kubernetesVersion.minor = \"$(echo "1.23" | awk -F. '{print $2}')\"" \
    yq eval -i ".parameters.dynamic_facts.openshiftVersion.Major = \"$(echo "4.10" | awk -F. '{print $1}')\"" \
    yq eval -i ".parameters.dynamic_facts.openshiftVersion.Minor = \"$(echo "4.10" | awk -F. '{print $2}')\"" \

    You now have the option to further customize the cluster by editing terraform_variables. Most importantly you have the option to change node sizes or add additional specialized worker nodes.

    Please look at the configuration reference for the available options.

    If you use a custom "apps" domain, make sure to also change it in parameters.openshift.appsDomain.

Commit changes and compile cluster catalog

  1. Review changes. Have a look at the file ${CLUSTER_ID}.yml. Override default parameters or add more component configurations as required for your cluster.

  2. Commit changes

    git commit -a -m "Setup cluster ${CLUSTER_ID}"
    git push
  3. Compile and push cluster catalog

    commodore catalog compile ${CLUSTER_ID} --push -i

Provision Infrastructure

  1. Configure Terraform secrets

    cat <<EOF > ./terraform.env
  2. Setup Terraform

    Prepare Terraform execution environment
    # Set terraform image and tag to be used
      yq eval ".parameters.openshift4_terraform.images.terraform.image" \
      yq eval ".parameters.openshift4_terraform.images.terraform.tag" \
    # Generate the terraform alias
    alias terraform='docker run -it --rm \
      -e REAL_UID=$(id -u) \
      --env-file ${base_dir}/terraform.env \
      -w /tf \
      -v $(pwd):/tf \
      --ulimit memlock=-1 \
      "${tf_image}:${tf_tag}" /tf/'
    export GITLAB_REPOSITORY_URL=$(curl -sH "Authorization: Bearer $(commodore fetch-token)" ${COMMODORE_API_URL}/clusters/${CLUSTER_ID} | jq -r '.gitRepo.url' | sed 's|ssh://||; s|/|:|')
    export GITLAB_CATALOG_PROJECT_ID=$(curl -sH "Authorization: Bearer ${GITLAB_TOKEN}" "${GITLAB_REPOSITORY_NAME/.git}" | jq -r ".[] | select(.ssh_url_to_repo == \"${GITLAB_REPOSITORY_URL}\") | .id")
    export GITLAB_STATE_URL="${GITLAB_CATALOG_PROJECT_ID}/terraform/state/cluster"
    pushd catalog/manifests/openshift4-terraform/
    Initialize Terraform
    terraform init \
      "-backend-config=address=${GITLAB_STATE_URL}" \
      "-backend-config=lock_address=${GITLAB_STATE_URL}/lock" \
      "-backend-config=unlock_address=${GITLAB_STATE_URL}/lock" \
      "-backend-config=username=${GITLAB_USER}" \
      "-backend-config=password=${GITLAB_TOKEN}" \
      "-backend-config=lock_method=POST" \
      "-backend-config=unlock_method=DELETE" \
  3. Create LB hieradata

    cat > <<EOF
    module "cluster" {
      bootstrap_count          = 0
      master_count             = 0
      infra_count              = 0
      worker_count             = 0
      additional_worker_groups = {}
    terraform apply -target ""
  4. Review and merge the LB hieradata MR (listed in Terraform output hieradata_mr) and wait until the deploy pipeline after the merge is completed.

  5. Create LBs

    terraform apply
  6. Setup the DNS records shown in output variable dns_entries from the previous step in the cluster’s parent zone. If you use a custom apps domain, make the necessary changes to the DNS record for *.apps.

  7. Make LB FQDNs available for later steps

    Store LB FQDNs in environment
    declare -a LB_FQDNS
    for id in 1 2; do
      LB_FQDNS[$id]=$(terraform state show "[$(expr $id - 1)]" | grep fqdn | awk '{print $2}' | tr -d ' "\r\n')
    Verify FQDNs
    for lb in "${LB_FQDNS[@]}"; do echo $lb; done
  8. Check LB connectivity

    for lb in "${LB_FQDNS[@]}"; do
      ping -c1 "${lb}"
  9. Wait until LBs are fully initialized by Puppet

    # Wait for Puppet provisioning to complete
    while true; do
      curl --connect-timeout 1 "http://api.${CLUSTER_DOMAIN}:6443"
      if [ $? -eq 52 ]; then
        echo -e "\nHAproxy up"
        echo -n "."
        sleep 5
    # Update sshop config, see
    # Check that you can access the LBs using your usual SSH config
    for lb in "${LB_FQDNS[@]}"; do
      ssh "${lb}" hostname -f

    While you’re waiting for the LBs to be provisioned, you can check the cloud-init logs with the following SSH commands

    ssh ubuntu@"${LB_FQDNS[1]}" tail -f /var/log/cloud-init-output.log
    ssh ubuntu@"${LB_FQDNS[2]}" tail -f /var/log/cloud-init-output.log
  10. Check the "Server created" tickets for the LBs and link them to the cluster setup ticket.

  11. Deploy bootstrap node

    cat > <<EOF
    module "cluster" {
      bootstrap_count          = 1
      master_count             = 0
      infra_count              = 0
      worker_count             = 0
      additional_worker_groups = {}
    terraform apply
  12. Review and merge the LB hieradata MR (listed in Terraform output hieradata_mr) and run Puppet on the LBs after the deploy job has completed

    for fqdn in "${LB_FQDNS[@]}"; do
      ssh "${fqdn}" sudo puppetctl run
  13. Wait for bootstrap API to come up

    API_URL=$(yq e '.clusters[0].cluster.server' "${INSTALLER_DIR}/auth/kubeconfig")
    while ! curl --connect-timeout 1 "${API_URL}/healthz" -k &>/dev/null; do
      echo -n "."
      sleep 5
    done && echo -e "\nAPI is up"
  14. Deploy control plane nodes

    cat > <<EOF
    module "cluster" {
      bootstrap_count          = 1
      infra_count              = 0
      worker_count             = 0
      additional_worker_groups = {}
    terraform apply
  15. Add the DNS records for etcd shown in output variable dns_entries from the previous step to the cluster’s parent zone

  16. Wait for bootstrap to complete

    openshift-install --dir "${INSTALLER_DIR}" \
      wait-for bootstrap-complete --log-level debug
  17. Remove bootstrap node and provision infra nodes

    cat > <<EOF
    module "cluster" {
      worker_count             = 0
      additional_worker_groups = {}
    terraform apply
  18. Approve infra certs

    export KUBECONFIG="${INSTALLER_DIR}/auth/kubeconfig"
    # Once CSRs in state Pending show up, approve them
    # Needs to be run twice, two CSRs for each node need to be approved
    kubectl get csr -w
    oc get csr -o go-template='{{range .items}}{{if not .status}}{{}}{{"\n"}}{{end}}{{end}}' | \
      xargs oc adm certificate approve
    kubectl get nodes
  19. Label infra nodes

    kubectl get nodes
    kubectl label node \""
  20. Enable proxy protocol on ingress controller

    kubectl -n openshift-ingress-operator patch ingresscontroller default --type=json \
      -p '[{
        "value": {"type": "HostNetwork", "hostNetwork": {"protocol": "PROXY"}}
    This step is only necessary if you enabled the proxy protocol on the load-balancers.
  21. Review and merge the LB hieradata MR (listed in Terraform output hieradata_mr) and run Puppet on the LBs after the deploy job has completed

    for fqdn in "${LB_FQDNS[@]}"; do
      ssh "${fqdn}" sudo puppetctl run
  22. Wait for installation to complete

    openshift-install --dir ${INSTALLER_DIR} \
      wait-for install-complete --log-level debug
  23. Provision worker nodes

    terraform apply
  24. Approve worker certs

    # Once CSRs in state Pending show up, approve them
    # Needs to be run twice, two CSRs for each node need to be approved
    kubectl get csr -w
    oc get csr -o go-template='{{range .items}}{{if not .status}}{{}}{{"\n"}}{{end}}{{end}}' | \
      xargs oc adm certificate approve
    kubectl get nodes
  25. Label worker nodes

    kubectl label --overwrite node \""
    kubectl label node \
    # This should show the worker nodes only
    kubectl get nodes -l
    At this point you may want to add extra labels to the additional worker groups, if there are any.
  26. Create secret with S3 credentials for the registry

    oc create secret generic image-registry-private-configuration-user \
    --namespace openshift-image-registry \
    --from-literal=REGISTRY_STORAGE_S3_ACCESSKEY=$(mc config host ls ${CLUSTER_ID} -json | jq -r .accessKey) \
    --from-literal=REGISTRY_STORAGE_S3_SECRETKEY=$(mc config host ls ${CLUSTER_ID} -json | jq -r .secretKey)

Enable Project Syn

  1. Make the cluster Project Syn enabled

  2. Remove bootstrap dynamic facts

    pushd "inventory/classes/${TENANT_ID}/"
    yq eval -i "del(.parameters.dynamic_facts)" ${CLUSTER_ID}.yml
    git commit -a -m "Remove bootstrapping dynamic_facts"
    git push

Setup acme-dns CNAME records for the cluster

You can skip this section if you’re not using Let’s Encrypt for the cluster’s API and default wildcard certificates.
  1. Extract the acme-dns subdomain for the cluster after cert-manager has been deployed via Project Syn.

    fulldomain=$(kubectl -n syn-cert-manager \
      get secret acme-dns-client \
      -o jsonpath='{.data.acmedns\.json}' | \
      base64 -d  | \
      jq -r '[.[]][0].fulldomain')
    echo "$fulldomain"
  2. Add the following CNAME records to the cluster’s DNS zone

    The _acme-challenge records must be created in the same zone as the cluster’s api and apps records respectively.

    $ORIGIN <cluster-zone> (2)
    _acme-challenge.api  IN CNAME <fulldomain>. (1)
    $ORIGIN <apps-base-domain> (3)
    _acme-challenge.apps IN CNAME <fulldomain>. (1)
    1 Replace <fulldomain> with the output of the previous step.
    2 The _acme-challenge.api record must be created in the same origin as the api record.
    3 The _acme-challenge.apps record must be created in the same origin as the apps record.

Store the cluster’s admin credentials in the password manager

  1. Once the cluster’s production API certificate has been deployed, edit the cluster’s admin kubeconfig file to remove the initial API certificate CA.

    You may see the error Unable to connect to the server: x509: certificate signed by unknown authority when executing kubectl or oc commands after the cluster’s production API certificate has been deployed by Project Syn.

    This error can be addressed by removing the initial CA certificate data from the admin kubeconfig as shown in this step.

    yq e -i 'del(.clusters[0].cluster.certificate-authority-data)' \
  2. Save the admin credentials in the password manager. You can find the password in the file target/auth/kubeadmin-password and the kubeconfig in target/auth/kubeconfig

    ls -l ${INSTALLER_DIR}/auth/

Configure access for registry bucket

OpenShift does configure a PublicAccessBlockConfiguration. Ceph currently has a bug, where pushing objects into the S3 bucket are prevented.

The error message in the docker-registry logs is `s3aws: AccessDenied: \n\tstatus code: 403, request id: tx00000000000003ea93fa6-00112504a0-4fa9e750e-rma1, host id: `.

See for more information.

  1. Install the aws cli tool

    pip install awscli
  2. Check the current S3 bucket configuration after openshift4-registry has been deployed via Project Syn.

    export AWS_ACCESS_KEY_ID=$(mc config host ls ${CLUSTER_ID} -json | jq -r .accessKey)
    export AWS_SECRET_ACCESS_KEY=$(mc config host ls ${CLUSTER_ID} -json | jq -r .secretKey)
    export REGION=$(curl -sH "Authorization: Bearer $(commodore fetch-token)" ${COMMODORE_API_URL}/clusters/${CLUSTER_ID} | jq -r .facts.region)
    aws --endpoint-url "https://objects.${REGION}" s3api get-public-access-block --bucket "${CLUSTER_ID}-image-registry"
  3. Configure BlockPublicAcls to false

    aws s3api put-public-access-block --endpoint-url "https://objects.${REGION}" --bucket "${CLUSTER_ID}-image-registry" --public-access-block-configuration BlockPublicAcls=false
  4. Verify the configuration BlockPublicAcls is false

    aws s3api get-public-access-block --endpoint-url "https://objects.${REGION}" --bucket "${CLUSTER_ID}-image-registry"

    The final configuration should look like this:

        "PublicAccessBlockConfiguration": {
            "BlockPublicAcls": false,
            "IgnorePublicAcls": false,
            "BlockPublicPolicy": false,
            "RestrictPublicBuckets": false

Finalize installation

  1. Configure the apt-dater groups for the LBs.

    git clone
    pushd nodes_hieradata
    cat >"${LB_FQDNS[1]}.yaml" <<EOF
    s_apt_dater::host::group: '2200_20_night_main'
    cat >"${LB_FQDNS[2]}.yaml" <<EOF
    s_apt_dater::host::group: '2200_40_night_second'
    git add *.yaml
    git commit -m"Configure apt-dater groups for LBs for OCP4 cluster ${CLUSTER_ID}"
    git push origin master

    This how-to defaults to the night maintenance window on Tuesday at 22:00. Adjust the apt-dater groups according to the documented groups (VSHN-internal only) if the cluster requires a different maintenance window.

  2. Wait for deploy job on nodes hieradata to complete and run Puppet on the LBs to update the apt-dater groups.

    for fqdn in "${LB_FQDNS[@]}"; do
      ssh "${fqdn}" sudo puppetctl run
  3. Delete local config files

    rm -r ${INSTALLER_DIR}/

Post tasks

  1. Doing a first maintenance

  2. Add the cluster to the maintenance template