AWS DevOps Infrastructure Code: The 9 Key Steps to Go from Zero to Operational

AWS DevOps Infrastructure Code: the essentials in one article — real code, diagrams and concrete steps, excerpts from a 39-lesson course.

AWS DevOps Infrastructure Code: The 9 Key Steps to Go from Zero to Operational

Everyone can learn AWS DevOps Infrastructure Code — provided they follow the steps in the right order. We have condensed a complete 39-lesson course into a clear path, with the most useful code snippets.

tl;dr
  • Introduction and AWS Configuration
  • DevOps and Infrastructure as Code
  • AWS CloudFormation
  • AWS CDK Cloud Development Kit
  • CICD with AWS CodePipeline
~$ cat ./parcours.md # AWS DevOps Infrastructure Code — 10 chapters
01
Introduction and AWS Configuration
→ Chapter 00 — Create an AWS Free Tier account→ Chapter 00 — IAM: Users, Roles and Policies+ 2 more lessons
02
DevOps and Infrastructure as Code
→ Chapter 01 — The DevOps Philosophy→ Chapter 01 — Infrastructure as Code (IaC)+ 2 more lessons
03
AWS CloudFormation
→ Introduction to AWS CloudFormation→ Parameters, Outputs, Mappings and Conditions+ 2 more lessons
04
AWS CDK Cloud Development Kit
→ Introduction to AWS CDK — Cloud Development Kit→ CDK in depth — Create real infrastructure+ 2 more lessons
05
CICD with AWS CodePipeline
→ CodeCommit and CodeBuild→ CodeDeploy and CodePipeline+ 2 more lessons
06
Containers on AWS Docker ECS Fargate
→ Docker and Amazon ECR→ Amazon ECS and Fargate+ 2 more lessons
07
Monitoring and Observability
→ CloudWatch — Logs, Metrics and Alarms→ AWS X-Ray and CloudTrail+ 2 more lessons
08
Terraform on AWS
→ Introduction to Terraform on AWS→ Terraform — State, Variables and Modules+ 2 more lessons
🏁
Final project (+ 2 chapters along the way)
→ You leave with a concrete and demonstrable project

Introduction to AWS CDK — Cloud Development Kit

Chapter 03 — Lesson 01 · AWS DevOps : Infrastructure as Code

1. What is AWS CDK?

CloudFormation is like filling out a CERFA administrative form by hand, box by box, while strictly following syntax rules. It is tedious but it works. CDK is having a computer program fill out that form for you: you tell it in Python "I want a medium-sized server with a database", and it automatically generates the complete, correctly filled CERFA form with all the mandatory boxes. You write far less, you achieve far more, and you benefit from the full power of a real programming language such as loops, functions and code reuse.

The AWS Cloud Development Kit (CDK) is an open-source framework that lets you define your AWS infrastructure using a real programming language: Python, TypeScript, Java, C# or Go. CDK then automatically generates CloudFormation templates from your code.

NOTEThe principle: you write Python (or TypeScript, etc.), CDK compiles that code into a valid CloudFormation template, then CloudFormation deploys the AWS resources. You benefit from the full power of a real language (loops, conditions, functions, inheritance) to describe your infrastructure.

CDK vs CloudFormation — comparison

CriterionCloudFormation (YAML)AWS CDK (Python)
LanguageDeclarative YAML / JSONPython, TypeScript, Java, C#, Go
ReusabilityCopy-paste, Nested StacksClasses, modules, inheritance
IDE autocompletionLimited (YAML)Full with types and documentation
LogicLimited conditions and mappingsFull if/else, for loops, functions
TestsDifficultUnit tests with pytest, jest
Learning curveLow for YAML, but verboseRequires knowledge of a programming language
Under the hoodExecuted directly by CloudFormationGenerates CloudFormation then deploys it

The four core CDK concepts

App (CDK Application)

The entry point of any CDK project. An App contains one or more Stacks. It is the root container of your infrastructure.

Stack (CDK Stack)

Corresponds to a CloudFormation stack. Contains Constructs (AWS resources). An App can have multiple Stacks for different environments.

