It ran green, so why is the system not in the promised state?
Goal
You learn how to run only part of a playbook with tags, and measure for yourself what risk comes paired with that convenience. At the end, you build a tool that judges which tag selections can run to completion.
Why it matters
Playbooks grow. A day comes when you have eighty tasks but want to fix just one line of configuration, and tags are the tool placed in that spot. The syntax is easy, so people use it in production the day after they learn it. That is where incidents happen — a tag is a knife that cuts the run, but a playbook is not designed to be cut. A later task uses the directory an earlier task created, and the task that changed the configuration calls the handler. If you choose only part, that link is broken, and if you are unlucky, it does not even fail and a half-correct state is left. So this lab has two halves: one for learning the tag syntax and one for seeing with your own eyes what a cut run fails to guarantee.
Steps
- Create
/root/anstags/hosts.ini— under[web], putweb1(ansible_host=127.0.0.1,ansible_port=2222), and under[all:vars], putansible_user=root. Create/root/anstags/site.yml: a play variableapp_env(defaultlab), a handlerreload app(it writes to/root/anstags/out/reload.markerthe one linereloaded), and four tasks — a task that creates the directory/root/anstags/appwith 0755, named배포 자리를 만든다(create the deploy location; tagsetup); a task that writes to/root/anstags/app/app.confthe one lineenv=<app_env>with 0644 and doesnotifyon the handler, named설정을 쓴다(write the configuration; tagconfig); a task that setsrelease_idtor-2026, named배포 번호를 정한다(set the release number; tagprep, set_fact); and a task that writes to/root/anstags/app/release.txtthe value ofrelease_idwith 0644, named배포 번호를 기록한다(record the release number; tagdeploy). Converge the whole thing once and save the output to/root/anstags/out/full.txt, then run once more with--tags configand save it to/root/anstags/out/config.txt. - Save the output of
ansible-playbook -i hosts.ini site.yml --list-tagsto/root/anstags/out/list-tags.txt. Then save the output of--list-tasks --tags configto/root/anstags/out/list-config.txt. The second file must contain설정을 쓴다and must not contain배포 번호를 기록한다. - Actually run the playbook with
--skip-tags prep,deployand save the output to/root/anstags/out/skip.txt. The output must contain lines for배포 자리를 만든다and설정을 쓴다in theTASK [...]form, and must not contain the lines of배포 번호를 정한다and배포 번호를 기록한다. And save the output of--list-tasks --skip-tags prep,deployto/root/anstags/out/list-skip.txt. - Put the tag
platformon the play itself. Then add a block named설정을 검증한다(verify the configuration) at the end of the tasks and give that block the tagverify— the block contains two tasks with no tags of their own: one that reads the state of/root/anstags/app/app.confand registers it asconf_stat, named설정 파일의 상태를 읽는다(read the state of the configuration file), and설정 파일이 있는지 단언한다(assert that the configuration file exists), which asserts from that result that the file exists. Save the entire output of--list-tasksto/root/anstags/out/inherit.txt, and the output of--list-tasks --tags verifyto/root/anstags/out/block.txt. - Add two more tasks.
어떤 선택에서도 남기는 표식(a marker left in any selection) writes to/root/anstags/out/always.markerthe one linealwayswith 0644 and has one tag,always.함부로 돌면 안 되는 태스크(a task that must not run carelessly) writes to/root/anstags/out/never.markerthe one linedangerwith 0644 and has two tags,neveranddanger. Actually run with--tags configand save the output to/root/anstags/out/always.txt—어떤 선택에서도 남기는 표식must run along with it. And save the output of--list-tasks --tags dangerto/root/anstags/out/danger.txt./root/anstags/out/never.markermust not be created until this lab is over. - Create
/root/anstags/tasks/common.yml— it has two tasks.공통 점검 하나(common check one) printscommon-oneand has no tag.공통 점검 둘(common check two) printscommon-twoand has the tagdeep. Then add two tasks to the end ofsite.yml—import 로 공통 점검을 끌어온다(pull in the common checks with import), which pulls in that file withimport_tasksand has the tagimported, andinclude 로 공통 점검을 끌어온다(pull in the common checks with include), which pulls in the same file withinclude_tasksand has the tagincluded. Save the output of--list-tasks --tags importedto/root/anstags/out/reuse-import.txt, and the output of--list-tasks --tags includedto/root/anstags/out/reuse-include.txt. - First delete
/root/anstags/out/reload.marker. Then actually run the playbook with--start-at-task "배포 번호를 정한다" -e app_env=prodand save the output to/root/anstags/out/startat.txt. When it is done, leave the contents of/root/anstags/app/app.confand whether/root/anstags/out/reload.markerexists in/root/anstags/out/aftermath.txt— the first line isconf=<app.conf 의 env 줄>(the env line of app.conf), and the second line ismarker=<yes 또는 no>(yes or no). - Create
/root/anstags/tag-check.sh <태그목록>(the placeholder stands for the tag list). Runsite.ymlwith that selection in check mode; if it ends normally, printSAFE <태그목록>on the first line and end with 0, and if it does not finish, printUNSAFE <태그목록> rc=<종료코드>(the placeholder stands for the exit code) on the first line and end with 1. Then run./tag-check.sh deployand./tag-check.sh prep,deployin turn and append the two lines to/root/anstags/out/tagcheck.txt. The first must be UNSAFE and the second SAFE.
Notes
- First create the inventory and the playbook in step 1 and converge the whole thing once. Tags are a tool you use from then on.
- Command hint:
--list-tagstells you what tags exist, and--list-tasks [--tags X]tells you what would run for that selection. Neither connects to the target, so both are safe even in production. - Command hint: task names contain spaces, so wrap them in quotes, as in
--start-at-task '배포 번호를 정한다'. - Common mistake: expecting tasks filtered out by
--tagsto be shown asskipping. Filtered tasks drop out of the output entirely, and the skipped count in the summary stays 0. - Common mistake: putting a tag on
include_tasks, running with that tag, and being baffled that "nothing happens". - Common mistake: seeing a run from the middle end with
failed=0and judging the deployment to be finished. --stepis an interactive mode that proceeds only when a person presses a key, so it is not covered in this lab. Tagging roles is also not covered here because roles themselves are the subject of the next course.- Running only part with tags · Running from the middle · import and include · Handlers · ansible-playbook options
Put tags on tasks and converge the whole thing once
Create /root/anstags/hosts.ini — under [web], put web1 (ansible_host=127.0.0.1, ansible_port=2222), and under [all:vars], put ansible_user=root. Create /root/anstags/site.yml: a play variable app_env (default lab), a handler reload app (it writes to /root/anstags/out/reload.marker the one line reloaded), and four tasks — a task that creates the directory /root/anstags/app with 0755, named 배포 자리를 만든다 (create the deploy location; tag setup); a task that writes to /root/anstags/app/app.conf the one line env=<app_env> with 0644 and does notify on the handler, named 설정을 쓴다 (write the configuration; tag config); a task that sets release_id to r-2026, named 배포 번호를 정한다 (set the release number; tag prep, set_fact); and a task that writes to /root/anstags/app/release.txt the value of release_id with 0644, named 배포 번호를 기록한다 (record the release number; tag deploy). Converge the whole thing once and save the output to /root/anstags/out/full.txt, then run once more with --tags config and save it to /root/anstags/out/config.txt.
Tags are a tool for a system where the whole playbook has already run once. If you apply only --tags config to a new server, it fails because the directory is missing, so always do the first convergence with everything — that is why the full run comes first in this step. You attach a tag to a task with tags: [이름] (the placeholder stands for the tag name). In the output of the --tags config run, confirm that the TASK [...] lines of the other three tasks are missing altogether — filtered tasks are not marked as skipped; they drop out of the output entirely.
Look first, by list, at what would run before running
Save the output of ansible-playbook -i hosts.ini site.yml --list-tags to /root/anstags/out/list-tags.txt. Then save the output of --list-tasks --tags config to /root/anstags/out/list-config.txt. The second file must contain 설정을 쓴다 and must not contain 배포 번호를 기록한다.
These two tools neither connect to the target nor change anything — when you use tags in production for the first time, these are the first thing to run. --list-tags tells you "what tags does this playbook have", and --list-tasks tells you, in order, "what will run for this selection". If you give --list-tasks the --tags option as well, the selection is reflected as it is. The list of tags attached to each task appears on the same line, so it is also used to see inheritance with your own eyes.
Choose by leaving things out
Actually run the playbook with --skip-tags prep,deploy and save the output to /root/anstags/out/skip.txt. The output must contain lines for 배포 자리를 만든다 and 설정을 쓴다 in the TASK [...] form, and must not contain the lines of 배포 번호를 정한다 and 배포 번호를 기록한다. And save the output of --list-tasks --skip-tags prep,deploy to /root/anstags/out/list-skip.txt.
There are two ways to choose — call what to include (--tags) or call what to leave out (--skip-tags). As tags increase, the latter is often more convenient. You can also give both together, in which case the one left out wins. Separate the list with commas. The habit of checking with a list before running is just as valuable with --skip-tags.
Tags put on a block and a play flow downward
Put the tag platform on the play itself. Then add a block named 설정을 검증한다 (verify the configuration) at the end of the tasks and give that block the tag verify — the block contains two tasks with no tags of their own: one that reads the state of /root/anstags/app/app.conf and registers it as conf_stat, named 설정 파일의 상태를 읽는다 (read the state of the configuration file), and 설정 파일이 있는지 단언한다 (assert that the configuration file exists), which asserts from that result that the file exists. Save the entire output of --list-tasks to /root/anstags/out/inherit.txt, and the output of --list-tasks --tags verify to /root/anstags/out/block.txt.
A tag flows downward from where it is attached. If you put it on a block, every task in the block has it, and if you put it on a play, every task of that play has it. That fact shows itself at the end of each line of --list-tasks, in the TAGS: [...] — if you put nothing on a task in the block and yet see two tags, the inheritance has become visible. If the names of the module that reads state and the module that asserts a precondition don't come to mind, find them with ansible-doc -l ansible.builtin | grep -iE 'stat|assert'.
A task that always runs and a task that never runs
Add two more tasks. 어떤 선택에서도 남기는 표식 (a marker left in any selection) writes to /root/anstags/out/always.marker the one line always with 0644 and has one tag, always. 함부로 돌면 안 되는 태스크 (a task that must not run carelessly) writes to /root/anstags/out/never.marker the one line danger with 0644 and has two tags, never and danger. Actually run with --tags config and save the output to /root/anstags/out/always.txt — 어떤 선택에서도 남기는 표식 must run along with it. And save the output of --list-tasks --tags danger to /root/anstags/out/danger.txt. /root/anstags/out/never.marker must not be created until this lab is over.
There are five special tags — always, never, tagged, untagged, and all. The first two are used in practice. Put always on tasks that must not be missing from any selection (setting common variables, collecting facts), and put never on tasks that should remain as code but must not run by a slip of the hand — a comment eventually gets uncommented, but never does not come off. To call a task with never, you must call the other label attached together with it. That task does not even appear in the list when you run --list-tasks plain — check that first.
import passes tags down and include does not
Create /root/anstags/tasks/common.yml — it has two tasks. 공통 점검 하나 (common check one) prints common-one and has no tag. 공통 점검 둘 (common check two) prints common-two and has the tag deep. Then add two tasks to the end of site.yml — import 로 공통 점검을 끌어온다 (pull in the common checks with import), which pulls in that file with import_tasks and has the tag imported, and include 로 공통 점검을 끌어온다 (pull in the common checks with include), which pulls in the same file with include_tasks and has the tag included. Save the output of --list-tasks --tags imported to /root/anstags/out/reuse-import.txt, and the output of --list-tasks --tags included to /root/anstags/out/reuse-include.txt.
The two pull in the same file, but when they pull it in differs. One is expanded in place at the time the playbook is read (static), and the other is inserted only during execution (dynamic). That difference divides tag inheritance — on the side expanded in advance, the tag attaches to each task, and on the side inserted during execution, it attaches only to the statement itself. Put the two list files side by side and check the side where the task names inside the pulled-in file are visible and the side where they are not. This is the identity of the failure where "nothing happens, with no error".
If you run from the middle, the handler does not run either
First delete /root/anstags/out/reload.marker. Then actually run the playbook with --start-at-task "배포 번호를 정한다" -e app_env=prod and save the output to /root/anstags/out/startat.txt. When it is done, leave the contents of /root/anstags/app/app.conf and whether /root/anstags/out/reload.marker exists in /root/anstags/out/aftermath.txt — the first line is conf=<app.conf 의 env 줄> (the env line of app.conf), and the second line is marker=<yes 또는 no> (yes or no).
--start-at-task starts from the task with that name. It is the tool you use so as not to rerun from task 1 when a long playbook died in the middle. But it starts without what the earlier tasks should have made and without the notify the earlier tasks should have sent — so even if it ends with failed=0, the system is not in the state the playbook promised. You gave -e app_env=prod along with it, so check for yourself what the configuration file looks like and whether the handler marker was created. The answer of this step is not a command but those two facts.
A tool that judges which tag selections can run to completion
Create /root/anstags/tag-check.sh <태그목록> (the placeholder stands for the tag list). Run site.yml with that selection in check mode; if it ends normally, print SAFE <태그목록> on the first line and end with 0, and if it does not finish, print UNSAFE <태그목록> rc=<종료코드> (the placeholder stands for the exit code) on the first line and end with 1. Then run ./tag-check.sh deploy and ./tag-check.sh prep,deploy in turn and append the two lines to /root/anstags/out/tagcheck.txt. The first must be UNSAFE and the second SAFE.
배포 번호를 기록한다 (record the release number) uses the value that 배포 번호를 정한다 (set the release number) set. If you leave out the earlier one and choose only the later one, that variable stays undefined, and that is the first way a partial run is dangerous. This tool turns that risk from a person's memory into a command. The reason to use check mode for the judgment is that it must be something you can run as it is in production — if you really change things in order to judge, it is an incident, not a tool. The gate ends with 1, so make sure the shell does not stop there when you collect the results into a file.