Automation Containers

23-apr-2020

Ansible en OpenShift: Hoe rol je automatisch applicaties uit?

ansible openshift automatisch applicaties uitrollen

In het eerste deel in deze serie hebben we gekeken hoe we met de Ansible k8s module kunnen werken op basis van een OpenShift cluster vanuit Ansible. In dit artikel zullen we een (basis) Python applicatie op basis van Flask en Gunicorn uitrollen, inclusief de build. Op deze manier kun je met Ansible het uitrollen van een applicatie, inclusief het bouwproces, gemakkelijk automatiseren.

De volledige source voor de applicatie (en de bijbehorende Ansible bestanden) zijn hier te vinden. De applicatie zelf laat in een webpagina de Kubernetes spec zien van de pod waarin hij draait.

app
├── __init__.py
├── static
│   └── style.css
└── templates
    ├── base.html
    └── helloworld.html

2 directories, 4 files

Dit is een python module genaamd "app", met als extra een "static" en een "templates" directory. Deze twee directories zijn voor het Flask framework speciaal. Voor mensen die met Jinja2 bekend zijn (in principe iedereen die Ansible kent) zullen de bestanden in de "templates" directory bekend voorkomen.

De applicatie zelf heeft geen rechtstreeks executable, en zal als WSGI applicatie geladen moeten worden door een webserver, in ons geval Gunicorn.

De applicatie bouwen

Als je de applicatie met de hand wilt bouwen in OpenShift dan kan dat met de volgende commando's:

oc new-app  https://gitlab.com/wanderb/example-app.git
oc policy add-role-to-user view -z default
oc expose svc/example-app

Het "oc new-app" commando maakt in OpenShift een BuildConfig, ImageStream, DeploymentConfig en Service aan. Het weet welk Source-to-Image (S2I) basis image gebruikt moet worden door naar de inhoud van het Git repository te kijken. In dit geval staat daar een bestand "requirements.txt" en wordt het Python basis image gekozen. In dit bestand staan de python modules die moeten worden geïnstalleerd, in ons geval "Flask", "kubernetes" en "gunicorn".

Het S2I image weet hoe de applicatie gestart moet worden vanwege het bestand ".s2i/environment", waarin de environment variabele "APP_MODULE=app:create_app()" wordt gezet. Hierdoor wordt de betreffende functie ("create_app") binnen de gekozen module ("app") als entrypoint gezet voor Gunicorn.

Omdat deze applicatie zelf ook tegen Kubernetes aanpraat heeft het serviceaccount waarmee we onze pods draaien wat rechten nodig, het "oc policy" commando geeft deze rechten aan het "default" serviceaccount.

Als laatste zorgt het "oc expose" ervoor dat er een Ingress route wordt aangemaakt, zodat onze applicatie bezocht kan worden.

Deployment met Ansible

Om de applicatie makkelijker uit te rollen kunnen we een Ansible rol schrijven voor de uitrol. Hiervoor hebben we van de eindgebruiker wat informatie nodig:

  • Hoe moet de applicatie gaan heten?
  • In welke namespace moet de applicatie uitgerold worden?
  • Waar is de sourcecode te vinden?
  • Onder welk serviceaccount moet de applicatie draaien?

Omdat we willen dat de rol die we gaan schrijven ook gebruikt kan worden door andere mensen, en voor andere applicaties, gaan we de antwoorden op deze vragen in variabelen zetten in onze Ansible rol. Om te zorgen dat eventuele gebruikers geen conflicten krijgen met variabele namen zoals "namespace" of "serviceaccount" zorgen we ervoor dat we daar unieke namen voor kiezen.

Omdat onze rol "deploy_flask_app" heet hebben we de volgende inhoud gezet in "ansible/roles/deploy_flask_app/defaults/main.yml":

deploy_flask_app:
 name: example-app
 namespace: example-app-ns
 serviceaccount: flask
 gituri: https://gitlab.com/wanderb/example-app.git

We hebben gekozen voor "defaults" zodat het voor gebruikers van de rol makkelijk is om andere waarden mee te geven in hun playbook of inventory, defaults hebben één van de laagste prioriteiten die een variabele kan hebben.

Templates maken

Nu we wat variabelen hebben kunnen we Kubernetes objecten gaan maken voor de volgende zaken:

  • Namespace
  • ServiceAccount
  • RoleBinding
  • Imagestream
  • BuildConfig
  • DeploymentConfig
  • Service
  • Rout

