├── .gitignore ├── CHANGES.md ├── LICENSE ├── README.md ├── boot ├── boot.properties ├── build.boot └── src │ └── boot_clj_lambda │ └── boot.clj ├── lein-plugin ├── project.clj └── src │ └── leiningen │ └── lambda.clj └── library ├── project.clj ├── resources └── empty.jar ├── src └── clj_lambda │ ├── api_gateway.clj │ ├── aws.clj │ ├── iam.clj │ └── schema.clj └── test └── clj_lambda └── tester.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | lein-plugin/target 3 | library/target 4 | /classes 5 | /checkouts 6 | pom.xml 7 | pom.xml.asc 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .hgignore 12 | .hg/ 13 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## 0.8.2 4 | 5 | * Use default values for S3 if not configured 6 | 7 | ## 0.8.1 8 | 9 | * Bug fix. Remove hard coded region from API Gateway installation. 10 | 11 | ## 0.8.0 12 | 13 | * Use environment as API Gateway stage name 14 | 15 | ## 0.7.1 16 | 17 | * Add support for ```--only-api-gateway``` cli option 18 | 19 | ## 0.7.0 20 | 21 | * Add API Gateway support 22 | 23 | ## 0.6.1 24 | 25 | * Bug fix 26 | 27 | ## 0.6.0 28 | 29 | * Separate to library and Leiningen plugin 30 | 31 | ## 0.5.1 32 | 33 | Fix bug in IAM client creation 34 | 35 | ## 0.5.0 36 | 37 | * Add possibility to specify additional statements in role policy. Remove default s3 bucket access right. 38 | * Raise an error if given environment is not configured properly 39 | 40 | ## 0.4.1 41 | 42 | * Fix broken role creation. See https://github.com/mhjort/lein-clj-lambda/issues/2 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clj-lambda-utils 2 | 3 | Clojure utilities for deploying AWS Lambda (JVM) function(s) to one or multiple regions via S3. 4 | 5 | Note! The name of the repo used to be ```lein-clj-lambda``` when it contained only the Leiningen plugin. 6 | 7 | ## Usage 8 | 9 | You can use utilities as a plugin for Leiningen or Boot (or just as Clojure library). 10 | 11 | Note! When installing Lambda all created resource names are logged to console. 12 | 13 | ### Leiningen plugin 14 | 15 | Put `[lein-clj-lambda "0.12.1"]` into the `:plugins` vector of your project.clj (or your profile if you prefer that). 16 | 17 | Create S3 bucket and create following configuration into `project.clj` 18 | 19 | ```clojure 20 | :lambda {"test" [{:api-gateway {:name "DemoApiTest"} ; Optional, if you want to access via API Gateway 21 | :handler "lambda-demo.LambdaFn" 22 | :memory-size 512 23 | :timeout 60 24 | :function-name "my-func-test" 25 | :environment {"MY_ENVIRONMENT_VAR" "some value" ;Optional 26 | "SOME_OTHER_ENV_VAR" "another val"} 27 | :region "eu-west-1" ; Optional, when not specified the default region specified in your AWS config will be used 28 | :policy-statements [{:Effect "Allow" 29 | :Action ["sqs:*"] 30 | :Resource ["arn:aws:sqs:eu-west-1:*"]}] 31 | :s3 {:bucket "your-bucket" ; Optional, if not specified default bucket will be generated 32 | :object-key "lambda.jar"} 33 | :vpc {:security-group-ids ["sg-xxxx"] 34 | :subnet-ids ["subnet-xxxxxx"]}}] 35 | "production" [{:api-gateway {:name "DemoApiProduction"} ; Optional, if you want to access via API Gateway 36 | :handler "lambda-demo.LambdaFn" 37 | :memory-size 1024 38 | :timeout 300 39 | :function-name "my-func-prod" 40 | :environment {"MY_ENVIRONMENT_VAR" "some value" 41 | "SOME_OTHER_ENV_VAR" "another val"} 42 | :region "eu-west-1" ; Optional, when not specified the default region specified in your AWS config will be used 43 | :s3 {:bucket "your-bucket" 44 | :object-key "lambda.jar"} 45 | :vpc {:security-group-ids ["sg-xxxx"] 46 | :subnet-ids ["subnet-xxxxxx"]}}]} 47 | ``` 48 | 49 | Then run 50 | 51 | $ lein lambda install test 52 | 53 | or 54 | 55 | $ lein lambda install production 56 | 57 | This will create S3 buckets that will be used for uploading code to Lambda. 58 | Also, this creates new IAM roles and policies so the Lambda function can write to 59 | Cloudwatch logs. If you need to set up additional access rights, you can pass 60 | `:policy-statements`. The format of statements are specified in a Clojure EDN map 61 | but they will be passed as JSON to AWS IAM (See here the details of policy 62 | statements http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Statement) 63 | If an S3 bucket or role already exists, its creation will be skipped. 64 | 65 | If the Lambda function already exists and you want to just configure API gateway for that you can run: 66 | 67 | $ lein lambda install test --only-api-gateway 68 | 69 | After the Lambda function is installed you should not run `install` anymore but instead just run the 70 | `update` task to update the latest code to Lambda environment. 71 | 72 | $ lein lambda update test 73 | 74 | or 75 | 76 | $ lein lambda update production 77 | 78 | You can delete Lambda and all related resources with following command 79 | 80 | $ lein lambda uninstall test 81 | 82 | ### Boot task 83 | 84 | A single boot task `lambda` is provided. 85 | 86 | Put `[boot-clj-lambda "0.2.0"]` into your boot dependency vector. 87 | 88 | ``` clojure 89 | (require '[boot-clj-lambda.boot :refer [lambda]]) 90 | 91 | (boot (lambda :action :update ;; either :update or :install 92 | :lambda-config lambda-config 93 | :stage-name "test" 94 | :jar-file "/path/to/jar.jar" 95 | :only-api-gateway true)) 96 | ``` 97 | 98 | Where `lambda-config` is a map just like the example in the [Leiningen plug-in](#leiningen-plugin) section above, 99 | keyed by `stage-name`. 100 | 101 | It is recommended to read in `lambda-config` from a file before passing it to the task CLI: 102 | 103 | $ boot -a install :lambda-config "$(< /path/to/config.edn)" ... 104 | 105 | ### Library 106 | 107 | Add the following to your `project.clj` `:dependencies`: 108 | 109 | ```clojure 110 | [clj-lambda "0.8.1"] 111 | ``` 112 | 113 | Then run 114 | 115 | ```clojure 116 | (require '[clj-lambda.aws :as aws]) 117 | 118 | (aws/update-lambda stage-name config jar-file opts) 119 | 120 | (aws/install-lambda stage-name config jar-file opts) 121 | 122 | (aws/uninstall-lambda stage-name config opts) 123 | ``` 124 | 125 | where `config` is a vector of configuration options; see the example from the Leiningen plugin documentation above. 126 | 127 | ## License 128 | 129 | Copyright © 2016-2017 Markus Hjort 130 | 131 | Distributed under the Eclipse Public License 1.0. 132 | -------------------------------------------------------------------------------- /boot/boot.properties: -------------------------------------------------------------------------------- 1 | BOOT_CLOJURE_NAME=org.clojure/clojure 2 | BOOT_CLOJURE_VERSION=1.8.0 3 | BOOT_VERSION=2.7.1 4 | BOOT_EMIT_TARGET=no 5 | -------------------------------------------------------------------------------- /boot/build.boot: -------------------------------------------------------------------------------- 1 | (def project 'boot-clj-lambda/boot-clj-lambda) 2 | 3 | (set-env! :dependencies '[[clj-lambda "0.7.0"]] 4 | :source-paths #{"src"} 5 | :repositories [["clojars" {:url "https://clojars.org/repo/" 6 | :username (System/getenv "CLOJARS_USER") 7 | :password (System/getenv "CLOJARS_PASS")}]]) 8 | 9 | (deftask package 10 | [v version VERSION str "The version number to package"] 11 | (comp (sift :to-resource #{#"^boot_clj_lambda.*"}) 12 | (pom :project project :version version) 13 | (jar) 14 | (target))) 15 | -------------------------------------------------------------------------------- /boot/src/boot_clj_lambda/boot.clj: -------------------------------------------------------------------------------- 1 | (ns boot-clj-lambda.boot 2 | {:boot/export-tasks true} 3 | (:require [boot.core :refer [deftask]] 4 | [clj-lambda.aws :as aws])) 5 | 6 | (deftask lambda 7 | [a action ACTION kw "Action to perform: 'install' or 'update'" 8 | l lambda-config CONFIG edn "Lambda configuration keyed by stage name" 9 | s stage-name STAGE str "The environment to install" 10 | j jar-file JARFILE str "Path to the jar file to upload" 11 | o only-api-gateway bool "Only update API gateway option"] 12 | (fn [next-task] 13 | (fn [fileset] 14 | (let [lambda-fn (case action 15 | :install aws/install-lambda 16 | :update aws/update-lambda)] 17 | (lambda-fn stage-name 18 | (get lambda-config stage-name) 19 | jar-file 20 | {:only-api-gateway (boolean only-api-gateway)})) 21 | (next-task fileset)))) 22 | -------------------------------------------------------------------------------- /lein-plugin/project.clj: -------------------------------------------------------------------------------- 1 | (defproject lein-clj-lambda "0.12.1" 2 | :description "Leiningen plugin for AWS Lambda deployment" 3 | :url "https://github.com/mhjort/clj-lambda-deploy" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.8.0"] 7 | [clj-lambda "0.8.1"] 8 | [org.clojure/tools.cli "0.3.5"]] 9 | :min-lein-version "2.7.1" 10 | :eval-in-leiningen true) 11 | -------------------------------------------------------------------------------- /lein-plugin/src/leiningen/lambda.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.lambda 2 | (:require [leiningen.uberjar :refer [uberjar]] 3 | [clojure.tools.cli :refer [parse-opts]] 4 | [clj-lambda.aws :as aws])) 5 | 6 | (defn- get-config [project environment] 7 | (let [config (get-in project [:lambda environment])] 8 | (when (empty? config) 9 | (throw (ex-info "Could not find anything to install or deploy" {:environment environment}))) 10 | config)) 11 | 12 | (defn- lambdatask [f project environment flags] 13 | (let [config (get-config project environment) 14 | jar-file (uberjar project)] 15 | (f environment config jar-file flags))) 16 | 17 | (defn update-lambda-task [project environment flags] 18 | (lambdatask aws/update-lambda project environment flags)) 19 | 20 | (defn install-lambda-task [project environment flags] 21 | (lambdatask aws/install-lambda project environment flags)) 22 | 23 | (defn uninstall-lambda-task [project environment flags] 24 | (let [config (get-config project environment)] 25 | (aws/uninstall-lambda environment config flags))) 26 | 27 | (def flags 28 | [["-o" "--only-api-gateway" "Apply only API Gateway changes"]]) 29 | 30 | (defn lambda [project task environment & args] 31 | (let [opts (:options (parse-opts args flags))] 32 | (condp = task 33 | "update" (update-lambda-task project environment opts) 34 | "install" (install-lambda-task project environment opts) 35 | "uninstall" (uninstall-lambda-task project environment opts) 36 | (println "Currently only tasks 'update', 'install' and 'uninstall' are supported.")))) 37 | -------------------------------------------------------------------------------- /library/project.clj: -------------------------------------------------------------------------------- 1 | (defproject clj-lambda "0.8.1" 2 | :description "Clojure utilities for AWS Lambda deployment" 3 | :url "https://github.com/mhjort/lein-clj-lambda/library" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.8.0"] 7 | [org.clojure/data.json "0.2.6"] 8 | [prismatic/schema "1.1.4"] 9 | [com.amazonaws/aws-java-sdk-bundle "1.11.112"] 10 | [robert/bruce "0.8.0"]]) 11 | -------------------------------------------------------------------------------- /library/resources/empty.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhjort/clj-lambda-utils/a58832422b4666a19d500cca694464c991386f84/library/resources/empty.jar -------------------------------------------------------------------------------- /library/src/clj_lambda/api_gateway.clj: -------------------------------------------------------------------------------- 1 | (ns clj-lambda.api-gateway 2 | (:require [clj-lambda.iam :as iam]) 3 | (:import [com.amazonaws.auth DefaultAWSCredentialsProviderChain] 4 | [com.amazonaws.services.apigateway AmazonApiGatewayClient] 5 | [com.amazonaws.services.apigateway.model CreateRestApiRequest 6 | CreateResourceRequest 7 | CreateDeploymentRequest 8 | GetResourcesRequest 9 | PutIntegrationRequest 10 | PutMethodRequest] 11 | [com.amazonaws.regions Regions])) 12 | 13 | (def aws-credentials 14 | (.getCredentials (DefaultAWSCredentialsProviderChain.))) 15 | 16 | (defn- create-api-gateway-client [region] 17 | (-> (AmazonApiGatewayClient. aws-credentials) 18 | (.withRegion (Regions/fromName region)))) 19 | 20 | (defn- create-rest-api [api-name region] 21 | (-> (.createRestApi (create-api-gateway-client region) (-> (CreateRestApiRequest.) 22 | (.withName api-name))) 23 | (.getId))) 24 | 25 | (defn- get-root-path-id [rest-api-id region] 26 | (let [raw-items (-> (.getResources (create-api-gateway-client region) (-> (GetResourcesRequest.) 27 | (.withRestApiId rest-api-id))) 28 | (.getItems))] 29 | (-> (filter #(= "/" (.getPath %)) raw-items) 30 | (first) 31 | (.getId)))) 32 | 33 | (defn- create-proxy-resource [rest-api-id region] 34 | (-> (.createResource (create-api-gateway-client region) (-> (CreateResourceRequest.) 35 | (.withParentId (get-root-path-id rest-api-id region)) 36 | (.withRestApiId rest-api-id) 37 | (.withPathPart "{proxy+}"))) 38 | (.getId))) 39 | 40 | (defn- create-method [rest-api-id proxy-resource-id http-method region] 41 | (.putMethod (create-api-gateway-client region) (-> (PutMethodRequest.) 42 | (.withRestApiId rest-api-id) 43 | (.withResourceId proxy-resource-id) 44 | (.withHttpMethod http-method) 45 | (.withApiKeyRequired false) 46 | (.withAuthorizationType "NONE") 47 | (.withRequestParameters {"method.request.path.proxy" true})))) 48 | 49 | 50 | (defn- create-integration [rest-api-id resource-id region function-name] 51 | (let [account-id (iam/get-account-id) 52 | role-arn (iam/create-role-and-policy (str "api-gateway-role-" rest-api-id) 53 | (str "api-gateway-role-policy-" rest-api-id) 54 | "apigateway.amazonaws.com" 55 | (iam/lambda-invoke-policy account-id region function-name))] 56 | (Thread/sleep 1000) ; Role creation is async 57 | (println "Creating integration with role-arn" role-arn) 58 | (.putIntegration (create-api-gateway-client region) (-> (PutIntegrationRequest.) 59 | (.withRestApiId rest-api-id) 60 | (.withResourceId resource-id) 61 | (.withHttpMethod "ANY") 62 | (.withIntegrationHttpMethod "POST") 63 | (.withPassthroughBehavior "WHEN_NO_MATCH") 64 | (.withType "AWS_PROXY") 65 | (.withUri (str "arn:aws:apigateway:" 66 | region 67 | ":lambda:path/2015-03-31/functions/arn:aws:lambda:" 68 | region 69 | ":" 70 | account-id 71 | ":function:" 72 | function-name 73 | "/invocations")) 74 | (.withCacheKeyParameters ["method.request.path.proxy"]) 75 | (.withCacheNamespace "7wcnin") 76 | (.withCredentials role-arn))))) 77 | 78 | (defn- create-deployment [rest-api-id stage-name region] 79 | (.createDeployment (create-api-gateway-client region) (-> (CreateDeploymentRequest.) 80 | (.withRestApiId rest-api-id) 81 | (.withStageName stage-name))) 82 | (str "https://" rest-api-id ".execute-api." region ".amazonaws.com/" stage-name)) 83 | 84 | (defn setup-api-gateway [stage-name api-name region function-name] 85 | (println "Setting up API Gateway with api name" api-name) 86 | (let [rest-api-id (create-rest-api api-name region) 87 | resource-id (create-proxy-resource rest-api-id region)] 88 | (create-method rest-api-id resource-id "ANY" region) 89 | (create-integration rest-api-id resource-id region function-name) 90 | (let [api-url (create-deployment rest-api-id stage-name region)] 91 | (println "Deployed to" api-url)))) 92 | -------------------------------------------------------------------------------- /library/src/clj_lambda/aws.clj: -------------------------------------------------------------------------------- 1 | (ns clj-lambda.aws 2 | (:require [clj-lambda.iam :as iam] 3 | [clj-lambda.api-gateway :as ag] 4 | [clj-lambda.schema :refer [OptionsSchema 5 | ConfigSchemaForUpdate 6 | ConfigSchemaForInstall]] 7 | [schema.core :as s] 8 | [robert.bruce :refer [try-try-again]]) 9 | (:import [com.amazonaws.auth DefaultAWSCredentialsProviderChain] 10 | [com.amazonaws.regions DefaultAwsRegionProviderChain] 11 | [com.amazonaws.services.lambda.model CreateFunctionRequest 12 | DeleteFunctionRequest 13 | UpdateFunctionCodeRequest 14 | FunctionCode 15 | Environment 16 | VpcConfig] 17 | [com.amazonaws.services.lambda AWSLambdaClient] 18 | [com.amazonaws.services.s3 AmazonS3Client] 19 | [com.amazonaws.regions Regions] 20 | [java.io File])) 21 | 22 | (def aws-credentials 23 | (.getCredentials (DefaultAWSCredentialsProviderChain.))) 24 | 25 | (def default-region 26 | (.getRegion (DefaultAwsRegionProviderChain.))) 27 | 28 | (defn- determine-region [config] 29 | (or (:region (first config)) default-region)) 30 | 31 | (defonce s3-client 32 | (delay (AmazonS3Client. aws-credentials))) 33 | 34 | (defn- create-lambda-client [region] 35 | (-> (AWSLambdaClient. aws-credentials) 36 | (.withRegion (Regions/fromName region)))) 37 | 38 | (defn create-bucket-if-needed [bucket-name region] 39 | (if (.doesBucketExist @s3-client bucket-name) 40 | (println bucket-name "S3 bucket already exists. Skipping creation.") 41 | (do (println "Creating bucket" bucket-name "in region" region ".") 42 | (if (= "us-east-1" region) 43 | (.createBucket @s3-client bucket-name) 44 | (.createBucket @s3-client bucket-name region))))) 45 | 46 | (defn- delete-all-objects-from-bucket [bucket-name] 47 | (println "Deleting all objects from bucket" bucket-name) 48 | (let [object-keys (map #(.getKey %) 49 | (.getObjectSummaries (.listObjects @s3-client bucket-name)))] 50 | (doseq [object-key object-keys] 51 | (println "Deleting" object-key) 52 | (.deleteObject @s3-client bucket-name object-key)))) 53 | 54 | (defn- delete-bucket [bucket-name] 55 | (println "Deleting bucket" bucket-name) 56 | (.deleteBucket @s3-client bucket-name)) 57 | 58 | (defn store-jar-to-bucket [^File jar-file bucket-name object-key] 59 | (println "Uploading code to S3 bucket" bucket-name "with name" object-key) 60 | (.putObject @s3-client 61 | bucket-name 62 | object-key 63 | jar-file)) 64 | 65 | (defn create-lambda-fn [{:keys [function-name handler bucket 66 | object-key environment memory-size 67 | timeout region role-arn vpc]}] 68 | (println "Creating Lambda function" function-name "to region" region) 69 | (let [client (create-lambda-client region) 70 | env-vars (.withVariables (Environment.) environment) 71 | vpc-config (when-let [{:keys [security-group-ids subnet-ids]} vpc] 72 | (-> (VpcConfig.) 73 | (.withSecurityGroupIds security-group-ids) 74 | (.withSubnetIds subnet-ids)))] 75 | (.createFunction client (-> (CreateFunctionRequest.) 76 | (.withFunctionName function-name) 77 | (.withMemorySize (int memory-size)) 78 | (.withTimeout (int timeout)) 79 | (.withRuntime "java8") 80 | (.withHandler handler) 81 | (.withCode (-> (FunctionCode.) 82 | (.withS3Bucket bucket) 83 | (.withS3Key object-key))) 84 | (.withEnvironment env-vars) 85 | (.withRole role-arn) 86 | (cond-> vpc-config 87 | (.withVpcConfig vpc-config)))))) 88 | 89 | (defn update-lambda-fn [lambda-name bucket-name region object-key] 90 | (println "Updating Lambda function" lambda-name "in region" region) 91 | (let [client (create-lambda-client region)] 92 | (.updateFunctionCode client (-> (UpdateFunctionCodeRequest.) 93 | (.withFunctionName lambda-name) 94 | (.withS3Bucket bucket-name) 95 | (.withS3Key object-key))))) 96 | 97 | (defn- deployment-s3-config [user-s3-config function-name] 98 | (if user-s3-config 99 | user-s3-config 100 | (let [default-s3-config {:bucket (str function-name "-" (iam/get-account-id)) 101 | :object-key (str function-name ".jar")}] 102 | (println "No S3 settings defined, using defaults" default-s3-config) 103 | default-s3-config))) 104 | 105 | (defn delete-lambda-fn [lambda-name region] 106 | (println "Deleting Lambda function" lambda-name "from region" region) 107 | (let [client (create-lambda-client region)] 108 | (.deleteFunction client (-> (DeleteFunctionRequest.) 109 | (.withFunctionName lambda-name))))) 110 | 111 | (defn- validate-input [config-schema config opts] 112 | (s/validate config-schema config) 113 | (s/validate OptionsSchema opts)) 114 | 115 | (defn update-lambda [stage-name config jar-file & [opts]] 116 | (validate-input ConfigSchemaForUpdate config (or opts {})) 117 | (println "Updating env" stage-name "with options" opts) 118 | (let [[{:keys [function-name s3]}] config 119 | region (determine-region config) 120 | {:keys [bucket object-key]} (deployment-s3-config s3 function-name)] 121 | (println "Deploying to region" region) 122 | (store-jar-to-bucket (File. jar-file) 123 | bucket 124 | object-key) 125 | (update-lambda-fn function-name bucket region object-key))) 126 | 127 | (defn install-lambda [stage-name config jar-file & [opts]] 128 | (validate-input ConfigSchemaForInstall config (or opts {})) 129 | (println "Installing env" stage-name "with options" opts) 130 | (let [[{:keys [api-gateway function-name environment 131 | handler memory-size timeout s3 policy-statements] :as env-settings}] config 132 | region (determine-region config) 133 | install-all? (not (:only-api-gateway opts))] 134 | (println "Installing with settings" env-settings) 135 | (when api-gateway 136 | (ag/setup-api-gateway stage-name (:name api-gateway) region function-name)) 137 | (if install-all? 138 | (let [{:keys [bucket object-key]} (deployment-s3-config s3 function-name) 139 | role-arn (iam/create-role-and-policy (str function-name "-role") 140 | (str function-name "-policy") 141 | "lambda.amazonaws.com" 142 | (iam/log-policy-with-statements policy-statements))] 143 | 144 | (create-bucket-if-needed bucket region) 145 | (store-jar-to-bucket (File. jar-file) 146 | bucket 147 | object-key) 148 | ; There seems to be a race condition in the Amazon API which can cause function creation 149 | ; to fail if the role used has only recently been created. So retry until this succeeds. 150 | ; See: 151 | ; https://stackoverflow.com/questions/36419442/the-role-defined-for-the-function-cannot-be-assumed-by-lambda 152 | ; https://stackoverflow.com/questions/37503075/invalidparametervalueexception-the-role-defined-for-the-function-cannot-be-assu 153 | (try-try-again 154 | {:decay :exponential :sleep 1000 :tries 6} 155 | create-lambda-fn (-> env-settings 156 | (select-keys [:function-name :handler :timeout 157 | :environment :memory-size :vpc]) 158 | (assoc :role-arn role-arn 159 | :bucket bucket 160 | :object-key object-key 161 | :region region)))) 162 | (println "Skipping Lambda installation")))) 163 | 164 | (defn uninstall-lambda [stage-name config & [opts]] 165 | (let [[{:keys [function-name s3] :as env-settings}] config 166 | region (determine-region config) 167 | role (str function-name "-role") 168 | policy (str function-name "-policy") 169 | bucket (:bucket (deployment-s3-config s3 function-name))] 170 | (iam/delete-role-and-policy role policy) 171 | (delete-all-objects-from-bucket bucket) 172 | (delete-bucket bucket) 173 | (delete-lambda-fn function-name region))) 174 | -------------------------------------------------------------------------------- /library/src/clj_lambda/iam.clj: -------------------------------------------------------------------------------- 1 | (ns clj-lambda.iam 2 | (:require [clojure.string :as string] 3 | [clojure.data.json :as json]) 4 | (:import [com.amazonaws.auth DefaultAWSCredentialsProviderChain] 5 | [com.amazonaws.services.identitymanagement AmazonIdentityManagementClient] 6 | [com.amazonaws.services.identitymanagement.model AttachRolePolicyRequest 7 | CreatePolicyRequest 8 | CreateRoleRequest 9 | DeleteRoleRequest 10 | DeletePolicyRequest 11 | EntityAlreadyExistsException 12 | GetRoleRequest 13 | ListRolePoliciesRequest 14 | DetachRolePolicyRequest])) 15 | 16 | (def aws-credentials 17 | (.getCredentials (DefaultAWSCredentialsProviderChain.))) 18 | 19 | (defonce iam-client 20 | (delay (AmazonIdentityManagementClient. aws-credentials))) 21 | 22 | (defn- trust-policy [service] 23 | {:Version "2012-10-17" 24 | :Statement {:Effect "Allow" 25 | :Principal {:Service service} 26 | :Action "sts:AssumeRole"}}) 27 | 28 | (defn log-policy-with-statements [additional-statements] 29 | {:Version "2012-10-17" 30 | :Statement (concat [{:Effect "Allow" 31 | :Action ["logs:CreateLogGroup" 32 | "logs:CreateLogStream" 33 | "logs:PutLogEvents"] 34 | :Resource ["arn:aws:logs:*:*:*"]}] 35 | additional-statements)}) 36 | 37 | (defn lambda-invoke-policy [account-id region function-name] 38 | {:Version "2012-10-17" 39 | :Statement [{:Effect "Allow" 40 | :Action ["lambda:InvokeFunction"] 41 | :Resource [(str "arn:aws:lambda:" 42 | region 43 | ":" 44 | account-id 45 | ":function:" 46 | function-name)]}]}) 47 | 48 | (defn get-account-id [] 49 | (-> (.getUser @iam-client) 50 | (.getUser) 51 | (.getArn) 52 | (string/split #":") 53 | (nth 4))) 54 | 55 | (defn create-role-and-policy [role-name policy-name trust-policy-service policy] 56 | (println "Creating role" role-name "with policy" policy-name "and statements" policy) 57 | (try 58 | (let [role (.createRole @iam-client (-> (CreateRoleRequest.) 59 | (.withRoleName role-name) 60 | (.withAssumeRolePolicyDocument (json/write-str (trust-policy trust-policy-service))))) 61 | policy-result (.createPolicy @iam-client (-> (CreatePolicyRequest.) 62 | (.withPolicyName policy-name) 63 | (.withPolicyDocument (json/write-str policy))))] 64 | (.attachRolePolicy @iam-client (-> (AttachRolePolicyRequest.) 65 | (.withPolicyArn (-> policy-result .getPolicy .getArn)) 66 | (.withRoleName role-name))) 67 | (-> role .getRole .getArn)) 68 | (catch EntityAlreadyExistsException _ 69 | (println "Note! Role" role-name "already exists.") 70 | (-> (.getRole @iam-client (-> (GetRoleRequest.) 71 | (.withRoleName role-name))) 72 | (.getRole) 73 | (.getArn))))) 74 | 75 | (defn delete-role-and-policy [role-name policy-name] 76 | (println "Deleting role" role-name "with policy" policy-name) 77 | (let [policy-arn (.getArn (first (filter #(= policy-name (.getPolicyName %)) 78 | (.getPolicies (.listPolicies @iam-client)))))] 79 | (.detachRolePolicy @iam-client (-> (DetachRolePolicyRequest.) 80 | (.withPolicyArn policy-arn) 81 | (.withRoleName role-name))) 82 | (.deletePolicy @iam-client (-> (DeletePolicyRequest.) 83 | (.withPolicyArn policy-arn))) 84 | (.deleteRole @iam-client (-> (DeleteRoleRequest.) 85 | (.withRoleName role-name))))) 86 | -------------------------------------------------------------------------------- /library/src/clj_lambda/schema.clj: -------------------------------------------------------------------------------- 1 | (ns clj-lambda.schema 2 | (:require [schema.core :as s])) 3 | 4 | (defn- define-key [key-name required?] 5 | (if required? 6 | key-name 7 | (s/optional-key key-name))) 8 | 9 | (defn- environment-config [install?] 10 | {(s/optional-key :api-gateway) {:name String} 11 | :function-name String 12 | (s/optional-key :region) String 13 | (define-key :handler install?) String 14 | (define-key :memory-size install?) s/Int 15 | (define-key :timeout install?) s/Int 16 | (s/optional-key :environment) {String String} 17 | (s/optional-key :policy-statements) [{s/Keyword s/Any}] 18 | (s/optional-key :s3) {:bucket String 19 | :object-key String} 20 | (s/optional-key :vpc) {:security-group-ids [String] 21 | :subnet-ids [String]}}) 22 | 23 | (def ConfigSchemaForInstall 24 | [(environment-config true)]) 25 | 26 | (def ConfigSchemaForUpdate 27 | [(environment-config false)]) 28 | 29 | (def OptionsSchema 30 | {(s/optional-key :only-api-gateway) Boolean}) 31 | -------------------------------------------------------------------------------- /library/test/clj_lambda/tester.clj: -------------------------------------------------------------------------------- 1 | (ns clj-lambda.tester 2 | (:require [clj-lambda.aws :refer [install-lambda uninstall-lambda]])) 3 | 4 | (defn- test-config [bucket-name] 5 | {:handler "lambda-demo.LambdaFn" 6 | :memory-size 512 7 | :timeout 60 8 | :function-name "my-func-test2" 9 | :region "eu-west-1" 10 | :s3 {:bucket bucket-name 11 | :object-key "lambda.jar"}}) 12 | 13 | ;(let [bucket-name "my-testing-123456"] 14 | ; (install-lambda "test" [(test-config bucket-name)] "resources/empty.jar") 15 | ; (uninstall-lambda "test" [(test-config bucket-name)])) 16 | --------------------------------------------------------------------------------