Skip to content
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

Docker options for running Docker Lambda within a Docker container #1164

Merged
merged 26 commits into from
Feb 9, 2021
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d29f80c
dockerHost feature
Apr 14, 2020
1c0b7db
eslint fix
Apr 14, 2020
43dd282
dockerHostServicePath param
Apr 14, 2020
d5c9b99
dynamic port binding for Docker runner
Apr 15, 2020
609c92c
eslint fix
Apr 15, 2020
510b5a6
Merge remote-tracking branch 'upstream/master'
apancutt Jan 20, 2021
c44de53
Restore image pull on start
apancutt Jan 20, 2021
a983dd8
Remove redundant var
apancutt Jan 20, 2021
d892ce2
Enable docker watch mode
apancutt Jan 20, 2021
a5ef191
Resolve prettier issues
apancutt Jan 20, 2021
5566ef2
Added dockerNetwork option
apancutt Jan 20, 2021
854e45e
Whitespace fixes
apancutt Jan 20, 2021
c7c659f
Docker options docs
apancutt Jan 20, 2021
9bbbb4e
Fix typo
apancutt Jan 20, 2021
6b91d1d
Merge remote-tracking branch 'upstream/master'
apancutt Jan 29, 2021
c490730
Merge remote-tracking branch 'upstream/master'
apancutt Feb 2, 2021
271b5bd
Merge remote-tracking branch 'upstream/master'
apancutt Feb 4, 2021
8b5352a
Overwrite codeDir with hostServicePath only if function is not publis…
apancutt Feb 7, 2021
7cbb54e
Code style
apancutt Feb 7, 2021
79f425d
Merge remote-tracking branch 'upstream/master'
apancutt Feb 7, 2021
c9340d0
Move lamda dir into service path
frozenbonito Feb 8, 2021
2b164e5
Fix code dir for docker container
frozenbonito Feb 8, 2021
6e532ce
Fix layer dir for docker container
frozenbonito Feb 8, 2021
9ca993a
Remove unused code
frozenbonito Feb 8, 2021
9b6cd6b
Refactor docker container port
frozenbonito Feb 8, 2021
5c142da
Add test for docker-in-docker
frozenbonito Feb 8, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
41 changes: 28 additions & 13 deletions README.md
Expand Up @@ -108,34 +108,37 @@ to list all the options for the plugin run:
All CLI options are optional:

```
--allowCache Allows the code of lambda functions to cache if supported.
--apiKey Defines the API key value to be used for endpoints marked as private Defaults to a random hash.
--corsAllowHeaders Used as default Access-Control-Allow-Headers header value for responses. Delimit multiple values with commas. Default: 'accept,content-type,x-api-key'
--corsAllowOrigin Used as default Access-Control-Allow-Origin header value for responses. Delimit multiple values with commas. Default: '*'
--corsDisallowCredentials When provided, the default Access-Control-Allow-Credentials header value will be passed as 'false'. Default: true
--corsExposedHeaders Used as additional Access-Control-Exposed-Headers header value for responses. Delimit multiple values with commas. Default: 'WWW-Authenticate,Server-Authorization'
--disableCookieValidation Used to disable cookie-validation on hapi.js-server
--dockerHost The host name of Docker. Default: localhost
--dockerHostServicePath Defines service path which is used by SLS running inside Docker container
--dockerNetwork The network that the Docker container will connect to
--dockerReadOnly Marks if the docker code layer should be read only. Default: true
--enforceSecureCookies Enforce secure cookies
--hideStackTraces Hide the stack trace on lambda failure. Default: false
--host -o Host name to listen on. Default: localhost
--httpPort Http port to listen on. Default: 3000
--httpsProtocol -H To enable HTTPS, specify directory (relative to your cwd, typically your project dir) for both cert.pem and key.pem files
--ignoreJWTSignature When using HttpApi with a JWT authorizer, don't check the signature of the JWT token. This should only be used for local development.
--lambdaPort Lambda http port to listen on. Default: 3002
--noPrependStageInUrl Don't prepend http routes with the stage.
--layersDir The directory layers should be stored in. Default: ${codeDir}/.serverless-offline/layers'
--noAuth Turns off all authorizers
--noPrependStageInUrl Don't prepend http routes with the stage.
--noTimeout -t Disables the timeout feature.
--prefix -p Adds a prefix to every path, to send your requests to http://localhost:3000/[prefix]/[your_path] instead. Default: ''
--printOutput Turns on logging of your lambda outputs in the terminal.
--resourceRoutes Turns on loading of your HTTP proxy settings from serverless.yml
--useChildProcesses Run handlers in a child process
--useDocker Run handlers in a docker container.
--useWorkerThreads Uses worker threads for handlers. Requires node.js v11.7.0 or higher
--websocketPort WebSocket port to listen on. Default: 3001
--webSocketHardTimeout Set WebSocket hard timeout in seconds to reproduce AWS limits (https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html#apigateway-execution-service-websocket-limits-table). Default: 7200 (2 hours)
--webSocketIdleTimeout Set WebSocket idle timeout in seconds to reproduce AWS limits (https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html#apigateway-execution-service-websocket-limits-table). Default: 600 (10 minutes)
--useDocker Run handlers in a docker container.
--layersDir The directory layers should be stored in. Default: ${codeDir}/.serverless-offline/layers'
--dockerReadOnly Marks if the docker code layer should be read only. Default: true
--allowCache Allows the code of lambda functions to cache if supported.
--websocketPort WebSocket port to listen on. Default: 3001
```

