Play-with-Docker: Container-Workshops auf AWS

Keine Kommentare

Kubernetes- und Docker-Workshops sind sehr schwer vorzubereiten, Play-with-Docker und Play-with-Kubernetes können dabei aber eine große Hilfe sein. Die Dokumentation dazu ist leider nicht sehr umfangreich, wie man es schnell und einfach installieren kann erfahrt ihr hier.

Cloud statt localhost

Ende 2018 habe ich mit einem Kollegen meinen ersten Workshop als Consultant gehalten. Das Thema war “Eine Einführung in Docker und Container”. Die Vorbereitung lief gut, wir hatten viel Material. Leider bedachten wir zu spät, die Systemanforderungen an den Kunden mitzuteilen. Somit hatte er zwar eine erstellte Ubuntu VM, allerdings nur auf einem USB-Stick. Während der Stick nun umher ging und repliziert wurde, fiel einigen Teilnehmern auf, dass sie gar keine Virtualisierungssoftware auf dem Windows-System haben. Sicherheitseinstellungen des Betriebs verhinderten zudem bei einigen die Installation der Software. Das ganze erinnerte etwas an LAN-Partys in den 90er Jahren. Damals verbrachte man die ersten Stunden auch immer damit, die Rechner zu konfigurieren. Meinem Kollegen fiel dann ein, dass die Teilnehmer mit Virtualisierungsproblemen mal versuchen sollten das Ganze auf Play-with-Docker zu probieren. Abgesehen davon, dass einige Teilnehmer nicht sofort einen Slot bekamen, ging es von da an problemlos weiter.

Nach dieser Erfahrung haben wir uns viele Gedanken gemacht, wie man den Prozess in Zukunft verbessern kann. Wir dachten z.B. darüber nach, eigene Hardware samt WIFI Hotspot mitzubringen, aber im Grunde waren die meisten Ideen zu aufwendig. Die beste Lösung war meines Erachtens eine eigene Play-with-Docker Installation in der Cloud. Da Play-with-Docker Open Source ist, musste nur noch eine geeignete Installationsroutine her.

Ansible als Automatisierungs-Werkzeug

Ansible ist ein Configuration Management Tool das sich besonders in DevOps-Projekten eignet. Da (fast) alles in einfachen YAML auszudrücken ist, braucht es kaum Programmierkenntnisse. Dennoch ist es sehr umfangreich und bietet für vieles eigene Plug-ins.
Wir benutzen es für dieses Projekt da wir im Gegensatz zu Chef und Puppet keinen Client auf dem Zielhost benötigen und die Konfiguration sehr übersichtlich ist. Obendrein gibt es bereits fertige Plug-ins für AWS, die das Leben vereinfachen.

Vorbereitung der Umgebung

Zum Ausführen benötigen wir wie bereits erwähnt Ansible (aktuell in Version 2.8.5) und boto3 (aktuell in Version 1.9.244). Beides ist am schnellsten mit python-pip zu installieren. In den meisten Package Managern ist es sowohl für Python2 als auch Python3 vertreten. Alternativ kann es aber auch manuell installiert werden.

curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python get-pip.py

Um das Ansible Playbook auszuführen, muss als Erstes das Git Repository ausgecheckt werden.

git clone https://github.com/skornehl/ansible-play-with-docker-aws.git

Als nächstes müssen die erforderlichen Abhängigkeiten installiert werden.

pip install -r requirements.txt

Im Anschluss wird der AWS Account vorbereitet. Der Einfachheit halber können wir einen neuen User erstellen und ihm die Rechte “AmazonEC2FullAccess” und “AmazonRoute53FullAccess” geben. Für mehr Sicherheit kann das aber noch weiter eingeschränkt werden. Credentials herunterladen und entweder als Umgebungsvariablen setzen

export AWS_ACCESS_KEY_ID=AKAA11111111AAAAAA
export AWS_SECRET_ACCESS_KEY=AAAAAAAAAAA1111111111111111AAAAAAA

oder als an die Credentials-Datei im Home einfügen ~/.aws/credentials

[play-with-docker]
aws_access_key_id = AKAA11111111AAAAAA
aws_secret_access_key =AAAAAAAAAAA1111111111111111AAAAAAA

