1
0
mirror of https://github.com/erlang/docker-erlang-example.git synced 2025-07-30 22:43:04 +03:00

Refactoring: Copy simple minikube example

This commit copies Lukas Larsson's (@garazdawi) and Siri Hansen's
(@sirihansen) example from:

https://github.com/erlang/docker-erlang-example/tree/minikube-simple
This commit is contained in:
Kjell Winblad
2019-05-20 14:23:21 +02:00
parent 6b805ff343
commit 233aaaa8b4
16 changed files with 655 additions and 0 deletions

View File

@ -0,0 +1,2 @@
_build/
rebar.lock

View File

@ -0,0 +1,48 @@
sudo: required
dist: xenial
addons:
apt:
packages:
- curl
env:
- CHANGE_MINIKUBE_NONE_USER=true
before_script:
# Download minikube.
- MINIKUBE_VERSION=latest
- curl -Lo minikube https://storage.googleapis.com/minikube/releases/$MINIKUBE_VERSION/minikube-linux-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/
# Download kubectl, which is a requirement for using minikube.
- KUBERNETES_VERSION=$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)
- curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/$KUBERNETES_VERSION/bin/linux/amd64/kubectl && chmod +x kubectl && sudo mv kubectl /usr/local/bin/
# Test that it works
- kubectl -h
- sudo minikube start -v 7 --logtostderr --vm-driver=none --kubernetes-version "$KUBERNETES_VERSION"
# Fix the kubectl context, as it's often stale.
- minikube update-context
# Wait for Kubernetes to be up and ready.
- JSONPATH='{range .items[*]}{@.metadata.name}:{range @.status.conditions[*]}{@.type}={@.status};{end}{end}'; until kubectl get nodes -o jsonpath="$JSONPATH" 2>&1 | grep -q "Ready=True"; do sleep 1; done
script:
- kubectl cluster-info
# kube-addon-manager is responsible for managing other kubernetes components, such as kube-dns, dashboard, storage-provisioner..
- JSONPATH='{range .items[*]}{@.metadata.name}:{range @.status.conditions[*]}{@.type}={@.status};{end}{end}'; until kubectl -n kube-system get pods -lcomponent=kube-addon-manager -o jsonpath="$JSONPATH" 2>&1 | grep -q "Ready=True"; do sleep 5;echo "waiting for kube-addon-manager to be available"; kubectl get pods --all-namespaces; done
# Wait for kube-dns to be ready.
- JSONPATH='{range .items[*]}{@.metadata.name}:{range @.status.conditions[*]}{@.type}={@.status};{end}{end}'; until kubectl -n kube-system get pods -lk8s-app=kube-dns -o jsonpath="$JSONPATH" 2>&1 | grep -q "Ready=True"; do sleep 5;echo "waiting for kube-dns to be available"; kubectl get pods --all-namespaces; done
- kubectl create service nodeport dockerwatch --tcp=8080:8080 --tcp=8443:8443
- kubectl get service
- ./create-certs $(minikube ip)
- kubectl create secret generic dockerwatch --from-file=ssl/
- kubectl get secret
- docker build -t dockerwatch .
- kubectl apply -f dockerwatch-deploy.yaml
# Wait for dockerwatch to be ready.
- JSONPATH='{range .items[*]}{@.metadata.name}:{range @.status.conditions[*]}{@.type}={@.status};{end}{end}'; until kubectl -n default get pods -lapp=dockerwatch -o jsonpath="$JSONPATH" 2>&1 | grep -q "Ready=True"; do sleep 5;echo "waiting for dockerwatch to be available"; kubectl get pods --all-namespaces; done
- HTTP=$(minikube service dockerwatch --url | head -1)
- HTTPS=$(minikube service dockerwatch --url --https | tail -1)
- "curl -v -H 'Content-Type: application/json' -X POST -d '' $HTTP/cnt"
- "curl -v -H 'Content-Type: application/json' -X POST -d '{}' $HTTP/cnt"
- "curl -v --cacert ssl/dockerwatch-ca.pem -H 'Accept: application/json' $HTTPS/"
- "curl -v --cacert ssl/dockerwatch-ca.pem -H 'Accept: application/json' $HTTPS/cnt"

View File