Any of the CLI options can be added to your `serverless.yml`. For example:
Expand Down Expand Up @@ -215,7 +218,7 @@ offline: Function names exposed for local invocation by aws-sdk:
* invokedHandler: myServiceName-dev-invokedHandler
```

To list the available manual invocation paths exposed for targeting
To list the available manual invocation paths exposed for targeting
by `aws-sdk` and `aws-cli`, use `SLS_DEBUG=*` with `serverless offline`. After the invoke server starts up, full list of endpoints will be displayed:
```
SLS_DEBUG=* serverless offline
Expand Down Expand Up @@ -251,7 +254,7 @@ This will allow the docker container to look up any information about layers, do
* AWS as a provider, it won't work with other provider types.
* Layers that are compatible with your runtime.
* ARNs for layers. Local layers aren't supported as yet.
* A local AWS account set-up that can query and download layers.
* A local AWS account set-up that can query and download layers.

If you're using least-privilege principals for your AWS roles, this policy should get you by:
```json
Expand All @@ -266,21 +269,33 @@ If you're using least-privilege principals for your AWS roles, this policy shoul
]
}
```
Once you run a function that boots up the Docker container, it'll look through the layers for that function, download them in order to your layers folder, and save a hash of your layers so it can be re-used in future. You'll only need to re-download your layers if they change in the future. If you want your layers to re-download, simply remove your layers folder.
Once you run a function that boots up the Docker container, it'll look through the layers for that function, download them in order to your layers folder, and save a hash of your layers so it can be re-used in future. You'll only need to re-download your layers if they change in the future. If you want your layers to re-download, simply remove your layers folder.

You should then be able to invoke functions as normal, and they're executed against the layers in your docker container.

### Additional Options
There are 2 additional options available for Docker and Layer usage.
* layersDir
There are 5 additional options available for Docker and Layer usage.
* dockerHost
* dockerHostServicePath
* dockerNetwork
* dockerReadOnly
* layersDir

#### layersDir
By default layers are downloaded on a per-project basis, however, if you want to share them across projects, you can download them to a common place. For example, `layersDir: /tmp/layers` would allow them to be shared across projects. Make sure when using this setting that the directory you are writing layers to can be shared by docker.
#### dockerHost
When running Docker Lambda inside another Docker container, you may need to configure the host name for the host machine to resolve networking issues between Docker Lambda and the host. Typically in such cases you would set this to `host.docker.internal`.

#### dockerHostServicePath
When running Docker Lambda inside another Docker container, you may need to override the code path that gets mounted to the Docker Lambda container relative to the host machine. Typically in such cases you would set this to `${PWD}`.

#### dockerNetwork
When running Docker Lambda inside another Docker container, you may need to override network that Docker Lambda connects to in order to communicate with other containers.

#### dockerReadOnly
For certain programming languages and frameworks, it's desirable to be able to write to the filesystem for things like testing with local SQLite databases, or other testing-only modifications. For this, you can set `dockerReadOnly: false`, and this will allow local filesystem modifications. This does not strictly mimic AWS Lambda, as Lambda has a Read-Only filesystem, so this should be used as a last resort.

#### layersDir
By default layers are downloaded on a per-project basis, however, if you want to share them across projects, you can download them to a common place. For example, `layersDir: /tmp/layers` would allow them to be shared across projects. Make sure when using this setting that the directory you are writing layers to can be shared by docker.

## Token authorizers

