AWS

AWS LZA vs NTC Deep Dive Part 3: Day 2 Operations and the Conclusion

Pipelines, upgrades and AI agent context after the deployment is done, plus a decision table from running both platforms.

Oct 30, 2026 · 13 min read · 2,652 words

Photo generated by chatGPT

Introduction

A landing zone is never finished when the first deployment is green. Modules get new versions, AWS changes defaults, and someone has to add the next account on a Tuesday afternoon. Day 2 is where you spend most of the years, and it is the part that feature comparisons skip.

In Part 1 I covered the bootstrap, in Part 2 logging and security. This last part is about living with both platforms, and about the decision I made: how a change flows, how I keep NTC current (I did a full module upgrade round in October 2026), how I give an AI agent enough context to help, what it costs, and then the conclusion. Let's dive in.

How a change flows

LZA: one pipeline

Every change to the configuration repository runs the same pipeline: validate, build, deploy. 40 to 75 minutes, whether you edited one line or twenty. That changes how you work. I batch changes, double-check before I push, and use the waiting time for something else. If the fix lives in a CDK customization, add the cdk synth step, commit the generated template, and an emergency change takes well over an hour.

The upside is a single place where everything is validated together, and a pipeline that orders itself through CloudFormation dependencies and runOrder. The aws-luminarlz-cli from Part 1 can run single stages and takes some of the pain out of it.

NTC: a pull request per phase

In NTC every phase has the same workflow. A pull request runs tofu plan, a merge to main runs tofu apply, and you can also start it manually. Authentication is OIDC, and the shared workflow runs OPA policy checks. A phase takes a few minutes, so I can try something, review the plan, and fix it within the same hour. Several fix commits in one afternoon are normal, not a sign that something is wrong.

There is exactly one place where one phase starts another:

flowchart LR
  A["Phase 2: push to main"] --> B["tofu apply"]
  B -->|"only if the apply succeeded"| C["Phase 3: workflow_dispatch"]
  B -.->|"if the baseline config changed"| D["EventBridge starts the baseline pipelines"]

After every successful push to Phase 2, a job dispatches the Phase 3 workflow and waits for it, so a Phase 2 run is not finished until Phase 3 is. On pull requests, it is skipped, and it is skipped when the apply fails. The Phase 3 run is a normal apply of unchanged code, usually "No changes". The second trigger is on the AWS side: when a Phase 2 apply changes the baseline configuration, an EventBridge rule starts the baseline pipelines in the member accounts within seconds.

No other phase chains anything. The rest are data dependencies through the parameter bucket, and you apply them in the right order yourself. A change in the log archive does not re-run security tooling, and the consumer only sees new parameters on its next run. That is why my Phase 5 repository has empty "trigger CI" commits in its history. I don't love that, but it is a pretty honest model: when a downstream phase should run, a human decided it.

Keeping NTC current

The first deployment is the easy part. Here is what keeping the organization up to date looked like in my case.

The bot bumps modules, not your config

In August 2026, Nuvibit's patcher bot opened dependency PRs against my phase repositories, and in some of them it moved a module to a new major version while the old providers blocks stayed. The lesson is to always re-read main before you plan an update, because your local checkout is stale the moment a bot has been around.

A major upgrade is a staged state migration

The NTC v2 account factory upgrade took about four hours and happened in September. The first step moved the account factory, lifecycle templates, and baseline templates to their 2.x and 4.x lines and put the baseline into plan-only mode, so I could review the state migration without touching any member account. Applying that first commit failed.

A few weeks later, Nuvibit added moved blocks that map the per-region baseline resources to a unified baseline keyed by region, dropped the import blocks that were no longer needed, and then plan-only was switched off so the migration was applied:

# illustrative, addresses simplified
moved {
  from = module.baseline_eu_central_1.aws_s3_bucket.tfstate
  to   = module.baseline_unified["eu-central-1"].aws_s3_bucket.tfstate
}

The pattern is the point: plan-only first, moved blocks for the state, remove obsolete imports, apply last.

The October round

