ac-lambda-deployment 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +334 -0
- package/index.js +346 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# ac-lambda-deployment
|
|
2
|
+
|
|
3
|
+
Simple AWS Lambda deployment tool using AWS SDK v3. Deploys code, manages layers, and configures SQS triggers without the complexity of full infrastructure-as-code tools.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Code-focused deployment** - Updates Lambda function code quickly
|
|
8
|
+
- **Layer management** - Attach and update Lambda layers
|
|
9
|
+
- **SQS trigger configuration** - Manage event source mappings
|
|
10
|
+
- **Multi-environment support** - Different configs for dev/prod
|
|
11
|
+
- **Automatic retry logic** - Handles concurrent update conflicts
|
|
12
|
+
- **Include-based packaging** - Only package specified files
|
|
13
|
+
- **AWS profile support** - Use different AWS profiles per environment
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install ac-lambda-deployment --save-dev
|
|
19
|
+
# or
|
|
20
|
+
yarn add ac-lambda-deployment --dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
**1. Create a configuration file:**
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
// lambda.config.js
|
|
29
|
+
module.exports = {
|
|
30
|
+
functionName: 'my-lambda-function',
|
|
31
|
+
roleArn: 'arn:aws:iam::123456789012:role/lambda-execution-role',
|
|
32
|
+
handler: 'lambda.handler',
|
|
33
|
+
includes: ['lambda.js'],
|
|
34
|
+
layers: [
|
|
35
|
+
'arn:aws:lambda:eu-central-1:123456789012:layer:my-utils:1'
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**2. Add to package.json scripts:**
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"scripts": {
|
|
45
|
+
"deploy": "lambda-deploy",
|
|
46
|
+
"deploy:prod": "NODE_ENV=production lambda-deploy --profile=prod"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**3. Deploy:**
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm run deploy
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Configuration
|
|
58
|
+
|
|
59
|
+
### Basic Configuration
|
|
60
|
+
|
|
61
|
+
Create `lambda.config.js` in your project root:
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
module.exports = {
|
|
65
|
+
functionName: 'my-function',
|
|
66
|
+
roleArn: 'arn:aws:iam::123456789012:role/lambda-role',
|
|
67
|
+
handler: 'lambda.handler',
|
|
68
|
+
runtime: 'nodejs18.x',
|
|
69
|
+
timeout: 30,
|
|
70
|
+
memorySize: 128,
|
|
71
|
+
includes: ['lambda.js', 'config.json'],
|
|
72
|
+
environment: {
|
|
73
|
+
NODE_ENV: 'production',
|
|
74
|
+
API_KEY: 'your-api-key'
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Multi-Environment Configuration
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
// lambda.config.js
|
|
83
|
+
const env = process.env.NODE_ENV || 'dev'
|
|
84
|
+
|
|
85
|
+
const environments = {
|
|
86
|
+
dev: {
|
|
87
|
+
functionName: 'my-function-dev',
|
|
88
|
+
roleArn: 'arn:aws:iam::123456789012:role/lambda-dev-role',
|
|
89
|
+
profile: 'default',
|
|
90
|
+
layers: [
|
|
91
|
+
'arn:aws:lambda:eu-central-1:123456789012:layer:dev-utils:1'
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
production: {
|
|
95
|
+
functionName: 'my-function-prod',
|
|
96
|
+
roleArn: 'arn:aws:iam::123456789012:role/lambda-prod-role',
|
|
97
|
+
profile: 'production',
|
|
98
|
+
layers: [
|
|
99
|
+
'arn:aws:lambda:eu-central-1:123456789012:layer:prod-utils:2'
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
handler: 'lambda.handler',
|
|
106
|
+
runtime: 'nodejs18.x',
|
|
107
|
+
includes: ['lambda.js'],
|
|
108
|
+
...environments[env]
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### SQS Triggers
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
module.exports = {
|
|
116
|
+
functionName: 'my-sqs-processor',
|
|
117
|
+
roleArn: 'arn:aws:iam::123456789012:role/lambda-sqs-role',
|
|
118
|
+
sqsTriggers: [
|
|
119
|
+
{
|
|
120
|
+
queueArn: 'arn:aws:sqs:eu-central-1:123456789012:my-queue',
|
|
121
|
+
batchSize: 10,
|
|
122
|
+
maxBatchingWindow: 5,
|
|
123
|
+
enabled: true
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Configuration Options
|
|
130
|
+
|
|
131
|
+
| Option | Type | Default | Description |
|
|
132
|
+
|--------|------|---------|-------------|
|
|
133
|
+
| `functionName` | string | **required** | Lambda function name |
|
|
134
|
+
| `roleArn` | string | **required** | IAM role ARN for the function |
|
|
135
|
+
| `handler` | string | `lambda.handler` | Function entry point |
|
|
136
|
+
| `runtime` | string | `nodejs18.x` | Lambda runtime |
|
|
137
|
+
| `timeout` | number | `30` | Function timeout in seconds |
|
|
138
|
+
| `memorySize` | number | `128` | Memory allocation in MB |
|
|
139
|
+
| `includes` | array | `['lambda.js']` | Files to include in deployment package |
|
|
140
|
+
| `layers` | array | `[]` | Lambda layer ARNs |
|
|
141
|
+
| `environment` | object | `{}` | Environment variables |
|
|
142
|
+
| `profile` | string | default | AWS profile to use |
|
|
143
|
+
| `region` | string | `eu-central-1` | AWS region |
|
|
144
|
+
| `sqsTriggers` | array | `[]` | SQS event source mappings |
|
|
145
|
+
|
|
146
|
+
## Usage
|
|
147
|
+
|
|
148
|
+
### Command Line
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Deploy with default configuration
|
|
152
|
+
npx lambda-deploy
|
|
153
|
+
|
|
154
|
+
# Deploy with specific AWS profile
|
|
155
|
+
npx lambda-deploy --profile=production
|
|
156
|
+
|
|
157
|
+
# Deploy with different region
|
|
158
|
+
npx lambda-deploy --region=us-east-1
|
|
159
|
+
|
|
160
|
+
# Deploy with custom config file
|
|
161
|
+
npx lambda-deploy --config=prod.config.js
|
|
162
|
+
|
|
163
|
+
# Multi-environment
|
|
164
|
+
NODE_ENV=production npx lambda-deploy --profile=prod
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Programmatic Usage
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
const LambdaDeployer = require('ac-lambda-deployment')
|
|
171
|
+
|
|
172
|
+
async function deploy() {
|
|
173
|
+
const deployer = new LambdaDeployer({
|
|
174
|
+
region: 'eu-central-1',
|
|
175
|
+
profile: 'production'
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
await deployer.deploy()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
deploy().catch(console.error)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Package Structure
|
|
185
|
+
|
|
186
|
+
The tool uses an **include-based** approach - only specified files are packaged:
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
project/
|
|
190
|
+
├── lambda.js # Your Lambda function (included)
|
|
191
|
+
├── lambda.config.js # Configuration (not included)
|
|
192
|
+
├── package.json # Dependencies info (not included)
|
|
193
|
+
├── node_modules/ # Always included in package
|
|
194
|
+
└── other-files.js # Only if specified in includes
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Default includes:** `['lambda.js']`
|
|
198
|
+
**Always included:** `node_modules/` (production dependencies)
|
|
199
|
+
|
|
200
|
+
## AWS Permissions
|
|
201
|
+
|
|
202
|
+
Your Lambda execution role needs these permissions for SQS triggers:
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"Version": "2012-10-17",
|
|
207
|
+
"Statement": [
|
|
208
|
+
{
|
|
209
|
+
"Effect": "Allow",
|
|
210
|
+
"Action": [
|
|
211
|
+
"sqs:ReceiveMessage",
|
|
212
|
+
"sqs:DeleteMessage",
|
|
213
|
+
"sqs:GetQueueAttributes"
|
|
214
|
+
],
|
|
215
|
+
"Resource": "arn:aws:sqs:*:*:*"
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Alternative Configuration
|
|
222
|
+
|
|
223
|
+
You can also configure in `package.json`:
|
|
224
|
+
|
|
225
|
+
```json
|
|
226
|
+
{
|
|
227
|
+
"name": "my-project",
|
|
228
|
+
"lambda": {
|
|
229
|
+
"functionName": "my-function",
|
|
230
|
+
"roleArn": "arn:aws:iam::123456789012:role/lambda-role",
|
|
231
|
+
"includes": ["lambda.js", "utils.js"]
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Security
|
|
237
|
+
|
|
238
|
+
**Important:** Never commit `lambda.config.js` to version control if it contains sensitive data.
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
# .gitignore
|
|
242
|
+
lambda.config.js
|
|
243
|
+
lambda.*.config.js
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Create a template instead:
|
|
247
|
+
|
|
248
|
+
```javascript
|
|
249
|
+
// lambda.config.example.js
|
|
250
|
+
module.exports = {
|
|
251
|
+
functionName: 'your-function-name',
|
|
252
|
+
roleArn: 'arn:aws:iam::YOUR-ACCOUNT:role/YOUR-ROLE',
|
|
253
|
+
environment: {
|
|
254
|
+
API_KEY: 'your-api-key'
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Comparison with Other Tools
|
|
260
|
+
|
|
261
|
+
| Tool | Code Deploy | Infrastructure | Complexity |
|
|
262
|
+
|------|-------------|----------------|------------|
|
|
263
|
+
| **ac-lambda-deployment** | ✅ Fast | Layers, SQS only | Low |
|
|
264
|
+
| Serverless Framework | ✅ | ✅ Full | High |
|
|
265
|
+
| AWS SAM | ✅ | ✅ Full | Medium |
|
|
266
|
+
| Terraform | ❌ | ✅ Full | High |
|
|
267
|
+
| ClaudiaJS | ✅ Fast | Basic | Low (deprecated) |
|
|
268
|
+
|
|
269
|
+
**Use ac-lambda-deployment when:**
|
|
270
|
+
- You want fast code deployments
|
|
271
|
+
- Infrastructure is managed separately (Terraform/CDK)
|
|
272
|
+
- You need simple layer and SQS trigger management
|
|
273
|
+
- You want ClaudiaJS-like simplicity with modern AWS SDK
|
|
274
|
+
|
|
275
|
+
## Examples
|
|
276
|
+
|
|
277
|
+
### Simple API Function
|
|
278
|
+
|
|
279
|
+
```javascript
|
|
280
|
+
// lambda.config.js
|
|
281
|
+
module.exports = {
|
|
282
|
+
functionName: 'api-handler',
|
|
283
|
+
roleArn: 'arn:aws:iam::123456789012:role/api-lambda-role',
|
|
284
|
+
handler: 'lambda.handler',
|
|
285
|
+
timeout: 10,
|
|
286
|
+
memorySize: 256,
|
|
287
|
+
environment: {
|
|
288
|
+
DATABASE_URL: process.env.DATABASE_URL
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### SQS Message Processor
|
|
294
|
+
|
|
295
|
+
```javascript
|
|
296
|
+
// lambda.config.js
|
|
297
|
+
module.exports = {
|
|
298
|
+
functionName: 'queue-processor',
|
|
299
|
+
roleArn: 'arn:aws:iam::123456789012:role/sqs-lambda-role',
|
|
300
|
+
sqsTriggers: [
|
|
301
|
+
{
|
|
302
|
+
queueArn: 'arn:aws:sqs:eu-central-1:123456789012:process-queue',
|
|
303
|
+
batchSize: 5,
|
|
304
|
+
maxBatchingWindow: 10
|
|
305
|
+
}
|
|
306
|
+
],
|
|
307
|
+
layers: [
|
|
308
|
+
'arn:aws:lambda:eu-central-1:123456789012:layer:shared-utils:1'
|
|
309
|
+
]
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Troubleshooting
|
|
314
|
+
|
|
315
|
+
### Permission Denied
|
|
316
|
+
```bash
|
|
317
|
+
chmod +x ./node_modules/ac-lambda-deployment/index.js
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Concurrent Update Error
|
|
321
|
+
The tool automatically retries when Lambda is being updated. Wait 30-60 seconds between deployments.
|
|
322
|
+
|
|
323
|
+
### Function Not Found
|
|
324
|
+
Make sure the function exists or provide `roleArn` to create it automatically.
|
|
325
|
+
|
|
326
|
+
## License
|
|
327
|
+
|
|
328
|
+
MIT © 2025 AdmiralCloud AG, Mark Poepping
|
|
329
|
+
|
|
330
|
+
## Support
|
|
331
|
+
|
|
332
|
+
- Check AWS credentials: `aws sts get-caller-identity`
|
|
333
|
+
- Verify function exists: `aws lambda get-function --function-name your-function`
|
|
334
|
+
- Enable debug logging: `AWS_SDK_LOAD_CONFIG=1 DEBUG=* npx lambda-deploy`
|
package/index.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { LambdaClient, UpdateFunctionCodeCommand, CreateFunctionCommand, GetFunctionCommand, UpdateFunctionConfigurationCommand, CreateEventSourceMappingCommand, ListEventSourceMappingsCommand, UpdateEventSourceMappingCommand, DeleteEventSourceMappingCommand } = require('@aws-sdk/client-lambda')
|
|
4
|
+
const { fromIni } = require('@aws-sdk/credential-provider-ini')
|
|
5
|
+
const fs = require('fs')
|
|
6
|
+
const path = require('path')
|
|
7
|
+
const archiver = require('archiver')
|
|
8
|
+
const { execSync } = require('child_process')
|
|
9
|
+
|
|
10
|
+
class LambdaDeployer {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
const { region = 'eu-central-1', profile } = options
|
|
13
|
+
|
|
14
|
+
const clientConfig = { region }
|
|
15
|
+
if (profile) {
|
|
16
|
+
clientConfig.credentials = fromIni({ profile })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
this.lambda = new LambdaClient(clientConfig)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Load configuration from lambda.config.js or package.json
|
|
23
|
+
loadConfig(configPath) {
|
|
24
|
+
const cwd = process.cwd()
|
|
25
|
+
|
|
26
|
+
// Try lambda.config.js first
|
|
27
|
+
const configFile = path.join(cwd, configPath || 'lambda.config.js')
|
|
28
|
+
if (fs.existsSync(configFile)) {
|
|
29
|
+
delete require.cache[require.resolve(configFile)]
|
|
30
|
+
return require(configFile)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Try package.json lambda section
|
|
34
|
+
const packageFile = path.join(cwd, 'package.json')
|
|
35
|
+
if (fs.existsSync(packageFile)) {
|
|
36
|
+
const pkg = JSON.parse(fs.readFileSync(packageFile, 'utf8'))
|
|
37
|
+
if (pkg.lambda) {
|
|
38
|
+
return pkg.lambda
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
throw new Error('No configuration found. Create lambda.config.js or add "lambda" section to package.json')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Create ZIP archive with code and dependencies
|
|
46
|
+
createZip(sourceDir, outputPath, includes = ['lambda.js']) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const output = fs.createWriteStream(outputPath)
|
|
49
|
+
const archive = archiver('zip', { zlib: { level: 9 } })
|
|
50
|
+
|
|
51
|
+
output.on('close', () => resolve(outputPath))
|
|
52
|
+
archive.on('error', reject)
|
|
53
|
+
|
|
54
|
+
archive.pipe(output)
|
|
55
|
+
|
|
56
|
+
// Add specified files only
|
|
57
|
+
includes.forEach(pattern => {
|
|
58
|
+
const fullPath = path.join(sourceDir, pattern)
|
|
59
|
+
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
|
|
60
|
+
archive.file(fullPath, { name: pattern })
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// Handle glob patterns
|
|
64
|
+
archive.glob(pattern, { cwd: sourceDir })
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// Install and add production dependencies
|
|
69
|
+
console.log('Installing production dependencies...')
|
|
70
|
+
try {
|
|
71
|
+
// Detect package manager
|
|
72
|
+
const hasYarnLock = fs.existsSync(path.join(sourceDir, 'yarn.lock'))
|
|
73
|
+
const hasPnpmLock = fs.existsSync(path.join(sourceDir, 'pnpm-lock.yaml'))
|
|
74
|
+
|
|
75
|
+
let installCmd
|
|
76
|
+
if (hasYarnLock) {
|
|
77
|
+
installCmd = 'yarn install --production --silent'
|
|
78
|
+
}
|
|
79
|
+
else if (hasPnpmLock) {
|
|
80
|
+
installCmd = 'pnpm install --production'
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
installCmd = 'npm install --production --silent'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
execSync(installCmd, { cwd: sourceDir })
|
|
87
|
+
archive.directory(path.join(sourceDir, 'node_modules'), 'node_modules')
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
console.warn('Warning: dependency installation failed, continuing without dependencies')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
archive.finalize()
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check if function exists
|
|
98
|
+
async functionExists(functionName) {
|
|
99
|
+
try {
|
|
100
|
+
await this.lambda.send(new GetFunctionCommand({ FunctionName: functionName }))
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
if (err.name === 'ResourceNotFoundException') {
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
throw err
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Create new Lambda function
|
|
112
|
+
createFunction(config) {
|
|
113
|
+
const zipBuffer = fs.readFileSync(config.zipPath)
|
|
114
|
+
|
|
115
|
+
const params = {
|
|
116
|
+
FunctionName: config.functionName,
|
|
117
|
+
Runtime: config.runtime || 'nodejs18.x',
|
|
118
|
+
Role: config.roleArn,
|
|
119
|
+
Handler: config.handler || 'lambda.handler',
|
|
120
|
+
Code: { ZipFile: zipBuffer },
|
|
121
|
+
Description: config.description || 'Deployed with lambda-deployer',
|
|
122
|
+
Timeout: config.timeout || 30,
|
|
123
|
+
MemorySize: config.memorySize || 128,
|
|
124
|
+
Environment: config.environment ? { Variables: config.environment } : undefined,
|
|
125
|
+
Layers: config.layers || []
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const command = new CreateFunctionCommand(params)
|
|
129
|
+
return this.lambda.send(command)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Update existing Lambda function code with retry
|
|
133
|
+
async updateFunction(functionName, zipPath) {
|
|
134
|
+
const zipBuffer = fs.readFileSync(zipPath)
|
|
135
|
+
|
|
136
|
+
const params = {
|
|
137
|
+
FunctionName: functionName,
|
|
138
|
+
ZipFile: zipBuffer
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const command = new UpdateFunctionCodeCommand(params)
|
|
142
|
+
|
|
143
|
+
// Retry logic for concurrent updates
|
|
144
|
+
for (let i = 0; i < 3; i++) {
|
|
145
|
+
try {
|
|
146
|
+
return this.lambda.send(command)
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
if (err.name === 'ResourceConflictException' && i < 2) {
|
|
150
|
+
console.log(`Function is being updated, waiting 30 seconds... (attempt ${i + 1}/3)`)
|
|
151
|
+
await new Promise(resolve => setTimeout(resolve, 30000))
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
throw err
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Update function configuration (layers, environment, etc.) with retry
|
|
160
|
+
async updateFunctionConfig(config) {
|
|
161
|
+
const params = {
|
|
162
|
+
FunctionName: config.functionName,
|
|
163
|
+
Runtime: config.runtime,
|
|
164
|
+
Handler: config.handler,
|
|
165
|
+
Description: config.description,
|
|
166
|
+
Timeout: config.timeout,
|
|
167
|
+
MemorySize: config.memorySize,
|
|
168
|
+
Environment: config.environment ? { Variables: config.environment } : undefined,
|
|
169
|
+
Layers: config.layers || []
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const command = new UpdateFunctionConfigurationCommand(params)
|
|
173
|
+
|
|
174
|
+
// Retry logic for concurrent updates
|
|
175
|
+
for (let i = 0; i < 3; i++) {
|
|
176
|
+
try {
|
|
177
|
+
return this.lambda.send(command)
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
if (err.name === 'ResourceConflictException' && i < 2) {
|
|
181
|
+
console.log(`Function config is being updated, waiting 20 seconds... (attempt ${i + 1}/3)`)
|
|
182
|
+
await new Promise(resolve => setTimeout(resolve, 20000))
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
throw err
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Manage SQS event source mappings
|
|
191
|
+
async updateEventSourceMappings(functionName, sqsTriggers = []) {
|
|
192
|
+
if (sqsTriggers.length === 0) return
|
|
193
|
+
|
|
194
|
+
// Get existing mappings
|
|
195
|
+
const listCommand = new ListEventSourceMappingsCommand({
|
|
196
|
+
FunctionName: functionName
|
|
197
|
+
})
|
|
198
|
+
const existing = await this.lambda.send(listCommand)
|
|
199
|
+
|
|
200
|
+
// Process each SQS trigger
|
|
201
|
+
for (const trigger of sqsTriggers) {
|
|
202
|
+
const existingMapping = existing.EventSourceMappings?.find(
|
|
203
|
+
m => m.EventSourceArn === trigger.queueArn
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if (existingMapping) {
|
|
207
|
+
// Update existing mapping
|
|
208
|
+
console.log(`Updating SQS trigger: ${trigger.queueArn}`)
|
|
209
|
+
const updateCommand = new UpdateEventSourceMappingCommand({
|
|
210
|
+
UUID: existingMapping.UUID,
|
|
211
|
+
BatchSize: trigger.batchSize || 10,
|
|
212
|
+
MaximumBatchingWindowInSeconds: trigger.maxBatchingWindow || 0,
|
|
213
|
+
Enabled: trigger.enabled !== false
|
|
214
|
+
})
|
|
215
|
+
await this.lambda.send(updateCommand)
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// Create new mapping
|
|
219
|
+
console.log(`Creating SQS trigger: ${trigger.queueArn}`)
|
|
220
|
+
const createCommand = new CreateEventSourceMappingCommand({
|
|
221
|
+
EventSourceArn: trigger.queueArn,
|
|
222
|
+
FunctionName: functionName,
|
|
223
|
+
BatchSize: trigger.batchSize || 10,
|
|
224
|
+
MaximumBatchingWindowInSeconds: trigger.maxBatchingWindow || 0,
|
|
225
|
+
Enabled: trigger.enabled !== false
|
|
226
|
+
})
|
|
227
|
+
await this.lambda.send(createCommand)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Remove mappings not in config
|
|
232
|
+
const configuredArns = sqsTriggers.map(t => t.queueArn)
|
|
233
|
+
for (const mapping of existing.EventSourceMappings || []) {
|
|
234
|
+
if (!configuredArns.includes(mapping.EventSourceArn)) {
|
|
235
|
+
console.log(`Removing SQS trigger: ${mapping.EventSourceArn}`)
|
|
236
|
+
const deleteCommand = new DeleteEventSourceMappingCommand({
|
|
237
|
+
UUID: mapping.UUID
|
|
238
|
+
})
|
|
239
|
+
await this.lambda.send(deleteCommand)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Main deployment function
|
|
245
|
+
async deploy(configPath) {
|
|
246
|
+
const config = this.loadConfig(configPath)
|
|
247
|
+
|
|
248
|
+
const {
|
|
249
|
+
functionName,
|
|
250
|
+
sourceDir = '.',
|
|
251
|
+
roleArn,
|
|
252
|
+
includes = ['lambda.js'],
|
|
253
|
+
region,
|
|
254
|
+
profile
|
|
255
|
+
} = config
|
|
256
|
+
|
|
257
|
+
// Override AWS config if specified in config file
|
|
258
|
+
if (region || profile) {
|
|
259
|
+
const clientConfig = { region: region || 'eu-central-1' }
|
|
260
|
+
if (profile) {
|
|
261
|
+
clientConfig.credentials = fromIni({ profile })
|
|
262
|
+
}
|
|
263
|
+
this.lambda = new LambdaClient(clientConfig)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!functionName) {
|
|
267
|
+
throw new Error('functionName is required in configuration')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
console.log(`Deploying Lambda function: ${functionName}`)
|
|
271
|
+
|
|
272
|
+
// Create ZIP
|
|
273
|
+
const zipPath = path.join(sourceDir, `${functionName}.zip`)
|
|
274
|
+
console.log('Creating deployment package...')
|
|
275
|
+
await this.createZip(sourceDir, zipPath, includes)
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const exists = await this.functionExists(functionName)
|
|
279
|
+
|
|
280
|
+
if (exists) {
|
|
281
|
+
console.log('Updating existing function...')
|
|
282
|
+
await this.updateFunction(functionName, zipPath)
|
|
283
|
+
|
|
284
|
+
// Update function configuration (layers, environment, etc.)
|
|
285
|
+
if (config.layers || config.environment || config.timeout || config.memorySize) {
|
|
286
|
+
console.log('Updating function configuration...')
|
|
287
|
+
await this.updateFunctionConfig(config)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Update SQS triggers
|
|
291
|
+
if (config.sqsTriggers) {
|
|
292
|
+
console.log('Updating SQS triggers...')
|
|
293
|
+
await this.updateEventSourceMappings(functionName, config.sqsTriggers)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log('Function updated successfully!')
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
if (!roleArn) {
|
|
300
|
+
throw new Error('roleArn is required for creating new functions')
|
|
301
|
+
}
|
|
302
|
+
console.log('Creating new function...')
|
|
303
|
+
await this.createFunction({ ...config, zipPath })
|
|
304
|
+
|
|
305
|
+
// Add SQS triggers after function creation
|
|
306
|
+
if (config.sqsTriggers) {
|
|
307
|
+
console.log('Creating SQS triggers...')
|
|
308
|
+
await this.updateEventSourceMappings(functionName, config.sqsTriggers)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
console.log('Function created successfully!')
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
finally {
|
|
315
|
+
// Cleanup ZIP file
|
|
316
|
+
if (fs.existsSync(zipPath)) {
|
|
317
|
+
fs.unlinkSync(zipPath)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// CLI interface
|
|
324
|
+
async function cli() {
|
|
325
|
+
const args = process.argv.slice(2)
|
|
326
|
+
const profile = args.find(arg => arg.startsWith('--profile='))?.split('=')[1]
|
|
327
|
+
const region = args.find(arg => arg.startsWith('--region='))?.split('=')[1]
|
|
328
|
+
const config = args.find(arg => arg.startsWith('--config='))?.split('=')[1]
|
|
329
|
+
|
|
330
|
+
const deployer = new LambdaDeployer({ region, profile })
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
await deployer.deploy(config)
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
console.error('Deployment failed:', err.message)
|
|
337
|
+
process.exit(1)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Run CLI if called directly
|
|
342
|
+
if (require.main === module) {
|
|
343
|
+
cli()
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
module.exports = LambdaDeployer
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ac-lambda-deployment",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Simple AWS Lambda deployment tool using AWS SDK v3",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lambda-deploy": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"author": "Mark Poepping (https://www.admiralcloud.com)",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"packageManager": "yarn@1.22.22",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"aws",
|
|
14
|
+
"lambda",
|
|
15
|
+
"deployment",
|
|
16
|
+
"serverless",
|
|
17
|
+
"aws-sdk-v3"
|
|
18
|
+
],
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/admiralcloud/ac-lambda-deployment"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/admiralcloud/ac-lambda-deployment/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/admiralcloud/ac-lambda-deployment#readme",
|
|
27
|
+
"files": [
|
|
28
|
+
"index.js",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"lint": "eslint index.js",
|
|
33
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@aws-sdk/client-lambda": "^3.848.0",
|
|
37
|
+
"@aws-sdk/credential-provider-ini": "^3.848.0",
|
|
38
|
+
"archiver": "^7.0.1"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=20.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"eslint": "^9.31.0"
|
|
45
|
+
}
|
|
46
|
+
}
|