Red Hat Satellite Content Lifecycle as Code
Every Satellite I have inherited was managed by clicking through the UI, and
nobody could say why a content view contained what it contained. For a recent
multi-environment RHEL estate I put the whole content lifecycle - products,
repositories, GPG keys, content views, activation keys, publish and promote -
into a git repo driven by the redhat.satellite Ansible collection. These are
the patterns that worked and the traps that cost me an afternoon each.
Why Satellite state belongs in git
Content views, filters and activation keys are exactly the kind of state that rots in a UI: changes are invisible, unreviewable and unrepeatable. Once they are YAML in a repo, adding a repository is a merge request, a colleague can review the diff, and rebuilding the Satellite from scratch is one playbook run instead of a week of clicking from memory.
Collection and repo layout
The certified redhat.satellite collection wraps the Satellite API; pin it in
collections/requirements.yml like any other dependency:
collections:
- name: redhat.satellite
version: 3.14.0
- name: redhat.satellite_operations
version: 2.1.0
The repo is a plain Ansible project. All the actual content definitions live in group_vars, split per topic so the diffs stay readable:
inventory/group_vars/satellite/
products.yml
content_credentials.yml
content_views.yml
activation_keys.yml
lifecycle_environments.yml
credentials.yml # vault-encrypted
playbooks/
configure.yml
publish.yml
promote.yml
The configure playbook is almost embarrassingly small, because the collection ships a role per object type and each role reads its own variable:
- name: Configure Satellite from the repo
hosts: satellite
gather_facts: false
collections:
- redhat.satellite
roles:
- redhat.satellite.organizations
- redhat.satellite.locations
- redhat.satellite.manifest
- redhat.satellite.content_credentials
- redhat.satellite.repositories
- redhat.satellite.sync_plans
- redhat.satellite.lifecycle_environments
- redhat.satellite.content_views
- redhat.satellite.activation_keys
Run it with diff mode so you see what changes on the server before it does:
$ ansible-playbook -Dv playbooks/configure.yml --ask-vault-pass
One early mistake: my first version of configure.yml also published and
promoted content view versions at the end. Do not do that. Configuration is
idempotent and safe to run any time; publishing creates new versions and
belongs in its own playbook with its own schedule.
Products and repositories
Red Hat content is enabled through repository sets from the subscription manifest. Third-party products are defined with an URL and a GPG key:
satellite_products:
- name: Red Hat Enterprise Linux for x86_64
repository_sets:
- name: Red Hat Enterprise Linux 9 for x86_64 - BaseOS (RPMs)
releasever: 9
- name: Red Hat Enterprise Linux 9 for x86_64 - AppStream (RPMs)
releasever: 9
- name: EPEL
repositories:
- name: EPEL for RHEL 9
content_type: yum
url: https://dl.fedoraproject.org/pub/epel/9/Everything/x86_64/
gpg_key: EPEL 9 Signing Key
download_policy: immediate
- name: Veeam
repositories:
- name: Veeam Agent for RHEL 9
content_type: yum
url: https://repository.veeam.com/backup/linux/agent/rpm/el/9/
gpg_key: Veeam Signing Key
download_policy: immediate
The GPG keys themselves are content credentials, and a URL lookup keeps even those out of the repo as blobs:
satellite_content_credentials:
- name: EPEL 9 Signing Key
content_type: gpg_key
content: "{{ lookup('url', 'https://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-9', split_lines=False) }}"
- name: Veeam Signing Key
content_type: gpg_key
content: "{{ lookup('url', 'https://repository.veeam.com/keys/RPM-E6FBD664', split_lines=False) }}"
Two traps here. First, vendors publish more than one key: my initial Veeam
definition pointed at the obvious veeam.gpg from their keys directory, and
package installs failed GPG verification until I switched to the key that
actually signs the RPMs, published as RPM-E6FBD664. Check the key listing
before you trust a filename.
Second, Kickstart repositories. For PXE provisioning you also want the BaseOS
and AppStream Kickstart repository sets, and I first enabled them the same way
as the RPM ones, with releasever: 9. The repositories role cannot enable
that, because Kickstart trees only exist per minor release. The fix:
- name: Red Hat Enterprise Linux 9 for x86_64 - BaseOS (Kickstart)
- releasever: 9
+ releasever: 9.3
- name: Red Hat Enterprise Linux 9 for x86_64 - AppStream (Kickstart)
- releasever: 9
+ releasever: 9.3
Keep the previous minor release enabled next to the current one while any host group still kickstarts from it.
Content views: do not encode the environment in the name
This is the part I got most wrong. My first modelling pass created one
content view per lifecycle environment: cv-rhel-dev, cv-rhel-test,
cv-rhel-prod, each listing identical repositories. It felt symmetrical and
it was wrong. Content views are versioned; lifecycle environments are where
versions get promoted to. Duplicated per-environment views triple the YAML,
publish three times the versions, and quietly break the promise that what you
tested is what production gets.
The refactor deleted two thirds of the definitions and left one view per content set, plus composite views stacking them per workload:
satellite_content_views:
- name: cv-rhel-base
repositories:
- name: Red Hat Enterprise Linux 9 for x86_64 - BaseOS RPMs 9
product: Red Hat Enterprise Linux for x86_64
- name: Red Hat Enterprise Linux 9 for x86_64 - AppStream RPMs 9
product: Red Hat Enterprise Linux for x86_64
- name: Veeam Agent for RHEL 9
product: Veeam
- name: cv-gitlab
repositories:
- name: GitLab CE for RHEL 9
product: GitLab
- name: ccv-gitlab
components:
- content_view: cv-rhel-base
latest: true
- content_view: cv-gitlab
latest: true
A GitLab server consumes ccv-gitlab: the base OS content everyone gets,
plus its own product repos, as one promotable unit.
Activation keys as data
Activation keys are where environment and content view meet, so this is the only place the environment name appears:
satellite_activation_keys:
- name: ak-rhel-base-test
lifecycle_environment: TEST
content_view: cv-rhel-base
- name: ak-rhel-base-prod
lifecycle_environment: PROD
content_view: cv-rhel-base
- name: ak-gitlab-prod
lifecycle_environment: PROD
content_view: ccv-gitlab
These keys are the contract with every other automation you run: kickstart templates reference them, and migration playbooks re-register hosts with them, like the RHEL upgrade campaign I described in Surviving Leapp.
Publishing and promoting on a calendar
Publishing new versions is its own playbook, using the collection’s
content_view_publish role against the same satellite_content_views list:
- name: Publish new content view versions
hosts: satellite
collections:
- redhat.satellite
roles:
- redhat.satellite.content_view_publish
Promotion follows a fixed cadence instead of ad-hoc clicking: new versions land in TEST on the first Monday of the month, move to QA on the third, and reach PROD on the following first Monday. The playbook works out which Monday it is and refuses to run on any other day:
- name: Work out which Monday of the month today is
set_fact:
monday_of_month: "{{ ((ansible_date_time.day | int - 1) // 7) + 1 }}"
- name: Stop unless today is a Monday
ansible.builtin.fail:
msg: "This playbook only runs on Mondays."
when: ansible_date_time.weekday != "Monday"
- name: Promote from TEST to QA on the third Monday
redhat.satellite.content_view_version:
username: "{{ satellite_username }}"
password: "{{ satellite_password }}"
server_url: "{{ satellite_server_url }}"
content_view: "{{ item.name }}"
organization: "{{ satellite_organization }}"
current_lifecycle_environment: TEST
lifecycle_environments:
- QA
loop: "{{ satellite_content_views | default([], true) }}"
when: monday_of_month | int == 3
Schedule it weekly from your automation controller and the guard clauses turn a dumb cron trigger into the right action for that week. Hosts in PROD now run content that spent a month being exercised in the earlier environments, and the promotion history in Satellite matches the git history of the repo.
Day-2: adding a new product without the UI
The payoff shows up the first time somebody asks for a new repository. Adding GitLab for the RHEL 9 hosts was one merge request touching four files:
content_credentials.yml: the GitLab package signing key, via URL lookupproducts.yml: the product with its yum repositorycontent_views.yml:cv-gitlabplus theccv-gitlabcompositeactivation_keys.yml:ak-gitlab-testandak-gitlab-prod
Review, merge, run configure.yml, and the next publish/promote cycle carries
it through the environments. No screenshots in the change ticket, no “step 14
of 23” wiki page, and six months later the diff still explains itself.