Serve originals from Amazon S3 or S3-compatible storage, such as MinIO or
Cloudflare R2, in private buckets. This guide assumes ImagePipe is running in
your app (see Getting started with Phoenix) or as image_pipe_server (see
getting started with the server).
Add an S3 source
Give the source the bucket's region and endpoint, and credentials allowed to
read objects (s3:GetObject):
Plug
sources: [
media: [
adapter: ImagePipe.Source.S3,
match: [scheme: "s3"],
options: [
default: [
region: "eu-west-1",
endpoint: "https://s3.eu-west-1.amazonaws.com",
credentials: {:provider, ImagePipe.Source.S3.InstanceRole, []}
]
]
]
]image_pipe_server
[sources.media]
adapter = "s3"
match = { scheme = "s3" }
region = "eu-west-1"
endpoint = "https://s3.eu-west-1.amazonaws.com"
credentials = { provider = "instance_role" }For other S3-compatible stores, set endpoint to their API URL. Objects are
requested path-style, as <endpoint>/<bucket>/<key>. Image URLs name an
object after src/ as s3://bucket/key:
/w=400/src/s3://my-bucket/photos/beach.jpgChoose credentials
Pick the credentials that match where ImagePipe runs:
- Static keys work anywhere, including outside AWS.
- An EC2 instance role on EC2 and Elastic Beanstalk.
- Container credentials on ECS and Fargate.
- A service account role on EKS.
- An assumed role for a bucket in another account.
ImagePipe refreshes temporary credentials before they expire, and never
sends expired ones. If they can't be refreshed, requests fail with 500
until they can.
Use static keys
Plug
credentials:
{:static,
access_key_id: System.fetch_env!("AWS_ACCESS_KEY_ID"),
secret_access_key: System.fetch_env!("AWS_SECRET_ACCESS_KEY")}For temporary keys, add token: System.get_env("AWS_SESSION_TOKEN"). When
AWS_SESSION_TOKEN is unset, the keys are used without a session token.
image_pipe_server
Remove credentials from the file, and set the keys with environment
variables, reading the secret from a file:
IPS_SOURCES__MEDIA__CREDENTIALS__STATIC__ACCESS_KEY_ID=AKIA…
IPS_SOURCES__MEDIA__CREDENTIALS__STATIC__SECRET_ACCESS_KEY_FILE=/run/secrets/s3_secret
For temporary keys, also set
IPS_SOURCES__MEDIA__CREDENTIALS__STATIC__TOKEN.
Use an EC2 instance role
Attach a role that can read the bucket to the instance. ImagePipe reads its credentials from the instance metadata service (IMDSv2):
Plug
credentials: {:provider, ImagePipe.Source.S3.InstanceRole, []}image_pipe_server
credentials = { provider = "instance_role" }Use container credentials
Give the ECS task a task role that can read the bucket. ECS sets
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI in the container, which locates the
task role's credentials:
Plug
credentials:
{:provider, ImagePipe.Source.S3.ContainerCredentials,
relative_uri: System.fetch_env!("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")}If the platform sets AWS_CONTAINER_CREDENTIALS_FULL_URI instead, pass it
as full_uri:, with the token from AWS_CONTAINER_AUTHORIZATION_TOKEN as
auth_token:. EKS Pod Identity sets AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE
instead of AWS_CONTAINER_AUTHORIZATION_TOKEN. Pass that path as
auth_token_file:. The file is read again on every refresh, since EKS
rotates the token.
image_pipe_server
credentials = { provider = "container_credentials" }The server fills relative_uri, full_uri, auth_token, and
auth_token_file from AWS_CONTAINER_CREDENTIALS_RELATIVE_URI,
AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_CONTAINER_AUTHORIZATION_TOKEN, and
AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, so the same setting works on ECS
and with EKS Pod Identity. Setting any of the four in credentials stops it filling the
others.
Use an EKS service account role
With IAM roles for service accounts (IRSA), EKS mounts a token file into the pod. ImagePipe exchanges it for credentials of the service account's role:
Plug
credentials:
{:provider, ImagePipe.Source.S3.WebIdentity,
token_file: System.fetch_env!("AWS_WEB_IDENTITY_TOKEN_FILE"),
role_arn: System.fetch_env!("AWS_ROLE_ARN"),
region: System.fetch_env!("AWS_REGION")}image_pipe_server
credentials = { provider = "web_identity" }The server reads token_file, role_arn, and region from
AWS_WEB_IDENTITY_TOKEN_FILE, AWS_ROLE_ARN, and AWS_REGION, which EKS
sets. Settings in credentials override these variables.
Assume a role in another account
To read a bucket in another account, assume a role there that can read it.
The base credentials, any of the forms above, must be allowed to assume
the role:
Plug
credentials:
{:provider, ImagePipe.Source.S3.AssumeRole,
base: {:provider, ImagePipe.Source.S3.InstanceRole, []},
role_arn: "arn:aws:iam::123456789012:role/image-read",
external_id: "image-pipe",
region: "eu-west-1"}image_pipe_server
[sources.media.credentials]
provider = "assume_role"
base = { provider = "instance_role" }
role_arn = "arn:aws:iam::123456789012:role/image-read"
external_id = "image-pipe"
region = "eu-west-1"external_id is needed only when the role's trust policy requires one.
Serve only some buckets
By default the source serves any bucket the credentials can read. List buckets to serve only those. Each one can override the source's settings, such as its region or credentials:
Plug
options: [
default: [
region: "eu-west-1",
endpoint: "https://s3.eu-west-1.amazonaws.com",
credentials: {:provider, ImagePipe.Source.S3.InstanceRole, []}
],
buckets: %{
"photos" => [],
"archive" => [
region: "us-east-1",
endpoint: "https://s3.us-east-1.amazonaws.com"
]
}
]image_pipe_server
[sources.media.buckets.photos]
[sources.media.buckets.archive]
region = "us-east-1"
endpoint = "https://s3.us-east-1.amazonaws.com"Other buckets then answer 404.
Fetch credentials at startup
Credentials from a provider are fetched on the first request for each bucket, which adds a round trip to that request. To fetch them at startup instead:
Plug
Add an ImagePipe.Source.S3.CredentialWarmup for each bucket to your
supervision tree, with the same provider and options as the source:
# lib/my_app/application.ex
children = [
{ImagePipe.Source.S3.CredentialWarmup,
provider: ImagePipe.Source.S3.InstanceRole, opts: [], scope: "photos"},
{ImagePipe.Source.S3.CredentialWarmup,
provider: ImagePipe.Source.S3.InstanceRole, opts: [], scope: "archive"},
MyAppWeb.Endpoint
]image_pipe_server
The server fetches credentials at startup for each bucket listed under
buckets whose credentials come from a provider.
The fetched credentials are kept, and refreshed before they expire, until the first request for the bucket. After that, credentials that no request uses are dropped after five to ten minutes. A failed fetch at startup is retried by the first request.
Request object versions
In a bucket with versioning on, add a version ID as the whole query to
request that version, written ? as %3F:
/w=400/src/s3://my-bucket/photos/beach.jpg%3F3HL4kqtJlcpXA versioned object never changes, so cached images made from it are served
without checking S3 (see
immutable sources). If the
store ignores version IDs, the request fails with 502 rather than serving
the current object.
Confirm it works
Request an object through the source. If your URLs are signed, sign this one too.
Plug
$ curl -s -o /dev/null -w '%{http_code} %{content_type}\n' http://localhost:4000/images/w=400/src/s3://my-bucket/photos/beach.jpg
200 image/jpeg
image_pipe_server
The server listens on port 8080 and serves from / unless [server] sets
another port or mount_path.
$ curl -s -o /dev/null -w '%{http_code} %{content_type}\n' http://localhost:8080/w=400/src/s3://my-bucket/photos/beach.jpg
200 image/jpeg
A missing object, or one the credentials can't read, answers 404. A 500
can mean the credentials couldn't be fetched.
Next steps
- Caching processed images keeps downloaded originals and resized copies.
ImagePipe.Source.S3, its credential providers, and the server'sadapter = "s3"reference list every option.