serverless-plugin-arn-prefixer
Version:
A serverless plugin that injects custom ARN properties for building AWS resource ARNs
369 lines (287 loc) • 12.7 kB
Markdown
# Serverless ARN Prefixer Plugin
A Serverless Framework plugin that automatically injects custom ARN properties into your serverless configuration, making it easier to build AWS resource ARNs consistently across your application. Supports both `${arn:*}` variable resolvers and `${arn:*}` object references for maximum compatibility.
## Installation
### From npm (Recommended)
```bash
npm install @hyperdrive.bot/serverless-arn-prefixer
```
### From GitLab Package Registry (Development)
```bash
# Configure npm to use GitLab registry for @hyperdrive.bot scope
npm config set @hyperdrive.bot:registry https://gitlab.com/api/v4/projects/PROJECT_ID/packages/npm/
# Install the package
npm install @hyperdrive.bot/serverless-arn-prefixer
```
### Local Development
If developing within the monorepo, no separate installation required.
## Usage
Add the plugin to your `serverless.yml`:
### When installed from npm
```yaml
plugins:
- "@hyperdrive.bot/serverless-arn-prefixer"
```
### When using locally in monorepo
```yaml
plugins:
- ./packages/serverless/arn-prefixer
```
Or if using from within the packages/serverless directory:
```yaml
plugins:
- ./arn-prefixer
```
## Configuration
### Runtime Code Generation (Default: Enabled)
Configure runtime file generation in your `serverless.yml`:
```yaml
custom:
arnPrefixer:
generateRuntime: true # Default: true - Enable/disable runtime generation
runtimePath: 'runtime.js' # Default: 'runtime.js' - Output filename
```
**Disable Runtime Generation:**
```yaml
custom:
arnPrefixer:
generateRuntime: false
```
**Custom Runtime Path:**
```yaml
custom:
arnPrefixer:
runtimePath: 'my-arn-values.js' # Will also generate my-arn-values.d.ts
```
## What it does
The plugin automatically injects a `custom.arn` object into your serverless configuration with the following properties:
**NEW**: ⚡ Runtime Code Generation (Default: Enabled)
The plugin now generates runtime JavaScript and TypeScript files that allow you to import ARN values directly in your Lambda code without environment variables:
```javascript
// Import specific values
import { arnPrefix, globalPrefix, dynamodb } from 'arn-prefixer/runtime'
// Use in your Lambda code
const tableName = `${arnPrefix}-users`
const tableArn = `${dynamodb}:table/${tableName}`
```
This provides **zero runtime overhead** - values are hardcoded strings at build time!
### Basic Properties
```yaml
custom:
arn:
accountId: "123456789012" # AWS Account ID (from AWS_ACCOUNT_ID env var or STS)
stage: "dev" # Serverless stage
service: "my-service" # Serverless service name
region: "us-east-1" # AWS region
prefix: "my-service-dev" # service-stage
regionalPrefix: "us-east-1-my-service-dev" # region-service-stage
globalPrefix: "123456789012-us-east-1-my-service-dev" # accountId-region-service-stage
```
### ARN Builder Properties
```yaml
custom:
arn:
# Generic ARN components
arnBase: "arn:aws" # AWS ARN base
accountRegion: "123456789012:us-east-1" # accountId:region
# Service-specific ARN bases
dynamodb: "arn:aws:dynamodb:123456789012:us-east-1" # DynamoDB ARN base
s3: "arn:aws:s3:123456789012:us-east-1" # S3 ARN base
lambda: "arn:aws:lambda:123456789012:us-east-1" # Lambda ARN base
iam: "arn:aws:iam:123456789012:us-east-1" # IAM ARN base
sns: "arn:aws:sns:123456789012:us-east-1" # SNS ARN base
sqs: "arn:aws:sqs:123456789012:us-east-1" # SQS ARN base
apigateway: "arn:aws:apigateway:123456789012:us-east-1" # API Gateway ARN base
events: "arn:aws:events:123456789012:us-east-1" # EventBridge ARN base
logs: "arn:aws:logs:123456789012:us-east-1" # CloudWatch Logs ARN base
kinesis: "arn:aws:kinesis:123456789012:us-east-1" # Kinesis ARN base
firehose: "arn:aws:firehose:123456789012:us-east-1" # Firehose ARN base
stepfunctions: "arn:aws:states:123456789012:us-east-1" # Step Functions ARN base
```
## Using the injected values
Once the plugin is loaded, you can reference these values anywhere in your serverless.yml using two methods:
### Method 1: Variable Resolver
Use the `${arn:property}` syntax:
```yaml
resources:
Resources:
MyDynamoDBTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${arn:prefix}-users
# Results in: my-service-dev-users
MyS3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: ${arn:globalPrefix}-assets
# Results in: 123456789012-us-east-1-my-service-dev-assets
functions:
myFunction:
name: ${arn:regionalPrefix}-handler
# Results in: us-east-1-my-service-dev-handler
```
### Method 2: Custom Object (Manual Definition Required)
Use the `${arn:property}` syntax (requires manual property definition):
```yaml
resources:
Resources:
MyDynamoDBTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${arn:prefix}-users
# Results in: my-service-dev-users
functions:
myFunction:
name: ${arn:regionalPrefix}-handler
# Results in: us-east-1-my-service-dev-handler
```
**⚠️ Important Compatibility Notes:**
- **Method 1** (`${arn:*}`): ✅ **Works with this plugin** - uses custom variable resolvers
- **Method 2** (`${arn:*}`): ❌ **Cannot work with plugin-injected properties** in Serverless v2 due to variable resolution timing
### Why Method 2 Doesn't Work With Plugin
Serverless v2 resolves ALL variables BEFORE loading plugins. This means:
1. `${arn:*}` tries to resolve during YAML parsing
2. Plugin hasn't loaded yet, so no `custom.arn` properties exist
3. Resolution fails and stops the process
### When Method 2 DOES Work
`${arn:*}` works when properties are **statically defined** in `serverless.yml`:
```yaml
custom:
arn:
accountId: ${env:AWS_ACCOUNT_ID, '123456789012'}
prefix: ${self:service}-${self:provider.stage}
# ... other properties defined manually
```
### Recommendations
- **With this plugin**: Use Method 1 (`${arn:*}`) syntax
- **With serverless-plugin-composer**: Define `custom.arn` manually and use Method 2 (`${arn:*}`)
- **Hybrid approach**: Define basic properties manually, let plugin handle complex ARN building
### Method 3: Runtime Imports (NEW! ⚡ Zero Runtime Overhead)
Import ARN values directly in your Lambda code:
```javascript
// ES6 imports
import { arnPrefix, globalPrefix, dynamodb, s3 } from 'arn-prefixer/runtime'
// CommonJS
const { arnPrefix, globalPrefix, dynamodb, s3 } = require('arn-prefixer/runtime')
// Use in your Lambda handlers
export const handler = async (event) => {
const tableName = `${arnPrefix}-users`
const bucketName = `${globalPrefix}-assets`
// Build complete ARNs
const tableArn = `${dynamodb}:table/${tableName}`
const bucketArn = `${s3}:bucket/${bucketName}`
// Your Lambda logic here
}
```
**Available Exports:**
- `arnPrefix` - service-stage (e.g., "my-service-dev")
- `globalPrefix` - accountId-region-service-stage (e.g., "123456789012-us-east-1-my-service-dev")
- `regionalPrefix` - region-service-stage (e.g., "us-east-1-my-service-dev")
- All AWS service ARN builders: `dynamodb`, `s3`, `lambda`, `sns`, `sqs`, etc.
### Method 4: ARN Builders (Serverless Config)
Build complete AWS ARNs easily:
```yaml
resources:
Resources:
# DynamoDB Table ARN
MyDynamoTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${arn:prefix}-users
Outputs:
TableArn:
Value: ${arn:dynamodb}:table/${arn:prefix}-users
# Results in: arn:aws:dynamodb:123456789012:us-east-1:table/my-service-dev-users
# S3 Bucket ARN
MyBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: ${arn:globalPrefix}-assets
Outputs:
BucketArn:
Value: ${arn:s3}:bucket/${arn:globalPrefix}-assets
# Results in: arn:aws:s3:123456789012:us-east-1:bucket/123456789012-us-east-1-my-service-dev-assets
# Lambda Function ARN
MyFunction:
Outputs:
FunctionArn:
Value: ${arn:lambda}:function:${arn:prefix}-processor
# Results in: arn:aws:lambda:123456789012:us-east-1:function:my-service-dev-processor
# IAM Policy using ARN builders
user-role-statements:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
Resource:
- ${arn:dynamodb}:table/${arn:prefix}-* # All tables matching pattern
- ${arn:dynamodb}:table/*/index/* # All indexes
```
### Common ARN Patterns
```yaml
# Your specific use cases:
DynamoDBTableArn: ${arn:dynamodb}:table/resource # arn:aws:dynamodb:account:region:table/resource
DynamoDBAllTablesArn: ${arn:dynamodb}:table/* # arn:aws:dynamodb:account:region:table/*
S3BucketArn: ${arn:s3}:bucket/${arn:prefix}-bucket # arn:aws:s3:account:region:bucket/service-stage-bucket
LambdaFunctionArn: ${arn:lambda}:function:${arn:prefix}-func # arn:aws:lambda:account:region:function:service-stage-func
SNSTopicArn: ${arn:sns}:${arn:prefix}-topic # arn:aws:sns:account:region:service-stage-topic
SQSQueueArn: ${arn:sqs}:${arn:prefix}-queue # arn:aws:sqs:account:region:service-stage-queue
```
## Comparison with Serverless Framework Defaults
### Serverless Framework Default Patterns
| Resource Type | Default Pattern | Example |
|---------------|-----------------|---------|
| **CloudFormation Stack** | `{service}-{stage}` | `my-service-dev` |
| **Lambda Functions** | `{service}-{stage}-{functionName}` | `my-service-dev-hello` |
| **API Gateway** | `{stage}-{service}` | `dev-my-service` |
| **IAM Roles** | `{service}-{stage}-{region}-lambdaRole` | `my-service-dev-us-east-1-lambdaRole` |
| **Custom Resources** | `{LogicalId}-{RandomSuffix}` | `MyTable-ABC123DEF456` |
### ARN-Prefixer Plugin Patterns
| Plugin Variable | Pattern | Example |
|----------------|---------|---------|
| `${arn:prefix}` | `{service}-{stage}` | `my-service-dev` |
| `${arn:regionalPrefix}` | `{region}-{service}-{stage}` | `us-east-1-my-service-dev` |
| `${arn:globalPrefix}` | `{accountId}-{region}-{service}-{stage}` | `123456789012-us-east-1-my-service-dev` |
### Key Advantages
- ✅ **Consistency**: All resources follow the same naming convention
- ✅ **Predictability**: No random suffixes, deterministic resource names
- ✅ **Uniqueness**: Account ID inclusion ensures cross-account uniqueness
- ✅ **Multi-region**: Region awareness for global deployments
- ✅ **Enterprise Ready**: Suitable for complex, multi-tenant architectures
## Account ID Resolution
The plugin will attempt to get the AWS Account ID in the following order:
1. **Environment Variable**: If `AWS_ACCOUNT_ID` is set, it will use that value
2. **AWS STS**: If the environment variable is not set, it will call AWS STS `getCallerIdentity()` to retrieve the account ID
3. **Error**: If both methods fail, the plugin will throw an error and stop deployment
**Important**: You must either set the `AWS_ACCOUNT_ID` environment variable or have valid AWS credentials configured. The plugin will not proceed with deployment if it cannot determine the AWS Account ID.
## Compatibility
### Tested Versions
| Serverless Framework | Status | Features Tested |
|---------------------|--------|----------------|
| 2.72.4 | ✅ Fully Tested | Prefix properties + ARN builders + Error handling |
| 3.38.0 | ✅ Fully Tested | Prefix properties + ARN builders + Error handling |
| 4.18.2 | ✅ Fully Tested | Prefix properties + ARN builders + Error handling |
**Requirements:**
- **Serverless Framework**: 2.0.0 and above (supports v2, v3, and v4)
- **Node.js**: 14.0.0 and above
## Plugin Execution
The plugin runs during the `after:aws:common:validate:validate` hook, ensuring that all properties are available before your resources are processed.
## Example Output
### Successful execution:
```
ARN Prefixer: Injected custom.arn properties
- accountId: 123456789012
- stage: dev
- service: my-service
- region: us-east-1
- prefix: my-service-dev
- regionalPrefix: us-east-1-my-service-dev
- globalPrefix: 123456789012-us-east-1-my-service-dev
```
### Error when Account ID cannot be determined:
```
ARN Prefixer Plugin Error: Cannot determine AWS Account ID.
Please either:
1. Set the AWS_ACCOUNT_ID environment variable, or
2. Ensure you have valid AWS credentials configured
AWS STS Error: The security token included in the request is expired
```