How-To Guide
Create S3 Buckets with Crossplane¶
Learn how to provision S3 buckets on Hetzner Object Storage using Crossplane for your applications.
Prerequisites¶
Access to the kup6s Kubernetes cluster
kubectl configured with valid kubeconfig
Understanding of Kubernetes manifests
When to Use Crossplane for S3 Buckets¶
Use Crossplane Bucket resources when:
Your application needs dedicated S3 storage
You want infrastructure-as-code bucket management
Buckets should be lifecycle-managed with your application
Available Regions / ProviderConfigs:
The endpoint is fixed per ProviderConfig — picking a region means picking the matching ProviderConfig, not just setting forProvider.region. Writing region: hel1 on a bucket that references hetzner-s3 still lands the data on fsn1.
Region |
|
Use for |
|---|---|---|
|
|
Cold/off-site backups, default |
|
|
Hot read/write storage next to the EX workers (Helsinki) — lowest latency |
nbg1 has no ProviderConfig yet. Never use hetzner-s3-dr for application buckets (etcd-DR only — see the ProviderConfig reference).
Bucket Naming Convention¶
CRITICAL: Hetzner S3 bucket names are globally unique across all Hetzner customers (like AWS S3). Generic names like “backup”, “logs”, or “artifacts” will fail because they’re already taken by other customers.
Naming Schema: LOCALPART-NAMESPACE-kup6s
LOCALPART: Descriptive purpose (e.g., “gitlab-artifacts”, “uploads”, “backups”)
NAMESPACE: Kubernetes namespace where the application runs
kup6s: Project identifier suffix (mandatory to ensure global uniqueness)
Examples:
Application buckets:
gitlab-artifacts-gitlabbda-kup6s,uploads-myapp-production-kup6sInfrastructure buckets:
backup-etcd-fsn1-kup6s,logs-loki-kup6s
Rule: Always suffix bucket names with -kup6s to avoid collisions with other Hetzner customers.
Basic Bucket Creation¶
1. Create Bucket Manifest¶
Create a file data-myapp-kup6s.yaml:
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
name: data-myapp-kup6s # Following naming convention: LOCALPART-NAMESPACE-kup6s
labels:
app: myapp
namespace: production
spec:
deletionPolicy: Delete # Or Orphan to keep bucket when deleting resource
managementPolicies:
- Observe
- Create
- Delete
# Skip Update to avoid tagging operations (not supported by Hetzner S3)
forProvider:
region: fsn1 # Hetzner region code
providerConfigRef:
name: hetzner-s3 # Infrastructure-managed ProviderConfig
2. Apply the Manifest¶
kubectl apply -f data-myapp-kup6s.yaml
3. Verify Bucket Creation¶
# Check bucket status
kubectl get bucket.s3.aws.upbound.io data-myapp-kup6s
# Should show both SYNCED=True and READY=True
# Example output:
# NAME SYNCED READY EXTERNAL-NAME AGE
# data-myapp-kup6s True True data-myapp-kup6s 1m
4. Access Bucket from Applications¶
The bucket will be available at: https://data-myapp-kup6s.fsn1.your-objectstorage.com
Use the shared S3 credentials (stored in hetzner-s3-creds secret in crossplane-system namespace).
Advanced Configuration¶
Deletion Policy Options¶
Delete (Default):
spec:
deletionPolicy: Delete # Bucket deleted when resource is deleted
managementPolicies: [Observe, Create, Delete]
Orphan (Preserve Bucket):
spec:
deletionPolicy: Orphan # Bucket remains when resource is deleted
managementPolicies: [Observe, Create, Delete]
Management Policies¶
Always include managementPolicies to skip Update operations:
spec:
managementPolicies:
- Observe # Monitor bucket state
- Create # Create bucket
- Delete # Delete bucket
# Skip Update/LateInitialize to avoid tagging (not supported by Hetzner)
Why this is required:
Hetzner S3 doesn’t support tagging operations (returns
501 Not Implemented)Without managementPolicies, Crossplane tries to apply tags after creation
This causes
SYNCED=Falsestatus despite bucket being functionalWith managementPolicies, bucket shows both
SYNCED=TrueandREADY=True
Regional Deployment¶
Choose region based on your use case. Always set region and the matching providerConfigRef.name together — the ProviderConfig determines the actual endpoint; the region field alone does not redirect traffic.
Hot storage near the EX workers (Helsinki, low latency):
spec:
managementPolicies: [Observe, Create, Delete]
forProvider:
region: hel1
providerConfigRef:
name: hetzner-s3-hel1 # REQUIRED — without this the bucket lands on fsn1
Cold / off-site backups (Falkenstein, geographic redundancy):
spec:
managementPolicies: [Observe, Create, Delete]
forProvider:
region: fsn1
providerConfigRef:
name: hetzner-s3
A typical app splits the two: hot files in hel1 via hetzner-s3-hel1, backups in fsn1 via hetzner-s3.
Integration with Applications¶
Accessing S3 Credentials¶
Applications need S3 credentials to access buckets. Use the infrastructure-managed credentials:
apiVersion: v1
kind: Pod
metadata:
name: myapp
namespace: production
spec:
containers:
- name: app
image: myapp:latest
env:
- name: S3_ENDPOINT
value: "https://fsn1.your-objectstorage.com"
- name: S3_BUCKET
value: "data-myapp-kup6s"
- name: S3_REGION
value: "fsn1"
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: hetzner-s3-creds
key: access_key_id
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: hetzner-s3-creds
key: secret_access_key
Note: You’ll need to copy the hetzner-s3-creds secret from crossplane-system namespace to your application’s namespace, or configure appropriate RBAC.
Bucket URL Format¶
Path-Style (Recommended for Hetzner):
https://fsn1.your-objectstorage.com/my-app-bucket/object-key
Virtual-Hosted Style (Also Supported):
https://my-app-bucket.fsn1.your-objectstorage.com/object-key
Troubleshooting¶
Bucket Shows SYNCED=False but READY=True¶
Cause: Missing managementPolicies in bucket spec
Solution: Add managementPolicies to skip Update operations:
kubectl patch bucket.s3.aws.upbound.io my-app-bucket \
-p '{"spec":{"managementPolicies":["Observe","Create","Delete"]}}' --type=merge
Explanation:
Without managementPolicies, Crossplane tries to apply tags after bucket creation
Hetzner S3 doesn’t support tagging (returns
501 Not Implemented)Adding managementPolicies skips Update operations (including tagging)
After patching, bucket should show both
SYNCED=TrueandREADY=True
Bucket Creation Fails¶
Check ProviderConfig:
kubectl get providerconfig.aws.upbound.io hetzner-s3 -o yaml
Ensure it has:
skip_region_validation: truehostnameImmutable: trueservices: [s3]Correct endpoint:
https://fsn1.your-objectstorage.com
Check Credentials:
kubectl get secret hetzner-s3-creds -n crossplane-system
Check Provider Pod:
kubectl get pods -n crossplane-system -l pkg.crossplane.io/provider=provider-aws-s3
kubectl logs -n crossplane-system <provider-pod-name>
Bucket Not Visible in Hetzner Console¶
Wait 30-60 seconds after READY=True status. Bucket creation is asynchronous.
Verify with AWS CLI:
export AWS_ACCESS_KEY_ID=<your-key>
export AWS_SECRET_ACCESS_KEY=<your-secret>
aws s3 ls --endpoint-url https://fsn1.your-objectstorage.com
Cleanup¶
Delete Bucket (with data deletion)¶
kubectl delete bucket.s3.aws.upbound.io data-myapp-kup6s
Warning: If deletionPolicy: Delete, this will delete the bucket and all its contents!
Preserve Bucket (orphan)¶
Ensure
deletionPolicy: Orphanis set:kubectl patch bucket.s3.aws.upbound.io data-myapp-kup6s \ -p '{"spec":{"deletionPolicy":"Orphan"}}' --type=merge
Then delete the resource:
kubectl delete bucket.s3.aws.upbound.io data-myapp-kup6s
The bucket remains in Hetzner Object Storage for manual cleanup later.
AI Assistance Prompt¶
When asking an AI to help create S3 buckets with Crossplane, use this prompt:
Prompt for AI:
I need to create an S3 bucket on Hetzner Object Storage using Crossplane in our kup6s Kubernetes cluster.
Context:
We use Upbound provider-aws-s3 configured for Hetzner Object Storage (S3-compatible)
One ProviderConfig per region — the ProviderConfig fixes the endpoint;
forProvider.regionalone does NOT redirect traffic:
hetzner-s3→ fsn1 endpoint (cold/off-site backups, default)
hetzner-s3-hel1→ hel1 endpoint (hot storage next to the EX workers in Helsinki)Do NOT use
hetzner-s3-drfor application buckets (etcd-DR only)IMPORTANT: Must include
managementPolicies: [Observe, Create, Delete]to avoid tagging issuesCRITICAL: Bucket names must follow convention
LOCALPART-NAMESPACE-kup6s(globally unique across all Hetzner customers)Requirements:
Create a Bucket resource manifest for a bucket named
[LOCALPART]-[NAMESPACE]-kup6sSet
regionAND the matchingproviderConfigRef.nametogether: hel1 →hetzner-s3-hel1, fsn1 →hetzner-s3Set appropriate deletionPolicy (
DeleteorOrphan)Include labels for application:
[APP-NAME]Include managementPolicies to skip Update operations
Ensure bucket name follows naming convention with
-kup6ssuffixPlease provide:
Complete Kubernetes manifest YAML
kubectl apply command
How to verify bucket creation
Environment variables needed in my application pod to access this bucket
Reference:
See 50-B-crossplane-provider-s3.yaml.tpl (
hetzner-s3/fsn1) and 50-D-crossplane-provider-s3-hel1.yaml.tpl (hetzner-s3-hel1/hel1) for the ProviderConfigsDocumentation: How-To: Create S3 Buckets with Crossplane
Replace [BUCKET-NAME], [APP-NAME], and the region/ProviderConfig pair (hel1+hetzner-s3-hel1 or fsn1+hetzner-s3) with your specific values.
See Also¶
ProviderConfig Reference - Complete ProviderConfig documentation
Hetzner Object Storage Docs - Official Hetzner S3 documentation
Apply Infrastructure Changes - How to update infrastructure with OpenTofu