@ -0,0 +1,26 @@
## Docker Cheatsheet
* Remove all containers that are not running:
$ docker rm $(docker ps -aq -f status=exited)
* Remove dangling images:
$ docker rmi $(docker images -f dangling=true -q)
* Attach to running docker:
$ docker exec -i -t NameOrId /bin/sh
## Core generation
* `/proc/sys/core_pattern` is clearly persisted on the host. Taking note of
its content before starting any endeavour is therefore highly encouraged.
* dockers `--privileged` is necessary for a gdb session to catch the stack,
without privileges, gdb just complains about No stack. Google still is
hardly knowledgeable about this phenomenon...
* setting ulimit on docker run works perfectly, for future googlers (syntax hard to find),
a docker-compose example:
ulimits:
core: -1

View File

@ -0,0 +1,37 @@
# Build stage 0
FROM erlang:alpine
# Install Rebar3
RUN mkdir -p /buildroot/rebar3/bin
ADD https://s3.amazonaws.com/rebar3/rebar3 /buildroot/rebar3/bin/rebar3
RUN chmod a+x /buildroot/rebar3/bin/rebar3
# Setup Environment
ENV PATH=/buildroot/rebar3/bin:$PATH
# Reset working directory
WORKDIR /buildroot
# Copy our Erlang test application
COPY dockerwatch dockerwatch
# And build the release
WORKDIR dockerwatch
RUN rebar3 as prod release
# Build stage 1
FROM alpine
# Install some libs
RUN apk add --no-cache openssl && \
apk add --no-cache ncurses-libs
# Install the released application
COPY --from=0 /buildroot/dockerwatch/_build/prod/rel/dockerwatch /dockerwatch
# Expose relevant ports
EXPOSE 8080
EXPOSE 8443
CMD ["/dockerwatch/bin/dockerwatch", "foreground"]

View File

@ -0,0 +1,23 @@
## Generating Certificate
Generate certificates in subdirectory `ssl`.
### Root CA
$ openssl genrsa -out dockerwatch-ca.key 4096
$ openssl req -x509 -new -nodes -key dockerwatch-ca.key -sha256 -days 1024 -out dockerwatch-ca.pem
### Server Certificate
$ openssl genrsa -out dockerwatch-server.key 4096
Certificate signing request
$ openssl req -new -key dockerwatch-server.key -out dockerwatch-server.csr
The most important field: `Common Name (eg, YOUR name) []: localhost`. We use localhost in this example.
### Sign it
$ openssl x509 -req -in dockerwatch-server.csr -CA dockerwatch-ca.pem -CAkey dockerwatch-ca.key -CAcreateserial -out dockerwatch-server.pem -days 500 -sha256

View File

