Aller au contenu

Setter une Github Action pour deployer son infra-as-code (Terraform) sur AWS via OIDC

Comment parametrer sa pipeline et ses credentials pour communiquer vers AWS depuis sa Github action, et utiliser ces services.

Deploiement le tf.state de son Terraform vers un AWS S3 Bucket, deploy son infrastructure, etc.

Je continue de développer ce blog ci, et vains le moment ou j'ai voulu initialiser une première version de pipeline, avec Github Action. Je souhaite que pour toute PR qui est mergé sur ma branche master, que cela trigger un workflow.

Pré-requis

Repo :

  • un répo configuré avec les github actions d'activé

Tool :

  • AWS compte déjà configuré

Github Self Hosted Runners

Pour t'éviter de cramer tes minutes gratos de github actions et de te retrouver à sec à ne plus pouvoir rien run, je te conseille de configurer ta machine pour heberger tes propres runners. GitHub fournit l'agent pour Linux, macOS et Windows, ça te prends 2min, et tu peux lancer autant de pipeline que tu veux sans te soucier de la consomation.

N'utilise jamais un runner self-hosted sur un repository public : n'importe qui pourrait ouvrir une PR depuis un fork et exécuter du code sur ta machine.

Création de notre CICD (workflow, Github Action)

Pour l'exemple on va se baser sur le même exemple que j'ai fais pour automatiser ce site là. C'est un site sous MkDocs. On veut :

Main/Common

# .github/workflows/cicd.yaml
name: cicd
on:
  workflow_dispatch:
  push:
    branches:
      - master 

permissions:
  id-token: write  # obligé pour request
  contents: read   # obligé pour action/checkout

env:
  BUILD_DIR: "public"
  BUCKET_NAME: "bastienmaurice.fr"
  • on: ici je défini que je puisse trigger ma pipeline soit depuis l'interface web, soit sur un merge d'une PR sur ma branche master

  • env: ici on s'en tape, ce n'est que des vars d'environnement propre à mon projet

Permissions

permissions: cette props est super importante. Sans id-token: write, ta pipeline ne peut pas requêter l'OIDC pour obtenir le token JWT/jeton.


Si tu veux la permission de request à travers tout ton workflow il te faut:

1
2
3
4
# .github/workflows/cicd.yaml
permissions:
  id-token: write  # obligé pour request
  contents: read   # obligé pour action/checkout

Si tu veux request juste depuis un job:

1
2
3
4
# .github/workflows/cicd.yaml

permissions:
  id-token: write  # obligé pour request


Plus tard je vais utiliser une props runs-on qui à pour value self-hosted. Ici je ne fais que run mes pipelines afin qu'elles target des runners qui sont labélisé self-hosted, qui tournent en local chez moi.

Job Build

# .github/workflows/cicd.yaml

jobs: 
  build:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-python@v7
        with:
          python-version: "3.13"
      - run: pip install -r requirements.txt
      - run: mkdocs build
      - uses: actions/upload-artifact@v7
        with: 
          name: site
          path: './${{ env.BUILD_DIR }}'
  • actions/checkout: une action me permettant de checkout mon répository, sur ma branche ou tourne la pipeline, sur mon runner
  • actions/setup-python: une action qui m'install python


Par la suite je lance pip pour m'install mes dependances nécéssaire à mon projet python.
Je build mon application Mkdocs, avec une commande de base de ce framerwork.

  • actions/upload-artifact: cette action va me sauvegarder le dossier de mon application web, fraichement buildé, comme artefact du workflow. Cela me permet de partager des données entre plusieurs jobs. Évite actions/cache pour ça : une clé de cache existante n'est jamais réécrite, tu risquerais de déployer un ancien build

Job Deploy

C'est ici, le job le plus important. C'est ici que je vous montre comment configurer l'action github de AWS, afin de setter les credentials permettant de communiquer avec depuis le pipeline.

# .github/workflows/cicd.yaml

jobs:
  # ... job build ...
  deploy:
    runs-on: self-hosted
    needs: build
    steps:
      - uses: actions/download-artifact@v8
        with:
          name: site
          path: './${{ env.BUILD_DIR }}'
      - uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-assume-role-access
          aws-region: eu-west-3
          role-session-name: githubactions
      - run: aws s3 sync --delete ./${{ env.BUILD_DIR }} s3://${{ env.BUCKET_NAME }}/
  • actions/download-artifact: on récupère ici mon site web, du job précédent

  • aws-actions/configure-aws-credentials: c'est l'action de AWS.