Construct

The basic building block of CDK. Represents an AWS resource (or group of resources). Exists at three levels: L1, L2, L3.

Environment (Env)

The combination of target AWS account + region for deployment. Allows deploying the same stack across multiple environments.

2. Installation — Node.js, CDK CLI and Python

The CDK CLI is a Node.js tool. Even if you code in Python, you must have Node.js installed.

Prerequisites

bash
# 1. Check Node.js (version 18+ recommended)
node --version
bash
# Bootstrap in the current region
cdk bootstrap

# Bootstrap in a specific region
cdk bootstrap aws://123456789012/eu-west-1

Constructs are the basic building blocks of CDK. They exist at three levels of abstraction, each offering a different trade-off between control and simplicity.

L1 — Low-level Constructs (CfnXxx)

L1 constructs (prefixed Cfn) are direct wrappers around CloudFormation resources. They give access to all properties but without any default values — you must configure everything explicitly.

output
from aws_cdk import aws_s3 as s3
import aws_cdk as cdk

class MaStack(cdk.Stack):
    def __init__(self, scope, id, **kwargs):
        super().__init__(scope, id, **kwargs)

        # L1 : CfnBucket = direct CloudFormation wrapper AWS::S3::Bucket
        bucket_l1 = s3.CfnBucket(self, "BucketL1",
            bucket_name="mon-bucket-l1",
            versioning_configuration=s3.CfnBucket.VersioningConfigurationProperty(
                status="Enabled"
            ),
            lifecycle_configuration=s3.CfnBucket.LifecycleConfigurationProperty(
                rules=[s3.CfnBucket.RuleProperty(
                    id="expiration-anciens-objets",
                    status="Enabled",
                    expiration_in_days=90
                )]
            )
        )

L2 — High-level Constructs (recommended)

Nested Stacks, StackSets, Change Sets and Best Practices

Chapter 02 — Lesson 03 · AWS DevOps : Infrastructure as Code

1. Nested Stacks — breaking down large infrastructures

Imagine building an office tower. Instead of a single contractor doing everything from A to Z and quickly becoming overwhelmed, specialists are brought in: an electrician for the entire electrical network, a plumber for the plumbing, a carpenter for the partitions, a network technician for IT. Each works in their domain, produces their work report, and a site manager coordinates everything. Nested Stacks work the same way: each module (network, security, database, application) is an independent template managed by a different team, and a parent stack coordinates everyone.

When infrastructure becomes complex (VPC, databases, applications, security...), grouping everything into a single template becomes unmanageable. Nested Stacks allow you to split the infrastructure into reusable modules, each in its own template.

Layered architecture with Nested Stacks

output
infrastructure/
├── stack-parent.yaml         # Root stack — orchestrates everything
├── modules/
│   ├── reseau.yaml           # VPC, subnets, Internet Gateway
│   ├── securite.yaml         # Security Groups, IAM Roles
│   ├── base-de-donnees.yaml  # RDS, ElastiCache cache
│   └── application.yaml      # EC2, Load Balancer, Auto Scaling

The parent stack that references the modules

output
AWSTemplateFormatVersion: "2010-09-09"
Description: "Stack parent — orchestrates toute l'infrastructure"

Parameters:
  Environnement:
    Type: String
    AllowedValues: [dev, prod]
  URLTemplates:
    Type: String
    Description: "Base S3 URL where the templates are stored"
    Default: "https://s3.amazonaws.com/mon-bucket-templates/v1"

