aws / aws-sam-cli

CLI tool to build, test, debug, and deploy Serverless applications using AWS SAM
https://aws.amazon.com/serverless/sam/
Apache License 2.0
6.51k stars 1.17k forks source link

Two copies of dependency jars included for gradle Kotlin build, as well as two copies of service classes and other useless files #1223

Open ivoanjo opened 5 years ago

ivoanjo commented 5 years ago

Description

I'm working on a very simple (public) example service, using lambda and sam (also gradle 5 and kotlin).

When I run sam build and later sam package, sam includes in the deployed jar two copies of my dependencies.

Steps to reproduce

Checkout example service at https://gitlab.com/ivoanjo/derpservice, checkout sam-issues branch or just download https://gitlab.com/ivoanjo/derpservice/-/archive/sam-issues/derpservice-sam-issues.tar.

Run sam build.

Observed result

Output of sam build:

2019-06-10 23:49:16 Using SAM Template at /home/knuckles/foo/derpservice-sam-issues/template.yaml
2019-06-10 23:49:16 Changing event name from creating-client-class.iot-data to creating-client-class.iot-data-plane
2019-06-10 23:49:16 Changing event name from before-call.apigateway to before-call.api-gateway
2019-06-10 23:49:16 Changing event name from request-created.machinelearning.Predict to request-created.machine-learning.Predict
2019-06-10 23:49:16 Changing event name from before-parameter-build.autoscaling.CreateLaunchConfiguration to before-parameter-build.auto-scaling.CreateLaunchConfiguration
2019-06-10 23:49:16 Changing event name from before-parameter-build.route53 to before-parameter-build.route-53
2019-06-10 23:49:16 Changing event name from request-created.cloudsearchdomain.Search to request-created.cloudsearch-domain.Search
2019-06-10 23:49:16 Changing event name from docs.*.autoscaling.CreateLaunchConfiguration.complete-section to docs.*.auto-scaling.CreateLaunchConfiguration.complete-section
2019-06-10 23:49:16 Changing event name from before-parameter-build.logs.CreateExportTask to before-parameter-build.cloudwatch-logs.CreateExportTask
2019-06-10 23:49:16 Changing event name from docs.*.logs.CreateExportTask.complete-section to docs.*.cloudwatch-logs.CreateExportTask.complete-section
2019-06-10 23:49:16 Changing event name from before-parameter-build.cloudsearchdomain.Search to before-parameter-build.cloudsearch-domain.Search
2019-06-10 23:49:16 Changing event name from docs.*.cloudsearchdomain.Search.complete-section to docs.*.cloudsearch-domain.Search.complete-section
2019-06-10 23:49:16 Changing event name from creating-client-class.iot-data to creating-client-class.iot-data-plane
2019-06-10 23:49:16 Changing event name from before-call.apigateway to before-call.api-gateway
2019-06-10 23:49:16 Changing event name from request-created.machinelearning.Predict to request-created.machine-learning.Predict
2019-06-10 23:49:16 Changing event name from before-parameter-build.autoscaling.CreateLaunchConfiguration to before-parameter-build.auto-scaling.CreateLaunchConfiguration
2019-06-10 23:49:16 Changing event name from before-parameter-build.route53 to before-parameter-build.route-53
2019-06-10 23:49:16 Changing event name from request-created.cloudsearchdomain.Search to request-created.cloudsearch-domain.Search
2019-06-10 23:49:16 Changing event name from docs.*.autoscaling.CreateLaunchConfiguration.complete-section to docs.*.auto-scaling.CreateLaunchConfiguration.complete-section
2019-06-10 23:49:16 Changing event name from before-parameter-build.logs.CreateExportTask to before-parameter-build.cloudwatch-logs.CreateExportTask
2019-06-10 23:49:16 Changing event name from docs.*.logs.CreateExportTask.complete-section to docs.*.cloudwatch-logs.CreateExportTask.complete-section
2019-06-10 23:49:16 Changing event name from before-parameter-build.cloudsearchdomain.Search to before-parameter-build.cloudsearch-domain.Search
2019-06-10 23:49:16 Changing event name from docs.*.cloudsearchdomain.Search.complete-section to docs.*.cloudsearch-domain.Search.complete-section
2019-06-10 23:49:16 'build' command is called
2019-06-10 23:49:16 No Parameters detected in the template
2019-06-10 23:49:16 2 resources found in the template
2019-06-10 23:49:16 Found Serverless function with name='DerpServiceLambda' and CodeUri='.'
2019-06-10 23:49:16 Building resource 'DerpServiceLambda'
2019-06-10 23:49:16 Looking for a supported build workflow in following directories: ['/home/knuckles/foo/derpservice-sam-issues', '/home/knuckles/foo/derpservice-sam-issues']
2019-06-10 23:49:16 Loading workflow module 'aws_lambda_builders.workflows'
2019-06-10 23:49:16 Registering workflow 'PythonPipBuilder' with capability 'Capability(language='python', dependency_manager='pip', application_framework=None)'
2019-06-10 23:49:16 Registering workflow 'NodejsNpmBuilder' with capability 'Capability(language='nodejs', dependency_manager='npm', application_framework=None)'
2019-06-10 23:49:16 Registering workflow 'RubyBundlerBuilder' with capability 'Capability(language='ruby', dependency_manager='bundler', application_framework=None)'
2019-06-10 23:49:16 Registering workflow 'GoDepBuilder' with capability 'Capability(language='go', dependency_manager='dep', application_framework=None)'
2019-06-10 23:49:16 Registering workflow 'GoModulesBuilder' with capability 'Capability(language='go', dependency_manager='modules', application_framework=None)'
2019-06-10 23:49:16 Registering workflow 'JavaGradleWorkflow' with capability 'Capability(language='java', dependency_manager='gradle', application_framework=None)'
2019-06-10 23:49:16 Registering workflow 'JavaMavenWorkflow' with capability 'Capability(language='java', dependency_manager='maven', application_framework=None)'
2019-06-10 23:49:16 Registering workflow 'DotnetCliPackageBuilder' with capability 'Capability(language='dotnet', dependency_manager='cli-package', application_framework=None)'
2019-06-10 23:49:16 Found workflow 'JavaGradleWorkflow' to support capabilities 'Capability(language='java', dependency_manager='gradle', application_framework=None)'
2019-06-10 23:49:17 /home/knuckles/foo/derpservice-sam-issues/gradlew is using a JVM with major version 11 which is newer than 8 that is supported by AWS Lambda. The compiled function code may not run in AWS Lambda unless the project has been configured to be compatible with Java 8 using 'targetCompatibility' in Gradle.
2019-06-10 23:49:17 Running workflow 'JavaGradleWorkflow'
2019-06-10 23:49:17 Running JavaGradleWorkflow:GradleBuild
2019-06-10 23:49:19 JavaGradleWorkflow:GradleBuild succeeded
2019-06-10 23:49:19 Running JavaGradleWorkflow:CopyArtifacts
2019-06-10 23:49:19 JavaGradleWorkflow:CopyArtifacts succeeded

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Package: sam package --s3-bucket <yourbucket>

