Si vous me lisez régulièrement, vous savez qu’il y a deux technologies qui me passionnent particulièrement, le langage Go et les architectures Serverless. Faire le choix d’implémenter ses fonctions lambda en Go est rudement pertinent à mon avis.
Mais qu’en est-il de l’utilisation des processeurs Graviton 2, basés sur Arm?
Quelles sont les avantages escomptés, est-ce possible et comment le faire concrètement ?
Dans ce nouvel article je vais tacher de répondre à ces trois questions.

Quel intérêt ?

Jusqu’en septembre 2021 la question ne se posait même pas. AWS ne proposait qu’un seul type de processeur, basé sur du x86. Mais après avoir proposé des instances EC2 basées sur les processeurs Graviton 2, les fonctions lambda ont elles aussi bénéficié de cette innovation technologique.

Les processeurs Graviton 2 utilisent un cœur Neoverse-N1 et prennent en charge Arm V8.2 (y compris les extensions CRC et crypto) ainsi que plusieurs autres extensions architecturales. En particulier, Graviton 2 prend en charge les extensions LSE (Large System Extensions), qui améliorent les performances de verrouillage et de synchronisation. En pratique, cela se traduit par de potentiels gains de performance pour les applications gourmandes en CPU.

Comme vous le savez probablement, avec Lambda vous êtes facturé en fonction du nombre de requêtes de vos fonctions et du temps d’exécution de celles-ci. J’évoquais dans le paragraphe précédent de potentielles améliorations de la performance, elles se traduisent par une durée d’exécution plus courte et par conséquent par une facture moins élevée.

Mais ce n’est pas tout, même à durée d’exécution égale votre facture va baisser. En effet, pour les fonctions utilisant l’architecture Graviton 2, les frais de durée d’exécution sont inférieurs de 20 % à la tarification pour x86. Si l’on prend l’exemple de la région eu-west-3, Paris, avec une allocation de 1 giga de mémoire on est à 0,0000000167 USD la milliseconde pour du x86 et 0,0000000133 USD pour du Graviton 2. Ce qui correspond précisément à une différence de 20.36 %.

Est-ce supporté ?

AWS offre le support de plusieurs langages de programmation grâce à ce que l’on appelle des runtimes. Un runtime se base sur un système d’exploitation, en l’occurrence Amazon Linux ou Amazon Linux 2, ainsi que des outils spécifiques au langage utilisé. Il s’agit par exemple d’un interpréteur pour les langages non compilés. Il existe une page officielle d’AWS recensant tous ces runtimes, avec en particulier les architectures de processeur associées. La plupart des runtimes, par exemple Python ou Node, supportent à la fois x86 et arm.
Surprise, Go ne supporte que x86.

A priori Go ne supporte pas Graviton 2

A priori Go ne supporte pas Graviton 2

On pourrait donc en conclure qu’il n’est pas possible d’avoir à la fois du Graviton 2 en processeur et d’implémenter la fonction lambda avec Go.
Il se trouve que le runtime Go officiel est quasiment un Amazon Linux sans outils supplémentaires. Go étant un langage compilé, il n’y a pas besoin d’un interpréteur ou d’un outil similaire. Si l’on creuse un peu ce raisonnement, on se rend compte que l’on peut aussi utiliser le custom runtime, qui lui supporte aussi bien x86 que arm. Typiquement le custom runtime est utilisé lorsque l’on souhaite utiliser un langage pour lequel aucun runtime spécifique n’est proposé par AWS. L’exemple le plus probant est Rust, pour lequel il existe un SDK mais pas de runtime spécifique.

La solution est donc de switcher du runtime officiel Go vers le custom runtime. Malheureusement cela nécessite quelques ajustements.
Premièrement il va falloir changer la valeur associée à la variable GOARCH lors de la compilation. Cette variable permet de préciser lors de la compilation l’architecture cible et ainsi utiliser les bons opcodes ou instructions d’assembleur pour ce type de processeur.
Ensuite il va falloir que votre binaire en sortie ait obligatoirement le nom boostrap. Autant avec le runtime Go on peut utiliser le nom que l’on souhaite, avec le custom runtime c’est bootstrap un point c’est tout. J’ai perdu pas mal de temps avant de comprendre cela !
Dernière étape il faut dans votre template d’infrastructure as code préciser l’architecture que vous souhaitez utiliser avec votre lambda. Je vais maintenant vous montrer concrètement comment cela se passe avec Terraform puis avec SAM.

Si vous utilisez Terraform

Dans le cas de Terraform vous allez tout d’abord devoir faire évoluer votre compilation afin de cibler l’architecture arm.
Par exemple avec un Makefile il faudra passer de :

buildx86:
	GOARCH=amd64 GOOS=linux  go build -o hello ./x86/main.go
	zip hellox86.zip hello

