Nullplatform dependency service that provisions and manages an Amazon S3 bucket on AWS. Each application link creates a dedicated IAM user + access key with scoped S3 permissions, so apps authenticate with standard AWS credentials (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY).
The service lives under aws-s3-bucket/ to keep the repo open for future S3-related services.
- Provisions an S3 bucket via OpenTofu (versioning, server-side encryption, public access block)
- Creates a dedicated IAM user per link with an inline policy scoped to the bucket (and optional path prefix)
- Exposes
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYper link (each linked app gets its own IAM user) plusBUCKET_NAME/BUCKET_ARN/BUCKET_REGIONfrom the service itself - Stores per-instance OpenTofu state in a dedicated bucket (
np-service-<SERVICE_ID>) with native S3 locking (use_lockfile=true)
.
├── aws-s3-bucket/
│ ├── specs/
│ │ ├── service-spec.json.tpl # Service schema (attributes user sees)
│ │ └── links/connect.json.tpl # Link schema (access_level, path_prefix, credentials)
│ ├── deployment/ # OpenTofu module: S3 bucket + versioning + encryption
│ ├── permissions/ # OpenTofu module: IAM user + access key + scoped policy (per link)
│ ├── requirements/ # OpenTofu module: IAM policies the agent role needs
│ ├── workflows/aws/ # Workflow YAMLs (create/update/delete/link/link-update/unlink)
│ ├── scripts/aws/ # build_context, do_tofu, write_service_outputs, write_link_outputs, delete_tfstate_bucket
│ ├── entrypoint/ # entrypoint/service/link (agent entrypoint)
│ └── values.yaml # Static config (aws_profile for local dev)
└── README.md
Exposed in the nullplatform UI when creating/updating the service:
| Parameter | Type | Default | Allowed Values | Editable After Create |
|---|---|---|---|---|
bucket_name_suffix |
string | — | lowercase alphanumeric + -, 3–40 chars |
No |
versioning |
bool | true |
Yes | |
encryption |
string | AES256 |
AES256, aws:kms |
Yes |
public_access_block |
bool | true |
Yes | |
force_destroy |
bool | false |
Yes |
Bucket naming: np-<sanitized-service-name>-<bucket_name_suffix> (computed once on first create, then persisted — immutable to prevent accidental replacement on service rename).
| Parameter | Type | Default | Description |
|---|---|---|---|
access_level |
enum | read-write |
read, write, read-write |
path_prefix |
string | "" |
Scope IAM permissions to a key prefix (empty = full bucket) |
| Attribute | Description |
|---|---|
bucket_name |
S3 bucket name |
bucket_arn |
S3 bucket ARN |
bucket_region |
AWS region |
Only credentials are exposed at the link level — bucket identity (name / ARN / region) comes from the service attributes above to avoid duplicate env vars in linked apps.
| Attribute | Env Var Type | Description |
|---|---|---|
aws_access_key_id |
plain | IAM user access key ID |
aws_secret_access_key |
secret | IAM user secret access key |
| Workflow | Trigger | What It Does |
|---|---|---|
create |
Service created | Creates bucket, versioning, encryption, public access block |
update |
Service updated | Re-applies terraform with updated attributes |
delete |
Service deleted | Destroys bucket (needs force_destroy=true if non-empty) and tfstate bucket |
link |
Application linked | Creates IAM user + access key scoped to the bucket and access level |
link-update |
Link updated | In-place update of the IAM user policy (access_level or path_prefix changes). Credentials are preserved. |
unlink |
Application unlinked | Destroys the IAM user and access key |
- An account-level provider exposing
account.region
The agent executing this service needs the IAM policies defined in aws-s3-bucket/requirements/main.tf:
- S3 bucket management (
s3:CreateBucket,s3:PutBucketVersioning, etc.) over* - IAM user management (
iam:CreateUser,iam:CreateAccessKey,iam:PutUserPolicy, etc.) overarn:aws:iam::*:user/np-s3-*(or*for simpler scope) - S3 tfstate management over
arn:aws:s3:::np-service-*
- OpenTofu 1.11.6 — auto-downloaded to
/tmp/np-tofu-bin/if not inPATH. Version 1.10+ is required for native S3 state locking (use_lockfile=true), which avoids the need for a DynamoDB lock table. - AWS CLI
jq
Use the nullplatform/service_definition and nullplatform/service_definition_agent_association modules. Example:
module "service_definition_aws_s3_bucket" {
source = "git::https://github.com/nullplatform/tofu-modules.git//nullplatform/service_definition?ref=v1.51.0"
nrn = var.nrn
git_provider = "github"
repository_org = "nullplatform"
repository_name = "services-s-3"
repository_branch = "main"
service_path = "aws-s3-bucket"
service_name = "AWS S3 Bucket"
}
module "service_definition_channel_association_aws_s3_bucket" {
source = "git::https://github.com/nullplatform/tofu-modules.git//nullplatform/service_definition_agent_association?ref=v1.51.0"
nrn = var.nrn
api_key = module.service_notification_api_key.api_key
tags_selectors = var.tags_selectors
service_specification_slug = module.service_definition_aws_s3_bucket.service_specification_slug
repository_service_spec_repo = "nullplatform/services-s-3"
service_path = "aws-s3-bucket"
}Deleting the service destroys the bucket. If force_destroy=false (default) and the bucket is non-empty, OpenTofu refuses to destroy. Set force_destroy=true before deleting if you want the agent to wipe objects.
Links use static IAM access keys. Rotate by re-creating the link or manually in the AWS console if needed.
S3 bucket names are globally unique across all AWS accounts. The combination np-<service-name>-<bucket_name_suffix> must be globally unique.
OpenTofu uses native S3 lockfile (use_lockfile=true). Concurrent apply runs are serialized — the second one waits for the lock and then sees the up-to-date state, producing idempotent behavior on retries.