Voici les params obligatoires à décrire:

1
2
3
4
5
6
7
# .github/workflows/cicd.yaml

- uses: aws-actions/configure-aws-credentials@v6
  with:
    role-to-assume: arn:aws:iam::123456789012:role/ton_role # l'IAM de l'assume role que tu créeras plus tard dans le tuto
    aws-region: eu-west-3                          # la région ou run tes services
    role-session-name: githubactions               # un nom de pour la session que tu lances ; utiles car apparaît dans tes logs

  • run: ....: la commande finale. Maintenant que tu es authentifié, tu peux runs ce que tu veux :
    • terraform plan/apply/destroy...: terraform va se connecter directement à ton Bucket S3 afin d'y récupérer son tf.state
    • aws s3 ....: tu peux utiliser toute la CLI de AWS dans ta pipeline
    • ...et bien d'autres !

Création de l'identity provider (OIDC)

Il existe plusieurs façons de faire pour créer une authentication entre ta pipeline Github et AWS.

Identity Used aws-access-key-id role-to-assume web-identity-token-file role-chaining
[✅ Recommended] Assume Role directly using GitHub OIDC provider ✔
IAM User ✔
Assume Role using IAM User credentials ✔ ✔
Assume Role using WebIdentity Token File credentials ✔ ✔
Assume Role using existing credentials ✔ ✔

Ma première authentication du genre à été bête méchant. Récuperer un access_id et un secret depuis mon compte. Mais il existe deux soucis pour cette façon de faire. Alors oui je te l'accorde ça prends 2 copié collé et c'est très simple et rapide, mais pas la façon la plus secure de faire. De un, tu prends tes credentials de ton compte root. Tu te les fais voler d'un façon ou d'une autre, RIP. Le second points négatifs est que ce sont des crédentials que tu set une seule et une unique fois en var d'environnement de pipeline. Ils ne changent jamais et donc laisse une authentication à long terme.

Pour ce tutoriel là, je te propose d'utiliser un OIDC. Flemme de t'expliquer ce que c'est, mais ça te permet d'avoir une authorité qui va aller intéroger AWS depuis ta pipeline, demander un token, qui lui sera temporaire. De plus, aucun credentials n'a besoin d'être configuré dans ton repository à la mano.

La seule chose qui va apparaître dans ta pipeline sont :

  • la région ou tourne tes services AWS
  • un assume role id (arn)

Depuis la console AWS

  1. On ouvre le service IAM
  2. Onglet identity provider, créer un nouveau avec les infos suivantes:
    • Type: OpenID Connect
    • Provider url: token.actions.githubusercontent.com
    • Audience: sts.amazonaws.com

Depuis terraform

On créer notre ressource concernant le provider :

1
2
3
4
5
6
# github-actions-runners-roles.tf

resource "aws_iam_openid_connect_provider" "github_actions_oidc_provider" {
  url            = "https://token.actions.githubusercontent.com"
  client_id_list = ["sts.amazonaws.com"]
}

Et le thumbprint ?

Les anciens tutos récupèrent l'empreinte du certificat TLS avec un data "tls_certificate" pour remplir thumbprint_list. C'est inutile pour GitHub : AWS valide le certificat de token.actions.githubusercontent.com via ses autorités de confiance et ignore cette empreinte. L'argument est optionnel dans le provider AWS.

Création de l'IAM rôle qui va être assume par Github Actions

On peut desormais demander un token via l'identity provider configuré précédement.

Il va nous falloir maintenant créer un rôle. On va devoir lui définir deux choses :

  • Une Identity based policy : celle-ci s'applique à un user, group ou rôle. Ici on souhaite ajouter des permissions d'accès à notre rôle. On va lui donner l'accès aux bucket S3, nous permettant d'upload notre site sur notre bucket.
  • Une Trust policy : c'est la resource-based policy du rôle lui-même. On souhaite ici restreindre qui peut assume ce rôle ( et donc avoir accès aux S3 buckets ), qui ne concerne que des identités qui ont reçu un token valide entre Github/AWS de notre OIDC crée précédemment, et qu'enfin cela ne puisse s'appliquer qu'a un seul repository

Depuis la console AWS