A ceci :

buildarm64:
	GOARCH=arm64 GOOS=linux go build -o bootstrap ./arm64/main.go
	zip bootstrap.zip bootstrap

On voit ici que la variable GOARCH n’est plus alimentée par la valeur amd64 mais par la valeur arm64. Autre changement notable, le binaire de sortie, identifié par le paramètre -o comme output, n’est plus un nom quelconque mais bien bootstrap comme expliqué plus haut.

Ensuite dans votre template Terraform il va falloir demander à utiliser l’architecture arm avec la fonction Lambda. De plus, on va aussi passer du runtime Go au custom runtime.

Au lieu de déclarer ceci :

resource "aws_lambda_function" "hello_world_x86" {
  filename         = "hellox86.zip"
  function_name    = "helloWorldx86"
  architectures    = ["x86_64"]
  role             = aws_iam_role.lambda_role.arn
  handler          = "hello"
  runtime          = "go1.x"
  source_code_hash = filebase64sha256("hellox86.zip")
}

Vous allez déclarer cela :

resource "aws_lambda_function" "hello_world_arm64" {
  filename         = "bootstrap.zip"
  function_name    = "helloWorldarm64"
  architectures    = ["arm64"]
  role             = aws_iam_role.lambda_role.arn
  handler          = "bootstrap"
  runtime          = "provided.al2"
  source_code_hash = filebase64sha256("bootstrap.zip")
}

On voit bien que l’architecture n’est plus la même, passant de x86_64 à arm64. De même le runtime à changé de go1.x à provided.al2. Enfin, le nom du handler est maintenant boostrap.

Si vous voulez voir un exemple concret, je vous propose de jeter un œil à mon repo golang-terraform-lambda-hello-world sur GitHub.
Il montre comment déployer deux fonctions lambda implémentées en Go, l’une en x86 et l’autre en arm.

Si vous utilisez SAM

Dans le cas de SAM on retrouve globalement les mêmes changements. Il y aura tout de fois une étape supplémentaire, à savoir l’ajout d’un Makefile.

En effet, quand on utilise SAM avec le runtime Go, SAM se charge de compiler et de packager le binaire en vue du déploiement. Si l’on utilise le custom runtime, SAM va avoir besoin d’un Makefile pour faire l’étape de compilation.

Concrètement, si votre fonction lambda se nomme SearchFunctionsFunction, votre makefile devra contenir ceci :

build-SearchFunctionsFunction:
	go get -v -t -d ./...
	GOARCH=arm64 GOOS=linux CGO_ENABLED=0 go build -o ./search-functions/bootstrap ./search-functions
	cp ./search-functions/bootstrap $(ARTIFACTS_DIR)/bootstrap

On retrouve ici les mêmes éléments, la variable GOARCH et l’output de compilation qui est toujours bootstrap.
La différence avec Terraform se situe sur la dernière ligne, ou l’on va copier le binaire obtenue dans un répertoire interne a SAM.
Remarque importante, si vous avez plusieurs fonctions lambda dans votre repository, la valeur de la variable ARTIFACTS_DIR sera différente pour chacune de ses fonctions. Le fait d’avoir plusieurs fois l’output boostrap ne posera donc aucun problème.

Les changements restants se situeront au niveau du template d’infrastructure as code.
On va ainsi remplacer :

  SearchFunctionsFunction:
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: search-functions/
      Handler: search-functions
      Runtime: go1.x
      FunctionName: !Sub '${Environment}-search-lambda-function'
      Architectures:
        - x86_64

Par :

  SearchFunctionsFunction:
    Type: AWS::Serverless::Function 
    Metadata:
      BuildMethod: makefile
    Properties:
      CodeUri: ./
      Handler: bootstrap
      Runtime: provided.al2
      FunctionName: !Sub '${Environment}-search-lambda-function'
      Architectures:
        - arm64

De même, on a donc changé le runtime et l’architecture cible.
On a aussi changé le nom du handler pour correspondre à bootstrap.
Enfin la variable CodeUri pointe maintenant faire le répertoire où se situe le Makefile, à la racine du repository en l’occurrence.

Si vous voulez voir un exemple concret, je vous propose de jeter un oeil à mon repo lambda-statistics-service sur GitHub et en particulier a ce commit : ⚡ update to Graviton lambda instances.


Si vous êtes arrivé jusqu’ici, merci beaucoup d’avoir lu cet article !
J’espère qu’il vous a permis d’approfondir vos connaissances sur Go et le servleress.
Qui sait, il vous permettra peut-être de faire des économies sur votre facture AWS !
Pensez à vous abonner à la liste de diffusion pour ne rater aucun article, le formulaire se trouve en bas de page.
Photo de couverture par Sergey Zolkin.