Resulting build folder (notice the two copies of the dependencies):

$ tree .aws-sam/build/DerpServiceLambda
.aws-sam/build/DerpServiceLambda
├── derpservice
│   ├── AppKt.class
│   ├── bin
│   │   ├── derpservice
│   │   └── derpservice.bat
│   ├── DerpService$app$1.class
│   ├── DerpService$app$2.class
│   ├── DerpService$app$3.class
│   ├── DerpService$timingFilter$1$1.class
│   ├── DerpService$timingFilter$1.class
│   ├── DerpService.class
│   ├── DerpServiceLambda.class
│   └── lib
│       ├── annotations-13.0.jar
│       ├── derpservice.jar
│       ├── http4k-core-3.154.0.jar
│       ├── http4k-serverless-lambda-3.154.0.jar
│       ├── javax.servlet-api-4.0.1.jar
│       ├── kotlin-reflect-1.3.31.jar
│       ├── kotlin-stdlib-1.3.31.jar
│       ├── kotlin-stdlib-common-1.3.31.jar
│       ├── kotlin-stdlib-jdk7-1.3.31.jar
│       └── kotlin-stdlib-jdk8-1.3.31.jar
├── lib
│   ├── annotations-13.0.jar
│   ├── http4k-core-3.154.0.jar
│   ├── http4k-serverless-lambda-3.154.0.jar
│   ├── javax.servlet-api-4.0.1.jar
│   ├── kotlin-reflect-1.3.31.jar
│   ├── kotlin-stdlib-1.3.31.jar
│   ├── kotlin-stdlib-common-1.3.31.jar
│   ├── kotlin-stdlib-jdk7-1.3.31.jar
│   └── kotlin-stdlib-jdk8-1.3.31.jar
└── META-INF
    ├── derpservice.kotlin_module
    └── MANIFEST.MF

5 directories, 31 files

Also, there seems to be an extra copy of the service as a jar (derpservice.jar) as well as the bin/ launchers which are also useless for lambda.

Expected result

Inside the build folder (and the zip file deployed), there should only be a single copy of every dependency, as well as a single copy of my service (either inside, or outside a jar, but not both).

Additional environment details (Ex: Windows, Mac, Amazon Linux etc)

  1. OS: Ubuntu 18.10
  2. sam --version: SAM CLI, version 0.17.0
jfuss commented 5 years ago

@ivoanjo This looks like an issue with your configuration on gradle. Running ./gradlew build, I see relatively the same output. That is I see the derpservice.jar in lib and the all the .class files. SAM CLI more or less shells out to gradle for the builder. I am not expert in gradle but this seems like something you would have control over in gradle no?

ivoanjo commented 5 years ago

