# One Storage Service, Two Operating Models: Bringing NFS, SMB, and S3 Under Automation

For years, enterprise storage was delivered through a familiar process: an administrator opened a management interface, created a filesystem or bucket, configured access, and handed the result to an application team.

That approach worked, but it also allowed every request to become a small design exercise. Two administrators could receive similar requirements and produce different configurations. Naming, access rules, capacity settings, encryption, lifecycle behavior, and documentation depended too much on who performed the work. The storage platform was centrally managed, but the service was not consistently expressed.

When we introduced a modern on-premises storage system, an **[Everpure FlashBlade//S200](https://www.everpuredata.com/content/dam/pdf/en/datasheets/ds-pure-storage-flashblade-s.pdf)** capable of serving NFS, SMB, and S3-compatible object storage from the same platform, I saw an opportunity to do more than reproduce that operating model on newer hardware. I wanted to define an institution-wide storage service that could be delivered consistently by two groups with very different working styles:

- Platform engineers who are comfortable with Git, code review, CI/CD, and AI-assisted development.
- Traditional systems administrators who understand storage operations but would rather use a form or an operations interface than work directly in a repository.

Before writing a line of automation, I spent time with the Everpure documentation and worked through manual configurations in the FlashBlade UI. Understanding the product's resource model, its boundaries, and its defaults proved essential before encoding any of that into manifests and Ansible tasks. I then designed and built the initial automation, repository structure, validation, delivery workflows, and constrained operations interface with AI assistance (Codex), which made the implementation considerably faster, especially when navigating the API surface, generating tests, and examining edge cases. The architecture and operating model, however, came from understanding the institutional problem and deciding where automation, human review, and team responsibilities should meet.

The most important design lesson was that storage administrators and the platform engineers did not need separate automation systems. They needed separate interfaces to the same controlled delivery process.

## The difficult part was defining the service

Writing an Ansible task to create an NFS export is relatively straightforward. Defining what every supported NFS export should look like is more important.

Before building the automation, I had to translate operational knowledge and agreed service standards into an explicit service contract. That included decisions such as:

- Which protocols and versions are supported?
- What are the standard filesystem and capacity settings?
- Which access modes are permitted?
- How are NFS client networks represented and validated?
- Which SMB administrative access must exist on every share?
- How are object storage accounts, buckets, users, and policies related?
- Where are generated S3-compatible object storage credentials stored?
- What does removal mean, and which deletions must be recoverable?
- Which requests are standard, and which require engineering review?

These standards became defaults and validation rules rather than instructions in a runbook. An operator should not have to remember twenty settings for every routine request. The interface should ask for the few pieces of information that genuinely vary and derive the rest from the approved service definition.

An illustrative request might be as small as:

```yaml
request:
  type: nfs
  name: application-data
  export: /application-data
  clients:
    - 192.0.2.40/32
    - 192.0.2.64/27
  change_reference: CHG0001234
```

This is deliberately not a complete storage configuration. The automation supplies the approved protocol, security, capacity, naming, and lifecycle defaults. A request that needs to override those defaults is classified as nonstandard and moves to the engineering path.

That boundary matters. A self-service interface becomes dangerous when it exposes every underlying product option. Good platform engineering reduces the number of decisions a consumer must make while preserving an explicit route for legitimate exceptions.

## I chose Git as the control plane

I made the repository the source of truth for the desired state of the service: manifests, automation, validation, tests, and deployment workflows. Changes to storage begin as changes to that desired state.

The basic flow is:

1. A user submits a standard request or an engineer proposes a broader change.
2. The request is converted into a small manifest change on a new branch.
3. Validation runs without production credentials.
4. A pull request shows the exact proposed state change.
5. The appropriate service owner or engineer approves it.
6. After merge, a protected deployment workflow reconciles the storage platform.
7. The deploy pipeline runs a **second apply immediately after the first and asserts zero changes**—idempotency is verified automatically, not assumed.
8. Operations verifies the live service outcome.

The pull request is not ceremony added for developers. It is the human-readable change record. It answers who requested the change, what will be different, which checks passed, who approved it, and which revision was deployed.

It also separates intent from execution. Pull-request validation does not need access to the storage array or credential store. Production credentials are available only to the protected deployment job after the change has passed review and been merged.

This is safer than allowing a script, chatbot, or web form to call the storage API directly. The interface can prepare a change, but it cannot silently bypass review or mutate production.

## The engineering interface: Git and an AI coding agent

Platform engineers need access to the full repository because their work is open-ended. They may add a feature, change validation, fix an Ansible module workaround, extend a lifecycle operation, update tests, or troubleshoot a failed deployment.

For that audience, Git and an AI coding agent work well together. I have used AI extensively while building the platform, but as an engineering accelerator rather than an autonomous operator. I can describe an intended change in plain English, allow the agent to inspect the repository's instructions and tests, and then review the resulting diff. It shortens the path from an idea to a tested implementation, but it does not become the production authority.

That distinction also matters when describing the work honestly. AI helped me implement and test faster; it did not identify the organizational problem, negotiate the boundaries between teams, or decide what a safe operating model should be. Those remained engineering decisions.

The deterministic controls remain in the pipeline:

- Schema and semantic validation.
- YAML and Ansible linting.
- Syntax checks.
- Unit tests for request generation.
- Offline check-mode tests where practical.
- Secret scanning and focused diff review.
- Code ownership and protected branches.
- Environment approval before deployment.
- Post-apply idempotency check: a second reconciliation run that must report zero changes.

This distinction is important. AI is useful for interpreting an engineering request and proposing code. It should not replace predictable validation, peer review, or the deployment boundary.

## The operations interface: a form, a CLI, and a consistent contract

The systems administrators did not need the repository's full flexibility. Most of their work involved standard additions: create an NFS share, create an SMB share, or create an S3 account and bucket using established defaults.

For them, I built two things that express the same constrained operation:

**A GitHub Actions form.** Operators open the repository's *Request standard Storage* workflow in the Actions UI, select a storage type, fill in only the fields specific to that request—filesystem name, export path, client IPs—and run it. The workflow validates the inputs, calls `storagectl` internally, and opens a draft pull request. The operator never touches a terminal or a YAML file. We are about to migrate this interface to our [Semaphore automation platform](https://semaphoreui.com/), which will present the same fields in a more purpose-built UI. The underlying contract — `storagectl`, the manifest schema, the validation pipeline, and the deployment workflow require no changes at all. That is the intended property of the design.

![The GitHub Actions form interface for requesting storage](https://cdn.hashnode.com/uploads/gql/6aa73ede13ae672728e4d165/5d2ae281-6f04-44c9-935d-219731667af9.png)

**A `storagectl` CLI** for engineers or scripts that need a programmatic path. Its purpose is intentionally narrow: validate a supported request, update the appropriate manifest, and open a draft pull request.

A CLI invocation resembles:

```shell
storagectl request nfs \
  --name application-data \
  --export /application-data \
  --client 192.0.2.40/32 \
  --change CHG0001234
```

Both the form and the CLI produce the same manifest change. Neither connects to the storage system, and neither deploys anything.

The same contract can later sit behind an automation portal such as Semaphore or a tightly constrained AI tool. Those are presentation layers. They should all invoke the same tested operation rather than reimplement storage logic independently.

This led to a simple design rule:

> Build one deterministic operations contract, then provide interfaces suited to different users.

The platform engineer can work directly with Git. The administrator can use a web form. Both produce the same manifest change, validation results, pull request, approval trail, and deployment behavior.

## Approval follows the change, not the interface

Introducing two interfaces raised an organizational question: who should approve a pull request created by an operations administrator?

The answer depends on what changed.

A routine manifest addition should be reviewed by another storage operator or service owner who can validate the requested service outcome. A change to automation code, tests, workflows, defaults, or security behavior should be reviewed by the platform engineering team. A change that affects both service intent and platform behavior should receive both perspectives.

This avoids two unhelpful extremes. Platform engineers do not become a ticket queue for every standard storage request, and operations staff are not expected to approve changes to automation internals they do not maintain.

The submitter's team and chosen interface do not determine governance. The content and risk of the change do.

## NFS, SMB, and S3 are not the same resource with different labels

One temptation was to create a highly abstract storage model that treated every protocol identically. That would have produced elegant-looking code and a confusing service.

The three offerings share a delivery pipeline, but their operational models are different:

- **NFS** centers on filesystems, export policies, client networks, protocol compatibility, and squash behavior.
- **SMB** adds directory-service identities, share permissions, administrative access, and SMB-specific security behavior.
- **S3-compatible object storage** on the FlashBlade involves accounts, buckets, users, access policies, generated keys, and a separate secrets-management lifecycle. This is *not* AWS S3 — it is the FlashBlade's own S3-compatible endpoint. The only AWS involvement is storing the generated credentials in AWS Secrets Manager, accessed via GitHub OIDC with no long-lived cloud key.

I reused common mechanics where they were genuinely common: request validation, Git operations, pull requests, approval, protected deployment, and audit history. I kept protocol-specific logic explicit and readable.

That balance has kept the implementation understandable. Modularity is useful when it reflects a real boundary. Abstraction for its own sake merely makes the next administrator reverse-engineer the framework before changing it.

## Credential handling shaped the architecture

Object storage made credential handling particularly important. I designed the workflow around a simple rule: an automation run may need to generate an access key, but that credential should never appear in a manifest, pull-request comment, workflow output, or AI prompt.

The safer pattern is to pass a generated credential directly from the storage API response into an enterprise secrets manager while suppressing sensitive task output. The manifest records only the desired identity and the approved secret location—not the credential value.

The deployment workflow also uses short-lived credentials obtained through **GitHub OIDC workload identity federation** to assume a scoped AWS role, rather than maintaining a long-lived cloud key in the repository. The FlashBlade API token and the AWS role are available only within the protected `prd` deployment environment, after environment protections pass. Storage API credentials remain limited to that protected environment and never appear in logs or pull-request output.

This produces three useful trust zones:

- **Request and pull request:** no production credentials and no production connectivity.
- **Validation:** enough context to reject an invalid or unsafe desired state, but no ability to apply it.
- **Deployment:** protected credentials, restricted network access, and only the permissions required to reconcile approved resources.

The same separation should survive whichever interface is added later. A friendly UI must not become a reason to flatten the security model.

## Deletion deserved a different workflow

Creating a storage resource is usually reversible. Deleting one may destroy data or credentials. I therefore avoided presenting deletion as just another self-service checkbox.

Routine creation can use the constrained operations interface. Changes to existing resources, migrations, and removals follow the engineering path and require broader review. Resources marked absent remain visible in desired state until operators verify the outcome; cleanup of the manifest happens separately.

This is less convenient than one-click deletion, and intentionally so. A good platform makes the safe path easy without pretending every operation has the same risk.

## Could this have been just Ansible and Semaphore?

I repeatedly asked myself whether this should have been just a set of Ansible playbooks launched from [Semaphore](https://semaphoreui.com/). For a small team managing a limited number of resources, that could be entirely sufficient. Not every automation effort needs a GitOps repository, a custom CLI, AI instructions, and multiple workflows.

In this case, the additional structure became worthwhile because the service crossed several boundaries:

- Three protocols with different resource and security models.
- Multiple teams with different technical preferences.
- Institution-wide standards that needed to remain consistent.
- Production changes requiring peer review and an audit trail.
- Credentials spanning the storage system and a separate secrets platform.
- A need to support both routine operations and continued engineering.

An MCP server could eventually expose those same operations to an internal assistant. Neither needs to replace the repository or deployment pipeline.

The mistake would be implementing storage behavior separately in every interface. The command, portal, and AI tool should all converge on one versioned contract.

## The role of platform engineering

My contribution was not learning how to operate the storage interface faster. It was recognizing that the institution needed a repeatable service, designing the operating model, and building the initial platform that could deliver it.

That meant encoding standards, creating safe defaults, building validation, separating credentials, defining lifecycle behavior, establishing review boundaries, and providing more than one way to consume the result.

The infrastructure operations team still owns essential parts of the service. Its administrators understand client behavior, access requirements, operational risk, live verification, and customer impact. Their knowledge informs the standards, and their verification establishes whether a deployed service actually works. Platform engineering packages that shared operational knowledge into a paved path and maintains the machinery beneath it.

This is the partnership I want platform engineering to create:

- Operations defines and validates the service outcome.
- Platform engineering turns recurring delivery into a reliable product.
- Consumers choose an interface appropriate to their skills.
- Governance remains consistent regardless of interface.

The result is not "developers replacing sysadmins." It is fewer handcrafted changes, clearer ownership, and more time for both groups to work on problems that actually require their judgment.

## Core Principles Behind the Architecture

1. **Define the service before automating the product.** A playbook without standards only reproduces inconsistency more quickly.
2. **Identify values that truly vary.** Everything else should come from reviewed defaults.
3. **Keep the operational interface constrained.** Standard requests and engineering changes should not pretend to be the same thing.
4. **Use Git as an audit and approval boundary, not as a user-interface requirement.** Consumers should not need to understand Git to benefit from Git-based controls.
5. **Let approval follow risk and change content.** Service owners review intent; platform engineers review platform behavior.
6. **Keep AI outside the trust boundary.** Let it propose changes while deterministic checks and humans retain authority.
7. **Design secrets handling before adding object storage.** A credential that reaches a log or pull request is already mishandled.
8. **Make destructive operations intentionally harder.** Convenience is not the primary design goal for deletion.
9. **Abstract shared delivery mechanics, not protocol differences.** Readable duplication is sometimes better than a clever universal model.
10. **Build the reusable operation once.** CLIs, portals, and AI tools should be adapters around the same contract.

## Conclusion

For me, modernizing storage was never primarily about replacing a graphical interface with YAML. It was about replacing individual interpretation with a service contract while preserving the operational judgment of the people responsible for the platform.

The important outcome is that a DevOps engineer and a traditional systems administrator can approach the same enterprise service through interfaces that make sense to each of them and still arrive at the same reviewed, secure, and supportable result.

That is where I believe platform engineering earns its place: not by adding another layer between teams, but by making good engineering reusable beyond the person or team that first created it.
