All Downloads are FREE. Search and download functionalities are using the official Maven repository.

br.com.ingenieux.mojo.lambda.DeployMojo Maven / Gradle / Ivy

There is a newer version: 1.5.6
Show newest version
/*
 * Copyright (c) 2016 ingenieux Labs
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package br.com.ingenieux.mojo.lambda;

import com.amazonaws.regions.Region;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.identitymanagement.AmazonIdentityManagementClient;
import com.amazonaws.services.lambda.AWSLambdaClient;
import com.amazonaws.services.lambda.model.CreateAliasRequest;
import com.amazonaws.services.lambda.model.CreateFunctionRequest;
import com.amazonaws.services.lambda.model.CreateFunctionResult;
import com.amazonaws.services.lambda.model.FunctionCode;
import com.amazonaws.services.lambda.model.ResourceConflictException;
import com.amazonaws.services.lambda.model.ResourceNotFoundException;
import com.amazonaws.services.lambda.model.Runtime;
import com.amazonaws.services.lambda.model.UpdateAliasRequest;
import com.amazonaws.services.lambda.model.UpdateFunctionCodeRequest;
import com.amazonaws.services.lambda.model.UpdateFunctionCodeResult;
import com.amazonaws.services.lambda.model.UpdateFunctionConfigurationRequest;
import com.amazonaws.services.lambda.model.UpdateFunctionConfigurationResult;
import com.amazonaws.services.lambda.model.VpcConfig;
import com.amazonaws.services.s3.AmazonS3URI;
import com.amazonaws.services.sns.AmazonSNSClient;
import com.amazonaws.services.sns.model.ListSubscriptionsRequest;
import com.amazonaws.services.sns.model.SubscribeRequest;
import com.amazonaws.services.sns.model.SubscribeResult;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.NotImplementedException;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.text.StrSubstitutor;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import br.com.ingenieux.mojo.aws.util.RoleResolver;

import static java.lang.String.format;
import static java.util.Arrays.asList;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.codehaus.plexus.util.StringUtils.isBlank;

/**
 * 

Represents the AWS Lambda Deployment Process, which means:

  • Parsing the * function-definition file
  • For each declared function, tries to update the function
  • *
  • if the function is missing, create it
  • Otherwise, compare the function definition * with the expected parameters, and changes the function configuration if needed
*/ @Mojo(name = "deploy-functions") public class DeployMojo extends AbstractLambdaMojo { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); /** * Lambda Function URL on S3, e.g. s3://somebucket/object/key/path.zip */ @Parameter(required = true, property = "lambda.s3url", defaultValue = "${beanstalk.lastUploadedS3Object}") String s3Url; /** * AWS Lambda Default Timeout, in seconds (used when missing in function definition) */ @Parameter(required = true, property = "lambda.default.timeout", defaultValue = "5") Integer defaultTimeout; /** * AWS Lambda Default Memory Size, in MB (used when missing in function definition) */ @Parameter(required = true, property = "lambda.default.memorySize", defaultValue = "128") Integer defaultMemorySize; /** *

AWS Lambda Default IAM Role (used when missing in function definition)

* *

Allows wildcards like '*' and '?' - will be looked up upon when deploying

*/ @Parameter(required = true, property = "lambda.default.role", defaultValue = "arn:aws:iam::*:role/lambda_basic_execution") String defaultRole; /** * Publish a new function version? */ @Parameter(property = "lambda.deploy.publish", defaultValue = "false") Boolean deployPublish; /** * Publish a new function version? */ @Parameter(property = "lambda.deploy.aliases", defaultValue = "false") Boolean deployAliases; /** *

Definition File

Consists of a JSON file array as such:

*

[ {
     *   "name": "AWS Function Name",
     *   "handler": "AWS Function Handler ref",
     *   "timeout": 5,
     *   "memorySize": 128,
     *   "role": "aws role"
     * }
     * ]
*

Where:

  • Name is the AWS Lambda Function Name
  • Handler is the * Handler Ref (for Java, it is classname::functionName)
  • Timeout is the * timeout
  • memorySize is the memory
  • Role is the AWS Service Role
*

Of those, only name and handler are obligatory.

*/ @Parameter(required = true, property = "lambda.definition.file", defaultValue = "${project.build.outputDirectory}/META-INF/lambda-definitions.json") File definitionFile; /** * Security Group Ids */ @Parameter(property = "lambda.deploy.securityGroupIds", defaultValue = "") List securityGroupIds = new ArrayList<>(); public void setSecurityGroupIds(String securityGroupIds) { List securityGroupIdsAsList = asList(securityGroupIds.split(",")); this.securityGroupIds.addAll(securityGroupIdsAsList); } /** * Subnet Ids */ @Parameter(property = "lambda.deploy.subnetIds", defaultValue = "") List subnetIds; public void setSubnetIds(String subnetIds) { List subnetIdsAsList = asList(subnetIds.split(",")); this.subnetIds.addAll(subnetIdsAsList); } private AWSLambdaClient lambdaClient; private AmazonS3URI s3Uri; private RoleResolver roleResolver; // /** // * Glob of Functions to Include (default: all) // */ // @Parameter(property="lambda.function.includes") // List includes = Collections.singletonList("*"); // // /** // * Glob of Functions to Exclude (default: empty) // */ // @Parameter(property="lambda.function.excludes") // List excludes = Collections.emptyList(); @Override protected void configure() { super.configure(); try { configureInternal(); } catch (Exception exc) { throw new RuntimeException(exc); } } private void configureInternal() throws MojoExecutionException { lambdaClient = this.getService(); roleResolver = new RoleResolver(createServiceFor(AmazonIdentityManagementClient.class)); s3Uri = new AmazonS3URI(s3Url); defaultRole = roleResolver.lookupRoleGlob(defaultRole); } @Override protected Object executeInternal() throws Exception { Map functionDefinitions = parseFunctionDefinions(); String s3Bucket = s3Uri.getBucket(); String s3Key = s3Uri.getKey(); for (LambdaFunctionDefinition d : functionDefinitions.values()) { getLog().info(format("Deploying Function: %s (handler: %s)", d.getName(), d.getHandler())); String version = null; try { final UpdateFunctionCodeRequest updateFunctionCodeRequest = new UpdateFunctionCodeRequest(). withFunctionName(d.getName()). withS3Bucket(s3Bucket). withPublish(this.deployPublish). withS3Key(s3Key); final UpdateFunctionCodeResult updateFunctionCodeResult = lambdaClient.updateFunctionCode( updateFunctionCodeRequest); d.setArn(updateFunctionCodeResult.getFunctionArn()); d.setVersion(version = updateFunctionCodeResult.getVersion()); updateIfNeeded(d, updateFunctionCodeResult); } catch (ResourceNotFoundException exc) { getLog().info("Function does not exist. Creating it instead."); final CreateFunctionResult function = createFunction(d); d.setArn(function.getFunctionArn()); d.setVersion(version = function.getVersion()); } if (isNotBlank(d.getAlias()) && (deployAliases)) { updateAlias(d.getName(), version, d.getAlias()); } try { if (null != d.getBindings() && !d.getBindings().isEmpty()) { deployBindings(d); } } catch (Exception exc) { getLog().warn("Failure. Skipping. ", exc); } } return functionDefinitions; } private void deployBindings(LambdaFunctionDefinition d) throws Exception { for (String binding : d.getBindings()) { Arn arn = Arn.lookupArn(binding); if (isNotBlank(d.getAlias())) { arn = Arn.lookupArn(d.getAlias()); } if (null == arn) { getLog().warn("Unable to find binding for arn: " + arn); continue; } switch (arn.getService()) { case "sns": { updateSNSFunction(arn, d); break; } case "dynamodb": { updateDynamoDBFunction(arn, d); break; } case "kinesis": { updateKinesisFunction(arn, d); break; } case "cognito": { updateCognitoFunction(arn, d); break; } case "s3": { updateS3Function(arn, d); } } } } private void updateS3Function(Arn bindingArn, LambdaFunctionDefinition d) throws Exception { throw new NotImplementedException("We don't support S3 yet. Sorry. :/"); } private void updateCognitoFunction(Arn bindingArn, LambdaFunctionDefinition d) throws Exception { throw new NotImplementedException("We don't support Cognito yet. Sorry. :/"); } private void updateKinesisFunction(Arn bindingArn, LambdaFunctionDefinition d) throws Exception { throw new NotImplementedException("AWS SDK for Java doesn't support Kinesis Streams yet. Sorry. :/"); } private void updateDynamoDBFunction(Arn bindingArn, LambdaFunctionDefinition d) throws Exception { throw new NotImplementedException("AWS SDK for Java doesn't support DynamoDB Streams yet. Sorry. :/"); } private void updateSNSFunction(Arn bindingArn, LambdaFunctionDefinition d) throws Exception { AmazonSNSClient client = createServiceFor(AmazonSNSClient.class); client.setRegion(Region.getRegion(Regions.fromName(bindingArn.getRegion()))); SubscribeRequest req = new SubscribeRequest() .withTopicArn(bindingArn.getSourceArn()) .withProtocol("lambda") .withEndpoint(d.getArn()); final SubscribeResult subscribe = client.subscribe(req); getLog().info("Subscribed topic arn " + bindingArn.getSourceArn() + " to function " + d.getArn()); // TODO: Unsubscribe older versions } protected Object updateAlias(String functionName, String version, String alias) { try { CreateAliasRequest req = new CreateAliasRequest(). withFunctionName(functionName). withFunctionVersion(version). withName(alias); return lambdaClient.createAlias(req); } catch (ResourceConflictException exc) { UpdateAliasRequest req = new UpdateAliasRequest(). withFunctionName(functionName). withFunctionVersion(version). withName(alias); return lambdaClient.updateAlias(req); } } private CreateFunctionResult createFunction(LambdaFunctionDefinition d) { CreateFunctionRequest req = new CreateFunctionRequest(). withCode(new FunctionCode().withS3Bucket(s3Uri.getBucket()).withS3Key(s3Uri.getKey())). withDescription(d.getDescription()). withFunctionName(d.getName()). withHandler(d.getHandler()). withMemorySize(d.getMemorySize()). withRole(d.getRole()). withRuntime(Runtime.Java8). withPublish(this.deployPublish). withVpcConfig(new VpcConfig().withSecurityGroupIds(securityGroupIds).withSubnetIds(subnetIds)). withTimeout(d.getTimeout()); final CreateFunctionResult createFunctionResult = lambdaClient.createFunction(req); return createFunctionResult; } private UpdateFunctionConfigurationResult updateIfNeeded(LambdaFunctionDefinition d, UpdateFunctionCodeResult curFc) { List returnedSecurityGroupIdsToMatch = Collections.emptyList(); if (null != curFc.getVpcConfig() && !curFc.getVpcConfig().getSecurityGroupIds().isEmpty()) returnedSecurityGroupIdsToMatch = curFc.getVpcConfig().getSecurityGroupIds(); List returnedSubnetIdsToMatch = Collections.emptyList(); if (null != curFc.getVpcConfig() && !curFc.getVpcConfig().getSubnetIds().isEmpty()) returnedSubnetIdsToMatch = curFc.getVpcConfig().getSubnetIds(); boolean bEquals = new EqualsBuilder(). append(d.getDescription(), curFc.getDescription()). append(d.getHandler(), curFc.getHandler()). append(d.getMemorySize(), curFc.getMemorySize().intValue()). append(d.getRole(), curFc.getRole()). append(d.getTimeout(), curFc.getTimeout().intValue()). append(this.securityGroupIds, returnedSecurityGroupIdsToMatch). append(this.subnetIds, returnedSubnetIdsToMatch). isEquals(); if (!bEquals) { final UpdateFunctionConfigurationRequest updRequest = new UpdateFunctionConfigurationRequest(); updRequest.setFunctionName(d.getName()); updRequest.setDescription(d.getDescription()); updRequest.setHandler(d.getHandler()); updRequest.setMemorySize(d.getMemorySize()); updRequest.setRole(d.getRole()); updRequest.setTimeout(d.getTimeout()); VpcConfig vpcConfig = new VpcConfig(). withSecurityGroupIds(this.securityGroupIds). withSubnetIds(this.subnetIds); updRequest.setVpcConfig(vpcConfig); getLog().info( format("Function Configuration doesn't match expected defaults. Updating it to %s.", updRequest)); final UpdateFunctionConfigurationResult result = lambdaClient.updateFunctionConfiguration(updRequest); return result; } return null; } private Map parseFunctionDefinions() throws Exception { String source = IOUtils.toString(new FileInputStream(definitionFile)); // TODO: Consider PluginParameterExpressionEvaluator source = new StrSubstitutor(session.getSystemProperties()).replace(source); getLog() .info(format("Loaded and replaced definitions from file '%s'", definitionFile.getPath())); List definitionList = OBJECT_MAPPER.readValue(source, new TypeReference>() { }); getLog().info(format("Found %d definitions: ", definitionList.size())); Map result = new TreeMap(); for (LambdaFunctionDefinition d : definitionList) { if (0 == d.getMemorySize()) { d.setMemorySize(defaultMemorySize); } if (isBlank(d.getRole())) { d.setRole(defaultRole); } else { d.setRole(roleResolver.lookupRoleGlob(d.getRole())); } if (0 == d.getTimeout()) { d.setTimeout(defaultTimeout); } result.put(d.getName(), d); } { source = OBJECT_MAPPER.writeValueAsString(definitionList); getLog() .debug(format("Parsed function definitions: %s", source)); IOUtils.write(source, new FileOutputStream(definitionFile)); } getLog().info(format("Merged into %d definitions: ", result.size())); return result; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy