Aller au contenu principal

Bonnes Pratiques CloudFormation


Organisation des templates

Structure recommandée

project/
├── templates/
│ ├── main.yaml # Stack principale
│ ├── network/
│ │ └── vpc.yaml
│ ├── security/
│ │ └── iam.yaml
│ └── compute/
│ └── ec2.yaml
├── parameters/
│ ├── dev.json
│ ├── staging.json
│ └── prod.json
├── scripts/
│ ├── deploy.sh
│ └── validate.sh
├── tests/
│ └── taskcat.yml
└── README.md

Convention de nommage

# ✅ Bon - Noms descriptifs et cohérents
Resources:
ProductionVPC:
Type: AWS::EC2::VPC

ProductionPublicSubnet1:
Type: AWS::EC2::Subnet

ProductionPrivateSubnet1:
Type: AWS::EC2::Subnet

WebServerSecurityGroup:
Type: AWS::EC2::SecurityGroup

# ❌ Mauvais - Noms cryptiques
Resources:
VPC1:
Type: AWS::EC2::VPC

Sub1:
Type: AWS::EC2::Subnet

SG:
Type: AWS::EC2::SecurityGroup

Paramètres

Validation stricte

Parameters:
Environment:
Type: String
AllowedValues:
- development
- staging
- production
ConstraintDescription: Must be development, staging, or production

InstanceType:
Type: String
AllowedValues:
- t3.micro
- t3.small
- t3.medium
- t3.large
Default: t3.micro

VPCCidr:
Type: String
AllowedPattern: '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$'
ConstraintDescription: Must be a valid CIDR block
Default: '10.0.0.0/16'

DatabasePassword:
Type: String
NoEcho: true
MinLength: 12
MaxLength: 41
AllowedPattern: '[a-zA-Z0-9!@#$%^&*()_+-=]*'
ConstraintDescription: Min 12 chars, alphanumeric + special chars

Utiliser SSM Parameter Store

Parameters:
# AMI dynamique depuis SSM
AMIId:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2

# Secrets depuis Secrets Manager
DatabasePassword:
Type: String
Default: '{{resolve:secretsmanager:prod/db/password:SecretString:password}}'
NoEcho: true

Sécurité

Principe du moindre privilège

# ✅ Bon - Permissions minimales
Resources:
LambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: MinimalAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource: !Sub '${MyBucket.Arn}/data/*'
- Effect: Allow
Action:
- logs:CreateLogStream
- logs:PutLogEvents
Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*'

# ❌ Mauvais - Permissions trop larges
Resources:
LambdaRole:
Type: AWS::IAM::Role
Properties:
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AdministratorAccess

Chiffrement par défaut

Resources:
# S3 avec chiffrement
SecureBucket:
Type: AWS::S3::Bucket
Properties:
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: aws:kms
KMSMasterKeyID: !Ref KMSKey
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true

# RDS avec chiffrement
Database:
Type: AWS::RDS::DBInstance
Properties:
StorageEncrypted: true
KmsKeyId: !Ref KMSKey

# EBS avec chiffrement
Volume:
Type: AWS::EC2::Volume
Properties:
Encrypted: true
KmsKeyId: !Ref KMSKey

Security Groups restrictifs

Resources:
# ✅ Bon - Source spécifique
WebSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Web server security group
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: !Ref ALBSecurityGroup # Depuis l'ALB seulement

# ❌ Mauvais - Ouvert au monde
InsecureSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 0.0.0.0/0 # SSH ouvert au monde!

Gestion du cycle de vie

DeletionPolicy

Resources:
# Ressources critiques - RETAIN
ProductionDatabase:
Type: AWS::RDS::DBInstance
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
DeletionProtection: true
# ...

# Données importantes - SNAPSHOT
DataVolume:
Type: AWS::EC2::Volume
DeletionPolicy: Snapshot
Properties:
# ...

# Logs et données non-critiques
TempBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Delete
Properties:
LifecycleConfiguration:
Rules:
- ExpirationInDays: 30
Status: Enabled

UpdatePolicy pour ASG

Resources:
AutoScalingGroup:
Type: AWS::AutoScaling::AutoScalingGroup
UpdatePolicy:
AutoScalingRollingUpdate:
MinInstancesInService: 2
MaxBatchSize: 1
PauseTime: PT5M
WaitOnResourceSignals: true
SuspendProcesses:
- HealthCheck
- ReplaceUnhealthy
- AZRebalance
- AlarmNotification
- ScheduledActions
CreationPolicy:
ResourceSignal:
Count: 2
Timeout: PT15M

Modularité

Mappings pour la configuration

