View Javadoc
1   /*
2    * Copyright (c) 2016 ingenieux Labs
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package br.com.ingenieux.mojo.lambda;
18  
19  import com.amazonaws.regions.Region;
20  import com.amazonaws.regions.Regions;
21  import com.amazonaws.services.identitymanagement.AmazonIdentityManagementClient;
22  import com.amazonaws.services.lambda.AWSLambdaClient;
23  import com.amazonaws.services.lambda.model.CreateAliasRequest;
24  import com.amazonaws.services.lambda.model.CreateFunctionRequest;
25  import com.amazonaws.services.lambda.model.CreateFunctionResult;
26  import com.amazonaws.services.lambda.model.FunctionCode;
27  import com.amazonaws.services.lambda.model.ResourceConflictException;
28  import com.amazonaws.services.lambda.model.ResourceNotFoundException;
29  import com.amazonaws.services.lambda.model.Runtime;
30  import com.amazonaws.services.lambda.model.UpdateAliasRequest;
31  import com.amazonaws.services.lambda.model.UpdateFunctionCodeRequest;
32  import com.amazonaws.services.lambda.model.UpdateFunctionCodeResult;
33  import com.amazonaws.services.lambda.model.UpdateFunctionConfigurationRequest;
34  import com.amazonaws.services.lambda.model.UpdateFunctionConfigurationResult;
35  import com.amazonaws.services.lambda.model.VpcConfig;
36  import com.amazonaws.services.s3.AmazonS3URI;
37  import com.amazonaws.services.sns.AmazonSNSClient;
38  import com.amazonaws.services.sns.model.SubscribeRequest;
39  import com.amazonaws.services.sns.model.SubscribeResult;
40  import com.fasterxml.jackson.core.type.TypeReference;
41  import com.fasterxml.jackson.databind.ObjectMapper;
42  import com.fasterxml.jackson.databind.SerializationFeature;
43  
44  import org.apache.commons.io.IOUtils;
45  import org.apache.commons.lang.NotImplementedException;
46  import org.apache.commons.lang.builder.EqualsBuilder;
47  import org.apache.commons.lang.text.StrSubstitutor;
48  import org.apache.maven.plugin.MojoExecutionException;
49  import org.apache.maven.plugins.annotations.Mojo;
50  import org.apache.maven.plugins.annotations.Parameter;
51  import org.apache.maven.project.MavenProject;
52  
53  import java.io.File;
54  import java.io.FileInputStream;
55  import java.io.FileOutputStream;
56  import java.util.ArrayList;
57  import java.util.Collections;
58  import java.util.List;
59  import java.util.Map;
60  import java.util.TreeMap;
61  
62  import br.com.ingenieux.mojo.aws.util.RoleResolver;
63  
64  import static java.lang.String.format;
65  import static java.util.Arrays.asList;
66  import static org.apache.commons.lang.StringUtils.isNotBlank;
67  import static org.codehaus.plexus.util.StringUtils.isBlank;
68  
69  /**
70   * <p>Represents the AWS Lambda Deployment Process, which means:</p> <p> <ul> <li>Parsing the
71   * function-definition file</li> <li>For each declared function, tries to update the function</li>
72   * <li>if the function is missing, create it</li> <li>Otherwise, compare the function definition
73   * with the expected parameters, and changes the function configuration if needed</li> </ul>
74   */
75  @Mojo(name = "deploy-functions")
76  public class DeployMojo extends AbstractLambdaMojo {
77    @Parameter(property = "project", required = false)
78    protected MavenProject curProject;
79  
80    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
81  
82    /**
83     * Lambda Function URL on S3, e.g. <code>s3://somebucket/object/key/path.zip</code>
84     */
85    @Parameter(required = true, property = "lambda.s3url", defaultValue = "${beanstalk.lastUploadedS3Object}")
86    String s3Url;
87  
88    /**
89     * AWS Lambda Default Timeout, in seconds (used when missing in function definition)
90     */
91    @Parameter(required = true, property = "lambda.default.timeout", defaultValue = "5")
92    Integer defaultTimeout;
93  
94    /**
95     * AWS Lambda Default Memory Size, in MB (used when missing in function definition)
96     */
97    @Parameter(required = true, property = "lambda.default.memorySize", defaultValue = "128")
98    Integer defaultMemorySize;
99  
100   /**
101    * <p>AWS Lambda Default IAM Role (used when missing in function definition)</p>
102    *
103    * <p>Allows wildcards like '*' and '?' - will be looked up upon when deploying</p>
104    */
105   @Parameter(required = true, property = "lambda.default.role", defaultValue = "arn:aws:iam::*:role/lambda_basic_execution")
106   String defaultRole;
107 
108   /**
109    * Publish a new function version?
110    */
111   @Parameter(property = "lambda.deploy.publish", defaultValue = "false")
112   Boolean deployPublish;
113 
114   /**
115    * Publish a new function version?
116    */
117   @Parameter(property = "lambda.deploy.aliases", defaultValue = "false")
118   Boolean deployAliases;
119 
120   /**
121    * <p>Definition File</p> <p> <p>Consists of a JSON file array as such:</p> <p>
122    * <pre>[ {
123    *   "name": "AWS Function Name",
124    *   "handler": "AWS Function Handler ref",
125    *   "timeout": 5,
126    *   "memorySize": 128,
127    *   "role": "aws role"
128    * }
129    * ]</pre>
130    * <p> <p>Where:</p> <p> <ul> <li>Name is the AWS Lambda Function Name</li> <li>Handler is the
131    * Handler Ref (for Java, it is <code>classname::functionName</code>)</li> <li>Timeout is the
132    * timeout</li> <li>memorySize is the memory </li> <li>Role is the AWS Service Role</li> </ul>
133    * <p> <p>Of those, only <code>name</code> and <code>handler</code> are obligatory.</p>
134    */
135   @Parameter(required = true, property = "lambda.definition.file", defaultValue = "${project.build.outputDirectory}/META-INF/lambda-definitions.json")
136   File definitionFile;
137 
138   /**
139    * Security Group Ids
140    */
141   @Parameter(property = "lambda.deploy.securityGroupIds", defaultValue = "")
142   List<String> securityGroupIds = new ArrayList<>();
143 
144   public void setSecurityGroupIds(String securityGroupIds) {
145     List<String> securityGroupIdsAsList = asList(securityGroupIds.split(","));
146 
147     this.securityGroupIds.addAll(securityGroupIdsAsList);
148   }
149 
150   /**
151    * Subnet Ids
152    */
153   @Parameter(property = "lambda.deploy.subnetIds", defaultValue = "")
154   List<String> subnetIds;
155 
156   public void setSubnetIds(String subnetIds) {
157     List<String> subnetIdsAsList = asList(subnetIds.split(","));
158 
159     this.subnetIds.addAll(subnetIdsAsList);
160   }
161 
162   private AWSLambdaClient lambdaClient;
163 
164   private AmazonS3URI s3Uri;
165 
166   private RoleResolver roleResolver;
167 
168   //    /**
169   //     * Glob of Functions to Include (default: all)
170   //     */
171   //    @Parameter(property="lambda.function.includes")
172   //    List<String> includes = Collections.singletonList("*");
173   //
174   //    /**
175   //     * Glob of Functions to Exclude (default: empty)
176   //     */
177   //    @Parameter(property="lambda.function.excludes")
178   //    List<String> excludes = Collections.emptyList();
179 
180   @Override
181   protected void configure() {
182     super.configure();
183 
184     try {
185       configureInternal();
186     } catch (Exception exc) {
187       throw new RuntimeException(exc);
188     }
189   }
190 
191   private void configureInternal() throws MojoExecutionException {
192     lambdaClient = this.getService();
193 
194     roleResolver = new RoleResolver(createServiceFor(AmazonIdentityManagementClient.class));
195 
196     s3Uri = new AmazonS3URI(s3Url);
197 
198     try {
199       defaultRole = roleResolver.lookupRoleGlob(defaultRole);
200     } catch (Exception exc) {
201       getLog().warn("Role not found. Using default role. Reason?", exc);
202 
203       defaultRole = null;
204     }
205   }
206 
207   @Override
208   protected Object executeInternal() throws Exception {
209     Map<String, LambdaFunctionDefinition> functionDefinitions = parseFunctionDefinions();
210 
211     String s3Bucket = s3Uri.getBucket();
212     String s3Key = s3Uri.getKey();
213 
214     for (LambdaFunctionDefinition d : functionDefinitions.values()) {
215       getLog().info(format("Deploying Function: %s (handler: %s)", d.getName(), d.getHandler()));
216 
217       String version = null;
218 
219       try {
220         final UpdateFunctionCodeRequest updateFunctionCodeRequest =
221           new UpdateFunctionCodeRequest().withFunctionName(d.getName()).withS3Bucket(s3Bucket).withPublish(this.deployPublish).withS3Key(s3Key);
222         final UpdateFunctionCodeResult updateFunctionCodeResult = lambdaClient.updateFunctionCode(updateFunctionCodeRequest);
223 
224         d.setArn(updateFunctionCodeResult.getFunctionArn());
225 
226         d.setVersion(version = updateFunctionCodeResult.getVersion());
227 
228         updateIfNeeded(d, updateFunctionCodeResult);
229       } catch (ResourceNotFoundException exc) {
230         getLog().info("Function does not exist. Creating it instead.");
231 
232         final CreateFunctionResult function = createFunction(d);
233 
234         d.setArn(function.getFunctionArn());
235 
236         d.setVersion(version = function.getVersion());
237       }
238 
239       if (isNotBlank(d.getAlias()) && (deployAliases)) {
240         updateAlias(d.getName(), version, d.getAlias());
241       }
242 
243       try {
244         if (null != d.getBindings() && !d.getBindings().isEmpty()) {
245           deployBindings(d);
246         }
247       } catch (Exception exc) {
248         getLog().warn("Failure. Skipping. ", exc);
249       }
250     }
251 
252     {
253       getLog().info("Writing back definitions into file " + this.definitionFile.getPath());
254 
255       OBJECT_MAPPER.writeValue(this.definitionFile, functionDefinitions);
256     }
257 
258     return functionDefinitions;
259   }
260 
261   private void deployBindings(LambdaFunctionDefinition d) throws Exception {
262     for (String binding : d.getBindings()) {
263       Arn arn = Arn.lookupArn(binding);
264 
265       if (isNotBlank(d.getAlias())) {
266         arn = Arn.lookupArn(d.getAlias());
267       }
268 
269       if (null == arn) {
270         getLog().warn("Unable to find binding for arn: " + arn);
271 
272         continue;
273       }
274 
275       switch (arn.getService()) {
276         case "sns": {
277           updateSNSFunction(arn, d);
278           break;
279         }
280         case "dynamodb": {
281           updateDynamoDBFunction(arn, d);
282           break;
283         }
284         case "kinesis": {
285           updateKinesisFunction(arn, d);
286           break;
287         }
288         case "cognito": {
289           updateCognitoFunction(arn, d);
290           break;
291         }
292         case "s3": {
293           updateS3Function(arn, d);
294         }
295       }
296     }
297   }
298 
299   private void updateS3Function(Arn bindingArn, LambdaFunctionDefinition d) throws Exception {
300     throw new NotImplementedException("We don't support S3 yet. Sorry. :/");
301   }
302 
303   private void updateCognitoFunction(Arn bindingArn, LambdaFunctionDefinition d) throws Exception {
304     throw new NotImplementedException("We don't support Cognito yet. Sorry. :/");
305   }
306 
307   private void updateKinesisFunction(Arn bindingArn, LambdaFunctionDefinition d) throws Exception {
308     throw new NotImplementedException("AWS SDK for Java doesn't support Kinesis Streams yet. Sorry. :/");
309   }
310 
311   private void updateDynamoDBFunction(Arn bindingArn, LambdaFunctionDefinition d) throws Exception {
312     throw new NotImplementedException("AWS SDK for Java doesn't support DynamoDB Streams yet. Sorry. :/");
313   }
314 
315   private void updateSNSFunction(Arn bindingArn, LambdaFunctionDefinition d) throws Exception {
316     AmazonSNSClient client = createServiceFor(AmazonSNSClient.class);
317 
318     client.setRegion(Region.getRegion(Regions.fromName(bindingArn.getRegion())));
319 
320     SubscribeRequest req = new SubscribeRequest().withTopicArn(bindingArn.getSourceArn()).withProtocol("lambda").withEndpoint(d.getArn());
321 
322     final SubscribeResult subscribe = client.subscribe(req);
323 
324     getLog().info("Subscribed topic arn " + bindingArn.getSourceArn() + " to function " + d.getArn() + ": " + subscribe);
325 
326     // TODO: Unsubscribe older versions
327 
328   }
329 
330   protected Object updateAlias(String functionName, String version, String alias) {
331     try {
332       CreateAliasRequest req = new CreateAliasRequest().withFunctionName(functionName).withFunctionVersion(version).withName(alias);
333 
334       return lambdaClient.createAlias(req);
335     } catch (ResourceConflictException exc) {
336       UpdateAliasRequest req = new UpdateAliasRequest().withFunctionName(functionName).withFunctionVersion(version).withName(alias);
337 
338       return lambdaClient.updateAlias(req);
339     }
340   }
341 
342   private CreateFunctionResult createFunction(LambdaFunctionDefinition d) {
343     CreateFunctionRequest req =
344       new CreateFunctionRequest()
345         .withCode(new FunctionCode().withS3Bucket(s3Uri.getBucket()).withS3Key(s3Uri.getKey()))
346         .withDescription(d.getDescription())
347         .withFunctionName(d.getName())
348         .withHandler(d.getHandler())
349         .withMemorySize(d.getMemorySize())
350         .withRole(d.getRole())
351         .withRuntime(Runtime.Java8)
352         .withPublish(this.deployPublish)
353         .withVpcConfig(new VpcConfig().withSecurityGroupIds(securityGroupIds).withSubnetIds(subnetIds))
354         .withTimeout(d.getTimeout());
355 
356     final CreateFunctionResult createFunctionResult = lambdaClient.createFunction(req);
357 
358     return createFunctionResult;
359   }
360 
361   private UpdateFunctionConfigurationResult updateIfNeeded(LambdaFunctionDefinition d, UpdateFunctionCodeResult curFc) {
362     List<String> returnedSecurityGroupIdsToMatch = Collections.emptyList();
363 
364     if (null != curFc.getVpcConfig() && !curFc.getVpcConfig().getSecurityGroupIds().isEmpty())
365       returnedSecurityGroupIdsToMatch = curFc.getVpcConfig().getSecurityGroupIds();
366 
367     List<String> returnedSubnetIdsToMatch = Collections.emptyList();
368 
369     if (null != curFc.getVpcConfig() && !curFc.getVpcConfig().getSubnetIds().isEmpty())
370       returnedSubnetIdsToMatch = curFc.getVpcConfig().getSubnetIds();
371 
372     boolean bEquals =
373       new EqualsBuilder()
374         .append(d.getDescription(), curFc.getDescription())
375         .append(d.getHandler(), curFc.getHandler())
376         .append(d.getMemorySize(), curFc.getMemorySize().intValue())
377         .append(d.getRole(), curFc.getRole())
378         .append(d.getTimeout(), curFc.getTimeout().intValue())
379         .append(this.securityGroupIds, returnedSecurityGroupIdsToMatch)
380         .append(this.subnetIds, returnedSubnetIdsToMatch)
381         .isEquals();
382 
383     if (!bEquals) {
384       final UpdateFunctionConfigurationRequest updRequest = new UpdateFunctionConfigurationRequest();
385 
386       updRequest.setFunctionName(d.getName());
387       updRequest.setDescription(d.getDescription());
388       updRequest.setHandler(d.getHandler());
389       updRequest.setMemorySize(d.getMemorySize());
390       updRequest.setRole(d.getRole());
391       updRequest.setTimeout(d.getTimeout());
392 
393       VpcConfig vpcConfig = new VpcConfig().withSecurityGroupIds(this.securityGroupIds).withSubnetIds(this.subnetIds);
394 
395       updRequest.setVpcConfig(vpcConfig);
396 
397       getLog().info(format("Function Configuration doesn't match expected defaults. Updating it to %s.", updRequest));
398 
399       final UpdateFunctionConfigurationResult result = lambdaClient.updateFunctionConfiguration(updRequest);
400 
401       return result;
402     }
403 
404     return null;
405   }
406 
407   private Map<String, LambdaFunctionDefinition> parseFunctionDefinions() throws Exception {
408     String source = IOUtils.toString(new FileInputStream(definitionFile));
409 
410     // TODO: Consider PluginParameterExpressionEvaluator
411     source = new StrSubstitutor(getProperties()).replace(source);
412 
413     getLog().info(format("Loaded and replaced definitions from file '%s'", definitionFile.getPath()));
414 
415     List<LambdaFunctionDefinition> definitionList = OBJECT_MAPPER.readValue(source, new TypeReference<List<LambdaFunctionDefinition>>() {
416     });
417 
418     getLog().info(format("Found %d definitions: ", definitionList.size()));
419 
420     Map<String, LambdaFunctionDefinition> result = new TreeMap<String, LambdaFunctionDefinition>();
421 
422     for (LambdaFunctionDefinition d : definitionList) {
423       if (0 == d.getMemorySize()) {
424         d.setMemorySize(defaultMemorySize);
425       }
426 
427       if (isBlank(d.getRole())) {
428         d.setRole(defaultRole);
429       } else {
430         d.setRole(roleResolver.lookupRoleGlob(d.getRole()));
431       }
432 
433       if (0 == d.getTimeout()) {
434         d.setTimeout(defaultTimeout);
435       }
436 
437       result.put(d.getName(), d);
438     }
439 
440     {
441       source = OBJECT_MAPPER.writeValueAsString(definitionList);
442 
443       getLog().debug(format("Parsed function definitions: %s", source));
444 
445       IOUtils.write(source, new FileOutputStream(definitionFile));
446     }
447 
448     getLog().info(format("Merged into %d definitions: ", result.size()));
449 
450     return result;
451   }
452 }