Déployer un projet Symfony sur AWS Lambda avec Pulumi et Bref
CEO/CTO/CMO/Dev/Ops/Support at Panda Code
Préambule
Je me lance dans une petite série d'articles concernant le déploiement d'une application Symfony sur AWS Lambda en gérant la partie infrastructure (tout ou presque ce qui n'est pas Lambda) via l'outil d'IaC Pulumi.
Le contenu sera assez (con)centré sur le code utile pour arriver au résultat. Il n'y aura pas de longue explication de théorie ou de concept. J'essaierai cependant de fournir un maximum de liens pour vous aider à appréhender ou creuser les sujets.
Le contenu des articles ne sera pas des tutoriels pas à pas mais je fournirai un repository Github comprenant le code fonctionnel pour chaque article afin de permettre de bien comprendre comment fonctionnent ensemble tous les extraits de code.
Quelques outils/concepts/services avant de commencer
Il y a pas mal d'outils/concepts ou services évoqués dans cet article. Si vous êtes arrivés ici, c'est que vous en connaissez au moins un, mais je vais essayer de définir rapidement les autres et donner quelques liens utiles.
Amazon Web Services (AWS) et Lambda : Le CLOUD d'Amazon. Des centaines (pour de vrai, plus de 500) de services dont le service AWS Lambda qui permet d'exécuter du code "sans serveur".
Serverless Framework et serverless : avec une majuscule (au moins dans cet article), c'est un framework qui permet de déployer des applications sur le service AWS Lambda. Avec une minuscule, c'est le concept d'avoir du code exécuté sans (gérer soi-même les) serveurs.
Pulumi : c'est un outil d'Infrastructure As Code (IAC). A la différence d'autres outils comme Terraform/AWS CloudFormation, il n'impose pas d'apprendre un DSL mais permet d'utiliser un langage de programmation comme le Typescript dans le cas de cet article. Le tutoriel pour AWS, c'est par là.
Symfony : un framework PHP.
Bref : un (même plusieurs en fait) package Composer qui permet de faire tourner du PHP sur AWS Lambda.
Notre MVP (minimum viable "Project")
Le MVP de notre project sera une simple API (/api/ok) écrite en PHP avec le framework Symfony qui tournera sur AWS Lambda.
Dans les articles suivants, on ajoutera des fonctionnalités supplémentaires étape par étape :
utilisation d'un nom de domaine personnalisé
ajout d'une tâche planifiée CRON
ajout d'un serveur de base de données (Aurora Serverless of course)
utilisation de file de messages (SQS)
déploiement via Github Actions
La partie "Infrastructure"
Pour pouvoir déployer et exécuter notre code PHP sur AWS Lamba via un appel HTTP, nous allons avoir besoin de créer quelques ressources sur quelques services différents de la plate-forme AWS.
La "base" réseau : le VPC (Amazon Virtual Private Cloud )
Lorsque vous créez un compte AWS, vous avez un VPC créé par défaut, mais nous allons en créer un tout nouveau avec sous-réseau "isolé" dans lequel on ajoutera plus tard notre base de données et avec une passerelle (NAT Gateway) pour que notre code PHP puisse accéder à l'internet (pour appeler d'autres APIs).
// src/vpc/index.ts
import * as awsx from "@pulumi/awsx";
export const vpc = new awsx.ec2.Vpc("vpc", {
natGateways: {
strategy: "None", // Change to "Single" if you want your Lambda to be able to access the internet
},
subnetSpecs: [
{ type: "Public" },
{ type: "Private" },
{ type: "Isolated", name: "databases" } // => pour plus tard
],
});
API Gateway
Nous allons ensuite créer une API sur le service API Gateway pour nous permettre d'avoir une URL pour appeler le code PHP. Nous utiliserons la V2 qui a moins de fonctionnalités mais qui est aussi moins coûteuse.
// src/api-gateway/index.ts
import * as aws from "@pulumi/aws";
export const apiApi = new aws.apigatewayv2.Api("api", {
protocolType: "HTTP",
});
new aws.apigatewayv2.Stage("api.default", {
apiId: apiApi.id,
autoDeploy: true,
name: "$default"
});
Les droits (I.AM in hell ?^^)
Je pars du postulat que la personne ou l'outil qui déploie l'infra via Pulumi ou le code via Serverless Framework a tous les droits. Cependant, une fois l'infra et le code déployé, il faut définir des droits qui seront utilisés lors de l'exécution.
Nous allons créer un rôle que nous attribuerons à nos fonctions sur AWS Lambda. On donnera à ce rôle les droits nécessaires à l'accès au réseau (VPC), l'accès à la base de données (dans un futur article) et celui de lire ou envoyer des messages via le service SQS.
Dans ce premier article, on va se limiter à la partie réseau/VPC. On va attribuer au rôle une Managed Policy (c'est-à-dire qu'elle est fournie par AWS).
// src/iam/index.ts
import * as aws from "@pulumi/aws";
export const lambdaRole = new aws.iam.Role("lambda", {
assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal(aws.iam.Principals.LambdaPrincipal),
});
new aws.iam.RolePolicyAttachment("RoleLambdaPoliciesVpcAccessExecution", {
role: lambdaRole,
policyArn: aws.iam.ManagedPolicies.AWSLambdaVPCAccessExecutionRole,
});
Transition vers le projet PHP (AWS Systems Manager)
Nous avons désormais toutes les ressources "Infra" nécessaires pour faire tourner notre projet PHP dans le Cloud. Il va cependant falloir fournir à Serverless de quoi se "brancher" dessus.
Nous allons utiliser le service AWS Systems Manager qui va nous permettre de stocker des données que Serverless viendra lire au moment du déploiement. Ces données seront :
l'id de l'API (Gateway)
l'id (l'ARN pour être plus exacte) du rôle IAM
l'id du Security Group par défaut du VPC
les ids des sous-réseaux privés du VPC
// src/ssm/index.ts
import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";
import {apiApi} from "../api-gateway";
import {vpc} from "../vpc";
import {lambdaRole} from "../iam";
const config = new pulumi.Config();
new aws.ssm.Parameter("apigateway.api.id", {
type: "String",
name: "/my-project/api-gateway/http_api_id",
value: apiApi.id,
});
new aws.ssm.Parameter("lambda.role", {
type: "String",
name: "/my-project/lambda/role-arn",
value: lambdaRole.arn,
});
new aws.ssm.Parameter("lambda.securityGroup", {
type: "String",
name: "/my-project/lambda/security_group-id",
value: vpc.vpc.defaultSecurityGroupId,
});
export const ssmVpcPrivateSubnetIds = new aws.ssm.Parameter("vpc.subnet.ids", {
type: "StringList",
name: "/my-project/vpc/private_subnet_ids",
value: pulumi.concat(vpc.privateSubnetIds),
});
Nous allons aussi utiliser ce système pour définir les nombreuses variables d'environnement que l'on peut avoir besoin de définir dans notre projet Symfony. Pour notre MVP, cela se limitera à la variable APP_SECRET.
// src/ssm/index.ts
// ...
const symfonyEnvVars = config.requireObject<string[]>("symfony");
for (const key in symfonyEnvVars) {
new aws.ssm.Parameter(`symfony.env.vars-${key}`, {
type: "String",
name: `/my-project/symfony/envvars/${key}`,
value: symfonyEnvVars[key],
});
}
Le fichier README du repository indique comment ajouter de nouvelles variables.
Add a Symfony env vars :
```bash
pulumi config set --path 'symfony["APP_BASE_URL"]' https://localhost:5173
pulumi config set --path 'symfony["APP_SECRET"]' somesecret --secret
```
Let's create
Si on exécute la commande pulumi up, on peut voir l'ensemble des ressources qui vont être créées :

