Commit 66cd58f7 authored by Pierre Smeyers's avatar Pierre Smeyers
Browse files

Merge branch 'feat/support-parallel-matrix' into 'main'

Add parallel:matrix jobs support

See merge request to-be-continuous/ansible!87
parents c8cddaac 3aeaca69
Loading
Loading
Loading
Loading
+49 −0
Original line number Diff line number Diff line
@@ -105,6 +105,7 @@ that you might use in your hook scripts (as env. variables) and Ansible playbook

* `environment_type`: the current deployment environment type (`review`, `integration`, `staging` or `production`)
* `environment_name`: a generated application name to use for the current deployment environment (ex: `myapp-review-fix-bug-12` or `myapp-staging`) - _details below_
* `environment_namespace`: the sluggified namespace when using [multiple environments](#multiple-environments-support) (empty otherwise)
* `ssh_public_key_file`: the path of the private key file (if an appropriate `$ANSIBLE_xxx_PUBLIC_KEY` variable is set)
* `ssh_private_key_file`: the path of the public key file (if an appropriate `$ANSIBLE_xxx_PRIVATE_KEY` variable is set)

@@ -203,6 +204,43 @@ The **static way** can be implemented simply by setting the appropriate configur
To implement the **dynamic way**, your deployment script shall simply generate a `environment_url.txt` file in the working directory, containing only
the dynamically generated url. When detected by the template, it will use it as the newly deployed environment url.

### Multiple environments support

The Ansible template allows deploying multiple environments in parallel. Use cases of this include:

- monorepo, where a single Git repository might host several separate deployable components or apps,
- multi-instances deployment of the same application.

This feature can be enabled using the [parallel matrix jobs](https://docs.gitlab.com/ee/ci/yaml/#parallelmatrix)
pattern at the `.ansible-env-base` job level (this is the top parent job of all deployment jobs).
Environments namespacing is ensured by the `ANSIBLE_ENVIRONMENT_NAMESPACE` variable (must start with a slash `/`).

Here is the example of the `.gitlab-ci.yml` file for a project deploying both a frontend and a backend applications:

```yaml
.ansible-env-base:
  parallel:
    matrix:
      - ANSIBLE_ENVIRONMENT_NAMESPACE: "/front"
        # Ansible playbook and inventory for frontend
        ENV_PLAYBOOK_FILE: "front-playbook.yml"
      - ANSIBLE_ENVIRONMENT_NAMESPACE: "/back"
        # Ansible playbook and inventory for backend
        ENV_PLAYBOOK_FILE: "back-playbook.yml"

# ⚠ on_stop must be unset when defining parallel:matrix environments
# see: https://gitlab.com/gitlab-org/gitlab/-/issues/332247
ansible-review:
  environment:
    on_stop: null
```

The above configuration will deploy 2 environments on each pipeline:

- on feature branches: `review/front/$CI_COMMIT_REF_NAME` and `review/back/$CI_COMMIT_REF_NAME`
- on the integration branch: `integration/front` and `integration/back`
- on the production branch: `staging/front` and `staging/back` (and finally `production/front` and `production/back`)

### Deployment output variables

Each deployment job produces _output variables_ that are propagated to downstream jobs (using [dotenv artifacts](https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsdotenv)):
@@ -215,6 +253,16 @@ Those variables may be freely used in downstream jobs (for instance to run accep

You may also add and propagate your own custom variables, by pushing them to the `ansible.env` file in your [deployment script](#deployment-and-cleanup-scripts).

> [!important]
> If [multiple environments](#multiple-environments-support) are configured, the output variables are prefixed with a
> sluggified value of the `ANSIBLE_ENVIRONMENT_NAMESPACE` variable (stripped of punctuation characters and converted to lowercase):
> 
> * `<namespace_slug>_environment_type`: set to the type of environment (`review`, `integration`, `staging` or `production`),
> * `<namespace_slug>_environment_name`: the application name (see below),
> * `<namespace_slug>_environment_url`: set to the environment URL (whether determined statically or dynamically).
> 
> The output dotenv file will be `ansible.env.<namespace_slug>` instead, and the dynamic variable `${environment_namespace}` can be used in your hook scripts and Ansible playbooks to access the contextual value of `<namespace_slug>`.

### Ansible commands overrides

Instead of creating hook scripts, you can also override and/or decorate the Ansible commands 
@@ -268,6 +316,7 @@ This template can be configured with the following **environment variables**:
| `project-dir` / `ANSIBLE_PROJECT_DIR` | Ansible project root directory                                                       | `.`                                            |
| `base-app-name` / `ANSIBLE_BASE_APP_NAME` | Base application name                  | `$CI_PROJECT_NAME` ([see GitLab doc](https://docs.gitlab.com/ci/variables/predefined_variables/)) |
| `environment-url` / `ANSIBLE_ENVIRONMENT_URL`       | Default environments url _(only define for static environment URLs declaration)_<br/>_supports late variable expansion (ex: `https://%{environment_name}.acme.com`)_ | _none_ |
| `environment-namespace` / `ANSIBLE_ENVIRONMENT_NAMESPACE` | Extra [GitLab environments](https://docs.gitlab.com/ci/environments/) namespace _(only required when deploying [multiple environments](#multiple-environments-support))_<br/>:warning: must start with a slash `/` | _none_ |
| :lock: `ANSIBLE_VAULT_PASSWORD` | The Ansible vault password used to decrypt vars.                                     | **has to be defined in gitlab secret if used** |
| :lock: `ANSIBLE_PRIVATE_KEY`    | The Ansible SSH private key to use in all stages (can be overridden per env)         | **has to be defined in gitlab secret if used** |
| `public-key` / `ANSIBLE_PUBLIC_KEY` | The Ansible SSH public key associated to the private key to be use in all stages (can be overridden per env) | **has to be defined if used** |
+5 −0
Original line number Diff line number Diff line
@@ -28,6 +28,11 @@
      "type": "url",
      "description": "The default environments url _(only define for static environment URLs declaration)_\n\n_supports late variable expansion (ex: `https://%{environment_name}.acme.com`)_"
    },
    {
      "name": "ANSIBLE_ENVIRONMENT_NAMESPACE",
      "description": "Extra [GitLab environments](https://docs.gitlab.com/ci/environments/) namespace _(only required when deploying multiple environments)_\n\n:warning: must start with a slash `/`",
      "advanced": true
    },
    {
      "name": "ANSIBLE_VAULT_PASSWORD",
      "description": "The Ansible vault password used to decrypt vars",
+1 −16
Original line number Diff line number Diff line
@@ -54,7 +54,7 @@ variables:
      export AWS_ROLE_SESSION_NAME="GitLabRunner-${CI_PROJECT_ID}-${CI_PIPELINE_ID}"
    fi

.ansible-deploy:
.ansible-env-base:
  id_tokens:
    AWS_JWT:
      aud: "$AWS_OIDC_AUD"
@@ -63,18 +63,3 @@ variables:
    - !reference [.ansible-aws-sts]
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - cd $ANSIBLE_PROJECT_DIR
    - assert_defined "${ENV_INVENTORY:-${ANSIBLE_DEFAULT_INVENTORY}}" 'Missing required Ansible inventory'
    - assert_defined "${ENV_PLAYBOOK_FILE}" 'Missing required Ansible playbook'

.ansible-cleanup:
  id_tokens:
    AWS_JWT:
      aud: "$AWS_OIDC_AUD"
  before_script:
    - !reference [.ansible-scripts]
    - !reference [.ansible-aws-sts]
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - cd $ANSIBLE_PROJECT_DIR
    - assert_defined "${ENV_INVENTORY:-${ANSIBLE_DEFAULT_INVENTORY}}" 'Missing required Ansible inventory'
    - assert_defined "${ENV_CLEANUP_PLAYBOOK_FILE:-${ENV_PLAYBOOK_FILE}}" 'Missing required Ansible playbook'
    - assert_defined "$ENV_CLEANUP_TAGS" 'Missing required Ansible cleanup tags'
+1 −16
Original line number Diff line number Diff line
@@ -92,7 +92,7 @@ variables:
      echo '[WARN] $GCP_JWT is not set: cannot setup Application Default Credentials (ADC) authentication'
    fi

.ansible-deploy:
.ansible-env-base:
  id_tokens:
    GCP_JWT:
      aud: "$GCP_OIDC_AUD"
@@ -101,18 +101,3 @@ variables:
    - !reference [.ansible-gcp-adc]
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - cd $ANSIBLE_PROJECT_DIR
    - assert_defined "${ENV_INVENTORY:-${ANSIBLE_DEFAULT_INVENTORY}}" 'Missing required Ansible inventory'
    - assert_defined "${ENV_PLAYBOOK_FILE}" 'Missing required Ansible playbook'

.ansible-cleanup:
  id_tokens:
    GCP_JWT:
      aud: "$GCP_OIDC_AUD"
  before_script:
    - !reference [.ansible-scripts]
    - !reference [.ansible-gcp-adc]
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - cd $ANSIBLE_PROJECT_DIR
    - assert_defined "${ENV_INVENTORY:-${ANSIBLE_DEFAULT_INVENTORY}}" 'Missing required Ansible inventory'
    - assert_defined "${ENV_CLEANUP_PLAYBOOK_FILE:-${ENV_PLAYBOOK_FILE}}" 'Missing required Ansible playbook'
    - assert_defined "$ENV_CLEANUP_TAGS" 'Missing required Ansible cleanup tags'
+55 −49
Original line number Diff line number Diff line
@@ -31,6 +31,12 @@ spec:

        _supports late variable expansion (ex: `https://%{environment_name}.acme.com`)_
      default: ''
    environment-namespace:
      description: |-
        Extra [GitLab environments](https://docs.gitlab.com/ci/environments/) namespace _(only required when deploying multiple environments)_
        
        :warning: must start with a slash `/`
      default: ''
    public-key:
      description: The Ansible SSH public key associated to the private key to be use in all stages (can be overridden per env)
      default: ''
@@ -271,6 +277,7 @@ variables:
  INTEG_REF: /^develop$/

  ANSIBLE_ENVIRONMENT_URL: $[[ inputs.environment-url ]]
  ANSIBLE_ENVIRONMENT_NAMESPACE: $[[ inputs.environment-namespace ]]
  ANSIBLE_PUBLIC_KEY: $[[ inputs.public-key ]]
  ANSIBLE_DEFAULT_INVENTORY: $[[ inputs.default-inventory ]]
  ANSIBLE_DEFAULT_TAGS: $[[ inputs.default-tags ]]
@@ -649,6 +656,8 @@ stages:
    export environment_type=$ENV_TYPE
    export environment_name=${ENV_APP_NAME:-${ANSIBLE_BASE_APP_NAME}${ENV_APP_SUFFIX}}
    export environment_url=${ENV_URL:-${ANSIBLE_ENVIRONMENT_URL:-$CI_ENVIRONMENT_URL}}
    environment_namespace=$(echo "$ANSIBLE_ENVIRONMENT_NAMESPACE" | tr -d '[:punct:]' | tr '[:upper:]' '[:lower:]')
    export environment_namespace
    private_key=${ENV_PRIVATE_KEY:-$ANSIBLE_PRIVATE_KEY}
    public_key=${ENV_PUBLIC_KEY:-$ANSIBLE_PUBLIC_KEY}
    vault_password=${ENV_VAULT_PASSWORD:-$ANSIBLE_VAULT_PASSWORD}
@@ -667,7 +676,7 @@ stages:
    log_info "--- \$environment_name: \\e[33;1m${environment_name}\\e[0m"

    # unset any upstream deployment env & artifacts
    rm -f ansible.env
    rm -f ansible.env*
    rm -f environment_url.txt

    # maybe execute pre ansible-playbook script
@@ -682,7 +691,7 @@ stages:
    maybe_install_requirements

    # extra var environment_type & environment_name
    ansible_opts="-e environment_type=$environment_type -e environment_name=$environment_name"
    ansible_opts="-e environment_type=$environment_type -e environment_name=$environment_name -e environment_namespace=$environment_namespace"

    if [ -n "$vault_password" ]; then
      log_info "--- \\e[32mvault password\\e[0m found"
@@ -753,8 +762,15 @@ stages:
    else
      echo "$environment_url" > environment_url.txt
    fi
    echo -e "environment_type=$environment_type\\nenvironment_name=$environment_name\\nenvironment_url=$environment_url" >> ansible.env
    chmod 644 environment_url.txt ansible.env
    # var prefix ('_' if namespace)
    prefix="${environment_namespace:+${environment_namespace}_}"
    dotenvfile="ansible.env${environment_namespace:+.${environment_namespace}}"
    {
      echo "${prefix}environment_type=${environment_type}"
      echo "${prefix}environment_name=${environment_name}"
      echo "${prefix}environment_url=${environment_url}"
    } >> "$dotenvfile"
    chmod 644 environment_url.txt "$dotenvfile"
  }

  function cleanup_secrets() {
@@ -905,29 +921,36 @@ ansible-lint-production:
      when: never
    - !reference [.test-policy, rules]

# Deploy job prototype
# Env management job prototype
# Can be extended to define a concrete environment
#
# @arg ENV_TYPE              : environment type
# @arg ENV_APP_NAME          : env-specific application name
# @arg ENV_APP_SUFFIX        : env-specific application suffix
# @arg ENV_URL           : env-specific application url
# @arg ENV_INVENTORY         : env-specific Ansible inventory
# @arg ENV_PLAYBOOK_FILE : (mandatory) env-specific Ansible playbook
# @arg ENV_TAGS          : env-specific Ansible deploy tags
# @arg ENV_EXTRA_ARGS    : env-specific Ansible extra arguments
# @arg ENV_PRIVATE_KEY       : env-specific Ansible private key
# @arg ENV_PUBLIC_KEY        : env-specific Ansible public key
# @arg ENV_VAULT_PASSWORD    : env-specific Ansible vault password
.ansible-deploy:
.ansible-env-base:
  extends: .ansible-base
  stage: deploy
  variables:
    ENV_APP_SUFFIX: "-$CI_ENVIRONMENT_SLUG"
  environment:
    name: ${ENV_TYPE}${ANSIBLE_ENVIRONMENT_NAMESPACE}
  resource_group: ${ENV_TYPE}${ANSIBLE_ENVIRONMENT_NAMESPACE}

# Deploy job prototype
# Can be extended for each deployable environment
#
# @arg ENV_URL           : env-specific application url
# @arg ENV_PLAYBOOK_FILE : (mandatory) env-specific Ansible playbook
# @arg ENV_TAGS          : env-specific Ansible deploy tags
# @arg ENV_EXTRA_ARGS    : env-specific Ansible extra arguments
.ansible-deploy:
  extends: .ansible-env-base
  before_script:
    - !reference [.ansible-scripts]
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - cd $ANSIBLE_PROJECT_DIR
    - !reference [.ansible-base, before_script]
    - assert_defined "${ENV_INVENTORY:-${ANSIBLE_DEFAULT_INVENTORY}}" 'Missing required Ansible inventory'
    - assert_defined "${ENV_PLAYBOOK_FILE}" 'Missing required Ansible playbook'
  script:
@@ -940,33 +963,21 @@ ansible-lint-production:
    paths:
      - "$ANSIBLE_PROJECT_DIR/environment_url.txt"
    reports:
      dotenv: "$ANSIBLE_PROJECT_DIR/ansible.env"
      dotenv: "$ANSIBLE_PROJECT_DIR/ansible.env*"
  environment:
    url: "$environment_url" # can be either static or dynamic

# Cleanup job prototype
# Can be extended for each deletable environment
#
# @arg ENV_TYPE          : environment type
# @arg ENV_APP_NAME      : env-specific application name
# @arg ENV_APP_SUFFIX    : env-specific application suffix
# @arg ENV_INVENTORY     : env-specific Ansible inventory
# @arg ENV_CLEANUP_PLAYBOOK_FILE: env-specific Ansible playbook for cleanup (if different from deployment playbook)
# @arg ENV_PLAYBOOK_FILE : env-specific Ansible playbook
# @arg ENV_CLEANUP_TAGS  : (mandatory) env-specific Ansible cleanup tags
# @arg ENV_EXTRA_ARGS    : env-specific Ansible extra arguments
# @arg ENV_PRIVATE_KEY   : env-specific Ansible private key
# @arg ENV_PUBLIC_KEY    : env-specific Ansible public key
# @arg ENV_VAULT_PASSWORD: env-specific Ansible vault password
.ansible-cleanup:
  extends: .ansible-base
  stage: deploy
  variables:
    ENV_APP_SUFFIX: "-$CI_ENVIRONMENT_SLUG"
  extends: .ansible-env-base
  before_script:
    - !reference [.ansible-scripts]
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
    - cd $ANSIBLE_PROJECT_DIR
    - !reference [.ansible-base, before_script]
    - assert_defined "${ENV_INVENTORY:-${ANSIBLE_DEFAULT_INVENTORY}}" 'Missing required Ansible inventory'
    - assert_defined "${ENV_CLEANUP_PLAYBOOK_FILE:-${ENV_PLAYBOOK_FILE}}" 'Missing required Ansible playbook'
    - assert_defined "$ENV_CLEANUP_TAGS" 'Missing required Ansible cleanup tags'
@@ -995,10 +1006,12 @@ ansible-review:
    ENV_PUBLIC_KEY: "$ANSIBLE_REVIEW_PUBLIC_KEY"
    ENV_VAULT_PASSWORD: "$ANSIBLE_REVIEW_VAULT_PASSWORD"
  environment:
    name: review/$CI_COMMIT_REF_NAME
    name: ${ENV_TYPE}${ANSIBLE_ENVIRONMENT_NAMESPACE}/${CI_COMMIT_REF_NAME}
    # ⚠ on_stop must be unset when defining parallel:matrix environments
    # see: https://gitlab.com/gitlab-org/gitlab/-/issues/332247
    on_stop: ansible-cleanup-review
    auto_stop_in: "$ANSIBLE_REVIEW_AUTOSTOP_DURATION"
  resource_group: review/$CI_COMMIT_REF_NAME
  resource_group: ${ENV_TYPE}${ANSIBLE_ENVIRONMENT_NAMESPACE}/${CI_COMMIT_REF_NAME}
  rules:
    # exclude tags
    - if: $CI_COMMIT_TAG
@@ -1024,9 +1037,9 @@ ansible-cleanup-review:
    ENV_PUBLIC_KEY: "$ANSIBLE_REVIEW_PUBLIC_KEY"
    ENV_VAULT_PASSWORD: "$ANSIBLE_REVIEW_VAULT_PASSWORD"
  environment:
    name: review/$CI_COMMIT_REF_NAME
    name: ${ENV_TYPE}${ANSIBLE_ENVIRONMENT_NAMESPACE}/${CI_COMMIT_REF_NAME}
    action: stop
  resource_group: review/$CI_COMMIT_REF_NAME
  resource_group: ${ENV_TYPE}${ANSIBLE_ENVIRONMENT_NAMESPACE}/${CI_COMMIT_REF_NAME}
  rules:
    # exclude tags
    - if: $CI_COMMIT_TAG
@@ -1056,10 +1069,10 @@ ansible-integration:
    ENV_PUBLIC_KEY: "$ANSIBLE_INTEG_PUBLIC_KEY"
    ENV_VAULT_PASSWORD: "$ANSIBLE_INTEG_VAULT_PASSWORD"
  environment:
    name: integration
    # ⚠ on_stop must be unset when defining parallel:matrix environments
    # see: https://gitlab.com/gitlab-org/gitlab/-/issues/332247
    on_stop: ansible-cleanup-integration
    auto_stop_in: "$ANSIBLE_INTEG_AUTOSTOP_DURATION"
  resource_group: integration
  rules:
    # exclude if $ANSIBLE_INTEG_PLAYBOOK_FILE not set
    - if: '$ANSIBLE_INTEG_PLAYBOOK_FILE == null || $ANSIBLE_INTEG_PLAYBOOK_FILE == ""'
@@ -1083,9 +1096,7 @@ ansible-cleanup-integration:
    ENV_PUBLIC_KEY: "$ANSIBLE_INTEG_PUBLIC_KEY"
    ENV_VAULT_PASSWORD: "$ANSIBLE_INTEG_VAULT_PASSWORD"
  environment:
    name: integration
    action: stop
  resource_group: integration
  rules:
    # exclude if not integration branch
    - if: '$CI_COMMIT_REF_NAME !~ $INTEG_REF'
@@ -1112,10 +1123,10 @@ ansible-staging:
    ENV_PUBLIC_KEY: "$ANSIBLE_STAGING_PUBLIC_KEY"
    ENV_VAULT_PASSWORD: "$ANSIBLE_STAGING_VAULT_PASSWORD"
  environment:
    name: staging
    # ⚠ on_stop must be unset when defining parallel:matrix environments
    # see: https://gitlab.com/gitlab-org/gitlab/-/issues/332247
    on_stop: ansible-cleanup-staging
    auto_stop_in: "$ANSIBLE_STAGING_AUTOSTOP_DURATION"
  resource_group: staging
  rules:
    # exclude if $ANSIBLE_STAGING_PLAYBOOK_FILE not set
    - if: '$ANSIBLE_STAGING_PLAYBOOK_FILE == null || $ANSIBLE_STAGING_PLAYBOOK_FILE == ""'
@@ -1139,9 +1150,7 @@ ansible-cleanup-staging:
    ENV_PUBLIC_KEY: "$ANSIBLE_STAGING_PUBLIC_KEY"
    ENV_VAULT_PASSWORD: "$ANSIBLE_STAGING_VAULT_PASSWORD"
  environment:
    name: staging
    action: stop
  resource_group: staging
  rules:
    # exclude if not production branch
    - if: '$CI_COMMIT_REF_NAME !~ $PROD_REF'
@@ -1169,9 +1178,6 @@ ansible-production:
    ENV_PRIVATE_KEY: "$ANSIBLE_PROD_PRIVATE_KEY"
    ENV_PUBLIC_KEY: "$ANSIBLE_PROD_PUBLIC_KEY"
    ENV_VAULT_PASSWORD: "$ANSIBLE_PROD_VAULT_PASSWORD"
  environment:
    name: production
  resource_group: production
  rules:
    # exclude non-production branches
    - if: '$CI_COMMIT_REF_NAME !~ $PROD_REF'