Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ htmlcov/
.env
.devops_learn/
artifacts/security/
artifacts/deployment/

# Terraform working-directory artifacts (see projects/api_platform/infra/terraform/).
# .terraform.lock.hcl is deliberately NOT listed here: it pins exact provider
Expand All @@ -25,6 +26,8 @@ artifacts/security/
**/.terraform/
*.tfstate
*.tfstate.*
*.tfplan
*.tfplan.json
crash.log
crash.*.log
override.tf
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ Project -> Analysis -> Requirements -> Questions -> Recommendations -> Architect
evidence collection, compares an explicit Git base against the proposed
state, applies Conftest policy, redacts secrets structurally, and prevents
blocked changes from progressing toward deployment eligibility.
- **Azure release lifecycle implementation** (`devops-learn deploy
projects/api_platform --cloud azure --depth learning`) composes validation,
image scanning, saved Terraform plans, candidate-bound approvals, ACR push,
Container Apps deployment, Azure observation, and HTTP health verification.
It is real-only and stops at preflight when credentials or required tools are
absent. It has not been executed from this checkout because Azure CLI,
Docker, Trivy, and Conftest are unavailable here.
- **Competency evidence tracking**: records what you were exposed to, practiced,
or demonstrated, not fabricated certification.

Expand Down Expand Up @@ -67,6 +74,12 @@ devops-learn local projects/api_platform
# (plan requires Azure credentials; fails cleanly with an explanation without them)
devops-learn terraform

# Run the complete real-only Azure lifecycle after `az login` and tool preflight.
devops-learn deploy projects/api_platform --cloud azure --depth learning

# Explicitly approve and verify cleanup when finished learning.
devops-learn destroy projects/api_platform

# Check security prerequisites, then scan a proposed change against its base
devops-learn security doctor
devops-learn security scan projects/api_platform --base-ref origin/main
Expand Down
9 changes: 5 additions & 4 deletions docs/devsecops.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,11 @@ the bundled real FastAPI project, uploads the sanitized report, and never uses
Azure credentials or runs `terraform apply`. GitHub PR metadata supplies the
base SHA, not `HEAD~1`.

The future real path is: Terraform fmt, validate, plan, plan JSON risk
analysis, Trivy IaC/plan scan, normalized change-aware findings, Conftest
policy, deployment eligibility, human plan approval, real apply, then Azure
verification. Azure and apply are not implemented by this milestone.
The real-only `deploy` path composes Terraform fmt, validate and saved plan,
plan JSON risk analysis, Trivy filesystem/config/image evidence, normalized
change-aware findings, Conftest policy, deployment eligibility, human approval,
candidate-bound real apply, Azure observation, and health verification. It is
not marked live-verified until an opt-in Azure run succeeds.

## Limitations

Expand Down
17 changes: 9 additions & 8 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#0b1020">
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
<meta name="description" content="DevOps Learn by Doing is a simulation-first CLI that teaches DevOps through a production-style Python API project with progressive AI assistance.">
<meta name="description" content="DevOps Learn by Doing is an AI-assisted CLI for learning DevOps through a security-gated Azure deployment lifecycle and a production-style Python API project.">
<meta property="og:image" content="https://howlcipher.github.io/DevOps-Learn-by-Doing/assets/social-preview.png">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://howlcipher.github.io/DevOps-Learn-by-Doing/assets/social-preview.png">
Expand Down Expand Up @@ -596,10 +596,11 @@ <h2>One project. A full DevOps journey.</h2>
<div>
<div class="section-head">
<div class="label">03 // Truthful status</div>
<h2>Real local execution. Simulated cloud.</h2>
<h2>Real local execution. Real-only Azure lifecycle.</h2>
<p>
The boundary is deliberate: local Python and Docker work is real, while Azure,
Terraform apply, and Kubernetes remain simulated until the next milestone.
The boundary is deliberate: local work, saved Terraform plans, and the Azure
lifecycle implementation are real-only. Azure execution remains opt-in and is
never claimed as verified until a credentialed run succeeds.
</p>
</div>

Expand All @@ -618,7 +619,7 @@ <h2>Real local execution. Simulated cloud.</h2>
</div>
<div class="status-row">
<span class="badge sim">Sim</span>
<p>Azure provisioning, Terraform apply, kubectl, and cloud API calls are simulated.</p>
<p>Azure provisioning and Terraform apply require explicit approval and opt-in credentials; Kubernetes remains simulated.</p>
</div>
</div>
</div>
Expand Down Expand Up @@ -663,11 +664,11 @@ <h3>Real Python + Docker</h3>
</article>
<article class="card">
<h3>Real Terraform validation</h3>
<p>Use the Terraform binary for validate/plan while Azure apply/destroy remains simulated.</p>
<p>Use saved Terraform plans with explicit approval before real-only apply and verified cleanup.</p>
</article>
<article class="card">
<h3>First real Azure path</h3>
<p>Provision learner-owned Azure resources behind approval gates, then extend into real Kubernetes fundamentals.</p>
<h3>Azure lifecycle acceptance run</h3>
<p>Exercise the real, approval-controlled Container Apps path and independently verify cleanup.</p>
</article>
</div>
</div>
Expand Down
23 changes: 14 additions & 9 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,20 @@ declared on `RealTerraformTool` and stay simulated until Milestone 3. `plan`
requires Azure authentication and fails cleanly, with an explanation, on a
machine without it — `init`/`validate`/`fmt` need no credentials.

