Aller au contenu principal

Projet 6 : Pipeline CI/CD Terraform


Contexte

Jusqu'à présent, vous avez exécuté Terraform manuellement. Il est temps d'automatiser ! Ce projet met en place un pipeline CI/CD complet pour les déploiements Terraform.

Exigences

  • Pull Request avec plan automatique
  • Approbation manuelle pour production
  • State sécurisé sur S3
  • Secrets gérés via AWS Secrets Manager
  • Notifications Slack

Architecture CI/CD


Structure du projet

infra-cicd/
├── .github/
│ ├── workflows/
│ │ ├── terraform-plan.yml
│ │ ├── terraform-apply.yml
│ │ └── terraform-destroy.yml
│ └── CODEOWNERS
├── modules/
│ └── ...
├── environments/
│ ├── dev/
│ ├── staging/
│ └── prod/
├── scripts/
│ ├── setup-backend.sh
│ └── notify-slack.sh
└── .pre-commit-config.yaml

Configuration du Backend

scripts/setup-backend.sh

#!/bin/bash
set -e

PROJECT_NAME="myapp"
REGION="eu-west-1"

# Créer le bucket S3
aws s3api create-bucket \
--bucket ${PROJECT_NAME}-terraform-state \
--region ${REGION} \
--create-bucket-configuration LocationConstraint=${REGION}

# Activer le versioning
aws s3api put-bucket-versioning \
--bucket ${PROJECT_NAME}-terraform-state \
--versioning-configuration Status=Enabled

# Activer le chiffrement
aws s3api put-bucket-encryption \
--bucket ${PROJECT_NAME}-terraform-state \
--server-side-encryption-configuration '{
"Rules": [
{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms"
}
}
]
}'

# Bloquer l'accès public
aws s3api put-public-access-block \
--bucket ${PROJECT_NAME}-terraform-state \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

# Créer la table DynamoDB pour les locks
aws dynamodb create-table \
--table-name terraform-locks \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region ${REGION}

echo "Backend configured successfully!"

GitHub Actions Workflows

.github/workflows/terraform-plan.yml

name: Terraform Plan

on:
pull_request:
branches:
- main
paths:
- 'environments/**'
- 'modules/**'
- '.github/workflows/terraform-*.yml'

env:
TF_VERSION: "1.6.0"
AWS_REGION: "eu-west-1"

permissions:
id-token: write
contents: read
pull-requests: write

jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
environments: ${{ steps.filter.outputs.changes }}
steps:
- uses: actions/checkout@v4

- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
dev:
- 'environments/dev/**'
- 'modules/**'
staging:
- 'environments/staging/**'
- 'modules/**'
prod:
- 'environments/prod/**'
- 'modules/**'

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

- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}

- name: Terraform Format Check
run: terraform fmt -check -recursive

- name: TFLint
uses: reviewdog/action-tflint@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
reporter: github-pr-review
fail_on_error: true

- name: Checkov Security Scan
uses: bridgecrewio/checkov-action@v12
with:
directory: .
framework: terraform
output_format: github_failed_only
soft_fail: true

plan:
needs: [detect-changes, lint]
runs-on: ubuntu-latest
strategy:
matrix:
environment: ${{ fromJson(needs.detect-changes.outputs.environments) }}
fail-fast: false

steps:
- uses: actions/checkout@v4

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-terraform
aws-region: ${{ env.AWS_REGION }}

- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}

- name: Terraform Init
working-directory: environments/${{ matrix.environment }}
run: terraform init -input=false

- name: Terraform Validate
working-directory: environments/${{ matrix.environment }}
run: terraform validate

- name: Terraform Plan
id: plan
working-directory: environments/${{ matrix.environment }}
run: |
terraform plan -input=false -no-color -out=tfplan 2>&1 | tee plan.txt
echo "plan<<EOF" >> $GITHUB_OUTPUT
cat plan.txt >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
continue-on-error: true

- name: Save Plan Artifact
uses: actions/upload-artifact@v4
with:
name: tfplan-${{ matrix.environment }}
path: environments/${{ matrix.environment }}/tfplan
retention-days: 7

- name: Comment PR with Plan
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const environment = '${{ matrix.environment }}';
const plan = `${{ steps.plan.outputs.plan }}`;
const planStatus = '${{ steps.plan.outcome }}';

