ssh superuser.pt
~/ $ ~/projects/ /var/log/ /var/mail/

hugo@superuser:/var/log/ $ less leaseweb-s3-tofu.md

Leaseweb Object Storage with Tofu

Manage your Leaseweb buckets with tofu

2025-05-11 - 3 min read

Leaseweb’s Object Storage is one of many European alternatives to AWS S3. However, their dashboard only lets you to manage users and groups (with policies), everything else must be done with third party tools.

Leaseweb Object Storage Dashboard

Since it’s S3-compatible, we can use OpenTofu (or Terraform) with the AWS provider to manage buckets and their respective access and lifecycle policies. So let’s check it out.

Provider

To set up the AWS provider, we need to configure a few settings

  • Set the region1
  • Disable STS1
  • Force the S3 URL
  • Skip region validation

So our provider configuration would look something like this:

 1provider "aws" {
 2  region     = "nl-01"
 3  access_key = var.s3_access_key
 4  secret_key = var.s3_secret_key
 5
 6  # LSW implementation has no STS service
 7  skip_credentials_validation = true
 8  skip_requesting_account_id  = true
 9  # the AWS region is unknown to AWS hence skipping is needed.
10  skip_region_validation = true
11  endpoints {
12    s3 = "https://nl.object-storage.io"
13  }
14}

Resources

Creating buckets and their respective policies is no different than using AWS S3.

 1resource "aws_s3_bucket" "this" {
 2  bucket_prefix = "my-awesome-bucket-"
 3}
 4
 5resource "aws_s3_bucket_policy" "this" {
 6  bucket = aws_s3_bucket.this.id
 7  policy = jsonencode({
 8    Statement = [
 9      {
10        Action    = [
11          "s3:*"
12        ]
13        Effect    = "Allow"
14        Principal = {
15          AWS = ["arn:aws:iam::00000000000000000000:user/my-user"]
16        }
17        Resource  = ["arn:aws:s3:::${aws_s3_bucket.this.id}/*"]
18       }
19     ]
20   })
21}

The lifecycle policy is a bit different, though: we need to force transition_default_minimum_object_size to an empty string, otherwise the policy will fail to be created. This happens because Leaseweb doesn’t have storage classes.

 1resource "aws_s3_bucket_lifecycle_configuration" "this" {
 2  bucket = aws_s3_bucket.this.id
 3
 4  # LSW needs this in order for tofu to work
 5  transition_default_minimum_object_size = ""
 6  rule {
 7    id = "daily"
 8    filter {
 9      tag {
10        key = "retention"
11        value = "daily"
12      }
13    }
14
15    expiration {
16      days = 30
17    }
18
19    status = "Enabled"
20  }
21
22}

ARN

Because Leaseweb doesn’t support STS, we are unable to fetch the account ID via Tofu. The way I got mine was by using S3 Manager.

S3 Manager object listing

Full example

 1variable "s3_access_key" {
 2  type = string
 3  description = "S3 Access Key"
 4  sensitive = true
 5}
 6
 7variable "s3_secret_key" {
 8  type = string
 9  description = "S3 Secret Key"
10  sensitive = true
11}
12
13
14provider "aws" {
15  region     = "nl-01"
16  access_key = var.s3_access_key
17  secret_key = var.s3_secret_key
18
19  # LSW implementation has no STS service
20  skip_credentials_validation = true
21  skip_requesting_account_id  = true
22  # the AWS region is unknown to AWS hence skipping is needed.
23  skip_region_validation = true
24  endpoints {
25    s3 = "https://nl.object-storage.io"
26  }
27}
28
29resource "aws_s3_bucket" "this" {
30  bucket_prefix = "my-awesome-bucket-"
31}
32
33resource "aws_s3_bucket_policy" "this" {
34  bucket = aws_s3_bucket.this.id
35  policy = jsonencode({
36    Statement = [
37      {
38        Action    = [
39          "s3:*"
40        ]
41        Effect    = "Allow"
42        Principal = {
43          AWS = ["arn:aws:iam::00000000000000000000:user/my-user"]
44        }
45        Resource  = ["arn:aws:s3:::${aws_s3_bucket.this.id}/*"]
46       }
47     ]
48   })
49}
50
51resource "aws_s3_bucket_lifecycle_configuration" "this" {
52  bucket = aws_s3_bucket.this.id
53
54  # LSW needs this in order for tofu to work
55  transition_default_minimum_object_size = ""
56  rule {
57    id = "daily"
58    filter {
59      tag {
60        key = "retention"
61        value = "daily"
62      }
63    }
64
65    expiration {
66      days = 30
67    }
68
69    status = "Enabled"
70  }
71
72}