Orion – Default Values in Ansible Plays

I wanted modular code – edit in one place, use in many. I started trying out git submodules. They were cool, but static. Changing code bases would be a pain to keep track of and manipulate. But the separation of ansible role and builds. allows us to immediately reuse code.

I’m building a single instance that includes all four of the base parts of the Nebula CICD product. I keep sliding in the project name “Orion”, because it is Nebula-in-a-Box, the galaxy on a server, and I keep thinking of Men in Black and “The galaxy is on Orion’s belt…” I have four roles, three ansible roles covering Jenkins, Hashicorp consul-and-vault, and the install role for the nebula python code, and a repo of nebula python code itself, plus next week a fourth deployment piece, an ansible role for Judge, an automated deployment app written in ruby.

I needed an installer. Some way to create sane defaults, such as by default all roles and repos come from the same organization in which the build is triggered, and pull from HEAD. Plus an explicit mode in which every single detail can be specified, GitHub org, Github repo and Github commit id. And maybe a hybrid mode, where I could capture what’s set and revert to defaults on the fly.

I thought about eliminating the install_mode: default|hybrid|explicit key:value. But I wanted developers configuring their jobs to define what they did before going into the details of the config. I could just define what the results of the various settings would be, or the result of leaving them out – but I’m almost certain that by asking the devs to define explicitly their strategy, I’ll reduce the support effort needed by a measurable amount. They have to determine how they want their data represented and then proceed to define it.

I have the cicd/installer directory – one of the principles we designed this around was that all the changing code should live with the app – the Jenkinsfile is in cicd/pipeline/Jenkinsfile, ansible code blocks live in cicd/ansible/roles, cloud formation templates live in cicd/template, data lives in cicd/data, etc. So installer will have all of the pieces to bring in outside code as needed.

Here’s what ended up working.

The Jenkinsfile gets two new blocks. The first grabs the GitHub organization out of the URL for the repo checked out. The second calls and executes the installer.yml play. First it cd’s into the cicd/installer directory, in order to simplify retrieving a local file, cicd/installer/installer-config.yml, which carries the configuration values.

        stage('gitorg') {
            steps {
                script {
                    // org is?
                    def repositoryUrl = scm.userRemoteConfigs[0].url
                    def pieces = repositoryUrl.split('/')
                    def GIT_ORG="${pieces[3]}"
                    add_gitorg = sh (
                        script: """echo "gitorg: $GIT_ORG" >> cicd/installer/installer-config.yml""",
                        returnStdout: true
                    ).trim()

//                    // place in config file for use by play
//                    echo "" >> cicd/installer/installer-config.yml
//                    echo "gitorg: ${GIT_ORG}" >> cicd/installer/installer-config.yml
                }
            }
        }

        stage('installer') {
            // execute ansible installer.yml play
            steps {
                script {
                    install_out = sh (
                        script: "cd cicd/installer/; ansible-playbook installer.yml",
                        returnStdout: true
                    )
                    echo "Install Output"
                    echo "${install_out}"
                }
            }
        }

cicd/installer/installer.yml is the ansible play called from Jenkinsfile right after checkout to bring in the values defined in installer-config.yml, and then at the end bring in additional developer defined tasks.

Installer.yml has three code paths – “install_mode: default” requires installer-config.yml to have the guthub_repo defined for each ansible role to bring in, but nothing else. Defaults are all repos are pulled from the same organization, and all grab HEAD. “install_mode: explicit”. requires that GitHub_organization, GitHub_repo and GitHub_commit_id be defined for each role to explicitly pull exactly what is required. “install_mode: hybrid”. allows either, and brings us to ansible defaults.

One or many roles can be defined. No set number and no predetermination needed.

Here’s the installer.yml file.

---