Resources:
  # Network module
  StackReseau:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub "${URLTemplates}/reseau.yaml"
      Parameters:
        Environnement: !Ref Environnement
        CIDRVpc: "10.0.0.0/16"
      TimeoutInMinutes: 15

  # Security module — depends on network
  StackSecurite:
    Type: AWS::CloudFormation::Stack
    DependsOn: StackReseau
    Properties:
      TemplateURL: !Sub "${URLTemplates}/securite.yaml"
      Parameters:
        Environnement: !Ref Environnement
        VpcId: !GetAtt StackReseau.Outputs.VpcId

  # Database module
  StackBDD:
    Type: AWS::CloudFormation::Stack
    DependsOn: StackSecurite
    Properties:
      TemplateURL: !Sub "${URLTemplates}/base-de-donnees.yaml"
      Parameters:
        Environnement: !Ref Environnement
        VpcId: !GetAtt StackReseau.Outputs.VpcId
        SubnetIds: !GetAtt StackReseau.Outputs.SubnetsPrives
        SGBaseDeDonnees: !GetAtt StackSecurite.Outputs.SGBaseDeDonnees
      TimeoutInMinutes: 30

  # Application module
  StackApplication:
    Type: AWS::CloudFormation::Stack
    DependsOn: StackBDD
    Properties:
      TemplateURL: !Sub "${URLTemplates}/application.yaml"
      Parameters:
        Environnement: !Ref Environnement
        VpcId: !GetAtt StackReseau.Outputs.VpcId
        SubnetPublics: !GetAtt StackReseau.Outputs.SubnetsPublics
        URLBDD: !GetAtt StackBDD.Outputs.EndpointBDD
        SGApplication: !GetAtt StackSecurite.Outputs.SGApplication

Outputs:
  URLApplication:
    Description: "URL of the application Load Balancer"
    Value: !GetAtt StackApplication.Outputs.URLLoadBalancer

Example of the reseau.yaml module template

output
AWSTemplateFormatVersion: "2010-09-09"
Description: "Network module — VPC, public and private subnets"

Parameters:
  Environnement:
    Type: String
  CIDRVpc:
    Type: String

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref CIDRVpc
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub "vpc-${Environnement}"

  SubnetPublic1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: "10.0.1.0/24"
      AvailabilityZone: !Select [0, !GetAZs ""]
      MapPublicIpOnLaunch: true

  SubnetPrive1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: "10.0.10.0/24"
      AvailabilityZone: !Select [0, !GetAZs ""]

Outputs:
  VpcId:
    Value: !Ref VPC
    Export:
      Name: !Sub "${AWS::StackName}-VpcId"
  SubnetsPublics:
    Value: !Join [",", [!Ref SubnetPublic1]]
    Export:
      Name: !Sub "${AWS::StackName}-SubnetsPublics"
  SubnetsPrives:
    Value: !Join [",", [!Ref SubnetPrive1]]
    Export:
      Name: !Sub "${AWS::StackName}-SubnetsPrives"

Deploy using templates stored in S3

bash
# Upload the templates to S3 first
aws s3 sync ./modules/ s3://mon-bucket-templates/v1/ --exclude "*.md"

# Deploy the parent stack
aws cloudformation deploy \
  --stack-name infra-prod \
  --template-file stack-parent.yaml \
  --parameter-overrides \
    Environnement=prod \
    URLTemplates=https://s3.amazonaws.com/mon-bucket-templates/v1 \
  --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM
TIPBenefits of Nested Stacks: modularity (each module is independent), reusability (the same network module for dev and prod), readability (templates stay short), and CloudFormation limit (500 resources max per stack — nested stacks bypass this limit).

2. StackSets — deploy across multiple accounts and regions

Imagine a restaurant chain opening branches worldwide. The CEO creates a standard menu recipe, a graphic charter, and operating procedures that apply in every restaurant, whether in Paris, New York or Tokyo. When a new rule is added (new allergy to display, new health regulation), it is applied simultaneously across the entire chain with a single central decision. StackSets work the same way: you define your infrastructure once and deploy it to all your AWS accounts and all your regions in a single operation.

StackSets let you deploy the same CloudFormation template across multiple AWS accounts and/or multiple regions in a single operation. It is the ideal tool for multi-account organizations (Landing Zone, Control Tower).

Typical use cases

Centralized governance

Deploy AWS Config rules, CloudTrail, GuardDuty across all organization accounts automatically.

Uniform compliance

Apply the same IAM policies, baseline security groups and alert notifications in every account.

Multi-region

Deploy identical infrastructure in us-east-1, eu-west-1 and ap-southeast-1 for global redundancy.

Prerequisite: configure permissions

bash
# Two operating modes:

