With automation in mind, when you want to execute docker commands remotely, you want to do it in a secure manner, as you don’t want to expose your Docker port to the whole world.
One way in doing that, is forwarding the remote docker socket via a local port over a SSH Tunnel. With this way, you can execute docker commands locally on your workstation, as if the swarm is running on your workstation/laptop/node/bastion host etc.
Without the tunnel, I have a swarm on my laptop with no running services:
12
$ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
As you can see, we have no services running, but the remote swarm has a couple, so after forwarding the connection, we should see our remote services.
Setting up the SSH Tunnel:
Here we will forward the remote docker socket: /var/run/docker.sock to a local port bound to localhost: localhost:2377:
Now the SSH Tunnel will be established, and you can detach your screen session, or open a new shell session. To detach your screen session: 'ctrl + a' then d
Verifying that the tunnel is established:
You can use netstat to verify that the port is listening:
Now we need to inform the docker client, to use the new port to talk to the docker daemon. We do that by setting the DOCKER_HOST environment variable to point to localhost:2377:
1
$ export DOCKER_HOST="localhost:2377"
This will remain for the lifetime of the shell session.
Testing it Out:
Now we can run our commands locally, and we should see the output of our remote swarm:
12345
$ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
xjta8e3ek2u2 apps_flask_reminders replicated 3/3 rbekker87/flask-reminders:debian
0l7ruktbqj99 apps_kibana replicated 1/1 kibana:latest
...
Terminating our SSH Tunnel:
To terminate our SSH Tunnel, reconnect to your shell session, and hit ctrl + c:
1234
$ screen -ls
There is a screen on:
50413.docker (Detached)$ screen -r 50413
Hit ctrl + c :
1
CKilled by signal 2.
And exit the screen session:
1
$ exit
With this way, you can do lots of automation with docker swarm, not limited to swarm, but one of them.
While I’m learning a lot about encryption at the moment, I wanted to test out encryption with the PyCrypto module in Python using the Advanced Encryption Standard (AES) Symmetric Block Cipher.
Installing PyCrypto:
1
$ pip install pycrypto --user
PyCrypto Example:
Our AES Key needs to be either 16, 24 or 32 bytes long and our Initialization Vector needs to be 16 Bytes long. That will be generated using the random and string modules.
Encrypting:
12345678910111213141516
>>>fromCrypto.CipherimportAES>>>importrandom,string,base64>>>key=''.join(random.choice(string.ascii_uppercase+string.ascii_lowercase+string.digits)forxinrange(32))>>>iv=''.join(random.choice(string.ascii_uppercase+string.ascii_lowercase+string.digits)forxinrange(16))>>>print(key,len(key))('BLhgpCL81fdLBk23HkZp8BgbT913cqt0',32)>>>print(iv,len(iv))('OWFJATh1Zowac2xr',16)>>>enc_s=AES.new(key,AES.MODE_CFB,iv)>>>cipher_text=enc_s.encrypt('this is a super important message')>>>encoded_cipher_text=base64.b64encode(cipher_text)>>>print(encoded_cipher_text)'AtBa6zVB0UQ3U/50ogOb6g09FlyPdpmJB7UzoCqxhsQ6'
Having a Elasticsearch cluster on your laptop with Docker for testing is great. And in this post I will show you how quick and easy it is, to have a 3 node elasticsearch cluster running on docker for testing.
Pre-Requisites
We need to set the vm.max_map_count kernel parameter:
1
$ sudo sysctl -w vm.max_map_count=262144
To set this permanently, add it to /etc/sysctl.conf and reload with sudo sysctl -p
Docker Compose:
The docker compose file that we will reference:
The data of our elasticsearch container volumes will reside under /var/lib/docker, if you want them to persist in another location, you can use the driver_opts setting for the local volume driver.
Deploy
Deploy your elasticsearch cluster with docker compose:
1
$ docker-compose up
This will run in the foreground, and you should see console output.
Testing Elasticsearch
Let’s run a couple of queries, first up, check the cluster health api:
$ curl http://127.0.0.1:9200/_cat/indices?v
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open test w4p2Q3fTR4uMSYBfpNVPqw 5210 3.3kb 1.1kb
green open .monitoring-es-6-2018.04.29 W69lql-rSbORVfHZrj4vug 11160138 4mb 2mb
Kibana
Kibana is also included in the stack and is accessible via http://localhost:5601/ and you it should look more or less like:
Elasticsearch Head UI
I always prefer working directly with the RESTFul API, but if you would like to use a UI to interact with Elasticsearch, you can access it via http://localhost:9100/ and should look like this:
Deleting the Cluster:
As its running in the foreground, you can just hit ctrl + c and as we persisted data in our compose, when you spin up the cluster again, the data will still be there.
This tutorial will guide you how to use the Bulk API with Elasticsearch, this is great for when having a dataset that contains a lot of documents, where you want to insert them into elasticsearch in bulk uploads.
The Dataset
We will be using a dataset from elastic that contains 1000 documents that holds account data.
$ curl http://127.0.0.1:9200/_cat/indices/bank_accounts?v
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open bank_accounts BK_OJYOFTD67tqsQBUWSuQ 5110000 950.3kb 475.1kb
As you might know when you do a POST request to the type, the _id field gets auto populated. Timo, one of my friends had the requirement to use the Bulk API to post auto generated Id’s and not the static id’s that is given in the example dataset.
$ curl http://localhost:9200/_cat/indices?v
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open .monitoring-es-6-2018.05.06 3OgdIbDWQWCR8WJlQTXr9Q 111147156 104mb 50mb
Looking at my indices to verify that the index exist:
1234
$ curl http://localhost:9200/_cat/indices?v
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open bank_accounts u37MQvzhSPe97BJzp1u49Q 5110000 296.4kb 690b
green open .monitoring-es-6-2018.05.06 3OgdIbDWQWCR8WJlQTXr9Q 111147506 103.9mb 49.9mb
Today I wanted to encrypt sensitive information to not expose passwords, hostnames etc. I wanted to have a way to encrypt my strings with a master password and stumbled upon Simple Crypt.
Two simple examples to encrypt and decrypt data with simple-crypt. We will use a password sekret and we will encrypt the string: this is a secure message:
1234567
>>>fromsimplecryptimportencrypt,decrypt>>>password='sekret'>>>message='this is a secret message'>>>ciphertext=encrypt(password,message)>>>>>>print(ciphertext)sc#$%^&*(..........
Now that we have our encrypted string, lets decrypt it. First we will use the wrong password, so that you will see how the expected output should look when using a different password, than was used when it was encrypted:
12345678
>>>print(decrypt('badpass',ciphertext))Traceback(mostrecentcalllast):File"<stdin>",line1,in<module>File"/usr/lib/python2.7/site-packages/simplecrypt/__init__.py",line72,indecrypt_assert_hmac(hmac_key,hmac,hmac2)File"/usr/lib/python2.7/site-packages/simplecrypt/__init__.py",line116,in_assert_hmacraiseDecryptionException('Bad password or corrupt / modified data.')simplecrypt.DecryptionException:Badpasswordorcorrupt/modifieddata.
I wanted to store the encrypted string in a database, but the ciphertext has a combination of random special characters, so I decided to encode the ciphertext with base64. And the password input will be used with the getpass module.
$ python encrypt.py "this is a secret message"Password:
c2MAAnyfWIfOBV43vxo3sVCEYMG4C6hx69hv2Ii1JKlVHJUgBAlADJPOsD5cJO6MMI9faTDm1As/VfesvBzIe5S16mNyg2q7xfnP5iJ0RlK92vMNRbKOvNibg3M=
Now that we have our encoded ciphertext, lets decrypt it with the password that we encrypted it with:
123
$ python decrypt.py 'c2MAAnyfWIfOBV43vxo3sVCEYMG4C6hx69hv2Ii1JKlVHJUgBAlADJPOsD5cJO6MMI9faTDm1As/VfesvBzIe5S16mNyg2q7xfnP5iJ0RlK92vMNRbKOvNibg3M='Password:
this is a secret message
This is one way of working with sensitive info that you would like to encrypt/decrypt.
Sometimes we need to restrict access to a port, where a port should listen on localhost, but you want to access that port from a remote source. One secure way of doing that, is to establish a SSH Tunnel to the remote side, and forward to port via the SSH Tunnel.
Today we will setup a Flask Web Service on our Remote Server (Side B) which will be listening on 127.0.0.1:5000 and setup the SSH Tunnel with the sshtunnel module in Python from our client side (Side A). Then we will make a GET request on our client side to the port that we are forwarding via the tunnel to our remote side.
Here’s the complete code, as you can see I have a couple of decorators for each url endpoint, and a id_generator function, that will generate id’s for each document. The id will be used for getting users information, updates and deletes:
Now, let’s update some details. Let’s say that Stefan relocated to New Zealand. We will need to provide his id and also the key/value that we want to update:
Thats it. I’ve stumbled upon Flask-Restful which I still want to check out, and as soon as I do, I will do a post on it, maybe baked with a NoSQL db or something like that.
I used to work a lot with sys.argv for using arguments in my applications, until I stumbled upon the argparse module! (Thanks Donovan!)
What I like about argparse, is that it builds up the help menu for you, and you also have a lot of options, as you can set the argument to be required, set the datatypes, addtional help context etc.
The Basic Demonstration:
Today we will just run through a very basic example on how to use argparse:
Return the generated help menu
Return the required value
Return the additional arguments
Compare arguments with a IF statement
The Python Argparse Tutorial Code:
123456789101112131415161718
importargparseparser=argparse.ArgumentParser(description='argparse demo')parser.add_argument('-w','--word',help='a word (required)',required=True)parser.add_argument('-s','--sentence',help='a sentence (not required)',required=False)parser.add_argument('-c','--comparison',help='a word to compare (not required)',required=False)args=parser.parse_args()print("Word: {}".format(args.word))ifargs.sentence:print("Sentence: :{}".format(args.sentence))ifargs.comparison:ifargs.comparison==args.word:print("Comparison: the provided word argument and provided comparison argument is the same")else:print("Comparison: the provided word argument and provided comparison argument is NOT the same")
Seeing it in action:
To return a usage/help info, run it with the -h or --help argument:
123456789101112
$ python foo.py -h
usage: foo.py [-h] -w WORD [-s SENTENCE][-c COMPARISON]argparse demo
optional arguments:
-h, --help show this help message and exit -w WORD, --word WORD a word (required) -s SENTENCE, --sentence SENTENCE
a sentence (not required) -c COMPARISON, --comparison COMPARISON
a word to compare (not required)
For this to work, the application is expecting the word argument to run, as we declared it as required=True:
12
$ python foo.py -w hello
Word: hello
Now to use the arguments that is not required, which makes it optional:
We can also implement some if statements into our application to compare if arguments are the same (as a basic example):
1234
$ python foo.py -w hello -s "hello, world" -c goodbye
Word: hello
Sentence: :hello, world
Comparison: the provided word argument and provided comparison argument is NOT the same
We can see that the word and comparison arguments are not the same. When they match up:
1234
$ python foo.py -w hello -s "hello, world" -c hello
Word: hello
Sentence: :hello, world
Comparison: the provided word argument and provided comparison argument is the same
This was a very basic demonstration on the argparse module.
When you make a configuration change on Amazon’s Elasticsearch, it does a blue/green deployment. So new nodes will be allocated to the cluster (which you will notice from CloudWatch when looking at the nodes metrics). Once these nodes are deployed, data gets copied accross to the new nodes, and traffic gets directed to the new nodes, and once its done, the old nodes gets terminated.
Note: While there will be more nodes in the cluster, you will not get billed for the extra nodes.
While this process is going, you can monitor your cluster to see the progress:
The Shards API:
Using the /_cat/shards API, you will find that the shards are in a RELOCATING state (keeping in mind, this is when the change is still busy)
As Amazon masks their node ip addresses, we will find that the ips are not available. To make it more human readable, we will only pass the columns that we are interested in and not to show the shards that has been set to done:
123456789101112
$ curl -s -XGET 'https://search-example-elasticsearch-cluster-6-abc123defghijkl5airxticzvjaqy.eu-west-1.es.amazonaws.com/_cat/recovery?v&h=i,s,t,ty,st,shost,thost,f,fp,b,bp'| grep -v 'done'i s t ty st shost thost f fp b bp
example-app1-2018.04.11 1 2m peer index x.x.x.x x.x.x.x 139 97.1% 3435483673 65.9%
web-syslog-2018.04 4 7.6m peer finalize x.x.x.x x.x.x.x 109 100.0% 2854310892 100.0%
example-app1-2018.04.16 3 2.9m peer translog x.x.x.x x.x.x.x 130 100.0% 446180036 100.0%
example-app1-2018.03.30 3 2.1m peer index x.x.x.x x.x.x.x 127 97.6% 3862498583 62.5%
example-app1-2018.04.01 0 4.4m peer index x.x.x.x x.x.x.x 140 99.3% 3410543270 87.9%
example-app1-2018.04.06 0 5.1m peer index x.x.x.x x.x.x.x 128 97.7% 4291421948 66.3%
example-app1-2018.04.07 0 52.2s peer index x.x.x.x x.x.x.x 149 91.9% 3969581277 27.4%
network-capture-2018.04.01 2 11.4s peer index x.x.x.x x.x.x.x 107 95.3% 359987163 55.0%
example-app1-2018.03.17 1 1.7m peer index x.x.x.x x.x.x.x 117 98.3% 2104196548 74.5%
example-app1-2018.02.25 3 58.4s peer index x.x.x.x x.x.x.x 102 98.0% 945437614 74.7%
We can also see the human readable output, which is displayed in json format, with much more detail:
Amazon restricts most of the /_cluster API actions, but we can however see the health endpoint, where we can see the number of nodes, active_shards, relocating_shards, number_of_pending_tasks etc: