Plugin development for elasticsearch

Simply explained

by geek & poke

  • Why plugins

  • Getting started with a simple plugin

  • How they fit into Elasticsearch architecture

  • Examples walkthrough

    • Development

    • Testing

    • Distribution

  • Limitations

  • Not covered

Why plugins

  • If there is no configuration option for your need

  • If you need unexposed (but public) API

  • To enforce specific behaviour

  • If scripting is simply not enough

  • Site plugin vs JVM Plugin

Here

Plugin == JVM Plugin

  • For example:

    • Creating a new REST endpoint

    • Custom mapping

    • Custom analyzer

    • Transport layer manipulation

    • HTTP layer manipulation

Getting started with a simple plugin

  • Plugins are written in Java (1.7+)

  • They live within a node (or transport client)

  • They can access all internals

  •      They can make a node stop working (or even worse)

public class MyPlugin extends AbstractPlugin {
	
    private final Settings settings;
    
    public MyPlugin(Settings settings) {
      this.settings = settings;
    }
	
    @Override
    public String name() {
        return "MyPlugin";
    }
    
    @Override
    public String description() {
    	return "This is the description for my plugin";
    }
}
  • Extend AbstractPlugin class

  • ... but it does not anything useful yet

public class MyPlugin extends AbstractPlugin {
	
    ...

    //Define own modules
    @Override
    public Collection<Class<? extends Module>> modules() {
    	return ImmutableList.of(MyModule.class);
    }
    
    //Define own services
    @Override
    public Collection<Class<? extends LifecycleComponent>> services() {
    	return ImmutableList.of(MyService.class);
    }
}
  • So lets define our own modules

  • or services which do something meaningful

public class MyPlugin extends AbstractPlugin {
	
    ...
    //Define own modules
    @Override
    public void processModule(Module module) {
    	if ((module instanceof RestModule)) {
    	   ((RestModule)module).addRestAction(MyRestAction.class);
        }

        if ((module instanceof ???)) {
    	   ((???)module).doModuleSpecificStuffHere();
        }
    }
}
  • or do something with core modules

  • to accomplish your needs

plugin=org.company.es.plugins.MyPlugin
  • Add es-plugin.properties to src/main/resources

  • Jar it, then zip it with all dependencies included

  • Maven can do that for you

Main artefact

Your code

Dependecies

How they fit into Elasticsearch architecture

  • Elasticsearch is build upon Google guice 2.0

    • pronounced "juice"

    • Dependency injection

    • Inversion of control

  • Guice code is copied in ES codebase (slightly modified for lower memory footprint)

  • So elasticsearch is build upon guice modules

  • ... and services because of

    • Modules start up, but they don't shut down

    • Modules should be tested

    • Modules can be overridden

  • Services have a lifecycle

  • Interesting core modules (for plugins)

    • ActionModule

    • AnalysisModule

    • HttpServerModule

    • RestModule

    • ScriptModule

    • TransportModule

  • Interesting core services (for plugins)

    • ClusterService

    • TransportService

    • NettyTransport

    • IndicesService

Examples walkthrough

  • Audit Plugin

    • Capture index changes

    • Write them into elasticsearch

    • (Make them visible through kibana)

 

  • Transport SSL/TLS Plugin

    • Implement and enforce SSL/TLS encryption for transport protocol

Development

by geek & poke

  • Audit Plugin

    • AuditService registers listener on ShardIndexingService

    • Write changes into elasticsearch via BulkProcessor

public class AuditService extends AbstractLifecycleComponent<AuditService>{
   ...
   @Inject
   public AuditService(Settings settings,
                       IndicesService indicesService,
                       Client client, 
                       ClusterService clusterService,
                       TransportFlushAction tfa) {
      super(settings);
      this.indicesService = indicesService;
      this.clusterService = clusterService;
      ...
   }



   @Override
   protected void doStart() throws ElasticsearchException {
      ...
      this.indicesService.indicesLifecycle().addListener(auditIndicesLsListener);
}
  • Define a new service called AuditService

