Skip to main content

UNDERSTANDING GOOGLE CLOUD STORAGE AUTOMATION IN SWAMP

Google Cloud Storage plays two separate roles in Swamp. In the first, Cloud Storage is something Swamp manages. Buckets, objects, access controls and notifications are resources, and behaviors from the @swamp/gcp/storage extension create, read, change and delete them. In the second, Cloud Storage is where Swamp keeps its own records. The @swamp/gcs-datastore extension moves a repository's data into a bucket. The two roles can share a project and even the same tooling, but they answer different questions. One is what your infrastructure looks like. The other is where the history of your automation is stored. Both fit the primitives described in How Swamp Works.

Resources as behavior types

The storage extension follows the shape of the Cloud Storage JSON API. Each API resource gets its own behavior type: buckets, objects, folders, managed folders, bucket and object access control entries, default object access controls, HMAC keys, Pub/Sub notifications, Anywhere Cache and Rapid Cache instances, and the project's Cloud Storage service account. A behavior made from one of these types holds the configuration of one resource as its global arguments. Its methods are the operations the API allows on that resource. Most types share create, get, update, delete, sync and list. Others add operations specific to that resource, such as compose and rewrite on objects or pause and resume on Anywhere Cache.

The types are not perfectly uniform, and the differences come from the API. Folders exist only in buckets with hierarchical namespace enabled. The service account type has only get and sync, because the Cloud Storage service agent is created by Google, not by you. Several types require a name global argument as an instance name, because they follow the factory pattern, where one definition stands for many resources. Behaviors, Types, and Methods covers this relationship in general.

Each method writes the resource's state as Swamp data. After a list on objects or a sync on a bucket, the repository holds a record of what the API returned at that moment. Workflow steps can query that record without calling Google Cloud again. The Data Layer explains how that data is versioned and kept.

Choosing how access is controlled

Cloud Storage has two ways to control who can read and write. The first is uniform bucket-level access, where IAM policies on the bucket decide everything. The second is the older system of access control lists on the bucket and on each object. The extension supports both. IAM is handled by methods on the buckets type. ACLs are handled by the bucketaccesscontrols, objectaccesscontrols and defaultobjectaccesscontrols types.

For new buckets, uniform access is the better default. One policy describes the whole bucket, so you can read the policy and know who has access, and with uniform access turned on, ACLs are not used at all. The bootstrap provisioner turns it on for this reason. The ACL types are useful when you work with existing buckets that still depend on per-object grants, or when objects in one bucket need different audiences and splitting the bucket is not an option.

Within IAM, there are two styles of change. set_iam_policy replaces the entire policy, which suits a definition that is meant to be the single source of truth for the bucket. add_iam_binding and remove_iam_binding change one binding. They read the current policy, modify it, and write it back with the etag they read, so an edit made by someone else between the read and the write causes a conflict and is not silently lost. Teams that share IAM ownership with other tools or other people usually prefer the binding methods. Teams that want Swamp to own the whole policy may prefer replacement.

Some bucket settings are fixed when the bucket is created, location being the main one. Changing location later is a relocate operation, not an update. For that reason, location and similar settings belong in the behavior definition from the start.

Credentials and secrets

By default, the storage behaviors use the credentials already in the environment: Application Default Credentials, or the GOOGLE_APPLICATION_CREDENTIALS_JSON and GCP_ACCESS_TOKEN variables. This works well on a developer's machine or on a runner with an attached service account. Where credentials must come from somewhere else, the accessToken and credentialsJson global arguments accept a vault.get() expression, and the value is then redacted from output like any other vault secret. The Vaults reference describes the expression, and Auto-Refresh Cloud Credentials covers keeping short-lived tokens current.

Two arguments deal with billing, not identity. quotaProject sets the project that quota and billing are charged to. userProject names the project billed for requests to Requester Pays buckets. apiEndpoint points a behavior at an emulator, which lets a definition be tested without touching a real project.

Composing storage tasks in workflows

A single storage change rarely stands alone. A new bucket usually needs IAM bindings, perhaps a notification to a Pub/Sub topic, and perhaps a lifecycle rule. Running these as ordered workflow steps means each step starts only after the one before it has succeeded. If a later step fails, the record shows exactly how far the work got. The Workflow Execution Model describes how steps are ordered and how failures stop a run.

The bootstrap workflow in @swamp/gcs-datastore-bootstrap is a small worked example. Its first step runs the provisioner, which creates or verifies a bucket and a custom role. Its second step is a command/shell behavior that switches the repository's datastore. Because the provisioner verifies what already exists before creating anything, the workflow can be run again safely after a partial failure. This property is worth designing for in your own storage workflows, as Make Workflows Idempotent discusses.

Keeping repository data in Cloud Storage

A local datastore is enough when one person works in one checkout. Once several machines, CI runners or swamp serve instances need the same history, the data has to live somewhere they can all reach. The GCS datastore keeps a local cache directory and syncs it with the bucket in both directions, so commands read local files and only exchange changes with the bucket.

Concurrency is the hard part, and the design is built around Cloud Storage's object generations. Locks are taken with generation-based preconditions and retried with exponential backoff when another writer holds them. The index is split into partition shards per behavior under _index/. Every shard write and every write to _meta.json is a compare-and-swap on the generation, so two writers cannot drop each other's index entries. A sequence counter lets a sync with nothing to do finish without transferring files. Sync runs in two phases, which keeps the global lock held for as short a time as possible. Namespace-scoped sync lets several repositories share one bucket, which is the basis of a Giga-Swamp. Datastore Architecture places this design alongside the other datastore providers.

The datastore needs five permissions: storage.buckets.get and storage.objects.get, create, delete and list. The predefined roles/storage.objectAdmin role includes all of them along with others. The bootstrap's custom role grants exactly those five and is scoped to the project. That is narrower, at the cost of one more role to manage. Teams that already hand out objectAdmin freely may not need the custom role. Teams that audit permissions closely usually will.

The tuning settings matter mostly for large repositories or slow networks. pullConcurrency defaults to 50 and pushConcurrency to 25, and requestTimeoutMs defaults to 30000. Raising the concurrency limits shortens syncs of many small files. Raising the timeout helps over slow links. The datastore also emits OpenTelemetry spans for its operations, locks and syncs, which makes contention visible in traces. See Set Up OpenTelemetry to enable tracing and Datastore Configuration for the full set of options.