On 20 October I brought all modules to their latest release in one day. One pull request per phase, in dependency order, each merged only after I had read its CI plan. These are the things that would have bitten me without that discipline:

  • The plan of the account factory hides the real change. The baseline code runs inside the member accounts, so the PR plan only showed 12 in-place changes (zip hashes, pipeline policies, a few tags). I turned on baseline_maintenance_plan_only = true for one cycle, read the CodeBuild plans for all four core accounts, and only then switched it off. The real changes were moving Config recorders to the service-linked role and updating a few bucket and key policies. New module defaults also change things quietly, so I pinned two new settings (backup_support_enabled and s3_abac_enabled) to false, because I don't use the backup template.
  • Coupled modules move together. The log archive 2.1 grants the Config service principal on its buckets, and the baseline templates 4.6 move the recorders to the service-linked role. Both affect the same delivery path, so I applied the log archive first, then the baseline right after. A short delivery gap is expected for recorders still on the old role.
  • A new default can change a policy. ntc-guardrail-templates added whitelist_for_specific_regions in a minor release. By default, it opens us-east-1 and us-west-2 for a list of global services, which changed my existing workloads SCP. Setting it to {} keeps the old behavior. I accepted the new one, but only after reading the plan.
  • Module inputs change shape. In v2 the aliased providers = { aws = aws.euc1 } is replaced by a plain region = "eu-central-1" input (per bucket for the log archive). Apply provider v6 on the old module versions first, so the state already carries the region.
  • Check the unchanged main before you blame the upgrade. The security tooling plan showed SNS subscriptions and a GuardDuty setting changing. I opened a PR with an empty commit and got the identical plan on the old version—pre-existing drift, not the bump.
  • Look in the right places for what changed. JavaScript renders the release notes at docs.nuvibit.com, so a simple fetch returns an empty page, and the v1 to v2 migration guide did not cover every module. The module repositories are private, so I can't list tags either. I worked from the example config on the module pages.

If I had to condense it into a rule of thumb: one PR per phase, in dependency order, plan reviewed before merge, and anything stateful gets a plan-only cycle first.

LZA upgrades

LZA upgrades run through the same pipeline, so they inherit its rhythm: one change, one long end-to-end run, and a mistake costs another 45 to 75 minutes.

Giving an AI agent the context: a CLAUDE.md per phase

Seven repositories, large .tf files (25 to 53 KB each, because the modules ship long inline documentation), state spread over several accounts, and a deployment order that no single repository explains. That is a bad situation for anyone new to the setup, and an AI coding agent is always new to the setup.

So I created the context files myself:

  • A root AGENTS.md (imported by CLAUDE.md) with the phase order and dependency graph, the commands, the module list, the CI workflow, the gotchas, and where each phase keeps its state.
  • A CLAUDE.md in every phase repository: what the phase does, in which account it runs, how it is deployed, what it reads from and writes to the parameter bucket, and the traps.

A trimmed excerpt of the log archive's CLAUDE.md, to show the level of detail:

## Cross-Phase Dependencies
Writes (to the `log-archive` parameter node): bucket ARNs, bucket names,
KMS key ARNs. Consumed by Phase 1 (org CloudTrail), Phase 2 (Config
baseline) and Phase 5 (security tooling).
 
## Key Gotchas
- Runs in the Log Archive account, not the management account.
- Object Lock (COMPLIANCE) is commented out. Irreversible; enable it
  only after testing.
- GuardDuty cannot export to opt-in regions.
- Do not enable access logging on the access logging bucket itself.
- The .tf files are large. Read specific sections, not the whole file.

What goes into such a file is what the code can't tell you: the order, the account, the manual steps, the things that already went wrong once. Two habits make it worth the effort. Update the file in the same pull request as the change; otherwise it starts to lie. Keep account IDs and bucket names in a private repository only; that's why you don't see them in this series.

This is not an NTC feature. Nuvibit's blueprint repositories come with their own READMEs, and the context files are mine. The same idea works for an LZA configuration repository, and I would write one there too.

What it costs

The underlying AWS services are the same, so Security Hub, GuardDuty, and Config cost the same on both platforms. My full LZA setup ran at roughly USD 150 per month, with a budget alarm at USD 200. What differs is everything around the services:

LZANTC
LicenseNone, open sourceCommercial, pricing not public
Pipeline computeNegligible next to the servicesNegligible, GitHub Actions minutes
SupportDocs, GitHub issues, AWS support planIncluded, direct access to the engineers
Biggest hidden costWaiting for 40 to 75 minute runs, CDK knowledge for the gapsOrdering phases, reviewing upgrades

Pipeline cost is noise. The real cost is the time people spend waiting or debugging. That is where the vendor question lives.

Networking, which I skipped on purpose

Networking is the least developed area in both of my organizations, and I'd say that is a legitimate choice. My LZA network-config.yaml essentially deletes default VPCs and nothing else (transitGateways: [], vpcs: []). In NTC, Phase 6 is a scaffold: a repository, a backend, and a state bucket in the Connectivity account, and no connectivity module deployed yet.

