Skip to content

Conjure a private bucket

$ grim infra:aws-s3 --scope=per-env --project=shop --env=production

Uploads that live on the server's disk tie the app to that one server. The day you want a second machine, or a staging copy that cannot damage production files, they have to move to object storage. grim infra:aws-s3 makes the bucket, makes credentials that open that bucket and nothing else, and prints them in the shape Laravel's s3 disk reads.

It prints. It does not write your .env and it does not touch a server. Getting the values where they belong is a second, deliberate step.

Usage

$ grim infra:aws-s3 --dry-run                                        # the plan and an estimate, nothing created
$ grim infra:aws-s3                                                  # the wizard
$ grim infra:aws-s3 --scope=per-env --project=shop --env=staging     # no questions about scope
$ grim infra:aws-s3 --scope=standalone --bucket=acme-shared-assets   # a bucket outside any project

Run it from anywhere. It needs the aws CLI, terraform and working AWS credentials; grim infra:doctor checks them. For the two project scopes, the wizard offers the projects and environments registered in ~/.grim/servers.json.

Arguments and options

Create a private S3 bucket + scoped IAM user, ready for Laravel filesystems.disks.s3

Usage

grim infra:aws-s3 [--region REGION] [--scope SCOPE] [--project PROJECT] [--env ENV] [--bucket BUCKET] [--no-versioning] [--no-tiering] [--profile PROFILE] [--dry-run] [--force]

Options

Option Description
--region=REGION AWS region for the bucket (default: eu-central-1)
--scope=SCOPE Scope: per-env | per-project | standalone
--project=PROJECT Project name (for per-env / per-project scopes)
--env=ENV Environment (for per-env scope, defaults to project default)
--bucket=BUCKET Override bucket name (default: auto-suggested from scope)
--no-versioning Disable S3 object versioning
--no-tiering Skip Intelligent-Tiering lifecycle
--profile=PROFILE AWS CLI profile to use
--dry-run Show plan only, do not provision
--force Allow re-using an existing state directory

Three scopes

Scope Bucket name Use it when
per-env grim-<project>-<env>-storage-<account> The default. Staging and production get a bucket each, so a staging deploy can never overwrite a production upload.
per-project grim-<project>-storage-<account> One bucket for every environment of a project. Fewer buckets, and the risk above is yours.
standalone grim-bucket-<account>-<random>, or your own Shared assets, dumps, anything that belongs to no project. The wizard asks for the name.

--bucket overrides the name in any scope. Bucket names are global across all of AWS, which is why the account number is part of the suggestion.

What it actually does

  1. Shows the AWS identity and asks whether to create resources in that account. The default answer is no.
  2. Resolves scope, project, environment, bucket name and region. The region is eu-central-1 unless you pass --region; the wizard does not ask.
  3. Prints the plan with a monthly estimate for a small bucket, from the price snapshot built into grim. With --dry-run it stops here.
  4. Runs Terraform in ~/.grim/infra/s3-<id>/ after a second confirmation. The id is <project>-<env>, <project> or standalone-<random>.
  5. Records the bucket under aws_resources in ~/.grim/servers.json as s3-<id>: bucket name, IAM user, region, project, environment, scope, account and profile.
  6. Prints the .env block. AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, AWS_BUCKET and AWS_URL. For a per-env bucket it adds the matching grim env:set lines for that environment, with the secret left for you to fill in.

What exists in AWS afterwards

The bucket, with all four public access blocks on, AES256 encryption at rest, versioning, a CORS rule that lets any origin GET, PUT, POST and HEAD, and Intelligent-Tiering that moves objects to the archive tier after 90 days and the deep archive tier after 180. --no-versioning and --no-tiering switch those two off. The rest has no flag.

An IAM user named grim-<id>-s3 under the path /grim-cli/, with one inline policy and one access key. The policy allows listing that bucket and reading, writing and deleting objects and object versions in it, including multipart uploads and object ACLs. It names no other resource, so a leaked key costs you one bucket.

The secret key is printed once and grim never prints it again. It is not gone, though: Terraform keeps it in plain text in ~/.grim/infra/s3-<id>/terraform.tfstate. Treat that directory like a password store, and copy the block into the server's .env before you close the terminal.

Pitfalls

The bucket name is taken by someone else

Provisioning failed: followed by a hint about a bucket name conflict means another AWS customer owns that name. The state directory stays, so a plain second run stops with S3 state already exists. Run again with a different --bucket and --force, which reuses that directory.

The wizard finds no projects

No grim projects registered. Use --scope=standalone or run grim server:add-project first. appears when servers.json holds no project. The project scopes only pick from registered projects. Pass --project and --env yourself, or register the project first.

The app still writes to local disk

The credentials alone change nothing. Laravel uses the bucket only when the disk it writes to is s3, which in a stock app means FILESYSTEM_DISK=s3 next to the five lines above.

After it finishes

Put the values on the server and recreate the containers so the app reads them. From the project directory:

$ grim env:pull production               # fetch the server's .env as .env.production
$ grim env:push production --restart     # after pasting the block into it

grim env:pull describes that round trip. For a single value grim env:set is shorter. The bucket now shows in grim infra:list as s3-shop-production, and that id is what grim infra:destroy takes.