# Mode 1 : Self-Managed (manual IAM roles in each account)
# Create AWSCloudFormationStackSetAdministrationRole in the admin account
# Create AWSCloudFormationStackSetExecutionRole in each target account

# Mode 2 : Service-Managed (via AWS Organizations — recommended)
# Enable trust with Organizations in CloudFormation
aws organizations enable-aws-service-access \
  --service-principal cloudformation.amazonaws.com

Create and deploy a StackSet

Deploy and manage applications on EKS

Chapter 08 — Kubernetes on AWS EKS | AWS DevOps Infrastructure as Code

1. Deploy an application — Deployment YAML

A Kubernetes Deployment is like a fast-food chain. You define how many "counters" (replicas) you want open at the same time. If one fails, Kubernetes automatically opens a new one without customers noticing. When you want to change the menu (new image version), Kubernetes updates the counters one by one (rolling update) so that not all counters are ever closed at once. If the new menu is unpopular, you can instantly revert to the previous menu (rollback).

A Kubernetes Deployment declares the desired state of your application: which image to use, how many replicas, how to perform updates. Kubernetes continuously maintains that state.

Complete Deployment with all best practices

output
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mon-api-fastapi
  namespace: mon-application
  labels:
    app: mon-api-fastapi
    version: "1.0.0"
    equipe: backend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: mon-api-fastapi
  # Rolling update deployment strategy
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1      # At most 1 Pod unavailable during the update
      maxSurge: 1            # At most 1 Pod in surplus during the update
  template:
    metadata:
      labels:
        app: mon-api-fastapi
        version: "1.0.0"
    spec:
      serviceAccountName: sa-mon-api  # IRSA — IAM Role for Service Account
      terminationGracePeriodSeconds: 30
      # Pod-level security
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        fsGroup: 1000
      containers:
        - name: mon-api-fastapi
          image: 123456789012.dkr.ecr.ca-central-1.amazonaws.com/mon-api-fastapi:1.0.0
          ports:
            - containerPort: 8080
              protocol: TCP
          # Mandatory resource limits in production
          resources:
            requests:
              cpu: "250m"        # 0.25 vCPU guaranteed
              memory: "256Mi"    # 256 MiB guaranteed
            limits:
              cpu: "500m"        # Maximum 0.5 vCPU
              memory: "512Mi"    # Maximum 512 MiB
          # Environment variables from ConfigMap and Secret
          env:
            - name: ENVIRONNEMENT
              value: production
            - name: PORT
              value: "8080"
          envFrom:
            - configMapRef:
                name: config-mon-api
            - secretRef:
                name: secrets-mon-api
          # Health probes
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 15
            periodSeconds: 20
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
            successThreshold: 1
          startupProbe:
            httpGet:
              path: /health
              port: 8080
            failureThreshold: 30
            periodSeconds: 5
          # Container security
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop:
                - ALL
      # Distribution across availability zones
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: mon-api-fastapi

Essential deployment commands

bash
# Apply the manifest
kubectl apply -f deployment.yaml -n mon-application

# Follow the deployment in real time
kubectl rollout status deployment/mon-api-fastapi -n mon-application

# View deployment history
kubectl rollout history deployment/mon-api-fastapi -n mon-application

# Update the image (triggers a rolling update)
kubectl set image deployment/mon-api-fastapi \
  mon-api-fastapi=123456789012.dkr.ecr.ca-central-1.amazonaws.com/mon-api-fastapi:1.1.0 \
  -n mon-application

# Revert to the previous version (rollback)
kubectl rollout undo deployment/mon-api-fastapi -n mon-application

# Revert to a specific revision
kubectl rollout undo deployment/mon-api-fastapi --to-revision=2 -n mon-application

2. Kubernetes Services: ClusterIP, NodePort, LoadBalancer

In Kubernetes, Services are like stable postal addresses in a building whose tenants change frequently. ClusterIP is an internal office inside the building: no one from outside can access it; it is for employee-to-employee communication. LoadBalancer is the main entrance of the building with a security guard (the AWS Load Balancer) who directs external visitors to the right available office. Even if the tenants change (Pods restart), the building address remains the same.

