When most people start learning S3, they stop at creating buckets and uploading files.

That’s fine initially.

But once you start working on real infrastructure — managing client backups, storing Terraform state, handling compliance requirements — S3 becomes a lot more involved.

In this post, I’m covering five S3 features that I use regularly in production:

  • Lifecycle Rules
  • Versioning
  • Replication
  • Presigned URLs
  • Cross-Account Access

Let’s go through each one practically.

Lifecycle Rules

The first thing most people don’t think about with S3 is cost.

When you store data in S3 Standard, you’re paying the highest storage rate. That makes sense for frequently accessed data. But what about logs from 60 days ago? Or client backups from last quarter?

That data still needs to exist — but nobody is accessing it daily.

That’s exactly where lifecycle rules come in.

A lifecycle rule lets you define what happens to objects over time. For example:

  • After 30 days → move to S3 Standard-IA (Infrequent Access)
  • After 90 days → move to S3 Glacier
  • After 365 days → delete permanently

You set this once, and S3 handles the transitions automatically.

I use this for storing Terraform state backups. The active state file stays in Standard. Older versions move to Standard-IA after a month. Anything beyond 6 months gets cleaned up.

How to configure it:

Go to your S3 bucket → Management tab → Lifecycle rules → Create rule.

You can scope it to the entire bucket or a specific prefix — for example, only objects under backups/ or logs/.

One thing to keep in mind: transitions have minimum storage duration charges. Standard-IA has a 30-day minimum. If you move something to IA and delete it after 10 days, you still pay for 30 days. Plan your rules around actual access patterns.

Versioning

Versioning is one of those features that feels unnecessary until you accidentally delete the wrong file.

Once you enable versioning on a bucket, S3 keeps every version of every object. Overwrites don’t replace the file — they create a new version. Deletes don’t actually delete the file — they create a delete marker.

That delete marker is what makes restoration possible.

If someone runs aws s3 rm on an important file, you can go into the bucket, find the delete marker, remove it, and the file comes back. The original object was never actually gone.

How to enable it:

S3 bucket → Properties → Bucket Versioning → Enable.

After enabling, upload the same file twice with different content. Go to the Objects tab, toggle “Show versions,” and you’ll see both versions listed with their version IDs.

To restore a previous version, you either delete the newer version or copy the old version ID back as the current object.

One important thing: versioning cannot be disabled once enabled — only suspended. And versioning can increase storage costs significantly if you’re frequently overwriting large files. Use lifecycle rules to clean up non-current versions after a certain period.

Replication

Replication automatically copies objects from one S3 bucket to another — either in a different region or a completely different AWS account.

There are two types:

  • CRR (Cross-Region Replication) — source and destination buckets in different regions
  • SRR (Same-Region Replication) — both buckets in the same region

CRR is what I reach for when the requirement is disaster recovery or regional compliance. If your primary infrastructure is in ap-south-1 and a client needs a copy in us-east-1 for compliance, replication handles that automatically.

Requirements before setting it up:

  • Versioning must be enabled on both the source and destination bucket
  • An IAM role with permissions to read from source and write to destination
  • If cross-account, the destination bucket needs a bucket policy allowing the source account’s IAM role to replicate objects into it

How to configure:

Source bucket → Management → Replication rules → Create rule.

You’ll define the source (entire bucket or a prefix), the destination bucket ARN, and the IAM role. AWS can create the IAM role automatically if you don’t have one ready.

After saving the rule, upload a new object to the source bucket. Within a few seconds to a couple of minutes, it will appear in the destination bucket.

Important: replication only applies to new objects uploaded after the rule is created. Existing objects are not replicated unless you use S3 Batch Replication separately.

Presigned URLs

This is one of my favourite S3 features because it solves a very common problem cleanly.

The problem: you have a private S3 bucket (no public access), but you need to share a specific file with someone temporarily — maybe a client downloading their backup, or a user accessing a generated report.

Making the bucket public is obviously wrong. Creating a separate IAM user for them is overkill.

Presigned URLs solve this perfectly.

A presigned URL is a time-limited URL that grants access to a specific S3 object. The URL embeds the credentials and expiry time. When the time expires, the URL stops working. No bucket policy changes needed. The bucket stays private.

How to generate one using AWS CLI:

aws s3 presign s3://your-bucket-name/your-file.pdf --expires-in 3600

This generates a URL valid for 1 hour (3600 seconds). Share it with whoever needs temporary access. After an hour, it’s dead.

You can also generate presigned URLs programmatically using Boto3:

import boto3

s3 = boto3.client('s3', region_name='ap-south-1')

url = s3.generate_presigned_url(
    'get_object',
    Params={'Bucket': 'your-bucket-name', 'Key': 'your-file.pdf'},
    ExpiresIn=3600
)

print(url)

I use this pattern in applications where users need to download their own files. The backend generates a presigned URL on demand and returns it to the frontend. The frontend redirects the user to that URL. The file downloads directly from S3. No backend bandwidth consumed.

One thing to be aware of: the expiry time starts from when the URL is generated, not when it’s first used. If your application generates a URL and the user takes 2 hours to click it, it won’t work if you set expires-in to 3600.

Cross-Account Access

This one trips up a lot of people because it involves permissions from two different directions — and both need to be correct.

The scenario: you have an S3 bucket in Account A, and you want to access it from Account B.

It’s not enough to give the IAM user in Account B permission to access S3. The bucket in Account A also needs to explicitly allow Account B.

There are two sides to get right:

Side 1 — Bucket policy on Account A (resource-based policy):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::ACCOUNT-B-ID:root"
      },
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::your-bucket-name",
        "arn:aws:s3:::your-bucket-name/*"
      ]
    }
  ]
}

This tells the bucket: “Allow Account B to read objects from me.”

Side 2 — IAM policy on Account B (identity-based policy):

The IAM user or role in Account B also needs explicit permission:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::your-bucket-name",
        "arn:aws:s3:::your-bucket-name/*"
      ]
    }
  ]
}

Both sides need to allow the access. If only the bucket policy is set, it won’t work. If only the IAM policy is set, it won’t work. AWS evaluates both.

Once both are in place, you can access the bucket from Account B using:

aws s3 ls s3://your-bucket-name --profile account-b-profile

A practical use case: I use this pattern when client infrastructure lives in separate AWS accounts. The Terraform state bucket stays in a central DevOps account, and the workload accounts get cross-account read access to pull state when needed.

Putting It Together

These five features aren’t isolated — they work together.

A real-world setup might look like:

  • Versioning enabled to protect against accidental deletes
  • Lifecycle rules to automatically move older versions to cheaper storage
  • Replication to a different region for disaster recovery
  • Presigned URLs to give temporary access to specific objects
  • Cross-account bucket policy to allow the operations team in a separate account to access the bucket

That combination covers data protection, cost optimisation, access control, and compliance — all without writing a single line of application code.

If you’re only using S3 to upload and download files, you’re leaving a lot of its value unused.

Watch the Full Hands-On Walkthrough

I’ve covered all of this with live demonstrations — configuring each feature from scratch, showing what happens when things go wrong, and walking through the exact IAM policies and CLI commands.

👉 Watch here:

If this helped, drop a comment below or find me on YouTube at @awsandevops where I publish hands-on AWS and DevOps tutorials regularly.

Madhukar Reddy

DevOps engineer focused on AWS, Docker, Kubernetes, cloud infrastructure, and cyber security. Shares practical cloud and DevOps content based on hands-on deployments, infrastructure troubleshooting, and real-world projects.

$ This blog is currently running on AWS EC2 using Docker-based deployment.

Leave a response