Zur späteren Ausführen wird noch eine Konfigurierte DNS im Route53 benötigt. Hierzu kann man sich z.B. direkt bei Amazon oder bei freien Providern wie freenom.com eine Domain registrieren und diese als Hosted Zone im Route53 hinterlegen.

Konfiguration des Playbooks

Es gibt nicht viele Konfigurationsparameter, ihr findet alle unter group_vars/all.

  • public_domain: Die Route53 Hosted Zone (z.B. codecentric-workshop.de)
  • dns_prefix: DIe Subdomain
  • ec2_tag_Name: Name Tags vom EC2 um die Maschinen zu gruppieren. Wichtig für den Cleanup task
  • aws_vpc: Die VPC ID aus AWS
  • aws_profile: Falls es in der ~/.aws/credentials Datei mehrer Profile gibt kann hier das richtige angegeben werden
  • aws_region: In welcher Region soll Play-with-Docker gestartet werden?
  • aws_sec_grp_src: CIDR für die Security Group. 0.0.0.0/0 für alle aus dem Internet oder eure office IP (1.2.3.4/32)
  • ec2_instance_count: Wieviele Instanzen benötigt ihr?
  • ec2_instance_type:  Welche Instanzgrösse brauchen wir? Zusammen mit den ec2_instance_count ist man so sehr flexibel
  • ansible_user: Mit welchen User soll Ansible sich verbinden
  • cleanup: Um ein Cleanup zu machen auf true setzen
  • kubernetes: Soll auch Play-with-Kubernetes ausführbar sein
  • default_image: dind für Docker, k8s für Kubernetes

Das Playbook

In diesem Kapitel werden nur teile der Playbooks besprochen, die auch im Detail nicht vollständig sind. Den vollständigen Code gibt es im Github Repository.

In der Cloud

Im ersten Ausführungsteil wird der AWS Account vorbereitet. Hierzu kann man unter roles/ec2/vars/main.yaml noch zwei Einstellungen vornehmen.

  • ec2_volume_size: Festplattengrösse
  • ec2_keypair: Der Name des SSH Keypair das angelegt wird. Bitte kein bestehendes eintragen, da dies im ersten Schritt gelöscht wird
- name: Remove old EC2 key
  ec2_key:
    name: "{{ ec2_keypair }}"
    state: absent

- name: Create a new EC2 key
  ec2_key:
    name: "{{ ec2_keypair }}"
    force: yes
  register: ec2_key_result

- name: Save private key
  copy: 
    content: "{{ ec2_key_result.key.private_key }}" 
    dest: "./{{ ec2_keypair }}.pem" 
    mode: 0600
  when: ec2_key_result.changed

Der SSH-Schlüssel wird nach dem Erzeugen in der Cloud heruntergeladen um ihn für die Ansible-Ausführung nutzen zu können. Der Schlüssel besitzt aber keinen eigentlichen Wert und wird daher bei jeder Ausführung neu generiert. Im nächsten Schritt wird die Security Group erstellt und die Instanz hochgefahren.

- name: Add PWD security group
  ec2_group:
    name: "{{ ec2_tag_Name }}"
    vpc_id: "{{ aws_vpc }}"
    purge_rules: yes
    rules:
      - proto: tcp
        ports:
        - 80
        - 22
        cidr_ip: "{{ aws_sec_grp_src }}"
        rule_desc: allow all on port 80
      - proto: all
        group_name: "{{ ec2_tag_Name }}"
  register: var_awc_sec_grp

- name: Launch the new EC2 Instance
  ec2:
    group_id: "{{ var_awc_sec_grp.group_id }}"
    vpc_subnet_id: "{{ subnet_facts.subnets|map(attribute='id')|list|random }}"
    instance_type: "{{ ec2_instance_type }}"
    image: "ubuntu"
    wait: yes 
    key_name: "{{ ec2_keypair }}"
    assign_public_ip: yes
    count: "{{ ec2_instance_count }}"
  register: ec2