  • Register a indices lifecycle listener

Dependency Injection by guice

IndicesLifecycle.Listener auditIndicesLsListener = 
    new IndicesLifecycle.Listener() {

   @Override
   public void afterIndexShardStarted(final IndexShard indexShard) {

      if (indexShard.routingEntry().primary() 
           && !indexShard.indexService().index().name().equals(auditIndexName)) {

            AuditIndexOpListener auditListener = 
               new AuditIndexOpListener(indexShard);
            indexShard.indexingService().addListener(auditListener);
      }
}
  • If a shard starts, register an IndexingOperationListener

class AuditIndexOpListener extends IndexingOperationListener {

   private final IndexShard indexShard;
		
   public AuditIndexOpListener(IndexShard indexShard) {
      this.indexShard = indexShard;
   }

   @Override
   public void postIndex(Index index) {
      String nodeName = indexShard.nodeName();
      String indexName = indexShard.indexService().index().name();
		
      Change change = new Change(nodeName, indexName, ...);
     
      //store it in elasticsearch (or anywhere else)
      IndexRequest ir = new IndexRequest().source(change.sourceAsMap());
      addToBulkIndex(ir);
   }
}
  • Let the IndexingOperationListener store the change in elasticsearch (or anywhere else)

public class AuditModule extends AbstractModule {

	@Override
	protected void configure() {
            //nothing to bind here
	}

	@Override
	public void processModule(Module module) {
	    if ((module instanceof ActionModule)) {
	      ((ActionModule)module).registerAction(FlushAction.INSTANCE, 
                                                    TransportFlushAction.class);
	    }

            if ((module instanceof RestModule)) {
	      ((RestModule)module).addRestAction(AuditRestAction.class);
	    }
	}
}
  • Define the AuditModule (not covered today)

  • API for flushing outstanding bulk requests

public class AuditPlugin extends AbstractPlugin {
   
   ...
   public String name() {
      return "AuditPlugin";
   }

   public String description() {
      return "This is the description for the AuditPlugin";
   }

   @Override
   public Collection<Class<? extends Module>> modules() {
      return ImmutableList.of(AuditModule.class);
   }

   @Override
   public Collection<Class<? extends LifecycleComponent>> services() {
      return ImmutableList.of(AuditService.class);
   }
}
  • Last but not least define the AuditPlugin itself

plugin=de.saly.es.example.audit.plugin.AuditPlugin
  • Add es-plugin.properties 

  • Transport SSL Plugin

    • Elasticsearch uses Netty for tcp communication

    • Extend NettyTransport and inject SslHandler into pipeline

    • Replace original Netty transport in TransportModule

    • Expose some SSL/TLS related informations through a new REST API

public class SSLNettyTransport extends NettyTransport {

   @Override
   public ChannelPipelineFactory configureServerChannelPipelineFactory(String name,
                                                                           Settings settings) {
        return new SSLServerChannelPipelineFactory(this, name, settings, this.settings);
   }

   protected class SSLServerChannelPipelineFactory extends SecureServerChannelPipelineFactory {

        public SSLServerChannelPipelineFactory(NettyTransport nettyTransport, String name, 
                                               Settings sslsettings, Settings essettings) {
           super(nettyTransport, name, sslsettings);  
        }

        @Override
        public ChannelPipeline getPipeline() throws Exception {
           ChannelPipeline pipeline = super.getPipeline();
           SSLEngine engine = ...
           SslHandler sslHandler = new SslHandler(engine);
           pipeline.addFirst("ssl_server", sslHandler);
           return pipeline;
        }
    }
}
  • Extend NettyTransport and do some netty pipeline magic

public class TSslPlugin extends AbstractPlugin {

   ...
   public void onModule(TransportModule transportModule) {
      transportModule.setTransport(SSLNettyTransport.class, name());
   }

   public void onModule(RestModule restModule) {
      restModule.addRestAction(TSslRestAction.class);
   }
}
  • Replace transport in TransportModule

  • Register a custom rest action for ssl infos

public class TSslRestAction extends BaseRestHandler{

   @Inject
   public TSslRestAction(Settings settings, Client client,
                         RestController controller) {
      super(settings, controller, client);
      controller.registerHandler(Method.GET, "/_tssl/state", this);
      controller.registerHandler(Method.POST, "/_tssl/state", this);
   }

   @Override
   protected void handleRequest(RestRequest request, RestChannel channel,
			Client client) throws Exception {
      XContentBuilder builder = JsonXContent.contentBuilder();
      builder.startObject();
      builder.field("enabled_protocols", SecurityUtil.ENABLED_SSL_PROTOCOLS);
      builder.field("enabled_chipers", SecurityUtil.ENABLED_SSL_CIPHERS);
      builder.endObject();
      channel.sendResponse(new BytesRestResponse(RestStatus.OK, builder));	
   }
}
  • Register a handler for an endpoint

  • Send a JSON response

plugin=de.saly.es.example.tssl.plugin.TSslPlugin
version=${project.version}
  • Add es-plugin.properties 

  • Call the new REST endpoint 

Testing

Always test with multiple nodes (cluster scenario)

  • If you want use the randomized testing suite, include test dependencies in pom

<dependency>
  <groupId>org.apache.lucene</groupId>
  <artifactId>lucene-test-framework</artifactId>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.elasticsearch</groupId>
  <artifactId>elasticsearch</artifactId>
  <type>test-jar</type>
  <scope>test</scope>
</dependency>

Distribution

  • Use Maven for building plugins

    • because ES is using Maven

  • Plugins are best published in maven central

  • Or distributed as .zip file

# Install from maven central
bin/plugin --install de.saly/elasticsearch-sample-plugin-tssl/1.1

# Install from a .zip file
bin/plugin --url file:///Users/.../elasticsearch-sample-plugin-tssl-1.1.zip \
           --install elasticsearch-sample-plugin-tssl
  • src/main/assemblies/plugin.xml

<?xml version="1.0"?>
<assembly>
	<id>plugin</id>
	<formats>
		<format>zip</format>
	</formats>
	<includeBaseDirectory>false</includeBaseDirectory>
	<dependencySets>
		<dependencySet>
			<outputDirectory>/</outputDirectory>
			<useProjectArtifact>true</useProjectArtifact>
			<useTransitiveFiltering>true</useTransitiveFiltering>
			<excludes>
				<exclude>org.elasticsearch:elasticsearch</exclude>
			</excludes>
		</dependencySet>
	</dependencySets>
</assembly>
  • Use maven-assembly-plugin

<plugin>
   <artifactId>maven-assembly-plugin</artifactId>
      <configuration>
      <appendAssemblyId>false</appendAssemblyId>
         <outputDirectory>${project.build.directory}/releases/</outputDirectory>
            <descriptors>
               <descriptor>${basedir}/src/main/assemblies/plugin.xml</descriptor>
            </descriptors>
      </configuration>
      <executions>
         <execution>
            <phase>package</phase>
               <goals>
                 <goal>single</goal>
               </goals>
         </execution>
      </executions>
</plugin>

!!

  • Plugin related configuration options in elasticsearch.yml

# Load this plugin from the classpath 
# (does only makes sense if plugins.load_classpath_plugins is false)
# Order is respected
plugin.types: org.company.MyPlugin,com.guhgle.FantasticPlugin

# If a plugin listed here is not installed for current node, the node will not start.
plugin.mandatory: mapper-attachments,lang-groovy

# If its true (which is the default) load all plugins which are in the classpath
# No order guarantee
plugins.load_classpath_plugins: true

Limitations

  • No trust model (yet)

    • ​Plugins are allowed to do anything

    • With ES 2.0 maybe Java SecurityManager will be utilised

  • No isolation

    • Same classloader for ES and all plugins

    • Plugins can interfere with others

  • Shade your dependencies

  • Install plugins on as few nodes as possible

  • Consider using dedicated plugin nodes

Not covered

today

  • Creating a custom transport request/response

  • Manipulating the HTTP layer

  • Plugin scopes

  • (Rest)ActionFilter

  • Custom analyzers

  • Custom mapping types

  • Site plugins

  • ... maybe there will be a Part II of this talk

by geek & poke

System.exit(0);

Plugin development for elasticsearch

By Hendrik Saly

Plugin development for elasticsearch

Elasticsearch kann durch Plugins in seiner Kernfunktionalität und im Infrastrukturverhalten erweitert und verändert werden. Dieser Vortrag geht kurz auf die technische Architektur von ES ein und zeigt dann anhand von Beispielen wie verschiedene Erweiterungen über den Pluginmechnismus entwickelt werden können. Neben Testing und Deployment werden auch die Grenzen von Plugins und Alternativen aufgezeigt

  • 4,159