Thanks for the quick feedback, @jfuss! 👍 It seems the issue is triggered from my use of the gradle application plugin.

If I remove it from my build script...

diff --git a/build.gradle.kts b/build.gradle.kts
index 4748d99..3877ace 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,7 +1,7 @@
 plugins {
     id("org.jetbrains.kotlin.jvm").version("1.3.31")
     // id("com.github.johnrengelman.shadow").version("5.0.0")
-    application
+    // application
 }

 repositories {
@@ -18,6 +18,6 @@ dependencies {
     //testImplementation("org.http4k:http4k-client-okhttp:3.154.0")
 }

-application {
-    mainClassName = "derpservice.AppKt"
-}
+// application {
+//     mainClassName = "derpservice.AppKt"
+// }

... then the result of sam build is as expected:

.aws-sam/build/DerpServiceLambda/
├── derpservice
│   ├── AppKt.class
│   ├── DerpService$app$1.class
│   ├── DerpService$app$2.class
│   ├── DerpService$app$3.class
│   ├── DerpService$timingFilter$1$1.class
│   ├── DerpService$timingFilter$1.class
│   ├── DerpService.class
│   └── DerpServiceLambda.class
├── lib
│   ├── annotations-13.0.jar
│   ├── http4k-core-3.154.0.jar
│   ├── http4k-serverless-lambda-3.154.0.jar
│   ├── javax.servlet-api-4.0.1.jar
│   ├── kotlin-reflect-1.3.31.jar
│   ├── kotlin-stdlib-1.3.31.jar
│   ├── kotlin-stdlib-common-1.3.31.jar
│   ├── kotlin-stdlib-jdk7-1.3.31.jar
│   └── kotlin-stdlib-jdk8-1.3.31.jar
└── META-INF
    ├── derpservice.kotlin_module
    └── MANIFEST.MF

3 directories, 19 files

So it seems like the SAM CLI is having issues around copying the output when this plugin is enabled. The plugin itself is built-in to the default gradle distribution and was bootstrapped for me when I created the project with gradle init --type kotlin-application --dsl kotlin.

I'm not sure where/how the SAM CLI is deciding to copy all those files. Could you help me track down what command does the SAM CLI shell out to, and what options is it using?

jfuss commented 5 years ago

@ivoanjo All of our build gradle logic lives in here. Taking a quick look at the DESIGN.md, coping the libs is expected, as this is where the dependencies of the library are placed.

This build was built for the Java runtime, but noticing you are using Koltin. Maybe that has something to do with it? I am not familiar with how Koltin and Java are different in some of the build aspects.

ivoanjo commented 5 years ago

Apologies for the delay, and thanks @jfuss for pointing me in the right direction.

The issue here seems to be in the sam's gradle build logic, specifically this line (reproduced here):

def artifactJars = t.project.configurations.archives.artifacts.files.files

with gradle's application plugin disabled, this list contains:

/tmp/tmp08iioaae/aca5d9a94f5c7674c2a5ca3b4aeb36b04f81d177/build/libs/derpservice.jar

whereas in my setup the list is:

/tmp/tmpuqfthfqf/aca5d9a94f5c7674c2a5ca3b4aeb36b04f81d177/build/libs/derpservice.jar
/tmp/tmpuqfthfqf/aca5d9a94f5c7674c2a5ca3b4aeb36b04f81d177/build/distributions/derpservice.zip
/tmp/tmpuqfthfqf/aca5d9a94f5c7674c2a5ca3b4aeb36b04f81d177/build/distributions/derpservice.tar

which explains it. The next step is

copyToArtifactDir(artifactDir, artifactJars, runtimeCpJars)

which does

def copyToArtifactDir(artifactDir, artifactJars, classPathJars) {
    artifactJars.each {
        it.withInputStream({ jis ->
            def zipIs = new ZipInputStream(jis)
            for (def e = zipIs.getNextEntry(); e != null; e = zipIs.getNextEntry()) {
                def entryPath = artifactDir.resolve(e.name)
                if (e.isDirectory()) {
                    makeDirs(entryPath)
                } else {
                    copyToFile(zipIs, entryPath)
                }
                zipIs.closeEntry()
            }
            zipIs.close()
        })
    }
   // ...

Aka the problem is that as part of its work, the application plugin produces both a .zip and a .tar which include scripts to start the application, as well as all of the dependencies (e.g. if every gradle setup used the application plugin, SAM's life would be easy as telling gradle to build the distribution and uploading that zip to S3).

The SAM gradle build logic is picking up those zip/tar artifacts as if it was part of the application, and unzipping them into the build folder, thus we end up with the extra META-INF/, derpservice/bin and derpservice/lib.

This is somewhat where my gradle-foo starts to fail me. I believe the application plugin delegates to the distribution plugin, so I believe the fix here would be for SAM's gradle build logic to ignore any artifacts produced by this plugin (or just anything in a distributions/ folder?).

Let me know if I can provide any more info! 😃 🚀👍