In der Security Group werden sowohl Port 22 für die Provisionierung mit Ansible als auch Port 80 für die spätere Nutzung freigegeben. Die EC2 Instanz wird in einem zufälligen Subnetz in der VPC erstellt und hochgefahren. Anschliessend wird die Instanz in die Ansible Hosts eingetragen. Zuletzt wird gewartet bis sie per SSH erreichbar ist.

- name: Add to hosts
  add_host:
    name: "{{ item.public_ip }}"
    groups: play-with-docker
    url: "{{ dns_prefix }}{{ my_idx }}.{{ ec2_tag_Name }}.{{ public_domain }}"
    public_dns_name: "{{ item.public_dns_name }}"
    ansible_ssh_private_key_file: "./{{ ec2_keypair }}.pem" 
  loop: "{{ ec2.instances }}"
  loop_control:
    index_var: my_idx

- name: Wait for the instances to boot by checking the ssh port
  wait_for: 
    host: "{{ item.public_ip }}"
    port: 22 
    delay: 60 
    timeout: 320 
    state: started
  with_items: "{{ ec2.instances }}"

Zum Host wird die URL als temporäre Variable gespeichert damit diese später als DNS gesetzt werden kann.

Play-with-Docker

Für die Nutzung von Play-with-Docker müssen als erstes zwei DNS-Eintrage erstellt werden. Dieser Task wird an den lokalen Host delegiert. Die Variable public_dns_name wurde im Task Add to hosts gesetzt.

- name: Create R53 records
  route53:
    state: present
    zone: "{{ public_domain }}"
    record: "{{ item }}"
    type: CNAME
    value: "{{ public_dns_name  }}" 
    ttl: 60
    overwrite: yes
  loop:
    - "{{ url }}"
    - "*.{{ url }}"
  delegate_to: 127.0.0.1

Im Play install-packages werden alle notwendigen Pakete für Docker installiert und anschließend Play-with-Docker aus Github gecloned.

- name: Clone PWD
  git:
    force: yes
    repo: 'https://github.com/play-with-docker/play-with-docker.git'

Das Play pwd-config konfiguriert notwendige Details am Code. Als erstes wird der String localhost mit der URL des Hosts ersetzt.

- name: Replace localhost to actual DNS
  replace:
    path: "{{ PWD_HOME }}/config/config.go"
    regexp: 'localhost'
    replace: "{{ url }}"
    backup: no

Im Folgenden wird das Session Timeout von 4 auf 10 Stunden erhöht, sodass sich die Teilnehmer an einem Workshop-Tag nicht diverse Male neu einloggen müssen.

- name: Increase timeout to life for a workshop day
  replace:
    path: "{{ PWD_HOME }}/api.go"
    regexp: '4h'
    replace: '10h'
    backup: no

Darauf wird ein Swarm Cluster initialisiert, der für die Ausührung von Play-with-Docker zwingend benötigt wird.

- name: Init a new swarm with default parameters
  docker_swarm:
    state: present

Schließlich müssen noch die GO Abhängigkeiten installiert und das Docker Image heruntergeladen werden.

- name: Run dep ensure
  shell: dep ensure
  args:
    chdir: "{{ PWD_HOME }}"
  environment:
    GOPATH: "{{ GOPATH }}"

- name: Docker pull Images
  docker_image:
    name: "{{ item }}"
    source: pull
  become: yes
  loop:
    - "franela/dind"

Das verwendete Image ist ein Docker in Docker (dind) Container. Demnach wird also ein Docker Conatiner gestartet, der wiederum andere Container starten kann. Das ist das Kernprinzip von Play-with-Docker. Am Ende wird nun noch Docker Compose ausgeführt. Die Startzeit beträgt etwa 3 bis 5 Minuten, danach ist das System über den Browser erreichbar.

- name: Run docker compose
  shell: docker-compose up -d
  args:
    chdir: "{{ PWD_HOME }}"
  environment:
    GOPATH: "{{ GOPATH }}"

Play-with-Kubernetes

Der Unterschied zwischen Play-with-Docker und Play-with-Kubernetes ist nur das verwendete Docker Image. Statt franela/dind wird franela/k8s ausgeführt.

- name: Docker pull Images
  docker_image:
    name: "{{ item }}"
    source: pull
  loop:
    - "franela/dind"
    - "franela/k8s"

