简体   繁体   English

使用捆绑程序时,Lambda函数不会自动设置aws-sdk凭据

[英]Lambda function does not automatically set aws-sdk credentials when using a bundler

I have a function that looks like this (after being built): 我有一个看起来像这样的函数(构建后):

myFunction/
    package.json
    node_modules/
        myUtils/
            index.ts
            index.js
    index.ts
    index.js

Within myUtils/index.ts , I have: myUtils/index.ts ,我具有:

import { SecretsManager } from 'aws-sdk' 
...
const secretsManager = new SecretsManager()

If I zip up the contents of myFunction/ and deploy them to AWS Lambda, they work as intended. 如果我压缩myFunction/的内容并将其部署到AWS Lambda,它们将按预期工作。 However, the archive is needlessly bloated (around 60 MB vs. 3MB when using a bundler). 但是,存档不必要地膨胀(大约60 MB,而使用捆绑程序时则为3MB)。

So I installed Parcel and ran npx parcel build index.ts --no-source-maps , which creates a folder called dist/ with a single file ( index.js ). 所以我安装了Parcel并运行了npx parcel build index.ts --no-source-maps ,它创建了一个名为dist/的文件夹,其中包含一个文件( index.js )。 If I zip up and deploy dist/ to AWS Lambda, function invocations now fail with errors indicating that not region or credentials were provided to the SecretsManager constructor. 如果我将dist/压缩并部署到AWS Lambda,则函数调用现在会失败,并显示错误消息,指示未向SecretsManager构造函数提供区域或凭据。

Are there additional steps required to configure aws-sdk with credentials from the execution role when using a bundler? 使用捆绑程序时,是否需要其他步骤来使用执行角色中的凭据配置aws-sdk

To auto-configure aws-sdk 's credentials, aws-sdk must be loaded from the default Lambda runtime environment. 若要自动配置aws-sdk的凭据,必须从默认的Lambda运行时环境加载aws-sdk

The parcel bundler has two modes: parcel ... --target node and parcel ... --target node --bundle-node-modules . 包裹捆绑器有两种模式: parcel ... --target nodeparcel ... --target node --bundle-node-modules

  • The first option doesn't work because it omits all dependencies, even those not in the default Lambda runtime environment. 第一个选项不起作用,因为它忽略了所有依赖项,即使不是默认Lambda运行时环境中的依赖项也是如此。
  • The second option doesn't work because it includes aws-sdk in the bundle, overriding the default Lambda runtime environment. 第二个选项不起作用,因为它在捆绑包中包含aws-sdk ,从而覆盖了默认的Lambda运行时环境。

The solution is to use dynamic imports to suppress bundling of aws-sdk while bundling all other modules. 解决方案是使用动态导入来抑制aws-sdk捆绑,同时捆绑所有其他模块。

So this 所以这

import { SecretsManager } from 'aws-sdk' 
...
const secretsManager = new SecretsManager()

becomes 变成

const awsSdk = import('aws-sdk')
...
const { SecretsManager } = await awsSdk
const secretsManager = new SecretsManager()

And the CLI arguments are: CLI参数为:

parcel build index.ts --no-source-maps --target node --bundle-node-modules

Edit October 2019: 编辑2019年10月:

The above solution works, but I've been the Lambda Layer-based solution described below, and I think that it's superior. 上面的解决方案有效,但是我一直是下面描述的基于Lambda层的解决方案,我认为它是优越的。 The build script is a bit hacky in order to deal with monorepos (I'm using Rush), and probably requires modification to use it in your project. 为了处理monorepos(我正在使用Rush),构建脚本有点笨拙,可能需要修改才能在项目中使用它。 If you're not using a monorepo, you can ignore the section that splits internal and external dependencies. 如果您不使用monorepo,则可以忽略将内部和外部依赖项拆分的部分。

There's probably a better way to do this, so criticism is welcome. 这样做可能是更好的方法,因此欢迎提出批评。

// @my-scope/my-layer/package.json
{
  "name": "@my-scope/my-layer",
  "scripts": {
    "build": "build-layer"
  },
  "dependencies": {
    "uuid": "^3.3.3",
    "axios": "~0.19.0",
    "date-fns": "~2.4.1"
  },
  "devDependencies": {
    "@my-scope/build-scripts": "0.0.0"
  }
}
// @my-scope/build-scripts/package.json
{
  "name": "@meal-planner/tool-build-scripts",
  "files": [
    "out/**/*"
  ],
  "main": "out/lib/index.js",
  "bin": {
    "build-layer": "out/bin/build-layer.js",
  },
  "scripts": {
    "build": "tsc"
  },
  "dependencies": {
    "jsonc-parser": "~2.1.1",
    "fs-extra": "~8.1.0",
    "del": "~5.1.0"
  },
  "devDependencies": {
    "@types/node": "~12.11.1",
    "@types/fs-extra": "~8.0.1",
    "typescript": "~3.6.4",
  }
}

// @my-scope/build-scripts/bin/build-layer.ts

#!/usr/bin/env node
import * as jsonc from 'jsonc-parser'
import * as fs from 'fs'
import * as fse from 'fs-extra'
import * as path from 'path'
import * as util from 'util'
import * as del from 'del'

import { spawnSyncCore } from '../lib'

const access = util.promisify(fs.access)
const readFile = util.promisify(fs.readFile)
const writeFile = util.promisify(fs.writeFile)
const mkdir = util.promisify(fs.mkdir)

const BUILD_DIR = `dist`
const TARGET_PATH = path.join(BUILD_DIR, `nodejs`)
const MANIFEST_NAME = `package.json`
const MANIFEST_LOCK_NAME = `package-lock.json`
const INTERNAL_SCOPE = `@my-scope/`

interface IMinimalPackageManifest {
    description: string
    repository: unknown
    license: string
    dependencies: {
        [packageName: string]: string | undefined
    }
}

async function buildLayer(_argv: readonly string[] = process.argv): Promise<void> {
    const {
        description,
        repository,
        license,
        dependencies
    } = await readManifest(MANIFEST_NAME)

    const { internal, external } = splitDependencies(dependencies)
    const targetManifestPath = path.join(TARGET_PATH, MANIFEST_NAME)
    const targetManifestLockPath = path.join(TARGET_PATH, MANIFEST_LOCK_NAME)

    await writeManifest(
        targetManifestPath,
        {
            description,
            repository,
            license,
            dependencies: external
        }
    )

    installExternalDependencies(TARGET_PATH)
    await installInternalDependencies(internal, TARGET_PATH)

    del.sync(targetManifestPath)
    del.sync(targetManifestLockPath)
}

async function readManifest(sourcePath: string): Promise<IMinimalPackageManifest> {
    const raw = (await readFile(sourcePath)).toString()

    return jsonc.parse(raw)
}

async function writeManifest(targetPath: string, manifest: IMinimalPackageManifest): Promise<void> {
    const targetDir = path.dirname(targetPath)
    try {
        await access(targetDir)
    } catch {
        await mkdir(targetDir, {
            recursive: true
        })
    }

    const raw = JSON.stringify(manifest)

    await writeFile(targetPath, raw)
}

interface IDependencyMap {
    [key: string]: string | undefined
}

interface IDepedencyGroups {
    internal: IDependencyMap
    external: IDependencyMap
}

function splitDependencies(dependencies: IDependencyMap): IDepedencyGroups {
    return Object.keys(dependencies).reduce<IDepedencyGroups>(
        (groups, name) => {
            if (name.startsWith(INTERNAL_SCOPE)) {
                groups.internal[name] = dependencies[name]
            } else {
                groups.external[name] = dependencies[name]
            }

            return groups
        },
        {
            internal: {},
            external: {}
        }
    )
}

function installExternalDependencies(targetDir: string): void {
    spawnSyncCore({
        command: `npm`,
        cwd: targetDir,
        env: {
            ...process.env,
            NODE_ENV: `production`
        },
        args: [
            `install`
        ],
    })
}

async function installInternalDependencies(dependencies: IDependencyMap, targetDir: string): Promise<void> {
    const sourcePaths = Object.keys(dependencies)
        .map(dependency => path.join(`node_modules`, dependency))

    for (const sourcePath of sourcePaths) {
        const targetPath = path.join(targetDir, sourcePath)
        const sourceManifestPath = path.join(sourcePath, MANIFEST_NAME)
        const sourceDistPath = path.join(sourcePath, BUILD_DIR)

        await fse.copy(sourcePath, targetPath, {
            dereference: true,
            recursive: true,
            filter: (src, _dest) => {
                // Only copy package.json and dist/ folder.
                return src === sourcePath || src === sourceManifestPath || src.startsWith(sourceDistPath)
            }
        })
    }
}

/* eslint-disable @typescript-eslint/no-floating-promises */
buildLayer()
/* eslint-enable @typescript-eslint/no-floating-promises */
// @my-scope/build-scripts/lib/index.ts
import { spawnSync } from 'child_process'
const TAB_WIDTH = 4

export interface ISpawnSyncCoreOptions {
    command: string
    cwd?: string
    env?: NodeJS.ProcessEnv
    args?: readonly string[]
}

export function spawnSyncCore({
    command,
    cwd,
    env,
    args
}: ISpawnSyncCoreOptions): void {
    const result = spawnSync(command, args, {
        shell: true,
        cwd,
        env
    })

    if (result.error) {
        throw result.error
    }
    if (result.status !== 0) {
        throw new Error(`${command} returned a non-zero exit code: ${JSON.stringify({
            stdout: result.stdout.toString(),
            stderr: result.stderr.toString(),
            status: result.status,
            signal: result.signal
        }, undefined, TAB_WIDTH)}`)
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM