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

providerConfigRef.name

Use for

fsn1

hetzner-s3

Cold/off-site backups, default

hel1

hetzner-s3-hel1

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-kup6s

  • Infrastructure 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=False status despite bucket being functional

  • With managementPolicies, bucket shows both SYNCED=True and READY=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=True and READY=True

Bucket Creation Fails

Check ProviderConfig:

kubectl get providerconfig.aws.upbound.io hetzner-s3 -o yaml

Ensure it has:

  • skip_region_validation: true

  • hostnameImmutable: true

  • services: [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)

  1. Ensure deletionPolicy: Orphan is set:

    kubectl patch bucket.s3.aws.upbound.io data-myapp-kup6s \
      -p '{"spec":{"deletionPolicy":"Orphan"}}' --type=merge
    
  2. 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.region alone 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-dr for application buckets (etcd-DR only)

  • IMPORTANT: Must include managementPolicies: [Observe, Create, Delete] to avoid tagging issues

  • CRITICAL: Bucket names must follow convention LOCALPART-NAMESPACE-kup6s (globally unique across all Hetzner customers)

Requirements:

  1. Create a Bucket resource manifest for a bucket named [LOCALPART]-[NAMESPACE]-kup6s

  2. Set region AND the matching providerConfigRef.name together: hel1 → hetzner-s3-hel1, fsn1 → hetzner-s3

  3. Set appropriate deletionPolicy (Delete or Orphan)

  4. Include labels for application: [APP-NAME]

  5. Include managementPolicies to skip Update operations

  6. Ensure bucket name follows naming convention with -kup6s suffix

Please 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:


Replace [BUCKET-NAME], [APP-NAME], and the region/ProviderConfig pair (hel1+hetzner-s3-hel1 or fsn1+hetzner-s3) with your specific values.

See Also