In onze rol ("deploy_flask_app") doen we dat met templates, bijvoorbeeld "ansible/roles/deploy_flask_app/templates/rolebinding.yml.j2"

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: flask-view
  namespace: 
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: view
subjects:
- kind: ServiceAccount
  name: 
  namespace: {{ deploy_flask_app.namespace }


In dit template zien we een aantal van onze variabelen voorbij komen, zoals de naam van het serviceaccount en de namespace. Niet elk template hoeft uitgebreid en complex te zijn, vaak is een klein, eenvoudig template genoeg.

Templates uitrollen

Het is vaak belangrijk om Kubernetes objecten in de goede volgorde aan te maken. Anders loop je tijens het proces tegen zaken aan. Zo is het bijvoorbeeld onmogelijk om een serviceaccount in een namespace aan te maken, als de namespace zelf nog niet bestaat. Ook moeten alle inputs en outputs van een BuildConfig bestaan voordat hij wordt aangemaakt, voor het geval dat de BuildConfig getriggered wordt voordat de inputs en outputs bestaan.

In een Ansible rol/taak is dit redelijk eenvoudig af te vangen door de objecten in de goede volgorde aan te maken. In onze rol gebruiken we hiervoor precies één taak, die in een lus over de betreffende templates heen loopt in "ansible/roles/deploy_flask_app/tasks/main.yml"

- name: Apply templates
  k8s:
    definition: "{{ lookup('template', item) | from_yaml }}"
  loop:
  - namespace.yml.j2
  - serviceaccount.yml.j2
  - rolebinding.yml.j2
  - imagestream.yml.j2
  - buildconfig.yml.j2
  - deploymentconfig.yml.j2
  - service.yml.j2
  - route.yml.j2


Rol aanroepen

Met alle stukken voor de applicatie in positie hoeft alleen nog maar de rol te worden aangeroepen vanuit een playbook. In het Git repository staat een playbook dat alle defaults gebruikt in "ansible/deploy_flask_app.yml":

- name: Deploy flask app
  hosts:
  - localhost
  become: false
  gather_facts: false
  roles:
  - deploy_flask_app


Als we andere variabelen willen gebruiken kan dat met de volgende syntax, zoals in "ansible/deploy_flask_app_alt.yml":


- name: Deploy flask app
  hosts:
  - localhost
  become: false
  gather_facts: false
  roles:
  - role: deploy_flask_app
    deploy_flask_app:
      namespace: flopsels
      name: foobar
      serviceaccount: viewer
      gituri: https://gitlab.com/wanderb/example-app.git


In het tweede voorbeeld worden de benodigde variabelen meegegeven bij het oproepen van de rol. Ze hadden ook in de inventory gezet kunnen worden voor nog meer flexibiliteit. Denk bijvoorbeeld aan het hebben van meerdere applicaties met dit in "ansible/group_vars/all/main.yml"

app_one: 
name: example-app-one namespace: example-app-one-ns serviceaccount: viewer gituri: https://gitlab.com/wanderb/example-app.git app_two: name: example-app-two namespace: example-app-two-ns serviceaccount: flask gituri: https://gitlab.com/wanderb/example-app.git

Vanuit hetzelfde playbook kunnen beide apps dan uitgerold worden, dit voorbeeld staat in "ansible/deploy_flask_app_alt2.yml":

roles 
- role: deploy_flask_app deploy_flask_app: "{{ app_one }}" - role: deploy_flask_app deploy_flask_app: "{{ app_two }}"


Nog te doen en te verbeteren

Er zijn een aantal dingen die deze methode nog niet oplost:

  • Builds worden alleen gestart bij het aanmaken van de BuildConfig, daarna moet dat handmatig met "oc start-build".
  • Als er acties nodig zijn voor updates moeten die handmatig uitgevoerd worden.

Ook is het niet zo mooi om voor elke applicatie een playbook te moeten schrijven, terwijl er alleen maar andere variabelen worden meegegeven. Deze punten zullen we oppakken in het laatste deel van deze serie, wanneer we onze Ansible rol gaan veranderen in een Operator.

Conclusie

Het uitrollen van een applicatie, inclusief het bouwproces, kan makkelijk worden geautomatiseerd met Ansible. Door het gebruik van een rol kan dezelfde code meerdere malen worden hergebruikt voor het uitrollen van verschillende applicaties, of dezelfde applicatie op meerdere plekken. In de volgende, en laatste deel van deze serie, gaan we het werk uit de vorige delen uitbreiden en transformeren naar een operator.


Lees ook:
Deel 1: Ansible en OpenShift - een krachtige combinatie voor deployment


 
Laat een reactie achter