Skip to main content

Shaving off dollars with Rails 8 Credentials on AWS ECS (12-Factor Trade-off)

· 5 min read
Akhan Zhakiyanov
Lead engineer

This year as part of my role I work more often with Rails apps running on AWS ECS. We aim to follow the 12‑factor app principles, loading configuration (including secrets) via environment variables to keep apps portable, stateless, and deployment‑friendly.

AWS ECS supports passing Secrets Manager secrets through environment variables, which injects secrets into containers at startup. While Secrets Manager provides excellent security and rotation capabilities, its pricing model adds up quickly with many secrets.

Our apps have on average 20–40 secrets per application stored in AWS Secrets Manager. Since we load secrets at container startup, storage costs dominate over API call costs, even with daily deployments.

The Cost Problem

AWS Secrets Manager pricing:

  • Storage: $0.40 per secret per month
  • API Calls: $0.05 per 10,000 API calls (negligible when loading at startup)

For our typical app with 30 secrets:

  • Storage: 30 × $0.40 = $12/month = $144/year
  • API calls: ~$0.01/month (with daily deployments)

Trade-off with 12-Factor Config principle

note

From 12-factor config principle:

Apps sometimes store config as constants in the code. This is a violation of twelve-factor, which requires strict separation of config from code. Config varies substantially across deploys, code does not.

This principle recommends storing config strictly in environment variables.

We will disobey it by using Rails credentials for static secrets, and AWS Secrets Manager only for Rails master key or rotating secrets.

That means encrypted config lives in the repository and is decrypted at runtime with RAILS_MASTER_KEY. This is a pragmatic trade-off for cost savings on static secrets.

Mitigations:

  • Dedicated master key for each environment: development, test, staging, production
  • Keep master keys securely separately
  • Inject RAILS_MASTER_KEY via environment variables (never commit keys)
  • Keep rotating/short-lived secrets in AWS Secrets Manager
  • Rotate Rails credentials periodically
  • Restrict repository access and enable commit signing

Rails Credentials Setup

tip

All Rails credentials values can be overridden via environment variables if they're not empty. This gives you flexibility to override specific secrets without changing the credentials file.

# Generate credentials file
EDITOR="code --wait" rails credentials:edit --environment production

Add your static secrets:

production:
api_keys:
stripe: sk_live_...
sendgrid: SG....
github: ghp_...
jwt_secret: your_jwt_secret

Access in Rails:

# config/initializers/credentials.rb
Rails.application.config.after_initialize do
# Use ENV var if set, otherwise fall back to credentials
Stripe.api_key = ENV['STRIPE_API_KEY'].presence ||
Rails.application.credentials.dig(:production, :api_keys, :stripe)

# Database URL comes from Secrets Manager via ENV['DATABASE_URL']
end

Hybrid Setup with Pulumi

1. Store Rails Master Key in Secrets Manager

import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";

const config = new pulumi.Config();

// Store Rails master key in Secrets Manager
const masterKeySecret = new aws.secretsmanager.Secret("rails-master-key", {
name: "myapp/production/rails-master-key",
description: "Rails master key for credentials decryption",
});

const masterKeyVersion = new aws.secretsmanager.SecretVersion("rails-master-key-version", {
secretId: masterKeySecret.id,
secretString: config.requireSecret("railsMasterKey"),
});

2. Store Rotating Database Credentials

note

It's just convenient to use RDS resource outputs (host, dbname, etc) dynamically in Pulumi. Otherwise we would store db credentials in Rails config as well

// Database credentials that need rotation
const dbSecret = new aws.secretsmanager.Secret("db-credentials", {
name: "myapp/production/database",
description: "RDS credentials with automatic rotation",
});

const dbSecretVersion = new aws.secretsmanager.SecretVersion("db-credentials-version", {
secretId: dbSecret.id,
secretString: pulumi.interpolate`postgresql://dbuser:${dbPassword.result}@${dbInstance.endpoint}:5432/myapp_production`,
});

3. ECS Fargate Task Definition

const taskDefinition = new aws.ecs.FargateTaskDefinition("any-rails-app", {
cpu: "256",
memory: "512",
executionRole: taskExecutionRole,
taskRole: taskRole,
container: {
name: "any-rails-app",
image: ecrImageUri,
environment: [
{ name: "RAILS_ENV", value: "production" },
],
secrets: [
{
name: "RAILS_MASTER_KEY",
valueFrom: masterKeySecret.arn,
},
{
name: "DATABASE_URL",
valueFrom: dbSecret.arn,
},
],
portMappings: [{
containerPort: 3000,
protocol: "tcp",
}],
},
});

4. IAM Permissions

const taskExecutionRole = new aws.iam.Role("task-execution-role", {
assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({
Service: "ecs-tasks.amazonaws.com",
}),
});

// Allow reading secrets from Secrets Manager
new aws.iam.RolePolicy("task-execution-secrets-policy", {
role: taskExecutionRole.id,
policy: {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: ["secretsmanager:GetSecretValue"],
Resource: [masterKeySecret.arn, dbSecret.arn],
},
],
},
});

Cost Comparison

Scenario: 30 secrets per app

AWS Secrets Manager Only:

  • Storage: 30 × $0.40 = $12/month = $144/year
  • API calls: ~$0.01/month
  • Total: $144/year

Hybrid Approach (25 in Rails credentials + 5 in Secrets Manager):

  • Storage: 5 × $0.40 = $2/month = $24/year
  • API calls: ~$0.01/month
  • Total: $24/year

Savings: $120/year per app (83% reduction)

Multi-Environment Savings

SetupSecretsSM OnlyHybridAnnual Savings
1 app (prod)30$144$24$120
3 environments90$432$72$360
3 apps × 3 envs270$1,296$216$1,080

Summary

Use Rails Credentials for:

  • ✅ Static API keys (Stripe, SendGrid, GitHub, etc.)
  • ✅ JWT signing keys
  • ✅ Application configuration secrets

Use AWS Secrets Manager for:

  • ✅ Database credentials requiring rotation
  • ✅ Secrets accessed by multiple AWS services
  • ✅ Compliance-mandated rotation

Benefits:

  • 💰 80-90% cost reduction: from $144/year to $24/year per app
  • 🔒 Secure: credentials encrypted with AES-256-GCM, not plain text in code
  • 🚀 Unified deployment: same ECS Fargate task definition for all Rails projects
  • 🔑 Minimal secrets in ECS: only 1-2 secrets to maintain (master key + rotating DB credentials)
  • ♻️ 12-factor compliant: master key via environment variables
  • 🔄 Flexible: override any credential value via ENV vars if needed

Yes, 12-factor principles are good, but $20 is $20.