A Service exposes a set of Pods via a stable IP and internal DNS name, regardless of the lifecycle of individual Pods.

TypeAccessibilityUse case
ClusterIP Only inside the cluster Inter-service communication (database, cache)
NodePort From outside via the Node IP + a fixed port (30000-32767) Testing, development (not recommended in production)
LoadBalancer From outside via an AWS Load Balancer (CLB/NLB) automatically provisioned Expose a service directly (without Ingress)
ExternalName Resolves to an external DNS name Access RDS, ElastiCache services via an internal name
output
---
# ClusterIP Service — for internal communication
apiVersion: v1
kind: Service
metadata:
  name: svc-mon-api
  namespace: mon-application
  labels:
    app: mon-api-fastapi
spec:
  type: ClusterIP
  selector:
    app: mon-api-fastapi      # Matches the Deployment labels
  ports:
    - name: http
      port: 80                # Service port (internal)
      targetPort: 8080        # Container port
      protocol: TCP

---
# LoadBalancer Service — expose via an AWS NLB
apiVersion: v1
kind: Service
metadata:
  name: svc-mon-api-public
  namespace: mon-application
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "external"
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
    service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
spec:
  type: LoadBalancer
  selector:
    app: mon-api-fastapi
  ports:
    - name: http
      port: 80
      targetPort: 8080

3. Kubernetes ConfigMaps and Secrets

ConfigMap — non-sensitive configuration

output
apiVersion: v1
kind: ConfigMap
metadata:
  name: config-mon-api
  namespace: mon-application
data:
  # Environment variables
  LOG_LEVEL: "info"
  WORKERS: "4"
  TIMEOUT: "30"
  DATABASE_HOST: "rds-mon-app.ca-central-1.rds.amazonaws.com"
  DATABASE_NAME: "production"
  # Complete configuration file
  application.yaml: |
    server:
      host: 0.0.0.0
      port: 8080
    database:
      pool_size: 10
      max_overflow: 5
    cache:
      backend: redis
      ttl: 300

Kubernetes Secret

output
apiVersion: v1
kind: Secret
metadata:
  name: secrets-mon-api
  namespace: mon-application
type: Opaque
stringData:
  # stringData accepts clear-text values — K8s automatically base64-encodes them
  DATABASE_PASSWORD: "MonMotDePasseSecret"
  API_KEY: "sk-abc123..."
  JWT_SECRET: "ma-cle-jwt-super-secrete"
WARNINGCaution: By default, Kubernetes Secrets are simply base64-encoded (not encrypted) in etcd. For stronger security on EKS, enable Secret encryption with a KMS key, and consider using AWS Secrets Manager + External Secrets Operator to automatically synchronize secrets.

External Secrets Operator — synchronize from Secrets Manager

output
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: secrets-aws-synchronises
  namespace: mon-application
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: secrets-mon-api   # Name of the Kubernetes Secret to create
    creationPolicy: Owner
  data:
    - secretKey: DATABASE_PASSWORD
      remoteRef:
        key: "mon-app/production/secrets"
        property: "database_password"
    - secretKey: API_KEY
      remoteRef:
        key: "mon-app/production/secrets"
        property: "api_key"
va-plus-loin

This article covers the most useful snippets — the complete AWS DevOps Infrastructure Code course (10 chapters, 39 lessons, corrected exercises and final project) takes you all the way.

./access-the-complete-course free course: Mastering Claude Code

FAQ

How long does it take to learn AWS DevOps Infrastructure Code?
With a structured progression (10 chapters, 39 short and practical lessons), you reach an operational level in a few weeks at 30 to 60 minutes per day. The key is to practice each concept immediately.
Are there any prerequisites?
It is best to be comfortable with the fundamentals of the domain: this content goes in depth, with real-world cases.
Where to start concretely?
Reproduce the commands from this article, then follow the complete AWS DevOps Infrastructure Code course: it chains the 39 lessons in order, with exercises and a final project.

📬 Want to receive this type of guide every week? Subscribe for free — real code, zero fluff.