As defined in the [Serverless Documentation](https://serverless.com/framework/docs/providers/aws/events/apigateway/#setting-api-keys-for-your-rest-api) you can use API Keys as a simple authentication method.
Expand Down
10 changes: 10 additions & 0 deletions src/config/commandOptions.js
Expand Up @@ -102,4 +102,14 @@ export default {
allowCache: {
usage: 'Allows the code of lambda functions to cache if supported',
},
dockerHost: {
usage: 'The host name of Docker. Default: localhost',
},
dockerHostServicePath: {
usage:
'Defines service path which is used by SLS running inside Docker container',
},
dockerNetwork: {
usage: 'The network that the Docker container will connect to',
},
}
17 changes: 10 additions & 7 deletions src/config/defaultOptions.js
@@ -1,32 +1,35 @@
import { createApiKey } from '../utils/index.js'

export default {
allowCache: false,
apiKey: createApiKey(),
corsAllowCredentials: true, // TODO no CLI option
corsAllowHeaders: 'accept,content-type,x-api-key,authorization',
corsAllowOrigin: '*',
corsExposedHeaders: 'WWW-Authenticate,Server-Authorization',
disableCookieValidation: false,
dockerHost: 'localhost',
dockerHostServicePath: null,
dockerNetwork: null,
dockerReadOnly: true,
enforceSecureCookies: false,
functionCleanupIdleTimeSeconds: 60,
hideStackTraces: false,
host: 'localhost',
httpPort: 3000,
httpsProtocol: '',
lambdaPort: 3002,
noPrependStageInUrl: false,
layersDir: null,
noAuth: false,
noPrependStageInUrl: false,
noTimeout: false,
prefix: '',
printOutput: false,
resourceRoutes: false,
useChildProcesses: false,
useDocker: false,
useWorkerThreads: false,
websocketPort: 3001,
webSocketHardTimeout: 7200,
webSocketIdleTimeout: 600,
useDocker: false,
layersDir: null,
dockerReadOnly: true,
functionCleanupIdleTimeSeconds: 60,
allowCache: false,
websocketPort: 3001,
}
5 changes: 4 additions & 1 deletion src/lambda/handler-runner/HandlerRunner.js
Expand Up @@ -40,8 +40,11 @@ export default class HandlerRunner {

if (useDocker) {
const dockerOptions = {
readOnly: this.#options.dockerReadOnly,
host: this.#options.dockerHost,
hostServicePath: this.#options.dockerHostServicePath,
layersDir: this.#options.layersDir,
network: this.#options.dockerNetwork,
readOnly: this.#options.dockerReadOnly,
}

const { default: DockerRunner } = await import('./docker-runner/index.js')
Expand Down
41 changes: 27 additions & 14 deletions src/lambda/handler-runner/docker-runner/DockerContainer.js
Expand Up @@ -21,18 +21,18 @@ export default class DockerContainer {
static #dockerPort = new DockerPort()

#containerId = null
#containerPort = null
#dockerOptions = null
#env = null
#functionKey = null
#handler = null
#imageNameTag = null
#image = null
#runtime = null
#imageNameTag = null
#lambda = null
#layers = null
#port = null
#provider = null
#dockerOptions = null

#lambda = null
#runtime = null

constructor(
env,
Expand All @@ -59,10 +59,8 @@ export default class DockerContainer {
}

async start(codeDir) {
const [, port] = await Promise.all([
this.#image.pull(),
DockerContainer.#dockerPort.get(),
])
await this.#image.pull()
const port = '9001'

debugLog('Run Docker container...')

Expand All @@ -76,9 +74,11 @@ export default class DockerContainer {
'-v',
`${codeDir}:/var/task:${permissions},delegated`,
'-p',
`${port}:9001`,
port,
'-e',
'DOCKER_LAMBDA_STAY_OPEN=1', // API mode
'-e',
'DOCKER_LAMBDA_WATCH=1', // Watch mode
]

if (this.#layers.length > 0) {
Expand Down Expand Up @@ -138,6 +138,10 @@ export default class DockerContainer {
dockerArgs.push('--add-host', `host.docker.internal:${gatewayIp}`)
}

if (this.#dockerOptions.network) {
dockerArgs.push('--network', this.#dockerOptions.network)
}

const { stdout: containerId } = await execa('docker', [
'create',
...dockerArgs,
Expand All @@ -163,7 +167,14 @@ export default class DockerContainer {
})
})

const { stdout: containerPortBinding } = await execa('docker', [
'port',
containerId,
])
const containerPort = containerPortBinding.split(':')[1]

this.#containerId = containerId
this.#containerPort = containerPort
this.#port = port

await pRetry(() => this._ping(), {
Expand Down Expand Up @@ -278,7 +289,9 @@ export default class DockerContainer {
}

async _ping() {
const url = `http://localhost:${this.#port}/2018-06-01/ping`
const url = `http://${this.#dockerOptions.host}:${
this.#containerPort
}/2018-06-01/ping`
const res = await fetch(url)

if (!res.ok) {
Expand All @@ -289,9 +302,9 @@ export default class DockerContainer {
}

async request(event) {
const url = `http://localhost:${this.#port}/2015-03-31/functions/${
this.#functionKey
}/invocations`
const url = `http://${this.#dockerOptions.host}:${
this.#containerPort
}/2015-03-31/functions/${this.#functionKey}/invocations`

const res = await fetch(url, {
body: stringify(event),
Expand Down
2 changes: 1 addition & 1 deletion src/lambda/handler-runner/docker-runner/DockerRunner.js
Expand Up @@ -15,7 +15,7 @@ export default class DockerRunner {
provider,
} = funOptions

this.#codeDir = codeDir
this.#codeDir = dockerOptions.hostServicePath || codeDir
apancutt marked this conversation as resolved.
Show resolved Hide resolved
this.#container = new DockerContainer(
env,
functionKey,
Expand Down