In dieser Konfiguration kann sowohl Play-with-Docker als auch Play-with-Kubernetes vom Nutzer gestartet werden. Mit der Option default_image kann nun noch das Standard Image beim Start eingestellt werden.

- name: Add K8S Image to PWD
  replace:
    path: "{{ PWD_HOME }}/api.go"
    regexp:  '{"franela/dind"}'
    replace: '{"franela/dind", "franela/k8s"}'

Um Play-with-Kubernetes als DefaultDinDInstanceImage zu starten muss zusaetzlich in der group_vars/all das entsprechende Property gesetzt werden

default_image:      "k8s"

Nach dem Workshop

Wie bereits bei der Konfiguration angedeutet, gibt es die Möglichkeit, ein automatisiertes Cleanup des AWS Account auszuführen. Dazu muss die Variable cleanup beim Aufruf auf true gesetzt werden.

ansible-playbook site.yaml -e cleanup=true

Im Anschluss werden alle EC2 Instanzen mit dem konfigurierten Play-with-Docker Tag terminiert.

- name: Terminate instances
  ec2_instance:
    state: absent
    wait: yes 
    filters:
      "tag:Name": "{{ ec2_tag_Name }}"

Nachdem alle Instanzen heruntergefahren wurden, können die Security Group und der SSH Schlüssel gelöscht werden.

- name: Remove Security group
  ec2_group:
    name: "{{ ec2_tag_Name }}"
    vpc_id: "{{ aws_vpc }}"
    state: absent

- name: Remove EC2 key
  ec2_key:
    name: "{{ ec2_tag_Name }}-{{ aws_region }}"
    state: absent

Am Ende werden nun noch die DNS-Einträge gelöscht und der AWS Account ist wieder sauber.

- name: Get all hosted zones
  route53_facts:
    profile: "{{ aws_profile }}"
    query: hosted_zone
  register: hosted_zone

- name: Get ZoneID of PWD Zone
  set_fact:
    zone_id: "{{ item['Id'] }}"
  loop: "{{ hosted_zone['HostedZones'] }}"
  when: item['Name'][0:-1] == public_domain

Dazu werden als erstes alle gehosteten Zonen aus Route53 aufgelistet um die ZoneID bei AWS herauszufinden. Im Anschluss werden die vorhandenen DNS-Einträge in einer Variablen gespeichert sowie eine Teil-URL zusammengestellt.

- name: List record sets in Zone
  route53_facts:
    profile: "{{ aws_profile }}"
    query: record_sets
    hosted_zone_id: "{{ zone_id }}"
  register: record_sets

- name: Build URL Part
  set_fact:
    url: "{{ ec2_tag_Name }}.{{ public_domain }}"

Am Ende werden dann alle DNS-Einträge entfernt, die den Teilstring der URL enthalten.

- name: Remove all record sets from PWD
  route53:
    state: absent
    zone: "{{ public_domain }}"
    record: "{{ item['Name']| replace('\\052', '*') }}"
    type:  "{{ item['Type'] }}"
    ttl:  "{{ item['TTL'] }}"
    value: 
      - "{{ item['ResourceRecords'][0]['Value'] }}"
  loop: "{{ record_sets['ResourceRecordSets'] }}"
  when: dns_prefix in item['Name'] and url in item['Name']

Fazit

Durch Play-with-Docker, bzw. Play-with-Kubernetes können viele technische Probleme in einem Workshop gelöst werden. Alle Teilnehmer können Aufgaben im Browser ohne große Einschränkungen bearbeiten. Mittels Ansible und AWS ist der Aufwand dazu sehr gering und die Kosten durchaus überschaubar. Für viele Workshops reichen durchaus kleine Instanzen. Schon eine m5a.large kann 2 – 3 Kursteilnehmer ohne Probleme verkraften.

Wenn ihr mehr über Docker, Ansible oder AWS lernen wollt schaut euch weiter im Blog um.

Sebastian Kornehl

Sebastian ist seit 2018 für codecentric am Standort Berlin tätig. Sein Schwerpunkt liegt im (Dev-)Ops Umfeld. Zu seinen Interessen gehören Cloud Native Technologien, Container Orchestrierung, und „infrastructure as code“.

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.