How to build an enterprise MDM from scratch: Part 2
GitOps — Configuration as Code (And the Trap Nobody Warns You About)
Infrastructure gets you a running MDM platform. GitOps is how you structure and make it do anything useful.
A general framework for managing MDM configuration as code is a repository structure, CI/CD pipelines, secrets management, and the declarative configuration trap that can delete your entire setup in seconds.
Why GitOps for MDM?
Traditional MDM management looks like this: log in to a web console, click through menus, create a policy, and hope you remember what you changed. However, GitOps flips this. Configuration lives in a Git repository, and changes go through pull requests. CI/CD pipelines apply changes automatically, and the web console becomes a read-only dashboard rather than a control plane.
This matters for three reasons
Auditability: Every change has a commit. Every commit has an author. Every author had their PR reviewed. When security or compliance teams ask, "Who changed the FileVault policy and when?", you have an answer.
Reproducibility: If you need to rebuild your MDM (and you will eventually), you run the pipeline. Everything comes back exactly as it was. No clicking through 47 screens to recreate your configuration.
Testing: Changes can be validated before they hit production. Syntax errors get caught in CI. Logical errors get caught in review. Bad configurations don't reach devices.
The trade-off is complexity and accountability. You need a repository, a pipeline, secrets management, and discipline to never touch the web console directly. For small deployments, clicking might be fine. For anything you care about maintaining long-term, GitOps pays off.
Repository Structure
There's no single correct structure, but most MDM GitOps setups follow a similar pattern:
mdm-gitops/
├── default.yml # Org-wide settings
├── teams/ # Team-specific configuration
│ ├── team-one.yml
│ ├── team-two.yml
│ └── no-team.yml # Devices not assigned to teams
├── library/
│ └── macos/
│ ├── policies/ # Security policies
│ ├── queries/ # Osquery definitions
│ ├── profiles/ # MDM configuration profiles
│ ├── scripts/ # Automation scripts
│ └── software/ # Software deployment definitions
└── .github/
└── workflows/
└── deploy.yml # CI/CD pipeline
The Default File: The top-level configuration file contains org-wide settings: SSO configuration, MDM certificates, webhook URLs, and global features. This is the most dangerous file in the repository, more on this below.
Teams: Most MDM platforms support team-based device organization. Different teams can have different policies, different software, and different enforcement levels. Each team gets its own configuration file.
Common patterns:
By department: Engineering, Sales, Executive
By device state: New devices, production devices, high-security devices
By location: Office, remote, contractor
The structure depends on your organization. Start simple, as you can always add complexity later.
The Library: Policies, queries, profiles, and scripts live in a shared library. Team configuration files reference these by path. This keeps things DRY, defines a policy once, and applies it to multiple teams. Some resources can't be referenced by path and must be defined inline in team files. Check your platform's documentation. This inconsistency catches people off guard.
The CI/CD Pipeline: Example Below
Pipeline Design Considerations
Idempotency → Running apply twice with the same configuration should produce the same result. If your platform doesn't guarantee this, you'll end up with an inconsistent state.
Failure handling → What happens if apply fails halfway through? Does the platform roll back, or are you left in a partial state? Understand this before you're debugging it at 2 AM.
Timing → Some changes take time to propagate to devices. Your pipeline's success doesn't mean devices have received the update yet.
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Apply Configuration
run: |
if [ "${{ github.event_name }}" == "pull_request" ]; then
mdm-cli apply --dry-run
else
mdm-cli apply
fi
env:
MDM_URL: ${{ secrets.MDM_URL }}
MDM_API_TOKEN: ${{ secrets.MDM_API_TOKEN }}
The Declarative Trap
Here's the thing nobody warns you about: GitOps is declarative. What's in the repo is the truth. What's not in the repo gets deleted. This sounds obvious when stated directly. It's devastating when you learn it the hard way.
Why This Happens: Declarative systems don't know about things you didn't tell them. If your configuration file doesn't include a setting, the system interprets that as "this setting should not exist." It helpfully removes or resets it.
This applies to everything:
Settings configured through the console before GitOps was set up
Integrations you added manually and forgot about
Anything not explicitly defined in your configuration files
How to Avoid It
Export before you apply: Most platforms provide a way to export the current configuration. Do this before your first GitOps run. Compare the export to your repo. Anything in the export that's not in the repo will be deleted or reset.
Treat dry run warnings seriously: If a dry run shows something will be deleted or reset, stop. Investigate. It might be intentional, or it might be about to break your platform.
Include everything critical in your configuration: Authentication settings, certificates, integrations, and webhook URLs. If it matters, it's in the repo. If you're not sure whether something matters, include it.
Never configure through the console after GitOps is active: Once GitOps is running, the console is read-only. Any changes made through the console will be reverted on the next apply. This requires discipline, especially when troubleshooting.
Recovery
The severity of declarative deletion depends on what got removed:
Authentication settings: You might be locked out entirely.
Integrations: Re-add them to the repo and apply again.
Certificates: Depending on what was deleted, device re-enrollment might be required.
The worst case is losing access entirely. At that point, you're looking at infrastructure-level recovery, restoring databases, rebuilding the platform, and potentially re-enrolling devices. This is why you export before you apply. Always read the dry run output & test in a non-production environment first.
Secrets Management
Your GitOps configuration needs secrets: API tokens, certificate passwords, and integration credentials. These can't live in the repository in plaintext.
Options
CI/CD environment variables: Store secrets in your CI/CD platform (GitHub Actions secrets, GitLab CI variables, etc.). Reference them in your pipeline. Configuration files use placeholders that get replaced at apply time.
External secrets manager: Pull secrets from AWS Secrets Manager, HashiCorp Vault, or similar at apply time. More complex to set up, better for organizations with existing secrets infrastructure.
Encrypted files in repo: Tools like SOPS or git-crypt let you store encrypted secrets in the repository. Decryption happens in the pipeline. Works well for small teams, gets complex at scale.
Recommendation
For most teams, CI/CD environment variables are the right starting point. They're simple, secure enough for most use cases, and integrate easily with standard tooling. As you scale or if you have compliance requirements around secrets management, external secrets managers provide more control and auditability. Document what secrets exist. Keep a list (not the values, just the names) of required secrets. When someone new sets up the repository or you migrate CI systems, this list is essential. Rotate regularly. API tokens should be rotated periodically. Having them in CI/CD secrets makes rotation straightforward.
Multi-Environment Setup
Should you have staging and production?
Arguments for Staging
Test configuration changes before they hit real devices
Catch issues that only manifest on apply, not dry run
Safe environment to experiment with new features
Arguments Against Staging
MDM is device-centric — staging means maintaining test devices
Parallel environments double operational overhead
Most MDM changes are low-risk (policies, queries)
A Middle Ground
If full staging is too heavy, consider:
A test group within production: Create a device group for testing (your own device, spare hardware). Route experimental configurations there first. Less overhead, still provides a safety buffer.
Graduated rollout: Apply changes to a subset of devices, monitor, then expand. Some platforms support this natively; others require manual group management.
Comprehensive dry run review: If you can't have staging, make the dry run review thorough. Multiple reviewers, careful diff reading, explicit sign-off on changes.
Common Patterns
Mono-repo vs Multi-repo
Mono-repo: All MDM configuration in one repository. Simpler to manage, single source of truth, easier to understand dependencies.
Multi-repo: Separate repositories for different concerns (policies, software, team configs). Better for large organizations with separate teams managing different aspects.
For most organizations, mono-repo is the right choice. Multi-repo adds coordination complexity that's rarely worth it.
Branch Strategy
Keep it simple. The main branch represents the production state. All changes go through PRs to main. Feature branches for work in progress. Avoid complex branching strategies. MDM configuration is typically not "released" in versions, it's a continuously evolving state.
Review Requirements
At minimum:
One approval should be required for most changes
Passing dry run required for merge
Consider additional reviewers for authentication or certificate changes
Some organizations require security team approval for policy changes. Balance oversight with velocity. Too much friction, and people will start working around the process.
Common Mistakes
Treating GitOps as optional: Once you start, commit fully. Half-GitOps (some config in repo, some in console) is worse than no GitOps. You will lose track of what's managed where.
Skipping dry run review: It's tempting to glance at "dry run passed" and merge. The value is in reading what would change, not just that the syntax is valid.
Not testing the restore process: Your repo is your backup. But have you actually tested restoring from it? Point at a fresh instance, run apply. Does everything come back?
Hardcoding environment-specific values: URLs, identifiers, and environment-specific settings should be variables. This makes configuration portable and reduces mistakes.
Ignoring state drift: Even with GitOps, state can drift due to console changes, failed applies, and manual interventions. Run applies regularly to catch and correct drift.
Key Takeaways
For engineers: GitOps for MDM follows standard patterns, repo structure, CI/CD pipeline, and secrets in environment variables. The MDM-specific gotcha is declarative deletion. Export the current state before your first application, and read the dry run output carefully.
For IT leaders: GitOps provides audit trail, reproducibility, and change control. The investment is the initial setup and discipline to avoid console changes. Worth it for any deployment you plan to maintain long-term.
For Security: Every configuration change becomes a commit with an author. PR reviews provide oversight. Secrets stay in secrets management. The risk to manage is declarative deletion of critical settings.
What's Next
Configuration as code gives you control, but it's only useful once people can actually access the platform. In Part 3, we'll cover identity integration and a policy framework. Connecting your MDM to your identity provider for admin access, device enrollment authentication, and the user account creation decisions that affect every managed device, and managing the MDM with a set of policies for devices across your enterprise.