Sitemap
Fabio Reis

Fabio Reis

Platform Engineering | Kubernetes , OpenStack & Ceph

Automating OpenShift oc-mirror v2 with Ansible

2 min readAug 18, 2026

--

Mirroring OpenShift images in restricted environments usually means juggling .dockerconfigjson files and leaving plaintext credentials on your server.

Let’s fix that. Here is a production-ready Ansible playbook that automates oc-mirror v2 using a Zero-Trust approach: it pulls secrets directly into memory, handles optional proxies, and guarantees cleanup if something fails.

What you need

Create two simple files before running the playbook.

1. The Template (templates/imageset-config.yaml.j2) This defines what we are mirroring dynamically:

YAML

apiVersion: mirror.openshift.io/v2alpha1
kind: ImageSetConfiguration
mirror:
platform:
architectures:
- amd64
channels:
- name: {{ ocp_channel }}
minVersion: {{ ocp_version }}
maxVersion: {{ ocp_version }}

2. The Encrypted Credentials (vars/credentials.yml) Store your internal registry credentials here and encrypt it (ansible-vault encrypt vars/credentials.yml):

YAML

target_registry_user: "internal_user"
target_registry_password: "super_secret_password

The Playbook

Here is the complete playbook. It handles everything from extracting the cluster’s pull-secret to executing the mirror process.

Get Fabio Reis’s stories in your inbox

Join Medium for free to get updates from this writer.

YAML

---
- name: Automate OpenShift oc-mirror v2
hosts: localhost
connection: local
vars_files:
- vars/credentials.yml
vars:
target_registry_fqdn: "artifactory.internal.repo"
target_registry_url: "{{ target_registry_fqdn }}/ocp4"

workspace_path: "/tmp/workspace"
auth_file_path: "/tmp/mirroring.json"
imageset_path: "{{ workspace_path }}/imageset.yaml"

# Default versions (Override via CLI)
ocp_channel: "stable-4.16"
ocp_version: "4.16.5"
# Optional Proxy Setup
no_proxy_domains: "localhost,127.0.0.1,.internal.repo,{{ target_registry_fqdn }}"
proxy_env_dict:
HTTP_PROXY: "{{ proxy_url | default('') }}"
HTTPS_PROXY: "{{ proxy_url | default('') }}"
NO_PROXY: "{{ no_proxy_domains }}"
http_proxy: "{{ proxy_url | default('') }}"
https_proxy: "{{ proxy_url | default('') }}"
no_proxy: "{{ no_proxy_domains }}"

use_proxy: "{{ proxy_url is defined and proxy_url != '' }}"
tasks:
- name: Ensure workspace exists
ansible.builtin.file:
path: "{{ workspace_path }}"
state: directory
mode: '0755'
- name: Main Execution Block
environment: "{{ proxy_env_dict if use_proxy else {} }}"
block:
- name: Extract cluster pull-secret directly to RAM
ansible.builtin.command: >
oc extract secret/pull-secret -n openshift-config --keys=.dockerconfigjson --to=-
register: extracted_pull_secret
changed_when: false
no_log: true
- name: Merge cluster secret with Vault credentials
ansible.builtin.copy:
dest: "{{ auth_file_path }}"
mode: '0600'
content: "{{ merged_auths | to_nice_json }}"
no_log: true
vars:
base_secret_dict: "{{ extracted_pull_secret.stdout | from_json }}"
target_auth_base64: "{{ (target_registry_user + ':' + target_registry_password) | b64encode }}"
merged_auths: "{{ base_secret_dict | combine({'auths': {target_registry_fqdn: {'auth': target_auth_base64}}}, recursive=True) }}"
- name: Render ImageSet template
ansible.builtin.template:
src: "./templates/imageset-config.yaml.j2"
dest: "{{ imageset_path }}"
- name: Execute oc-mirror v2
ansible.builtin.command: >
./oc-mirror --v2 --authfile {{ auth_file_path }} --workspace file://{{ workspace_path }} -c {{ imageset_path }} docker://{{ target_registry_url }}
register: oc_mirror_exec
changed_when: oc_mirror_exec.rc == 0
always:
- name: Cleanup auth file (Runs even on failure)
ansible.builtin.file:
path: "{{ auth_file_path }}"
state: absent

Why this approach is better:

  • No secrets on disk: We use oc extract --to=- to pull the cluster secret directly into RAM, merge it with your Vault credentials, and create the file.
  • Guaranteed Cleanup: Thanks to the always block, the temporary mirroring.json is deleted immediately after the mirror finishes, even if the network fails midway.
  • Smart Proxy: The playbook checks if you provided a proxy_url. If you did, it applies it. If not, it runs normally. No need to maintain two separate playbooks.

How to run it

Normal execution:

Bash

ansible-playbook playbook.yml \
--ask-vault-pass \
-e "ocp_channel=stable-4.16" \
-e "ocp_version=4.16.8"

Running behind a corporate proxy:

Bash

ansible-playbook playbook.yml \
--ask-vault-pass \
-e "ocp_channel=stable-4.16" \
-e "ocp_version=4.16.8" \
-e "proxy_url=http://proxy.corp:8080"

--

--

Fabio Reis
Fabio Reis

Written by Fabio Reis

Platform Engineering | Kubernetes , OpenStack & Ceph