La partie "code" : PHP / Symfony
Bref
Cette deuxième partie du projet va être plus simple grâce à Bref qui permet de faire tourner du code PHP et fournit un package spécialement adapté pour Symfony (cf documentation pour l'installation). Bref utilise Serverless Framework pour le déploiement des fonctions Lambda sur la plate-forme AWS.
Nous allons modifier le fichier serverless.yml pour utiliser les informations que nous avons stockées dans AWS Systems Manager (SSM).
# serverless.yml
service: pulumi-php-bref-1
provider:
name: aws
region: ${opt:region, "eu-west-3"}
stage: prod
runtime: provided.al2
role: ${ssm:/my-project/lambda/role-arn} # Notre role IAM
vpc:
securityGroupIds:
- ${ssm:/my-project/lambda/security_group-id}
subnetIds: ${ssm:/my-project/vpc/private_subnet_ids}
httpApi:
id: ${ssm:/my-project/api-gateway/http_api_id}
environment:
APP_ENV: prod
# Notre variable d'environnement définie dans le projet Infra
APP_SECRET: ${ssm:/my-project/symfony/envvars/APP_SECRET}
#...
Il ne reste plus qu'à déployer en utilisant les instructions contenues dans le README du projet.
Deploy
```bash
bin/console cache:clear --env=prod
bin/console cache:warmup --env=prod
serverless deploy --stage prod
```
Et voilà, nous avons l'URL de notre API :

Et (normalement) ça marche ! ;)

This is only the beginning ?!
Nous avons mise en place notre MVP : mais ce n'est que le début ! Le code complet est disponible dans le repository Github.
Dans le prochain article, nous verrons comment utiliser un nom de domaine personnalisé pour notre API en utilisant le service AWS Route 53.
N'hésitez pas à commenter ce post, ou me laisser un message sur Twitter/Linkedin pour toutes remarques de tout type (erreur, avis, envie, question, ...) : j'essaierai d'en tenir compte et d'y répondre rapidement.