- hosts: localhost
  become: yes
  become_user: root

  tasks:

  # --------------------------------------------------------------------------------- #
  # import vars from installer-xonfig.yml file
  # --------------------------------------------------------------------------------- #

  # import the installer-config.yml
  # this is in the same directory as installer.yml
  # Jenkinsfile adds "gitorg: "
  # into the file before caling ansible-playbook

  - name: installer | import yaml
    include_vars:
      file: "./installer-config.yml"
      # NOT assigning a name, these will be top level

  # --------------------------------------------------------------------------------- #
  # three paths - default, explicit and hybrid
  # --------------------------------------------------------------------------------- #

  # three paths
  # install_mode: default
  #   team gets team organization, HEAD
  #   gold master gets (ALWAYS) gold master and HEAD
  #   local individual fork gets local + HEAD
  # install_mode:  explicit
  #   all roles must be specified explicitly in installer-config.yml
  #   github_repo, github_organization and github_commit_id must all be defined
  # install_mode:  hybrid
  #   what is set is respected, what is NOT set fails back to defults,
  #   e.g., local build org and HEAD

  # --------------------------------------------------------------------------------- #
  # default path - install_mode: 'default' in installer-config.yml
  # --------------------------------------------------------------------------------- #

  # ansible detects what org we are running in, e.g., individual, team or gold master forks
  # Jenkinsfile adds "gitorg: "dmunsinger15" for github_organization
  # at the end of the installer-config.yml file
  # commit_id is HEAD always

  - name: installer (default) | bring in roles
    git:
      repo:       "git@{{ github_server }}:{{ gitorg }}/{{ item.github_repo }}.git"
      version:    "HEAD"
      dest:       "../ansible/roles/{{ item.github_repo }}"
      force:      yes
    with_items:   "{{ ansible_roles }}"
    when:         install_mode == "default"

  # --------------------------------------------------------------------------------- #
  # explicit path - install_mode: 'explicit' in installer-config.yml
  # --------------------------------------------------------------------------------- #

  # this requires installer-config.yml to specify
  # ansible_roles:
  #   - github_organization: "dmunsinger15"
  #     github_repo:         "ansible_role-jenkins-2lts-controller"
  #     github_commit_id:    "HEAD"
  #        OR
  #     github_commit_id:    "bfdc948730f8427280327df34f726c83e7f9c260"
  # this will bring in very explicitly what you want to build

  - name: installer (explicit) | bring in roles
    git:
      repo:       "git@{{ github_server }}:{{ item.github_organization }}/{{ item.github_repo }}.git"
      version:    "{{ item.github_commit_id }}"
      dest:       "../ansible/roles/{{ item.github_repo }}"
      force:      yes
    with_items:   "{{ ansible_roles }}"
    when:         install_mode == "explicit"

  # --------------------------------------------------------------------------------- #
  # hybrid path - install_mode: 'hybrid' in installer-config.yml
  # --------------------------------------------------------------------------------- #

  # for this one we check for each line, if present, use it, otherwise
  # fall back to defaults

  - name: installer (hybrid) | bring in roles
    git:
      repo:       "git@{{ github_server }}:{{ item.github_organization | default(gitorg) }}/{{ item.github_repo }}.git"
      version:    "{{ item.github_commit_id | default('HEAD') }}"
      dest:       "../ansible/roles/{{ item.github_repo }}"
      force:      yes
    with_items:   "{{ ansible_roles }}"
    when:         install_mode == 'hybrid'


  # --------------------------------------------------------------------------------- #
  # set up reference files for source_repo.txt and source_repo_hash.txt
  # --------------------------------------------------------------------------------- #

  # this is used at /var/www/ to allow nebula-utils-api code to be checked out
  # at predictable org and commit_id version
  # this may need to move into nebula-utils-api code itself...
  # as if it stays it would only be used for nebula-utils-api code...

#  - name: installer | place url in /var/www/source_repo.txt for lookup
#    template:
#      src: ../ansible/local/source_repo.txt.j2
#      dest: ../ansible/roles/ansible_role-nebula-utils-api/files/source_repo.txt
#      force: yes
#
#  - name: installer | place version specifier in /var/www/source_repo_hash.txt for lookup
#    template:
#      src: ../ansible/local/source_repo_hash.txt.j2
#      dest: ../ansible/roles/ansible_role-nebula-utils-api/files/source_repo_hash.txt
#      force: yes
#
#  # make these owned by ec2-user
#  - name: installer | workspace owned by ec2-user throughout
#    file:
#      path: /home/ec2-user
#      state: directory
#      recurse: yes
#      owner: ec2-user
#      group: ec2-user

  # if installer/added_tasks.yml exists, import tasks and execute
  # this should allow repo-specific install tasks to be added on beyond the standard installer.yml tasks
  - name: installer | look for added_tasks.yml file
    stat:
      path: "added_tasks.yml"
    register: add_tasks
  - name: installer | include_tasks from added_tasks.yml if it exists
    include_tasks: added_tasks.yml
    when: add_tasks.stat.exists

  # ansible_role-judge
  # implement this later...

and the installer-config.yml:

---

# ----------------------------------------------------------------------- #
# install_mode
# ----------------------------------------------------------------------- #

# install_mode: explicit
#   installer.yml play will look for explicit definitions for
#   each role to be installed
# install_mode: default
#   installer.yml will checkout role(s) at HEAD for the repos
#   from the same organization in which the build is originated
# install_mode: hybrid
#   installer.yml accepts whatever is defined
#   (github_repo must be defined, always)
#   where org or commit id are not explicitly defined they default back
#   to local org and HEAD

install_mode:                          'hybrid'

# ----------------------------------------------------------------------- #
# meta
# ----------------------------------------------------------------------- #

# github_server is where to retrieve code from
# architecture is standalone, standalone+slaves, distributed
#   (TBD)
# secrets will path retrieving keys from vault or ckms or other sources

github_server:                         'your GitHub server'
architecture:                          'standalone'
secrets:                               'vault'


# ----------------------------------------------------------------------- #
# ansible roles
# ----------------------------------------------------------------------- #

# git ls-remote   gives you the available hashes
# one or more roles can be defined here
# *** the github_repo MUST be defined ***
# github_organization

ansible_roles:
  - github_repo:         "ansible_role-jenkins-2lts-controller"
    github_organization: "dmunsinger"
  - github_repo:         "ansible_role-consul-and-vault"
    github_organization: "dmunsinger"
  - github_repo:         "ansible_role-nebula-utils-api"
    github_organization: "dmunsinger"
    github_commit_id:    "HEAD"
  - github_repo:         "nebula-utils-api"
    github_organization: "dmunsinger"
    github_commit_id:    "bfdc948730f8427280327df34f726c83e7f9c260"

What’s new to me is the jinja default syntax.

  - name: installer (hybrid) | bring in roles
    git:
      repo:       "git@{{ github_server }}:{{ item.github_organization | default(gitorg) }}/{{ item.github_repo }}.git"
      version:    "{{ item.github_commit_id | default('HEAD') }}"
      dest:       "../ansible/roles/{{ item.github_repo }}"
      force:      yes
    with_items:   "{{ ansible_roles }}"
    when:         install_mode == 'hybrid'

It allows values to be set or not at will. Very cool. And, because the installer.yml is static code, it lives in a submodules repo at the gold master level and is brought in as cicd/installer/orion-installer/installer.yml, symlinked to cicd/installer/installer.yml.

— doug