Mappings:
EnvironmentConfig:
development:
InstanceType: t3.micro
MinSize: 1
MaxSize: 2
MultiAZ: false
BackupRetention: 1
staging:
InstanceType: t3.small
MinSize: 2
MaxSize: 4
MultiAZ: false
BackupRetention: 7
production:
InstanceType: t3.medium
MinSize: 2
MaxSize: 10
MultiAZ: true
BackupRetention: 30

Resources:
LaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateData:
InstanceType: !FindInMap [EnvironmentConfig, !Ref Environment, InstanceType]

Database:
Type: AWS::RDS::DBInstance
Properties:
MultiAZ: !FindInMap [EnvironmentConfig, !Ref Environment, MultiAZ]
BackupRetentionPeriod: !FindInMap [EnvironmentConfig, !Ref Environment, BackupRetention]

Conditions pour la flexibilité

Conditions:
IsProduction: !Equals [!Ref Environment, production]
CreateReadReplica: !And
- !Condition IsProduction
- !Equals [!Ref EnableReadReplica, 'true']

Resources:
Database:
Type: AWS::RDS::DBInstance
Properties:
MultiAZ: !If [IsProduction, true, false]
DeletionProtection: !If [IsProduction, true, false]

ReadReplica:
Type: AWS::RDS::DBInstance
Condition: CreateReadReplica
Properties:
SourceDBInstanceIdentifier: !Ref Database

Tagging

Tags cohérents

Parameters:
Environment:
Type: String
Project:
Type: String
Owner:
Type: String
Default: platform-team

Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
Tags:
- Key: Name
Value: !Sub '${Project}-${Environment}-vpc'
- Key: Environment
Value: !Ref Environment
- Key: Project
Value: !Ref Project
- Key: Owner
Value: !Ref Owner
- Key: ManagedBy
Value: CloudFormation
- Key: StackName
Value: !Ref AWS::StackName

# Utiliser des ancres YAML pour les tags communs
CommonTags: &common-tags
- Key: Environment
Value: !Ref Environment
- Key: Project
Value: !Ref Project
- Key: Owner
Value: !Ref Owner
- Key: ManagedBy
Value: CloudFormation

Subnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.1.0/24
Tags:
- Key: Name
Value: !Sub '${Project}-${Environment}-subnet'
# Réutiliser les tags communs
<<: *common-tags

CI/CD et validation

Script de validation

#!/bin/bash
# validate.sh

set -e

echo "=== Validating CloudFormation templates ==="

# Installer cfn-lint si nécessaire
pip install cfn-lint --quiet

# Valider tous les templates
for template in templates/**/*.yaml; do
echo "Validating: $template"

# Validation syntaxique AWS
aws cloudformation validate-template \
--template-body file://$template

# Linting avec cfn-lint
cfn-lint $template
done

echo "=== All templates valid ==="

Fichier cfn-lint

# .cfnlintrc
templates:
- templates/**/*.yaml

ignore_checks:
- W3002 # Hardcoded partition
- W2001 # Unused parameter

configure_rules:
E3012:
strict: false

GitHub Actions

# .github/workflows/cfn-validate.yml
name: CloudFormation Validation

on:
pull_request:
paths:
- 'templates/**'

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install cfn-lint
run: pip install cfn-lint

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1

- name: Validate templates
run: |
for template in templates/**/*.yaml; do
echo "Validating $template"
aws cloudformation validate-template --template-body file://$template
cfn-lint $template
done

Anti-patterns à éviter

1. Hardcoder les valeurs

# ❌ Mauvais
Resources:
Instance:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-12345678 # Hardcodé!
SubnetId: subnet-abcdef # Hardcodé!

# ✅ Bon
Parameters:
AMIId:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2

Resources:
Instance:
Type: AWS::EC2::Instance
Properties:
ImageId: !Ref AMIId
SubnetId: !Ref PrivateSubnet

2. Templates trop volumineux

# ❌ Mauvais - Un seul template énorme
# template.yaml (3000+ lignes)

# ✅ Bon - Nested stacks
Resources:
NetworkStack:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: !Sub 'https://s3.amazonaws.com/${Bucket}/network.yaml'

ComputeStack:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: !Sub 'https://s3.amazonaws.com/${Bucket}/compute.yaml'

3. Ignorer les dépendances

# ❌ Mauvais - Route créée avant IGW attaché
Resources:
PublicRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
# Manque DependsOn!

# ✅ Bon
Resources:
PublicRoute:
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway

Checklist

□ Templates validés avec cfn-lint
□ Paramètres avec contraintes et valeurs par défaut
□ DeletionPolicy sur les ressources critiques
□ Tags cohérents sur toutes les ressources
□ Chiffrement activé (S3, RDS, EBS)
□ Security Groups restrictifs
□ IAM avec moindre privilège
□ Outputs documentés et exportés si nécessaire
□ Pas de secrets hardcodés (utiliser SSM/Secrets Manager)
□ Code versionné dans Git

← Stacks Imbriquées | Exercices et Projets →