1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
71
72
73
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
84
85 @Parameter(required = true, property = "lambda.s3url", defaultValue = "${beanstalk.lastUploadedS3Object}")
86 String s3Url;
87
88
89
90
91 @Parameter(required = true, property = "lambda.default.timeout", defaultValue = "5")
92 Integer defaultTimeout;
93
94
95
96
97 @Parameter(required = true, property = "lambda.default.memorySize", defaultValue = "128")
98 Integer defaultMemorySize;
99
100
101
102
103
104
105 @Parameter(required = true, property = "lambda.default.role", defaultValue = "arn:aws:iam::*:role/lambda_basic_execution")
106 String defaultRole;
107
108
109
110
111 @Parameter(property = "lambda.deploy.publish", defaultValue = "false")
112 Boolean deployPublish;
113
114
115
116
117 @Parameter(property = "lambda.deploy.aliases", defaultValue = "false")
118 Boolean deployAliases;
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135 @Parameter(required = true, property = "lambda.definition.file", defaultValue = "${project.build.outputDirectory}/META-INF/lambda-definitions.json")
136 File definitionFile;
137
138
139
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
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
170
171
172
173
174
175
176
177
178
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
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
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 }