Both platforms can do far more with Transit Gateway, IPAM, and DNS, but I haven't deployed them and won't pretend otherwise. My rule of thumb: get logging, security, and identity right first, and add networking when the first workload needs it. Trying to do everything at once slowed me down on both platforms.

The conclusion

If you made it this far, the short version is that both platforms get you to a secure, multi-account AWS organization. They differ in what they ask from you.

Choose LZA if...Choose NTC if...
You already run Control Tower and want to extend itYou are Terraform/OpenTofu-native
Your team is strong in CDK and CloudFormationFast feedback matters (minutes against 40 to 75 minutes)
A free, open-source foundation mattersYou want an engineer from the vendor when you are stuck
Declarative CIS monitoring is a top priorityYou want a central Security Hub policy and delegated-admin setup out of the box
You prefer one repository and one pipelineYou can manage several repositories and their order
You accept CDK for the gapsGitOps through pull requests and OIDC is your standard

Vendor support is worth more than the invoice shows

On Day 1 of my NTC deployment, an engineer from Nuvibit made direct commits to my repositories. During the v2 migration, they wrote the moved blocks. I can't put a euro figure on that, but for a small team deploying its first landing zone, it is the difference between days of searching and an afternoon. With LZA, when I needed Security Hub central configuration, it was me, the documentation, and several 45-minute pipeline runs. For a large team with deep AWS knowledge and a platform group, the calculation can go the other way.

Lock-in is real on both sides

A common argument is that NTC's lock-in is small, because it is plain HCL. After a year with it, I'd put it differently. The modules are in private repositories, and even tofu init needs GitHub App authentication to fetch them. Your code is HCL, but the building blocks aren't yours. On the LZA side, AWS has a history of retiring solutions, and the enhancement layer on top of Control Tower would need a migration if it ever went away.

Neither risk disqualifies a platform. The AWS resources both create (organization, OUs, SCPs, Identity Center, Security Hub) are AWS-native and exist independently of either tool, so switching later is painful but possible. That is why I'd rather start now with the tool your team can adopt fastest than plan the perfect choice for another quarter.

What I'd do differently

LZA

  • Plan for CDK from the first day. The YAML covers the basics and little else.
  • Set up CIS monitoring early. It is one of the best things LZA offers.

NTC

  • Draw the dependency graph before the first line of HCL, and deploy the log archive right after the account factory. However, the Nuvibit onboarding team will help you set this up.
  • Delete the bootstrap IAM user, not just its key.
  • Use the plan-only mode for the baseline from the very first run, and review the blueprint defaults before you accept them.
  • Read the changelog and the plan before every module bump, and keep the vendor close in the first weeks.
  • Write the context files for your agent as you go, not at the end.

Conclusion

Neither platform is better in absolute terms. LZA is the marathon: a declarative, AWS-native foundation with a slow loop, no license fee, and CDK for whatever the YAML can't do. NTC is the sprint with hurdles: a fast GitOps loop and a vendor you can call, in exchange for phases you have to order yourself and modules you can't read in public.

Personally, I decided to decommission LZA, and NTC is now my baseline and my new production. The organization I used for the partner test and kept current through the upgrade rounds above is the one that carries real work from here on. Next are the accounts still under the old LZA management account: in one of the next posts, I will migrate them into the NTC organization, and I will write down what breaks.

Why NTC for me, in one sentence: I prefer the fast feedback loop, OIDC without long-lived credentials, and an engineer I can reach, and I accept the phase ordering and the private modules as the price. Your answer can be different, and if you ask me which one to pick, I won't give a generic one. I'd ask what your team already knows, how quickly you want feedback, and how much outside help you want. If NTC is the answer and you want a hand with the move, that is what my AWS cloud migration including EU solution is for.

I hope this series helps you make a better decision for your own landing zone. If you have run one of them, or both, I'd love to hear how Day 2 looks for you. Happy building!

This post was written with AI assistance and verified plus enhanced by a human.

More from the blog

Keep reading - related notes from recent engagements.

Ready to ship faster on AWS?

Tell us what you are building. We will map the fastest safe path to production and the platform to keep it there.

Notes from production

Occasional, no-fluff writing on AWS, DevOps, and running platforms that stay up. No spam, unsubscribe anytime.

Practical, not promotional
Real lessons from real engagements - architecture, automation, and incident post-mortems.
No spam
A few emails a year at most. Your address is never shared or sold.