## Milestone 3: first Azure vertical slice

A small, real path: Python application -> Docker -> ACR -> Terraform -> minimal
Azure infrastructure -> deployment -> health verification, behind the existing
approval gate confirming a real, non-simulated action. Provision a resource
group and container registry first. Introduce AKS/Kubernetes only after this
first real cloud execution path is reliable — not because Kubernetes is
technically harder, but because a working real deployment path should exist
before adding cluster-level complexity on top of it.
## Milestone 3: first Azure vertical slice (implemented, awaiting live verification)

The real-only `deploy` workflow uses a two-stage path: bootstrap Resource Group,
ACR, Log Analytics, Container Apps Environment, managed identity and AcrPull;
then push a digest-pinned FastAPI image and apply the Container App plan.
It persists saved plans and candidate evidence, requires explicit confirmation
and tool approval, checks Azure independently, verifies `/health`, and offers
an approved `destroy` flow with an Azure cleanup check. Container Apps is the
right first fit for a small HTTP workload: it is managed and has much less
operational overhead than AKS. AKS remains deliberately out of scope.

This feature is not yet labelled live-verified: the current execution host has
no Azure CLI, Docker, Trivy, or Conftest, so a real Azure deployment and cleanup
remain an opt-in acceptance test.

## DevSecOps security control plane (done)

Expand Down
9 changes: 6 additions & 3 deletions docs/safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,12 @@ resources, applies Terraform, or modifies remote state. It:
(`tools/terraform_tool.py`) — `devops-learn analyze --real-tools` and `devops-learn local` both
still use `SimulatedTerraformTool` for the `terraform` tool name; only `python`/`docker` become
real for those commands. `RealTerraformTool` declares only `fmt`/`init`/`validate`/`plan` —
`apply_approved_plan`/`destroy_approved_environment` are not implemented on it at all, so
invoking either raises `KeyError` rather than silently falling back to simulated behavior. Real
apply/destroy stay simulated until Milestone 3 (`docs/roadmap.md`).
`apply_approved_plan` and `destroy_approved_environment` are real-only Tool
operations. Apply accepts only a saved plan in `.devops_learn/plans` and
rechecks its digest, source identity, Terraform configuration digest, and
candidate context before invoking Terraform. Destroy requires an explicit
target environment and is followed by a read-only Azure resource-group check.
Neither operation can fall back to simulation; normal CI does not call either.

Every subprocess call goes through `tools/_subprocess_safety.py::run_safely`, which sets an
explicit timeout and always redacts secret-shaped `KEY=value` text and truncates long output
Expand Down
7 changes: 3 additions & 4 deletions docs/terraform-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ DESTRUCTIVE-adjacent operations in this platform's risk model (see
## Real vs. simulated in this platform

`devops-learn terraform` runs real `fmt`/`init`/`validate`/`plan` against
`projects/api_platform/infra/terraform/` and never applies or destroys.
`apply_approved_plan`/`destroy_approved_environment` -- and therefore any
real state file this platform would ever write -- stay simulated until
Milestone 3. See `docs/safety.md` and `docs/roadmap.md`.
`projects/api_platform/infra/terraform/`. The unified `deploy` command adds
real-only saved-plan apply and the `destroy` command adds real-only cleanup.
Both remain unverified in this checkout until an opt-in Azure acceptance run.
7 changes: 5 additions & 2 deletions projects/api_platform/infra/terraform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ requires Azure authentication (`az login`, or `ARM_CLIENT_ID` /
variables) to build the provider client -- without it, `plan` fails cleanly
and the workflow explains why rather than faking a result.

`apply`/`destroy` are not implemented against this directory yet; see
`docs/roadmap.md` (Milestone 3) and `docs/terraform-state.md`.
`devops-learn deploy` persists an approved plan before real apply; it never
accepts an arbitrary `terraform apply` command. `devops-learn destroy` is
separately destructive, requires explicit confirmation and tool approval, and
checks Azure after Terraform returns. These real-only operations are excluded
from CI and have not been exercised from this checkout without Azure credentials.

## Local state

Expand Down
65 changes: 65 additions & 0 deletions projects/api_platform/infra/terraform/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ resource "azurerm_container_registry" "main" {
tags = local.tags
}

resource "azurerm_user_assigned_identity" "container_app" {
name = "${var.project_name}-app-identity"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
tags = local.tags
}

resource "azurerm_role_assignment" "container_app_acr_pull" {
scope = azurerm_container_registry.main.id
role_definition_name = "AcrPull"
principal_id = azurerm_user_assigned_identity.container_app.principal_id
}

