diff --git a/Dockerfile b/Dockerfile index 8ff459c..e44c985 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,54 +1,88 @@ -FROM debian:jessie - -RUN apt-get update && \ - apt-get install -y \ - openjdk-7-jre-headless \ - curl \ - jq \ - && rm -rf /var/lib/apt/lists/* - -ENV JAVA_HOME /usr/lib/jvm/java-7-openjdk-amd64 - -# If we wanted the development version we could pull that instead but we want to -# run a production environment here. -RUN export ES_PKG=elasticsearch-2.2.0.deb && \ - curl -LO https://download.elasticsearch.org/elasticsearch/release/org/elasticsearch/distribution/deb/elasticsearch/2.2.0/${ES_PKG} && \ - dpkg -i ${ES_PKG} && \ - rm ${ES_PKG} && \ - rm /etc/elasticsearch/elasticsearch.yml - -# Add Containerpilot and set its configuration -ENV CONTAINERPILOT_VER 2.1.0 -ENV CONTAINERPILOT file:///etc/containerpilot.json - -RUN export CONTAINERPILOT_CHECKSUM=e7973bf036690b520b450c3a3e121fc7cd26f1a2 \ +FROM docker.elastic.co/elasticsearch/elasticsearch-alpine-base:latest + +# LICENSE - Apache License 2.0 +# https://github.com/elastic/elasticsearch-alpine-base/blob/master/LICENSE + +ENV ELASTIC_VERSION=5.3.0 \ + PATH=/usr/share/elasticsearch/bin:$PATH \ + JAVA_HOME=/usr/lib/jvm/java-1.8-openjdk \ + CONSUL_VERSION=0.7.5 \ + CONSUL_CLI_VER=0.3.1 \ + CONTAINERPILOT_VER=2.7.2 \ + CONTAINERPILOT=file:///etc/containerpilot.json + +WORKDIR /usr/share/elasticsearch + +# Required dependencies +RUN apk update && \ + apk add jq curl unzip tar && \ + rm -rf /var/cache/apk/* + +# Download/extract defined ES version. busybox tar can't strip leading dir. +RUN export ELASTIC_CHECKSUM=9273fdecb2251755887f1234d6cfcc91e44a384d && \ + wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-${ELASTIC_VERSION}.tar.gz && \ + test $ELASTIC_CHECKSUM == $(sha1sum elasticsearch-${ELASTIC_VERSION}.tar.gz | awk '{print $1}') && \ + tar zxf elasticsearch-${ELASTIC_VERSION}.tar.gz && \ + chown -R elasticsearch:elasticsearch elasticsearch-${ELASTIC_VERSION} && \ + mv elasticsearch-${ELASTIC_VERSION}/* . && \ + rmdir elasticsearch-${ELASTIC_VERSION} && \ + rm elasticsearch-${ELASTIC_VERSION}.tar.gz + +RUN set -ex && for esdirs in config data logs; do \ + mkdir -p "$esdirs"; \ + chown -R elasticsearch:elasticsearch "$esdirs"; \ + done + +# Add consul agent +RUN export CONSUL_CHECKSUM=40ce7175535551882ecdff21fdd276cef6eaab96be8a8260e0599fadb6f1f5b8 \ + && curl --retry 7 --fail -vo /tmp/consul.zip "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_amd64.zip" \ + && echo "${CONSUL_CHECKSUM} /tmp/consul.zip" | sha256sum -c \ + && unzip /tmp/consul -d /usr/local/bin \ + && rm /tmp/consul.zip + +# Consul client +RUN export CONSUL_CLIENT_CHECKSUM=037150d3d689a0babf4ba64c898b4497546e2fffeb16354e25cef19867e763f1 \ + && curl -Lso /tmp/consul-cli.tgz "https://github.com/CiscoCloud/consul-cli/releases/download/v${CONSUL_CLI_VER}/consul-cli_${CONSUL_CLI_VER}_linux_amd64.tar.gz" \ + && echo "${CONSUL_CLIENT_CHECKSUM} /tmp/consul-cli.tgz" | sha256sum -c \ + && tar zxf /tmp/consul-cli.tgz -C /usr/local/bin --strip-components 1 \ + && rm /tmp/consul-cli.tgz + +# Add ContainerPilot and set its configuration file path +RUN export CONTAINERPILOT_CHECKSUM=e886899467ced6d7c76027d58c7f7554c2fb2bcc \ && curl -Lso /tmp/containerpilot.tar.gz \ - "https://github.com/joyent/containerpilot/releases/download/${CONTAINERPILOT_VER}/containerpilot-${CONTAINERPILOT_VER}.tar.gz" \ + "https://github.com/joyent/containerpilot/releases/download/${CONTAINERPILOT_VER}/containerpilot-${CONTAINERPILOT_VER}.tar.gz" \ && echo "${CONTAINERPILOT_CHECKSUM} /tmp/containerpilot.tar.gz" | sha1sum -c \ && tar zxf /tmp/containerpilot.tar.gz -C /usr/local/bin \ && rm /tmp/containerpilot.tar.gz -# Create and take ownership over required directories -RUN mkdir -p /var/lib/elasticsearch/data && \ - chown -R elasticsearch:elasticsearch /var/lib/elasticsearch/data && \ - chown -R root:elasticsearch /etc/elasticsearch && \ - chmod g+w /etc/elasticsearch - USER elasticsearch +COPY /etc/containerpilot.json /etc/ +COPY /etc/elasticsearch.yml config/ +COPY /etc/log4j2.properties config/ +COPY /bin/es-docker bin/es-docker +COPY /bin/* /usr/local/bin/ -# Add our configuration files and scripts -COPY /etc/containerpilot.json /etc -COPY /etc/elasticsearch.yml /etc/elasticsearch/elasticsearch.yml -COPY /bin/manage.sh /usr/local/bin +USER root +RUN chown elasticsearch:elasticsearch config/elasticsearch.yml config/log4j2.properties bin/es-docker && \ + chmod 0750 bin/es-docker && \ + mkdir -p /opt/consul/config && \ + mkdir -p /opt/consul/data && \ + chmod 770 /opt/consul/data && \ + chown -R elasticsearch:elasticsearch /opt/consul && \ + mkdir -p /etc/containerpilot && \ + chmod -R g+w /etc/containerpilot && \ + chmod +x /usr/local/bin/elastic-server.sh && \ + chown -R elasticsearch:elasticsearch /etc/containerpilot + +USER elasticsearch # Expose the data directory as a volume in case we want to mount these # as a --volumes-from target; it's important that this VOLUME comes # after the creation of the directory so that we preserve ownership. -VOLUME /var/lib/elasticsearch/data - -# We don't need to expose these ports in order for other containers on Triton -# to reach this container in the default networking environment, but if we -# leave this here then we get the ports as well-known environment variables -# for purposes of linking. -EXPOSE 9200 -EXPOSE 9300 +VOLUME ["/usr/share/elasticsearch/data"] + +# Start with containerpilot then to our wrapper +CMD ["containerpilot", "/usr/local/bin/elastic-server.sh"] + + +EXPOSE 9200 9300 diff --git a/README.md b/README.md index 8325b32..f367132 100644 --- a/README.md +++ b/README.md @@ -141,3 +141,14 @@ $ curl "http://${MASTER_IP}:9200/_cluster/state?pretty=true" } ``` + +### Dockerfile + +Elastic has depreciated the official repository in favor of their own registry. + +> This image will receive no further updates after 2017-06-20 (June 20, 2017). Please adjust your usage accordingly. - [Docker Hub](https://hub.docker.com/_/elasticsearch/) + +Unfortunately they don't include the Dockerfile as part of the elasticsearch repository, links are included below for easy reference. + +* [Dockerfile](https://github.com/elastic/elasticsearch-docker/blob/master/build/elasticsearch/Dockerfile) +* [Base Dockerfile](https://github.com/elastic/elasticsearch-alpine-base/blob/master/build/elasticsearch-alpine-base/Dockerfile) diff --git a/bin/elastic-server.sh b/bin/elastic-server.sh new file mode 100644 index 0000000..160f9f3 --- /dev/null +++ b/bin/elastic-server.sh @@ -0,0 +1,6 @@ +#!/bin/sh -xe +manage.sh onStart #|| exit $? +if [[ $? != 0 ]]; then + exit $? +fi +exec /usr/share/elasticsearch/bin/es-docker $* diff --git a/bin/es-docker b/bin/es-docker new file mode 100644 index 0000000..c9c285c --- /dev/null +++ b/bin/es-docker @@ -0,0 +1,39 @@ +#!/bin/bash + +# Run Elasticsearch and allow setting default settings via env vars +# +# e.g. Setting the env var cluster.name=testcluster +# +# will cause Elasticsearch to be invoked with -Ecluster.name=testcluster +# +# see https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html#_setting_default_settings + +es_opts='' + +while IFS='=' read -r envvar_key envvar_value +do + # Elasticsearch env vars need to have at least two dot separated lowercase words, e.g. `cluster.name` + if [[ "$envvar_key" =~ ^[a-z]+\.[a-z]+ ]] + then + if [[ ! -z $envvar_value ]]; then + es_opt="-E${envvar_key}=${envvar_value}" + es_opts+=" ${es_opt}" + fi + fi +done < <(env) + +# The virtual file /proc/self/cgroup should list the current cgroup +# membership. For each hierarchy, you can follow the cgroup path from +# this file to the cgroup filesystem (usually /sys/fs/cgroup/) and +# introspect the statistics for the cgroup for the given +# hierarchy. Alas, Docker breaks this by mounting the container +# statistics at the root while leaving the cgroup paths as the actual +# paths. Therefore, Elasticsearch provides a mechanism to override +# reading the cgroup path from /proc/self/cgroup and instead uses the +# cgroup path defined the JVM system property +# es.cgroups.hierarchy.override. Therefore, we set this value here so +# that cgroup statistics are available for the container this process +# will run in. +export ES_JAVA_OPTS="-Des.cgroups.hierarchy.override=/ $ES_JAVA_OPTS" + +exec bin/elasticsearch ${es_opts} diff --git a/bin/manage.sh b/bin/manage.sh index d8bd9e7..0a73812 100755 --- a/bin/manage.sh +++ b/bin/manage.sh @@ -1,67 +1,122 @@ #!/bin/bash MASTER=null +CONSUL_HOST=${CONSUL} +CONSUL_AGENT=${CONSUL_AGENT:=false} -if [[ -z ${CONSUL} ]]; then +if [ $CONSUL_AGENT != false ]; then + CONSUL_HOST='localhost' +fi + +if [[ -z $CONSUL_HOST ]]; then echo "Missing CONSUL environment variable" exit 1 fi -preStart() { - # happy path is that there's a master available and we can cluster - configureMaster - - # data-only nodes can only loop until there's a master available - if [ ${ES_NODE_MASTER} == false ]; then - while true - do - sleep 1.7 - configureMaster - done - exit 0 - fi +consulCommand() { + consul-cli --quiet --consul="${CONSUL_HOST}:8500" $* +} + +onStart() { + logDebug "onStart" + + waitForLeader + + # wait for a healthy master + local i + for (( i = 0; i < ${MASTER_WAIT_TIMEOUT-60}; i++ )); do + getServiceAddresses "elasticsearch-master" + if [[ ${serviceAddresses} ]]; then + MASTER=$serviceAddresses + break + fi + sleep 1 + done + + # replace zen hosts + replaceZenHosts + + # disable seccomp (only supported on newer Linux kernels) + replaceSeccomp +} + +health() { + local privateIp=$(ip addr show eth0 | grep -o '[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}') + /usr/bin/curl --fail -s -o /dev/null http://${privateIp}:9200 +} - # for a master+data node, we'll retry to see if there's another - # master in the cluster in the process of starting up. But we - # bail out if we exceed the retries and just bootstrap the cluster - if [ ${ES_NODE_DATA} == true ]; then - local n=0 - until [ $n -ge 2 ] - do - sleep 1.7 - configureMaster - n=$((n+1)) - done +waitForLeader() { + # no need to wait for a leader unless we are using consul in agent mode + if [ $CONSUL_HOST != 'localhost' ]; then + return fi - # for a master-only node or master+data node that's exceeded the - # retry attempts, we'll assume this is the first master and bootstrap - # the cluster - MASTER=127.0.0.1 - replace + logDebug "Waiting for consul server" + local tries=0 + while true + do + logDebug "Waiting for consul server" + tries=$((tries + 1)) + local server=$(consul members -status alive | grep server) + if [[ -n "$server" ]]; then + break + elif [[ $tries -eq 60 ]]; then + echo "No consul server" + exit 1 + fi + sleep 1 + done +} + +getServiceAddresses() { + local serviceInfo=$(consulCommand health service --passing "$1") + serviceAddresses=($(echo $serviceInfo | jq -r '.[].Service.Address')) + logDebug "serviceAddresses $1 ${serviceAddresses[*]}" +} + +getRegisteredServiceName() { + registeredServiceName=$(jq -r '.services[0].name' /etc/containerpilot.json) +} + +getNodeAddress() { + nodeAddress=$(ifconfig eth0 | awk '/inet addr/ {gsub("addr:", "", $2); print $2}') } -# get the list of ES master nodes from Consul -configureMaster() { - MASTER=$(curl -Ls --fail http://${CONSUL}:8500/v1/catalog/service/elasticsearch-master | jq -r '.[0].ServiceAddress') - if [[ $MASTER != "null" ]] && [[ -n $MASTER ]]; then - replace - exit 0 +replaceZenHosts() { + REPLACEMENT=$(printf 's/^# discovery\.zen\.ping\.unicast\.hosts.*$/discovery.zen.ping.unicast.hosts: ["%s"]/' ${MASTER}) + sed -i "${REPLACEMENT}" /usr/share/elasticsearch/config/elasticsearch.yml +} + +replaceSeccomp() { + SECCOMP_ENABLED=$(zcat /proc/config.gz | grep CONFIG_SECCOMP=y) + if [[ "${SECCOMP_ENABLED}" != "CONFIG_SECCOMP=y" ]]; then + echo "WARNING: seccomp unavailable, disabling system_call_filter..." + REPLACEMENT=$(printf 's/^# bootstrap\.system_call_filter.*$/bootstrap.system_call_filter: false/') + sed -i "${REPLACEMENT}" /usr/share/elasticsearch/config/elasticsearch.yml fi - # if there's no master we fall thru and let the caller figure - # out what to do next } -# update discovery.zen.ping.unicast.hosts -replace() { - REPLACEMENT=$(printf 's/^discovery\.zen\.ping\.unicast\.hosts.*$/discovery.zen.ping.unicast.hosts: ["%s"]/' ${MASTER}) - sed -i "${REPLACEMENT}" /etc/elasticsearch/elasticsearch.yml +logDebug() { + if [[ "${LOG_LEVEL}" == "DEBUG" ]]; then + echo "manage: $*" + fi } -health() { - local privateIp=$(ip addr show eth0 | grep -o '[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}') - /usr/bin/curl --fail -s -o /dev/null http://${privateIp}:9200 +help() { + echo "Usage: ./manage.sh onStart => first-run configuration" + echo " ./manage.sh health => health check Elastic" + echo " ./manage.sh preStop => prepare for stop" } -# do whatever the arg is -$1 +until + cmd=$1 + if [[ -z "$cmd" ]]; then + help + fi + shift 1 + $cmd "$@" + [ "$?" -ne 127 ] +do + help + exit +done diff --git a/etc/containerpilot.json b/etc/containerpilot.json index 9adac5f..80d7486 100644 --- a/etc/containerpilot.json +++ b/etc/containerpilot.json @@ -1,13 +1,28 @@ { - "consul": "{{ .CONSUL }}:8500", - "preStart": "/usr/local/bin/manage.sh preStart", - "services": [ - { - "name": "{{ .ES_SERVICE_NAME }}", - "port": 9300, - "health": "/usr/local/bin/manage.sh health", - "poll": 10, - "ttl": 25 - } - ] + "consul": "{{ if .CONSUL_AGENT }}localhost{{ else }}{{ if .CONSUL }}{{ .CONSUL }}{{ else }}consul{{ end }}{{ end }}:8500", + "logging": { + "level": "{{ if .LOG_LEVEL }}{{ .LOG_LEVEL }}{{ else }}INFO{{ end }}", + "format": "text", + "output": "stdout" + }, + "services": [{ + "name": "{{ .ES_SERVICE_NAME }}", + "port": 9200, + "health": "/usr/local/bin/manage.sh health", + "poll": 10, + "ttl": 25 + }], + "coprocesses": [{{ if .CONSUL_AGENT }} + { + "command": ["/usr/local/bin/consul", "agent", + "-data-dir=/opt/consul/data", + "-config-dir=/opt/consul/config", + "-rejoin", + "-retry-join", "{{ if .CONSUL }}{{ .CONSUL }}{{ else }}consul{{ end }}", + "-retry-max", "10", + "-retry-interval", "10s" + ], + "restarts": "unlimited" + } + {{ end }}] } diff --git a/etc/elasticsearch.yml b/etc/elasticsearch.yml index a21bb11..435acc4 100644 --- a/etc/elasticsearch.yml +++ b/etc/elasticsearch.yml @@ -1,103 +1,23 @@ -# ======================== Elasticsearch Configuration ========================= -# -# NOTE: Elasticsearch comes with reasonable defaults for most settings. -# Before you set out to tweak and tune the configuration, make sure you -# understand what are you trying to accomplish and the consequences. -# -# The primary way of configuring a node is via this file. This template lists -# the most important settings you may want to configure for a production cluster. -# -# Please see the documentation for further information on configuration options: -# -# -# ---------------------------------- Cluster ----------------------------------- - # Use a descriptive name for your cluster: +cluster.name: ${ES_CLUSTER_NAME} -cluster.name: ${CLUSTER_NAME} - -# ------------------------------------ Node ------------------------------------ - -# Assign the container ID to the node name so to eliminate having to translate -# between the two while debugging. - -node.name: es-${HOSTNAME} - -# By default, all nodes are eligible to become masters and store data. - -node.master: ${ES_NODE_MASTER} -node.data: ${ES_NODE_DATA} - -# Add custom attributes to the node: -# -# node.rack: r1 - -# ----------------------------------- Paths ------------------------------------ - -# Path to directory where to store the data (separate multiple locations by comma): - -path.data: /var/lib/elasticsearch/data - -# Path to log files: - -path.logs: /var/log/elasticsearch - -# ----------------------------------- Memory ----------------------------------- -# -# Lock the memory on startup: -# -# bootstrap.mlockall: true -# -# Make sure that the `ES_HEAP_SIZE` environment variable is set to about half the memory -# available on the system and that the owner of the process is allowed to use this limit. -# -# Elasticsearch performs poorly when the system is swapping the memory. -# -# ---------------------------------- Network ----------------------------------- +# Use a descriptive name for the node: +node.name: node-${HOSTNAME} # Set the bind address to a specific IP (IPv4 or IPv6): - -network.host: _eth0:ipv4_ - -# Set a custom port for HTTP: -# -# http.port: 9200 -# -# For more information, see the documentation at: -# -# -# ---------------------------------- Gateway ----------------------------------- -# -# Block initial recovery after a full cluster restart until N nodes are started: -# -# gateway.recover_after_nodes: 3 -# -# For more information, see the documentation at: -# -# -# --------------------------------- Discovery ---------------------------------- -# -# Elasticsearch nodes will find each other via unicast, by default. -# For more information, see the documentation at: -# -# -# Prevent the "split brain" by configuring the majority of nodes (total number of nodes / 2 + 1): -# -# discovery.zen.minimum_master_nodes: 3 - -# We'll bootstrap cluster communications by getting a master node via -# the ContainerPilot `preStart` directive, which will rewrite this -# configuration value with the appropriate hostname. - -discovery.zen.ping.multicast.enabled: false -discovery.zen.ping.unicast.hosts: ["${ES_BOOTSTRAP_HOST}"] - -# ---------------------------------- Various ----------------------------------- -# -# Disable starting multiple nodes on a single system: -# -# node.max_local_storage_nodes: 1 -# -# Require explicit names when deleting indices: -# -# action.destructive_requires_name: true +# network.host: _eth0:ipv4_ +network.host: [_site_] + +# Pass an initial list of hosts to perform discovery when new node is started: +# The default list of hosts is ["127.0.0.1", "[::1]"] +# THIS GETS OVERWRITTEN AUTOMATICALLY via `bin/manage.sh onStart` +# discovery.zen.ping.unicast.hosts: [] + +# minimum_master_nodes need to be explicitly set when bound on a public IP +# set to 1 to allow single node clusters +# Details: https://github.com/elastic/elasticsearch/pull/17288 +discovery.zen.minimum_master_nodes: 1 + +# Turn off seccomp on systems that don't have it enabled (see manage.sh) +# https://docs.docker.com/engine/security/seccomp/ +# bootstrap.system_call_filter: true diff --git a/etc/log4j2.properties b/etc/log4j2.properties new file mode 100644 index 0000000..46877d0 --- /dev/null +++ b/etc/log4j2.properties @@ -0,0 +1,9 @@ +status = error + +appender.console.type = Console +appender.console.name = console +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] %marker%m%n + +rootLogger.level = info +rootLogger.appenderRef.console.ref = console diff --git a/local-compose.yml b/local-compose.yml index 642cfa3..7478f66 100644 --- a/local-compose.yml +++ b/local-compose.yml @@ -1,50 +1,48 @@ -# Local testing version of Elasticsearch stack designed for -# container-native deployment on Joyent's Triton platform. +version: '2.1' +services: + # Local testing version of Elasticsearch stack designed for + # container-native deployment on Joyent's Triton platform. + elasticsearch: + build: . + environment: + - CONSUL_AGENT=1 + - CONSUL=consul + - ES_CLUSTER_NAME=famished_test + - ES_SERVICE_NAME=elasticsearch-master + - ES_NODE_MASTER=true + - ES_NODE_DATA=true + - ES_ENVIRONMENT=dev + # - ES_CLUSTER_SIZE=1 -elasticsearch_master: - extends: - file: docker-compose.yml - service: elasticsearch - mem_limit: 512m - build: . - environment: - - CONSUL=consul - - ES_SERVICE_NAME=elasticsearch-master - - ES_NODE_MASTER=true - - ES_NODE_DATA=false - - CONTAINERPILOT=file:///etc/containerpilot.json - links: - - consul:consul + # are these even remotely accurate? + - ES_JAVA_OPTS=-Djava.security.egd=file:/dev/./urandom -XX:-UseGCTaskAffinity -XX:-BindGCTaskThreadsToCPUs -XX:ParallelGCThreads=1 -XX:ParallelCMSThreads=1 -Xms128m -Xmx128m -elasticsearch: - extends: - file: docker-compose.yml - service: elasticsearch - mem_limit: 512m - build: . - environment: - - CONSUL=consul - - ES_SERVICE_NAME=elasticsearch-master - links: - - consul:consul + elasticsearch_master: + extends: + service: elasticsearch + ports: + - 9200:9200 + - 9300:9300 + environment: + - ES_NODE_MASTER=true + - ES_NODE_DATA=false -elasticsearch_data: - extends: - file: docker-compose.yml - service: elasticsearch - mem_limit: 512m - build: . - environment: - - CONSUL=consul - - ES_SERVICE_NAME=elasticsearch-data - - ES_NODE_MASTER=false - - ES_NODE_DATA=true - links: - - consul:consul + elasticsearch_data: + extends: + service: elasticsearch + environment: + - ES_SERVICE_NAME=elasticsearch-data + - ES_NODE_MASTER=false + - ES_NODE_DATA=true -consul: - extends: - file: docker-compose.yml - service: consul - ports: - - 8500:8500 + consul: + image: autopilotpattern/consul:0.7.2-r0.8 + command: > + /usr/local/bin/containerpilot + /bin/consul agent -server + -bootstrap + -config-dir=/etc/consul + -ui-dir /ui + -client=0.0.0.0 + ports: + - 8500:8500