-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(lambda-python): bundle dependencies in a lambda layer #9582
Merged
Merged
Changes from 67 commits
Commits
Show all changes
68 commits
Select commit
Hold shift + click to select a range
b724a26
feat(aws-lambda-python): add support for placing python dependencies …
misterjoshua 4b96baa
Add compatibleRuntimes to the lambda layer
misterjoshua fd3025d
Review changes and add pipenv support
misterjoshua f2f6fa3
Insert the pipenvHome directory into the bundling output directory an…
misterjoshua 61a03b8
Try invoking pipenv via python -m pipenv
misterjoshua fdf70c4
README.md and a spelling fix
misterjoshua faa3db9
Integ tests for pipenv layers on each runtime and fix asset hash issues
misterjoshua a31d44c
Move pipenv install to an image that extends from the runtime bundlin…
misterjoshua 2edf744
Trial installing dependencies in a docker build
misterjoshua a3b8fc5
Fix some comments and un-expose some functions we don't need to expose.
misterjoshua f52c25b
Add back my layer bundling test
misterjoshua f3a6e39
Merge branch 'master' into lambda-python-layers
misterjoshua 236186b
Fix accidental disable of import order linting rule
misterjoshua ff8594f
Add #9763's convenient FROM build arg defaults
misterjoshua ec8f9cc
Apply suggestions from code review
misterjoshua d47cb34
Merge branch 'master' into lambda-python-layers
misterjoshua c742923
Install rsync if necessary
misterjoshua a7a1e4f
Change the default dependenciesLocation to LAYER
misterjoshua 3a1ffe7
Merge branch 'master' into lambda-python-layers
misterjoshua 628c38a
Merge branch 'master' into lambda-python-layers
misterjoshua 5ff6199
Add an integ test that checks dependency removal
misterjoshua 02208e2
Change the integ test to check a layer instead
misterjoshua 12a367f
Merge branch 'master' into lambda-python-layers
misterjoshua 53ca44e
Merge branch 'master' into lambda-python-layers
misterjoshua ba98c9c
Merge branch 'master' of github.com:aws/aws-cdk into lambda-python-la…
misterjoshua 92f3658
Change the dependency removal integ to synth twice
misterjoshua 5b95db6
Point the custom dockerfile name test to a real directory
misterjoshua 0a9facf
Simplify the bundling code
misterjoshua 3f9b271
Add PythonDependencyLayer and PythonSharedCodeLayer
misterjoshua b0b8f72
Fix compatibleRuntimes in PythonDependenciesLayer
misterjoshua dd9a3c3
Merge branch 'master' of github.com:aws/aws-cdk into lambda-python-la…
misterjoshua 1fb4a5c
Merge layer constructs into PythonLayerVersion
misterjoshua c69216a
Inline docs and cleanup
misterjoshua bac8dee
Add readonly to bundling functions options
misterjoshua 0aba325
Match up a code style
misterjoshua 830ce8a
Merge branch 'master' of github.com:aws/aws-cdk into lambda-python-la…
misterjoshua 257b657
Convert function to use the python layer for bundling dependencies
misterjoshua 217be2a
Change layer test to compare readDirSync outputs to enhance error vis…
misterjoshua 3be7e09
Add more tests
misterjoshua d0a9e40
Fix the PythonCodeLocalBundler rename I forgot to include. whoops.
misterjoshua b020cd7
Remove the dependency on @aws-cdk/assets
misterjoshua 988b5ea
Decrease assetHash thrashing based on cwd
misterjoshua 281d365
Remove DependenciesLocation, consolidate bundling
misterjoshua dbfc2af
Merge branch 'master' of github.com:aws/aws-cdk into lambda-python-la…
misterjoshua a940c85
Add more explanation for the two Dockerfiles
misterjoshua e7cce93
Restore more of the original code
misterjoshua f793c90
Update the readme
misterjoshua 90290b9
Consolidate bundle*Layer fns into bundleLayer
misterjoshua 942c615
Remove extraneous text fixtures
misterjoshua f0ac60f
Remove extraneous cdk.CopyOptions from PythonLayerVersionProps
misterjoshua acee7f9
Reword some of the readme, add missing semicolon.
misterjoshua 3ecdaab
Consolidate bundle{Function,Layer} into bundle
misterjoshua faa0771
Fix typo in PythonLayerVersionProps docblock
misterjoshua d55cb8c
Add lambda.LayerVersionOptions and extend from it
misterjoshua 9669e32
Add a test to cover layer runtime checking exceptions
misterjoshua dd2e1c5
Merge branch 'master' into lambda-python-layers
misterjoshua e04b07a
Update vpc test expected json
misterjoshua 045556c
Merge branch 'master' into lambda-python-layers
misterjoshua 191a013
Merge branch 'master' into lambda-python-layers
misterjoshua af8a79a
Merge branch 'master' into lambda-python-layers
misterjoshua d9e0f68
Fix variable destructuring oversight
misterjoshua 8747b6b
Merge branch 'master' into lambda-python-layers
misterjoshua 466cae6
Update integ expectations
misterjoshua f2c74d4
Merge branch 'master' of github.com:aws/aws-cdk into lambda-python-la…
misterjoshua 1eca86b
fix: stacks have changed
misterjoshua 9c2d272
Merge branch 'master' into lambda-python-layers
misterjoshua d4da14c
fix: duplicate identifier path
misterjoshua a82411d
Merge branch 'master' into lambda-python-layers
mergify[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
packages/@aws-cdk/aws-lambda-python/lib/Dockerfile.dependencies
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# The correct AWS SAM build image based on the runtime of the function will be | ||
# passed as build arg. The default allows to do `docker build .` when testing. | ||
ARG IMAGE=amazon/aws-sam-cli-build-image-python3.7 | ||
FROM $IMAGE | ||
|
||
# Ensure rsync is installed | ||
RUN yum -q list installed rsync &>/dev/null || yum install -y rsync | ||
|
||
# Install pipenv so we can create a requirements.txt if we detect pipfile | ||
RUN pip install pipenv | ||
|
||
# Install the dependencies in a cacheable layer | ||
WORKDIR /var/dependencies | ||
COPY Pipfile* requirements.tx[t] ./ | ||
RUN [ -f 'Pipfile' ] && pipenv lock -r >requirements.txt; \ | ||
[ -f 'requirements.txt' ] && pip install -r requirements.txt -t .; | ||
|
||
CMD [ "python" ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -3,6 +3,16 @@ import * as path from 'path'; | |||||||||||||||||||||||||||
import * as lambda from '@aws-cdk/aws-lambda'; | ||||||||||||||||||||||||||||
import * as cdk from '@aws-cdk/core'; | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||
* Dependency files to exclude from the asset hash. | ||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||
export const DEPENDENCY_EXCLUDES = ['*.pyc']; | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||
* The location in the image that the bundler image caches dependencies. | ||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||
export const BUNDLER_DEPENDENCIES_CACHE = '/var/dependencies'; | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||
* Options for bundling | ||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||
|
@@ -16,39 +26,65 @@ export interface BundlingOptions { | |||||||||||||||||||||||||||
* The runtime of the lambda function | ||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||
readonly runtime: lambda.Runtime; | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||
* Output path suffix ('python' for a layer, '.' otherwise) | ||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||
readonly outputPathSuffix: string; | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||
* Produce bundled Lambda asset code | ||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||
export function bundle(options: BundlingOptions): lambda.AssetCode { | ||||||||||||||||||||||||||||
// Bundling image derived from runtime bundling image (AWS SAM docker image) | ||||||||||||||||||||||||||||
const image = cdk.BundlingDockerImage.fromAsset(__dirname, { | ||||||||||||||||||||||||||||
buildArgs: { | ||||||||||||||||||||||||||||
IMAGE: options.runtime.bundlingDockerImage.image, | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
let installer = options.runtime === lambda.Runtime.PYTHON_2_7 ? Installer.PIP : Installer.PIP3; | ||||||||||||||||||||||||||||
const { entry, runtime, outputPathSuffix } = options; | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
let hasRequirements = fs.existsSync(path.join(options.entry, 'requirements.txt')); | ||||||||||||||||||||||||||||
const hasDeps = hasDependencies(entry); | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
let depsCommand = chain([ | ||||||||||||||||||||||||||||
hasRequirements ? `${installer} install -r requirements.txt -t ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}` : '', | ||||||||||||||||||||||||||||
`rsync -r . ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}`, | ||||||||||||||||||||||||||||
const depsCommand = chain([ | ||||||||||||||||||||||||||||
hasDeps ? `rsync -r ${BUNDLER_DEPENDENCIES_CACHE}/. ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}/${outputPathSuffix}` : '', | ||||||||||||||||||||||||||||
`rsync -r . ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}/${outputPathSuffix}`, | ||||||||||||||||||||||||||||
]); | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
return lambda.Code.fromAsset(options.entry, { | ||||||||||||||||||||||||||||
// Determine which dockerfile to use. When dependencies are present, we use a | ||||||||||||||||||||||||||||
// Dockerfile that can create a cacheable layer. We can't use this Dockerfile | ||||||||||||||||||||||||||||
// if there aren't dependencies or the Dockerfile will complain about missing | ||||||||||||||||||||||||||||
// sources. | ||||||||||||||||||||||||||||
const dockerfile = hasDeps | ||||||||||||||||||||||||||||
? 'Dockerfile.dependencies' | ||||||||||||||||||||||||||||
: 'Dockerfile'; | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
const image = cdk.BundlingDockerImage.fromAsset(entry, { | ||||||||||||||||||||||||||||
buildArgs: { | ||||||||||||||||||||||||||||
IMAGE: runtime.bundlingDockerImage.image, | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
file: path.join(__dirname, dockerfile), | ||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
return lambda.Code.fromAsset(entry, { | ||||||||||||||||||||||||||||
assetHashType: cdk.AssetHashType.BUNDLE, | ||||||||||||||||||||||||||||
exclude: DEPENDENCY_EXCLUDES, | ||||||||||||||||||||||||||||
bundling: { | ||||||||||||||||||||||||||||
image, | ||||||||||||||||||||||||||||
command: ['bash', '-c', depsCommand], | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
enum Installer { | ||||||||||||||||||||||||||||
PIP = 'pip', | ||||||||||||||||||||||||||||
PIP3 = 'pip3', | ||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||
* Checks to see if the `entry` directory contains a type of dependency that | ||||||||||||||||||||||||||||
* we know how to install. | ||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||
export function hasDependencies(entry: string): boolean { | ||||||||||||||||||||||||||||
if (fs.existsSync(path.join(entry, 'Pipfile'))) { | ||||||||||||||||||||||||||||
return true; | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
if (fs.existsSync(path.join(entry, 'requirements.txt'))) { | ||||||||||||||||||||||||||||
return true; | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
return false; | ||||||||||||||||||||||||||||
Comment on lines
+79
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. *personal preference/style
Suggested change
|
||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
function chain(commands: string[]): string { | ||||||||||||||||||||||||||||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from './function'; | ||
export * from './layer'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import * as path from 'path'; | ||
import * as lambda from '@aws-cdk/aws-lambda'; | ||
import * as cdk from '@aws-cdk/core'; | ||
import { bundle } from './bundling'; | ||
|
||
/** | ||
* Properties for PythonLayerVersion | ||
*/ | ||
export interface PythonLayerVersionProps extends lambda.LayerVersionOptions { | ||
/** | ||
* The path to the root directory of the lambda layer. | ||
*/ | ||
readonly entry: string; | ||
|
||
/** | ||
* The runtimes compatible with the python layer. | ||
* | ||
* @default - All runtimes are supported. | ||
*/ | ||
readonly compatibleRuntimes?: lambda.Runtime[]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See comment in lambda/lib/layers.ts |
||
} | ||
|
||
/** | ||
* A lambda layer version. | ||
* | ||
* @experimental | ||
*/ | ||
export class PythonLayerVersion extends lambda.LayerVersion { | ||
constructor(scope: cdk.Construct, id: string, props: PythonLayerVersionProps) { | ||
const compatibleRuntimes = props.compatibleRuntimes ?? [lambda.Runtime.PYTHON_3_7]; | ||
|
||
// Ensure that all compatible runtimes are python | ||
for (const runtime of compatibleRuntimes) { | ||
if (runtime && runtime.family !== lambda.RuntimeFamily.PYTHON) { | ||
throw new Error('Only `PYTHON` runtimes are supported.'); | ||
} | ||
} | ||
|
||
// Entry and defaults | ||
const entry = path.resolve(props.entry); | ||
// Pick the first compatibleRuntime to use for bundling or PYTHON_3_7 | ||
const runtime = compatibleRuntimes[0] ?? lambda.Runtime.PYTHON_3_7; | ||
|
||
super(scope, id, { | ||
...props, | ||
compatibleRuntimes, | ||
code: bundle({ | ||
entry, | ||
runtime, | ||
outputPathSuffix: 'python', | ||
}), | ||
}); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only other questions I had here were,
rsync
instead ofcp
? Will we ever copy from a URL? If not, iscp -r
sufficient?If
cp
is sufficient, and we can enable pip caching, then could we do away with these two Dockerfiles?I suppose pipenv support is another reason to keep them. I just wanted to make sure these artifacts were absolutely necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unsure as to why. Another PR #9763 added this after I started. Perhaps @adamelmore can answer your question.
It may be possible to expose a persistent location for the pip cache through a volume and dedicated directory in the user's project directory. But, Docker volume performance can be quite poor in macOS and Windows. Ultimately, the
Dockerfile
s were used to bake-in the pip dependencies to improve performance by only re-running pip when a change is detected inrequirements.txt
,Pipfile
orPipfile.lock
. For these reasons, I'd tend to prefer theDockerfile
approach.