resource "azurerm_log_analytics_workspace" "main" {
name = "${var.project_name}-logs"
resource_group_name = azurerm_resource_group.main.name
Expand All @@ -40,3 +53,55 @@ resource "azurerm_log_analytics_workspace" "main" {
retention_in_days = 30
tags = local.tags
}

resource "azurerm_container_app_environment" "main" {
name = "${var.project_name}-env"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
log_analytics_workspace_id = azurerm_log_analytics_workspace.main.id
tags = local.tags
}

resource "azurerm_container_app" "api" {
count = var.deploy_application ? 1 : 0
name = "${var.project_name}-api"
resource_group_name = azurerm_resource_group.main.name
container_app_environment_id = azurerm_container_app_environment.main.id
revision_mode = "Single"

identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.container_app.id]
}

registry {
server = azurerm_container_registry.main.login_server
identity = azurerm_user_assigned_identity.container_app.id
}

ingress {
external_enabled = true
target_port = 8000

traffic_weight {
latest_revision = true
percentage = 100
}
}

template {
min_replicas = 0
max_replicas = 1

container {
name = "api"
image = var.app_image
cpu = 0.25
memory = "0.5Gi"
}
}

tags = local.tags

depends_on = [azurerm_role_assignment.container_app_acr_pull]
}
12 changes: 12 additions & 0 deletions projects/api_platform/infra/terraform/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,15 @@ output "resource_group_name" {
output "container_registry_login_server" {
value = azurerm_container_registry.main.login_server
}

output "container_app_environment_name" {
value = azurerm_container_app_environment.main.name
}

output "container_app_name" {
value = try(azurerm_container_app.api[0].name, null)
}

output "container_app_endpoint" {
value = try("https://${azurerm_container_app.api[0].ingress[0].fqdn}", null)
}
17 changes: 17 additions & 0 deletions projects/api_platform/infra/terraform/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,20 @@ variable "location" {
type = string
default = "eastus"
}

variable "deploy_application" {
description = "Set after the ACR image has been pushed. Bootstrap stays image-independent."
type = bool
default = false
}

variable "app_image" {
description = "Immutable ACR image reference, including @sha256 digest, for the FastAPI app."
type = string
default = ""

validation {
condition = !var.deploy_application || can(regex("@sha256:[0-9a-f]{64}$", var.app_image))
error_message = "Application deployment requires an immutable image digest reference."
}
}
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ build-backend = "hatchling.build"

[project]
name = "devops-learn"
version = "0.1.0"
description = "AI mentored, learn by doing DevOps and platform engineering CLI"
version = "0.2.0"
description = "AI-assisted DevOps learning CLI with a security-gated Azure deployment lifecycle"
readme = "README.md"
requires-python = ">=3.11"
license = { text = "MIT" }
Expand Down
4 changes: 4 additions & 0 deletions src/devops_learn/cli/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from devops_learn.cli.commands import deploy
from devops_learn.cli.commands import destroy

__all__ = ["deploy", "destroy"]
41 changes: 41 additions & 0 deletions src/devops_learn/cli/commands/deploy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""CLI entry point for the first real Azure deployment lifecycle."""

from __future__ import annotations

import argparse

from devops_learn.bootstrap import Platform
from devops_learn.cli.terminal_ui import TerminalUi
from devops_learn.domain.enums import ExplanationDepth
from devops_learn.workflows.deploy_flow import DeployOptions, run_deploy_flow


def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
parser = subparsers.add_parser(
"deploy",
help="Run the security-gated, approval-controlled real Azure deployment lifecycle",
)
parser.add_argument("path", nargs="?", default="projects/api_platform")
parser.add_argument("--cloud", choices=("azure",), default="azure")
parser.add_argument(
"--depth", choices=("brief", "normal", "learning", "deep"), default="learning"
)
parser.add_argument("--location", default="eastus")
parser.add_argument("--environment", default="learning")
parser.add_argument("--base-ref", default="origin/main")
parser.set_defaults(handler=run)


def run(args: argparse.Namespace, platform: Platform) -> None:
run_deploy_flow(
platform,
TerminalUi(),
DeployOptions(
project_root=args.path,
cloud=args.cloud,
depth=ExplanationDepth[args.depth.upper()],
location=args.location,
environment=args.environment,
base_ref=args.base_ref,
),
)
35 changes: 35 additions & 0 deletions src/devops_learn/cli/commands/destroy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""CLI entry point for explicitly approved Azure learning-environment cleanup."""

from __future__ import annotations

import argparse

from devops_learn.bootstrap import Platform
from devops_learn.cli.terminal_ui import TerminalUi
from devops_learn.domain.enums import ExplanationDepth
from devops_learn.workflows.deploy_flow import DeployOptions, run_cleanup_flow


def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
parser = subparsers.add_parser(
"destroy", help="Destroy and independently verify an Azure lab cleanup"
)
parser.add_argument("path", nargs="?", default="projects/api_platform")
parser.add_argument("--location", default="eastus")
parser.add_argument("--environment", default="learning")
parser.set_defaults(handler=run)


def run(args: argparse.Namespace, platform: Platform) -> None:
success = run_cleanup_flow(
platform,
TerminalUi(),
DeployOptions(
project_root=args.path,
location=args.location,
environment=args.environment,
depth=ExplanationDepth.LEARNING,
),
)
if not success:
raise SystemExit(2)
Loading
Loading