Ici on défini une politique pour assumer ce role. Ici on va donner le rôle précédement crée 1. On ouvre le service IAM 2. Crée un rôle - Type: web identity - Identity provider: token.actions.githubusercontent.com - Audience: sts.amazonaws.com 3. On ajoute les permissions. Ce sont les droits que vont avoir tes github actions, au sein de ton cloud provider. Évite AmazonS3FullAccess, qui donne tous les droits sur tous les buckets du compte : crée plutôt une policy limitée à ton bucket (voir la version Terraform plus bas) 4. On ajoute une trust policy, celle-ci est un fichier JSON qui te défini pleins de chose.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "<iam_arn_de_ton_oidc_provider>"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                },
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:Momotoculteur/bastienmaurice-website:*"
                }
            }
        }
    ]
}

Le JSON n'accepte pas de commentaires (la console IAM refuserait la policy), donc les explications sont ici :

  • Effect Allow : l'action est une autorisation
  • Principal Federated : l'ARN de ton OIDC provider
  • Action sts:AssumeRoleWithWebIdentity : te permet d'assumer un role si ton web identity t'y autorise
  • StringEquals sur aud : on veut une valeur égale stricte
  • StringLike sur sub : on accepte un wildcard. Ici on target mon compte Github, sur un répo spécifique, dans sa globalité, ce qui veut dire toute branche

Example de policy pour une branche spécifique :

"token.actions.githubusercontent.com:sub": "repo:<mon_compte_github>/<mon_repo>:ref:refs/heads/<ma_remote_branch>"

Depuis terraform

On commence donc par crer le rôle, avec sa ressource based policy

# github-actions-runners-roles.tf

resource "aws_iam_role" "github_action_role" {  # Création d'un rôle
  name     = "github-actions-assume-role-access"

  assume_role_policy = jsonencode({   # Création de la policy
    Version = "2012-10-17"
    Statement = [
      {
        "Effect" : "Allow", # On autorise une action
        "Principal" : {
          "Federated" : "${aws_iam_openid_connect_provider.github_actions_oidc_provider.arn}" # On défini que cela provient de notre OIDC
        },
        "Action" : "sts:AssumeRoleWithWebIdentity", # L'action est d'assumer ce rôle via un token valide de l'OIDC
        "Condition" : {
          "StringEquals" : {
            "token.actions.githubusercontent.com:aud" : "sts.amazonaws.com",  # Communique avec le service de token de AWS
          },
          "StringLike" : {
            "token.actions.githubusercontent.com:sub" : ["repo:Momotoculteur/bastienmaurice-website:*"] # Ici on autorise d'assume le rôle de toute les branches d'un seul et unique répository github
          }
        }
      }
    ]
  })
}


On a plus qu' attacher une idendity based policy à notre rôle. Maintenant que l'on a défini qui peut assume de rôle, on souhaite ajouter quel droit d'accès à notre rôle. On applique le moindre privilège : juste ce qu'il faut à aws s3 sync --delete (lister le bucket, écrire et supprimer des objets), et uniquement sur le bucket du site. Si ta pipeline lance aussi du Terraform, ajoute les droits sur le bucket du state (voir l'initialisation d'un projet Terraform) :

# github-actions-runners-roles.tf

resource "aws_iam_role_policy" "github_action_role_permissions" {
  name = "github-actions-permissions"
  role = aws_iam_role.github_action_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["s3:ListBucket"]                # lister le contenu du bucket
        Resource = "arn:aws:s3:::bastienmaurice.fr"
      },
      {
        Effect   = "Allow"
        Action   = ["s3:PutObject", "s3:DeleteObject"] # upload et suppression des fichiers du site
        Resource = "arn:aws:s3:::bastienmaurice.fr/*"
      },
    ]
  })
}

On souhaite avoir en sortie de terraform apply l'arn à inscrire dans notre github action du role à assume:

1
2
3
4
5
# github-actions-runners-roles.tf

output "role_arn" {
  value = aws_iam_role.github_action_role.arn
}

Update de l'IAM assume role de notre pipeline

Maintenant que l'on a un identity provider, un rôle avec des droits, et un assume rôle défini sur ce rôle, on est bon. On n'a plus qu'a update une prop de l'action AWS dans notre pipeline pour setter les credentials. On récupère l'ARN de notre rôle et on l'ajoute comme ci-dessous :

1
2
3
- uses: aws-actions/configure-aws-credentials@v6
  with:
    role-to-assume: <arn_iam_de_ton_role_crée>

Pour aller plus loin, tu peux voir comment héberger ton site statique sur S3 et CloudFront avec Terraform.