@ -0,0 +1,206 @@
# Using Minikube and Erlang
This is a quick demo of using minikube to run an Erlang node. The example we will
use is the [Docker Watch](http://github.com/erlang/docker-erlang-example/tree/master)
node.
This is only meant to be an example of how to get started. It is not the only,
nor neccesarily the best way to setup minikube with Erlang.
# Other Demos
* [Using Docker](http://github.com/erlang/docker-erlang-example/)
* [Using Docker: Logstash](http://github.com/erlang/docker-erlang-example/tree/logstash)
* [Using Docker Compose: Logstash/ElasticSearch/Kibana](http://github.com/erlang/docker-erlang-example/tree/elk)
* [Using Minikube: Simple](http://github.com/erlang/docker-erlang-example/tree/minikube-simple)
* [Using Minikube: Prometheus/Grafana](http://github.com/erlang/docker-erlang-example/tree/minikube-prom-graf)
* [Using Minikube: Distributed Erlang](http://github.com/erlang/docker-erlang-example/tree/minikube-dist)
* [Using Minikube: Encrypted Distributed Erlang](http://github.com/erlang/docker-erlang-example/tree/minikube-tls-dist)
# Prerequisites
To start with you should familiarize yourself with minikube through this guide:
https://kubernetes.io/docs/setup/minikube/
In a nutshell:
## Install
* [VirtualBox](https://www.virtualbox.org/wiki/Downloads)
* [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
* [minikube](https://github.com/kubernetes/minikube/releases)
## Start and test
> minikube start
> kubectl run hello-minikube --image=k8s.gcr.io/echoserver:1.10 --port=8080
> kubectl expose deployment hello-minikube --type=NodePort
> curl $(minikube service hello-minikube --url)
## Should print a lot of text
> kubectl delete services hello-minikube
> kubectl delete deployment hello-minikube
> minikube stop
# Deploying Dockerwatch
In this demo we will be doing three things:
* Create a Service that will be used to access the dockerwatch API
* Create a Secret for our ssl keys
* Create a Deployment of dockerwatch that implements the Service
First however, make sure that the minikube cluster is started:
> minikube start
and that you have cloned this repo and checked out this branch:
> git clone https://github.com/erlang/docker-erlang-example
> cd docker-erlang-example
> git checkout minikube-simple
## Create a Service
The Service is what will be used to connect to the dockerwatch application
from outside the kubernetes cluster. It is not stricly neccesary to create the
Service before the deployment is done. However, it is considered good practice
to do so, as otherwise environment variables about the Service will not be
available in the Pods.
> kubectl create service nodeport dockerwatch --tcp=8080:8080 --tcp=8443:8443
service/dockerwatch created
Check that it was created:
> kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
dockerwatch NodePort 10.103.32.142 <none> 8080:31716/TCP,8443:30383/TCP 21m
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 1h
We can see the external IP and port used through the minikube API:
> minikube service dockerwatch --url
http://192.168.99.101:31716
http://192.168.99.101:30383
Take a note of the IP addresses you get as we will need them when creating the
ssl certifcates.
## Create a Secret
We then need to create a new Secret that will be used to store the ssl private key
and the certificate. The Secret will then be mounted into the running Pod just
as was done in the original example. We start by generating the CA and the
server certificate:
> ./create-certs $(minikube ip)
Note that I put the IP address we got from `minikube service dockerwatch` above
as an argument. This is needed in order for SNI to work properly and in extension
to be able to connect to the service. The command will put a some files into
the ssl folder. We then use the `kubectl` command to create a Secret with those files.
> kubectl create secret generic dockerwatch --from-file=ssl/
secret/dockerwatch created
We can then see that the secret has been created using:
> kubectl get secrets
NAME TYPE DATA AGE
dockerwatch Opaque 6 16s
## Deploy dockerwatch
Now that we have our Secret and Service configured we are ready to create the
dockerwatch Deployment. First we need to build the docker image just as in the
docker example. However to make things simple we will build the docker image
inside the kubernetes cluster. This is not really recommended, but it
simplifies things for these examples. In a realworld scenario you probably want
to setup your own docker registry.
So, to start with we need to setup our shell to contect the minikube docker
server instead of our local one. This is done through the `minikube` command:
> eval $(minikube docker-env)
Then we can use `docker` as normal to build the image:
> docker build -t dockerwatch .
Then the only thing that remains is to create the deployment.
```
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
## Name and labels of the Deployment
labels:
app: dockerwatch
name: dockerwatch
spec:
replicas: 1
selector:
matchLabels:
app: dockerwatch
template:
metadata:
labels:
app: dockerwatch
spec:
containers:
## The container to launch
- image: dockerwatch
name: dockerwatch
imagePullPolicy: Never ## Set to Never as we built the image in the cluster
ports:
- containerPort: 8080
protocol: TCP
- containerPort: 8443
protocol: TCP
volumeMounts:
- name: kube-keypair
readOnly: true
mountPath: /etc/ssl/certs
volumes:
- name: kube-keypair
secret:
secretName: dockerwatch
EOF
```
This will create a Deployment called `dockerwatch` that run 1 replica
of the specified dockerwatch container. The container exposes ports 8080
and 8443 to the cluster and has the Secreat we created above mounted at
`/etc/ssl/certs`. Most of the configuration above should be fairly self
evident, refer to the kubernetes documentation for more details.
The Service will know that this is the Deployment to connect to by looking
at the metadata labels on the pods for a match to `app: dockerwatch`.
## Test the API
So now we have a kubernetes cluster running with an Erlang node in it, how do we
test that it works? `minikube` provides the answer through it's service API:
> minikube service dockerwatch --url
http://192.168.99.101:31716
http://192.168.99.101:30383
The IP:Port pairs above are the external ports exposed to access our dockerwatch
service. So to work with the content we use the same REST API as in dockerwatch:
> curl -H "Content-Type: application/json" -X POST -d "" http://192.168.99.101:31716/cnt
> curl --cacert ssl/dockerwatch-ca.pem -H "Accept: application/json" https://192.168.99.101:30383
["cnt"]
## Further exploration
Minikube comes with the kubernetes dashboard, so we can easily look through a
web interface to look and change whatever we want. You access it by running:
> minikube dashboard
This should open up a tab in your default browser where you can poke about. For
instance one thing to try is to change the number of replicas of the dockerwatch
ReplicaSet to and see what happens.

View File

@ -0,0 +1,22 @@
#!/bin/sh
set -e
if [ ! -d ssl ]; then
mkdir ssl
fi
# Create the root CA (Certificate Authority)
openssl genrsa -out ssl/dockerwatch-ca.key 4096
## Certificate signing request for root CA
openssl req -x509 -new -nodes -key ssl/dockerwatch-ca.key -sha256 -days 1024 -subj "/C=SE/" -out ssl/dockerwatch-ca.pem
# Create the server certificate
openssl genrsa -out ssl/dockerwatch-server.key 4096
## Certificate signing request for server certificate
openssl req -new -key ssl/dockerwatch-server.key -subj "/C=SE/CN=$1/" -out ssl/dockerwatch-server.csr
## Sign the server certificate using the root CA
openssl x509 -req -in ssl/dockerwatch-server.csr -CA ssl/dockerwatch-ca.pem -CAkey ssl/dockerwatch-ca.key -CAcreateserial -out ssl/dockerwatch-server.pem -days 500 -sha256

View File

@ -0,0 +1,35 @@
apiVersion: apps/v1
kind: Deployment
metadata:
## Name and labels of the Deployment
labels:
app: dockerwatch
name: dockerwatch
spec:
replicas: 1
selector:
matchLabels:
app: dockerwatch
template:
metadata:
labels:
app: dockerwatch
spec:
containers:
## The container to launch
- image: dockerwatch
name: dockerwatch
imagePullPolicy: Never ## Set to Never as we built the image in the cluster
ports:
- containerPort: 8080
protocol: TCP
- containerPort: 8443
protocol: TCP
volumeMounts:
- name: kube-keypair
readOnly: true
mountPath: /etc/ssl/certs
volumes:
- name: kube-keypair
secret:
secretName: dockerwatch

View File

@ -0,0 +1,5 @@
[%% Kernel/logger
{kernel, [{logger,[{handler,default,logger_std_h,#{}}]}
%%,{logger_level,info}
]}
].

View File

@ -0,0 +1,2 @@
-sname dockerwatch

View File

@ -0,0 +1,17 @@
{deps, [{jsone, "1.4.7"}, %% JSON Encode/Decode
{cowboy, "2.5.0"}]}. %% HTTP Server
{relx, [{release, {"dockerwatch", "1.0.0"}, [dockerwatch]},
{vm_args, "config/vm.args"},
{sys_config, "config/sys.config"},
{dev_mode, true},
{include_erts, false},
{extended_start_script, true}
]}.
{profiles, [{prod, [{relx, [{dev_mode, false},
{include_erts, true},
{include_src, false}]}]}
]}.
%% vim: ft=erlang

View File

@ -0,0 +1,16 @@
%% Feel free to use, reuse and abuse the code in this file.
{application, dockerwatch, [
{description, "Cowboy REST Hello World example."},
{vsn, "1.0.0"},
{modules, []},
{registered, [dockerwatch_sup]},
{applications, [
kernel,
stdlib,
jsone,
cowboy
]},
{mod, {dockerwatch_app, []}},
{env, []}
]}.

View File

@ -0,0 +1,45 @@
%%
%% Copyright (C) 2014 Björn-Egil Dahlberg
%%
%% File: dockerwatch.erl
%% Author: Björn-Egil Dahlberg
%% Created: 2014-09-10
%%
-module(dockerwatch).
-export([start_link/0, all/0, create/1, get/1, increment/2, decrement/2]).
-type counter() :: binary().
-spec start_link() -> {ok, pid()}.
start_link() ->
{ok, spawn_link(fun() -> ets:new(?MODULE, [named_table, public]),
receive after infinity -> ok end end)}.
-spec all() -> [counter()].
all() ->
ets:select(?MODULE, [{{'$1','_'},[],['$1']}]).
-spec create(counter()) -> ok | already_exists.
create(CounterName) ->
case ets:insert_new(?MODULE, {CounterName, 0}) of
true ->
ok;
false ->
already_exists
end.
-spec get(counter()) -> integer().
get(CounterName) ->
ets:lookup_element(?MODULE, CounterName, 2).
-spec increment(counter(), integer()) -> ok.
increment(CounterName, Howmuch) ->
_ = ets:update_counter(?MODULE, CounterName, [{2, Howmuch}]),
ok.
-spec decrement(counter(), integer()) -> ok.
decrement(CounterName, Howmuch) ->
_ = ets:update_counter(?MODULE, CounterName, [{2, -1 * Howmuch}]),
ok.

View File

@ -0,0 +1,19 @@
%%
%% Copyright (C) 2014 Björn-Egil Dahlberg
%%
%% File: dockerwatch_app.erl
%% Author: Björn-Egil Dahlberg
%% Created: 2014-09-10
%%
-module(dockerwatch_app).
-behaviour(application).
-export([start/2,stop/1]).
%% API.
start(_Type, _Args) ->
dockerwatch_sup:start_link().
stop(_State) ->
ok.

View File

@ -0,0 +1,103 @@
%%
%% Copyright (C) 2014 Björn-Egil Dahlberg
%%
%% File: dockerwatch_handler.erl
%% Author: Björn-Egil Dahlberg
%% Created: 2014-09-10
%%
-module(dockerwatch_handler).
-export([init/2]).
-export([allowed_methods/2]).
-export([content_types_accepted/2]).
-export([content_types_provided/2]).
-export([handle_post/2]).
-export([to_html/2]).
-export([to_json/2]).
-export([to_text/2]).
init(Req, []) ->
{cowboy_rest, Req, []}.
%% Which HTTP methods are allowed
allowed_methods(Req, State) ->
{[<<"GET">>, <<"POST">>], Req, State}.
%% Which content types are accepted by POST/PUT requests
content_types_accepted(Req, State) ->
{[{{<<"application">>, <<"json">>, []}, handle_post}],
Req, State}.
%% Handle the POST/PUT request
handle_post(Req, State) ->
case cowboy_req:binding(counter_name, Req) of
undefined ->
{false, Req, State};
Name ->
case cowboy_req:has_body(Req) of
true ->
{ok, Body, Req3} = cowboy_req:read_body(Req),
Json = jsone:decode(Body),
ActionBin = maps:get(<<"action">>, Json, <<"increment">>),
Value = maps:get(<<"value">>, Json, 1),
Action = list_to_atom(binary_to_list(ActionBin)),
ok = dockerwatch:Action(Name, Value),
{true, Req3, State};
false ->
ok = dockerwatch:create(Name),
{true, Req, State}
end
end.
%% Which content types we handle for GET/HEAD requests
content_types_provided(Req, State) ->
{[{<<"text/html">>, to_html},
{<<"application/json">>, to_json},
{<<"text/plain">>, to_text}
], Req, State}.
%% Return counters/counter as json
to_json(Req, State) ->
Resp = case cowboy_req:binding(counter_name, Req) of
undefined ->
dockerwatch:all();
Counter ->
#{ Counter => dockerwatch:get(Counter) }
end,
{jsone:encode(Resp), Req, State}.
%% Return counters/counter as plain text
to_text(Req, State) ->
Resp = case cowboy_req:binding(counter_name, Req) of
undefined ->
[io_lib:format("~s~n",[Counter]) || Counter <- dockerwatch:all()];
Counter ->
io_lib:format("~p",[dockerwatch:get(Counter)])
end,
{Resp, Req, State}.
%% Return counters/counter as html
to_html(Req, State) ->
Body = case cowboy_req:binding(counter_name, Req) of
undefined ->
Counters = dockerwatch:all(),
["<ul>\n",
[io_lib:format("<li>~s</li>\n", [Counter]) || Counter <- Counters],
"</ul>\n"];
Counter ->
Value = dockerwatch:get(Counter),
io_lib:format("~s = ~p",[Counter, Value])
end,
{[html_head(),Body,html_tail()], Req, State}.
html_head() ->
<<"<html>
<head>
<meta charset=\"utf-8\">
<title>dockerwatch</title>
</head>">>.
html_tail() ->
<<"</body>
</html>">>.

View File

@ -0,0 +1,49 @@
%%
%% Copyright (C) 2014 Björn-Egil Dahlberg
%%
%% File: dockerwatch_sup.erl
%% Author: Björn-Egil Dahlberg
%% Created: 2014-09-10
%%
-module(dockerwatch_sup).
-behaviour(supervisor).
-export([start_link/0,init/1]).
%% API.
-spec start_link() -> {ok, pid()}.
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
%% supervisor.
init([]) ->
CertsDir = "/etc/ssl/certs/",
Dispatch = cowboy_router:compile([
{'_', [{"/[:counter_name]", dockerwatch_handler, []}]}
]),
HTTPS = ranch:child_spec(
cowboy_https, 100, ranch_ssl,
[{port, 8443},
{cacertfile, filename:join(CertsDir, "dockerwatch-ca.pem")},
{certfile, filename:join(CertsDir, "dockerwatch-server.pem")},
{keyfile, filename:join(CertsDir, "dockerwatch-server.key")}],
cowboy_tls,
#{env=>#{dispatch=>Dispatch}}),
HTTP = ranch:child_spec(
cowboy_http, 100, ranch_tcp,
[{port, 8080}],
cowboy_clear,
#{env=>#{dispatch=>Dispatch}}),
Counter = {dockerwatch, {dockerwatch, start_link, []},
permanent, 5000, worker, [dockerwatch]},
Procs = [Counter, HTTP, HTTPS],
{ok, {{one_for_one, 10, 10}, Procs}}.