const output = `## Terraform Plan - ${environment.toUpperCase()} 🏗️

**Status**: ${planStatus === 'success' ? '✅ Success' : '❌ Failed'}

<details>
<summary>Show Plan</summary>

\`\`\`terraform
${plan.substring(0, 65000)}
\`\`\`

</details>

*Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;

github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
});

- name: Plan Status
if: steps.plan.outcome == 'failure'
run: exit 1

cost-estimation:
needs: [detect-changes]
runs-on: ubuntu-latest
if: needs.detect-changes.outputs.environments != '[]'
steps:
- uses: actions/checkout@v4

- name: Infracost
uses: infracost/actions/setup@v3
with:
api-key: ${{ secrets.INFRACOST_API_KEY }}

- name: Generate Cost Estimate
run: |
infracost breakdown --path=. \
--format=json \
--out-file=/tmp/infracost.json

- name: Post Cost Comment
run: |
infracost comment github \
--path=/tmp/infracost.json \
--repo=${{ github.repository }} \
--pull-request=${{ github.event.pull_request.number }} \
--github-token=${{ secrets.GITHUB_TOKEN }} \
--behavior=update

.github/workflows/terraform-apply.yml

name: Terraform Apply

on:
push:
branches:
- main
paths:
- 'environments/**'
- 'modules/**'
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy'
required: true
type: choice
options:
- dev
- staging
- prod

env:
TF_VERSION: "1.6.0"
AWS_REGION: "eu-west-1"

permissions:
id-token: write
contents: read

jobs:
deploy-dev:
runs-on: ubuntu-latest
environment: dev
outputs:
apply_status: ${{ steps.apply.outcome }}
steps:
- uses: actions/checkout@v4

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-terraform
aws-region: ${{ env.AWS_REGION }}

- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}

- name: Terraform Init
working-directory: environments/dev
run: terraform init -input=false

- name: Terraform Apply
id: apply
working-directory: environments/dev
run: terraform apply -input=false -auto-approve

- name: Notify Slack
if: always()
uses: slackapi/slack-github-[email protected]
with:
payload: |
{
"text": "Terraform Apply DEV: ${{ steps.apply.outcome }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Terraform Apply - DEV*\n*Status*: ${{ steps.apply.outcome == 'success' && '✅ Success' || '❌ Failed' }}\n*Commit*: ${{ github.sha }}\n*Actor*: ${{ github.actor }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

deploy-staging:
needs: deploy-dev
runs-on: ubuntu-latest
environment: staging
if: needs.deploy-dev.outputs.apply_status == 'success'
outputs:
apply_status: ${{ steps.apply.outcome }}
steps:
- uses: actions/checkout@v4

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-terraform
aws-region: ${{ env.AWS_REGION }}

- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}

- name: Terraform Init
working-directory: environments/staging
run: terraform init -input=false

- name: Terraform Apply
id: apply
working-directory: environments/staging
run: terraform apply -input=false -auto-approve

# Tests post-déploiement
- name: Run Integration Tests
run: |
cd tests
./run-integration-tests.sh staging

deploy-prod:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production # Requiert approbation manuelle
if: needs.deploy-staging.outputs.apply_status == 'success'
steps:
- uses: actions/checkout@v4

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID_PROD }}:role/github-actions-terraform
aws-region: ${{ env.AWS_REGION }}

- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}

- name: Terraform Init
working-directory: environments/prod
run: terraform init -input=false

- name: Terraform Apply
id: apply
working-directory: environments/prod
run: terraform apply -input=false -auto-approve

- name: Notify Slack (Production)
if: always()
uses: slackapi/slack-github-[email protected]
with:
payload: |
{
"text": "🚀 PRODUCTION Deploy: ${{ steps.apply.outcome }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*🚀 Terraform Apply - PRODUCTION*\n*Status*: ${{ steps.apply.outcome == 'success' && '✅ Success' || '❌ Failed' }}\n*Commit*: ${{ github.sha }}\n*Actor*: ${{ github.actor }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Configuration AWS IAM pour OIDC

iam-oidc-role.tf

# OIDC Provider pour GitHub Actions
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

# IAM Role pour GitHub Actions
resource "aws_iam_role" "github_actions_terraform" {
name = "github-actions-terraform"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:${var.github_org}/${var.github_repo}:*"
}
}
}
]
})
}

# Politique Terraform
resource "aws_iam_role_policy" "terraform" {
name = "terraform-permissions"
role = aws_iam_role.github_actions_terraform.id

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket"
]
Resource = [
"arn:aws:s3:::${var.state_bucket}",
"arn:aws:s3:::${var.state_bucket}/*"
]
},
{
Effect = "Allow"
Action = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
]
Resource = "arn:aws:dynamodb:*:*:table/terraform-locks"
},
{
Effect = "Allow"
Action = [
"ec2:*",
"rds:*",
"elasticache:*",
"iam:*",
"eks:*",
"s3:*",
"cloudwatch:*",
"logs:*",
"kms:*"
]
Resource = "*"
Condition = {
StringEquals = {
"aws:RequestedRegion" = var.region
}
}
}
]
})
}

Pre-commit Hooks

.pre-commit-config.yaml

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: detect-private-key

- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.83.6
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_docs
args:
- --args=--config=.terraform-docs.yml
- id: terraform_tflint
args:
- --args=--config=__GIT_WORKING_DIR__/.tflint.hcl
- id: terraform_checkov
args:
- --args=--quiet
- --args=--compact

- repo: https://github.com/gruntwork-io/pre-commit
rev: v0.1.23
hooks:
- id: tflint
- id: terraform-validate

GitHub Environments

Configuration via Terraform

# Utiliser le provider GitHub
terraform {
required_providers {
github = {
source = "integrations/github"
version = "~> 5.0"
}
}
}

provider "github" {
owner = var.github_org
}

# Environment Dev
resource "github_repository_environment" "dev" {
environment = "dev"
repository = var.github_repo
}

# Environment Staging
resource "github_repository_environment" "staging" {
environment = "staging"
repository = var.github_repo

reviewers {
teams = [data.github_team.devops.id]
}

deployment_branch_policy {
protected_branches = true
custom_branch_policies = false
}
}

# Environment Production (avec approbation)
resource "github_repository_environment" "production" {
environment = "production"
repository = var.github_repo

reviewers {
users = [data.github_user.lead.id]
teams = [data.github_team.leads.id]
}

deployment_branch_policy {
protected_branches = true
custom_branch_policies = false
}
}

# Secrets par environnement
resource "github_actions_environment_secret" "aws_account_id" {
for_each = toset(["dev", "staging", "production"])

repository = var.github_repo
environment = each.key
secret_name = "AWS_ACCOUNT_ID"
plaintext_value = var.aws_accounts[each.key]
}

Workflow de travail


Résumé

Ce pipeline CI/CD offre :

  • Automatisation : Déploiement continu sans intervention
  • Sécurité : Approbation pour production
  • Visibilité : Plans dans les PRs
  • Traçabilité : Historique complet dans Git

← Projet 5 | Projet 7: Stack Monitoring →