7. Infrastructure and configuration in a chain#
In this tutorial
An infrastructure deploy "creates" two VMs and exports their addresses; a configuration deploy imports them and releases onto them, without anyone copying hosts by hand. You chain the two deploys, move the import into the environment configuration, and see what happens when the infrastructure code returns something unexpected.
Time: 25 minutes · You need: the result of tutorial 1 (binaries, lab/deploy_key, the manent-tutorial-target image), Docker, jq
flowchart LR
I["infra-web<br/>step provision"] -->|"exports: vms, motd_dir"| M["motd<br/>targetsFrom, paramsFrom"]
I -->|SSH :2222| H["infrastructure host"]
M -->|SSH :2223| V1["vm1"]
M -->|SSH :2224| V2["vm2"]
Why#
With infrastructure as code, the VMs are created by one deploy (Terraform, a cloud API, a hypervisor) and configured by another (Ansible, packages, files). The second one needs the output of the first: which hosts, which addresses, which paths. Copying them into a descriptor by hand breaks the chain at every change. With exports and imports:
- the producer declares which of its outputs others may read (
exports), each checked against a pattern; - the consumer reads them when it starts (
imports) and uses them as targets (deploy.targetsFrom) and plugin parameters (deploy.paramsFrom).
1. Two "VMs" and an infrastructure host#
The container of tutorial 1 (port 2222) plays the infrastructure host, where provisioning runs. Start two more containers from the same image: they are the VMs that provisioning "creates".
cd ~/manent-tutorial
docker run -d --name manent-tutorial-vm1 -p 2223:22 manent-tutorial-target
docker run -d --name manent-tutorial-vm2 -p 2224:22 manent-tutorial-target
ssh-keyscan -p 2223 127.0.0.1 >> lab/known_hosts
ssh-keyscan -p 2224 127.0.0.1 >> lab/known_hosts
2. A provisioning step that reports what it created#
A real infrastructure deploy would run terraform apply and read terraform output. Here a script on the infrastructure host simulates it: it prints the VMs as a JSON list and the directory where their configuration goes, in the outputs file of a custom step.
docker exec -i -u deploy manent-tutorial-target sh -c 'mkdir -p ~/bin && cat > ~/bin/provision && chmod 755 ~/bin/provision' <<'SH'
#!/bin/sh
# Simulated provisioning: a real one would run "terraform apply" here and read "terraform output".
echo "provisioning infra-web $1"
host=127.0.0.1
[ -e "$HOME/provision.wrong" ] && host=10.9.9.9 # a bug in the infrastructure code
cat >>"$MANENT_OUTPUT" <<EOF
vms=[{"host":"$host","port":2223,"user":"deploy"},{"host":"$host","port":2224,"user":"deploy"}]
motd_dir=/home/deploy/motd
EOF
SH
3. Configure manent#
Work in a new directory, with its own database. Stop the server of the previous tutorial first.
dbos:
database_url: sqlite:manent.db
workdir: ./work
plugins:
remote-copy:
known_hosts: ../lab/known_hosts
envconfig:
required: true
providers:
- name: default
type: file
file:
path: "./envconfig/{tenant}/{application}/{artifact}/{environment}.yml"
pipeline:
custom:
known_hosts: ../lab/known_hosts
steps:
- {name: secrets, mode: mandatory}
- {name: artifacts, mode: mandatory, max_retries: 3}
- {name: qa, mode: optional, enabled: true}
- name: provision # the infrastructure step, before the deploy
mode: optional
enabled: false # turned on by the descriptors that need it
type: command
timeout: 5m
ssh: {key_secret: deploy-key}
outputs: # only declared outputs, each with a pattern
vms: {pattern: '\[[^\n]{1,1000}\]'}
motd_dir: {pattern: '/[a-z0-9/_-]{1,100}'}
run:
run_once: true
command: ["/home/deploy/bin/provision", "{{ .Version }}"]
- {name: deploy, mode: mandatory, max_retries: 2}
- {name: post-test, mode: optional, enabled: false, max_retries: 5}
- {name: activation, mode: optional, enabled: true}
The environment configurations: infra-web runs on the infrastructure host; motd has no targets and no dest_dir, because they will come from the exports. Replace /home/you/manent-tutorial with your path.
plugin: remote-copy
targets:
- {host: 127.0.0.1, port: 2222, user: deploy}
params:
dest_dir: /home/deploy/infra
layout: releases
keep_releases: "3"
ssh_key_secret: deploy-key
secrets:
- name: deploy-key
ref: file:/home/you/manent-tutorial/lab/deploy_key
plugin: remote-copy
params:
layout: releases
keep_releases: "3"
ssh_key_secret: deploy-key
prepare_command: 'tar -xzf "$MANENT_ARTIFACT" -C "$MANENT_RELEASE_DIR"'
post_command: 'cat "$MANENT_CURRENT/motd"'
secrets:
- name: deploy-key
ref: file:/home/you/manent-tutorial/lab/deploy_key
4. The artifacts#
The infrastructure code and the configuration are both immutable, versioned archives:
mkdir -p infra-src motd-src dist
echo 'resource "null_resource" "vm" { count = 2 }' > infra-src/main.tf
tar -czf dist/infra-web-1.0.0.tar.gz -C infra-src .
echo "Welcome, version 1.0.0" > motd-src/motd
tar -czf dist/motd-1.0.0.tar.gz -C motd-src .
5. The producer exports#
apiVersion: manent/v1
application: infra-web
environment: test
artifact:
type: application
name: infra-web
version: 1.0.0
source: /home/you/manent-tutorial/chain/dist/infra-web-1.0.0.tar.gz
steps:
provision: true
exports:
vms: {from: provision.vms, format: targets, pattern: '127\.0\.0\.1'}
motd_dir: {from: provision.motd_dir, pattern: '/home/deploy/[a-z]+'}
fromis<step>.<key>: here the outputs of theprovisionstep. With a provisioning plugin it would bedeploy.<key>, from the plugin'sResult.Outputs.format: targetsis a JSON list of targets; thepatternmust match every host (the whole value, it is anchored). For a string export the pattern is required. Only declared outputs become exports, and only if they match: this is also the barrier that keeps secrets and surprises out of the chain.
6. The consumer imports#
apiVersion: manent/v1
application: motd
environment: test
artifact:
type: application
name: motd
version: 1.0.0
source: /home/you/manent-tutorial/chain/dist/motd-1.0.0.tar.gz
deploy:
targetsFrom:
- {import: infra, export: vms}
paramsFrom:
dest_dir: {import: infra, export: motd_dir}
The import infra is not declared here: the pipeline knows the ID of the infrastructure deploy only when it starts it, and passes it with --import. Release the two in a chain:
INFRA=$(manent deploy -f infra.yml --no-wait | jq -r .id)
manent deploy -f motd.yml --import infra=$INFRA
deploy waiting for its dependencies: 4d3857e0-7933-4bcc-9659-7d69d82f6cb4
deploy enqueued: 4d3857e0-7933-4bcc-9659-7d69d82f6cb4 (manent logs -f 4d3857e0-… to follow it)
--import infra=<id> declares the import and adds the infrastructure deploy to after: motd waits (WAITING_DEPENDENCY, without taking a slot) until infra-web has succeeded, then starts. Look at what each one recorded:
{
"state": "SUCCESS",
"exports": {
"motd_dir": "/home/deploy/motd",
"vms": "[{\"host\":\"127.0.0.1\",\"port\":2223,\"user\":\"deploy\"},{\"host\":\"127.0.0.1\",\"port\":2224,\"user\":\"deploy\"}]"
}
}
MOTD=$(manent list -a motd -n 1 | jq -r '.[0].id')
manent status $MOTD | jq -c '.result.steps[] | select(.name == "config") | .output'
{"import.infra":"06091bae-7712-4dd5-bc51-b884d5417b86@1.0.0","policy_revision":"builtin:4f53cda18c2b","rollback":"disabled","source":"file:./envconfig/untenanted/motd/motd/test.yml","targets":"2","tenant":"untenanted"}
The config step records where the targets came from: the infrastructure deploy and its version. On the VMs:
for vm in manent-tutorial-vm1 manent-tutorial-vm2; do docker exec $vm cat /home/deploy/motd/current/motd; done
# Welcome, version 1.0.0
# Welcome, version 1.0.0
The imports were resolved when motd started and saved with the step: a deploy resumed after a crash uses the same targets. Imported targets and parameters are additions of the descriptor, so the merge rules and the descriptor policy apply to them.
7. Move the import into the environment#
An import of a given deploy is right for the first deploy of a chain. Everything that comes later (a new version of motd, a rollback, a promotion, the reconciliation) should read the infrastructure in service, whatever deploy created it. That belongs to the environment configuration, which is authoritative. Add to envconfig/untenanted/motd/motd/test.yml:
imports:
infra: {current: {application: infra-web}} # the release in service of infra-web in test
targets_from:
- {import: infra, export: vms}
params_from:
dest_dir: {import: infra, export: motd_dir}
The descriptor is back to the artifact alone:
apiVersion: manent/v1
application: motd
environment: test
artifact:
type: application
name: motd
version: 1.1.0
source: /home/you/manent-tutorial/chain/dist/motd-1.1.0.tar.gz
echo "Welcome, version 1.1.0" > motd-src/motd
tar -czf dist/motd-1.1.0.tar.gz -C motd-src .
manent validate -f motd-min.yml | jq -c '{config, targets: [.deploy.targets[] | "\(.host):\(.port)"], dest: .deploy.params.dest_dir}'
{"config":{"import.infra":"06091bae-7712-4dd5-bc51-b884d5417b86@1.0.0","source":"file:./envconfig/untenanted/motd/motd/test.yml","targets":"2"},"targets":["127.0.0.1:2223","127.0.0.1:2224"],"dest":"/home/deploy/motd"}
validate already resolves the imports and shows the effective targets. Release:
With targets_from in the environment, a descriptor can no longer bring its own targets: the request would be refused.
8. When the infrastructure code is wrong#
Simulate a bug in the infrastructure code, which now reports hosts outside the expected network, and release a new version of it:
docker exec -u deploy manent-tutorial-target touch /home/deploy/provision.wrong
tar -czf dist/infra-web-1.1.0.tar.gz -C infra-src .
manent deploy -f infra.yml --version 1.1.0 --source "$PWD/dist/infra-web-1.1.0.tar.gz"
deploy 215f0b18-… failed: ERROR: exports of the deploy not valid: exports.vms (provision.vms): target 0: host "10.9.9.9" does not match the pattern "127\\.0\\.0\\.1"
The exports are checked when the deploy succeeds: a value outside its pattern fails the producer, so nothing reads it. A deploy waiting for it with after would end as DEPENDENCY_FAILED instead of configuring the wrong hosts. The infrastructure host itself was changed (its deploy step ran), as with any failure after the deploy step; the release in service is still the previous one:
manent inventory -a infra-web | jq -c '.[0] | {current: .current.version, failed: .last_failure.version}'
# {"current":"1.0.0","failed":"1.1.0"}
So motd keeps reading the VMs of 1.0.0:
echo "Welcome, version 1.2.0" > motd-src/motd
tar -czf dist/motd-1.2.0.tar.gz -C motd-src .
manent deploy -f motd-min.yml --version 1.2.0 --source "$PWD/dist/motd-1.2.0.tar.gz"
M=$(manent list -a motd -n 1 | jq -r '.[0].id')
manent status $M | jq -r '.result.steps[] | select(.name == "config") | .output["import.infra"]'
# 06091bae-7712-4dd5-bc51-b884d5417b86@1.0.0
Fix the bug before moving on:
What you learned#
- A deploy exports declared outputs, checked against a format and a pattern; an invalid export fails the producer, never the consumers.
- A deploy imports them as targets and plugin parameters, resolved when it starts and recorded with the
configstep. --import <name>=<id>chains a deploy to one known only at run time;currentin the environment configuration follows the infrastructure in service, for every later deploy.- The same mechanism works with a provisioning plugin (
Result.Outputs) and in a composite release, where an import names another deploy of the release.
Next#
Tutorial 8 runs manent itself as a production service.
Clean up
docker rm -f manent-tutorial-vm1 manent-tutorial-vm2 and rm -rf ~/manent-tutorial/chain. The infrastructure host stays for the other tutorials.