Jérôme Decoster

Jérôme Decoster

3x AWS Certified - Architect, Developer, Cloud Practionner

18 Feb 2021

Lambda + Terraform + Github Actions

The Goal
  • Create a deployment pipeline for a Lambda function with Terraform
  • Use Lambda versions and aliases to define two environments for development and production
  • Use the API Gateway stageVariables to target one of these 2 environments
  • Use Gihub Actions to automatically update the Lambda function
  • A git push on the develop branch will update the dev version of the Lambda function
  • A git push on the master branch will update the prod version of the Lambda function

    architecture.svg

    Install and setup the project

    Get the code from this github repository :

    # download the code
    $ git clone \
        --depth 1 \
        https://github.com/jeromedecoster/lambda-terraform-github-actions.git \
        /tmp/aws
    
    # cd
    $ cd /tmp/aws
    

    To setup the project, run the following command :

    # create env + terraform init
    $ make setup
    

    This command will :

    • Create an AWS user and put the access keys in the .env file
    • Initialize Terraform from the information defined in the infra directory

    Creating the architecture

    To create the architecture, run the following command :

    # terraform plan + apply
    $ make tf-apply
    

    This command will :

    The API Gateway is created :

    api-1-created

    The /hello resource has a GET method that targets our Lambda :

    api-2-method

    We can see in the Integration Request box that, by targeting the Lambda function, we associate a specific version defined by stageVariable :

    api-3-integration-request

    Two stages have also been published : dev and prod.

    Here is the source code to deploy the stage dev :

    resource "aws_api_gateway_deployment" "deployment_dev" {
      depends_on = [
        aws_api_gateway_integration.lambda
      ]
    
      rest_api_id = aws_api_gateway_rest_api.api_gateway.id
    }
      
    
    resource "aws_api_gateway_stage" "dev" {
      deployment_id = aws_api_gateway_deployment.deployment_dev.id
      rest_api_id   = aws_api_gateway_rest_api.api_gateway.id
      stage_name    = "dev"
    
      variables = {
        "stage" = "dev"
      }
    }
    

    The stage dev has a stageVariable called stage whose value is dev :

    api-4-stage-variable-dev

    The stage prod has a stageVariable called stage whose value is prod :

    api-5-stage-variable-prod

    Lambda function is created :

    lambda-1-created-1

    The code is simple :

    lambda-2-created-2

    Here is the source code to create the 2 aliases :

    resource "aws_lambda_alias" "alias_dev" {
      name             = "dev"
      description      = "dev"
      function_name    = aws_lambda_function.hello.arn
      function_version = "$LATEST"
    }
    
    resource "aws_lambda_alias" "alias_prod" {
      name             = "prod"
      description      = "prod"
      function_name    = aws_lambda_function.hello.arn
      function_version = "$LATEST"
    }
    

    We find these aliases in the web interface :

    lambda-3-aliases-1

    We can test access to the Lambda function by a curl call :

    $ make hello-dev
    "Hello from Lambda"
    

    Setup up the Github project

    The project uses Github Actions to automatically deploy updates

    Lambda function automatic deployment requires AWS credentials

    Here is the source code using the accesses :

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

    These accesses were created previously

    They are located in the .env file which is based on the .env.tmpl template

    We add these secrets in our project :

    github-1-secrets-1

    The secrets have been added :

    github-2-secrets-2

    Creating the develop branch

    As a reminder, the deployment of our project is done according to this principle :

    • A git push on the develop branch will update the dev version of the Lambda function

    • A git push on the master branch will update the prod version of the Lambda function

    At the moment we only have one master branch. So we create a develop branch from it :

    github-develop-1-create-branch

    We edit the code of the Lambda function of the develop branch :

    github-develop-2-edit

    We modify the message for Hello from develop :

    github-develop-3-modified

    We commit directly to the develop branch :

    github-develop-4-commit

    The cd.yml workflow starts :

    github-develop-5-action-started

    It ends quickly :

    github-develop-6-action-completed

    To test our deployment we run the following command :

    $ make hello-dev
    

    This command executes a simple curl call :

    $ curl $(terraform output -raw hello_dev)
    

    It get the URL from a Terraform output :

    output "hello_dev" {
      value = "${aws_api_gateway_stage.dev.invoke_url}/hello"
    }
    

    Our code modification, on the develop branch, returns the correct message :

    # successul return !
    $ make hello-dev
    "Hello from develop"
    

    Updating the master branch

    We quickly edit the master branch to test the proper functioning of our continuous deployment :

    github-master-1-edit

    The message is now Hello from master :

    github-master-2-modified

    We commit in the master branch :

    github-master-3-commit

    The cd.yml workflow starts :

    github-master-4-action-started

    It ends quickly. To test our deployment we run the following command :

    $ make hello-prod
    "Hello from master"
    

    In the Lambda web interface, we can see that aliases now point to the published versions of a Lambda function and no longer to the $LATEST version :

    lambda-4-aliases-updated

    The prod and dev versions have an alias label :

    lambda-5-versions-updated

    This publication and alias association is performed by this part of the publish.sh script :

    VERSION=$(aws lambda publish-version \
        --function-name $PROJECT_NAME \
        --description $1 \
        --region $AWS_REGION \
        --query Version \
        --output text)
    
    aws lambda create-alias \
        --function-name $PROJECT_NAME \
        --name $1 \
        --function-version $VERSION \
        --region $AWS_REGION
    

    Updating with the Github flow

    Our project works :

    • Updating the code on the master branch deploys a Lambda accessible via the URL /prod/hello

    • Updating the code on the develop branch deploys a Lambda accessible via the URL /dev/hello

    We now want to simulate a real update of our project using the Github flow.

    We will create a Pull Request to add a new feature to our function.

    We will name this feature feature-1.

    We edit our hello.js file from our develop branch :

    feature-1-edit

    To simulate the first version of our feature-1, we modify our message to Hello with feature-1-v1 :

    feature-2-modified

    By committing our code, we choose to create a new branch and name it feature-1 :

    feature-3-commit

    The github web interface offers us to create a Pull Request.

    We name it feature-1 and we indicate that this branch should be merged into the develop branch :

    feature-4-pull-request-open

    The pull request has been created but we do not merge it right away :

    feature-5-pull-request-created

    We will first update our Lambda function :

    feature-6-edit-again

    It is important to note that the previous commit on the new feature-1 branch did not trigger our continuous deployment workflow.

    This is because the workflow is configured to be triggered only after a push action on the master or the develop branches :

    name: cd
    
    on:
      push:
        branches:
          - master
          - develop
    

    We are changing the message to Hello with feature-1-v2 :

    feature-7-modified-again

    We commit this update in the feature-1 branch :

    feature-8-commit-again

    We are now satisfied with all these modifications, we will be able to merge the pull request :

    feature-9-pull-request-merge

    The workflow is started :

    feature-10-action-started

    He finishes quickly :

    feature-11-action-completed

    Our new version is quickly accessible from the stage dev :

    $ make hello-dev
    "Hello with feature-1-v2"
    

    If we are satisfied with this version, we can deploy it in production.

    We just need to create a new Pull Request to merge the develop branch to the master branch.

    The deployment will be automatic.