2018-03-03

Peanut Butter and Chocolate: Azure Functions CI/CD Pipeline with AWS CodeCommit (Part 4 of 6)

2018-02-22-015

Part 4

In Part 3 we successfully made the first glue between Azure Functions and AWS CodeCommit by making it possible to manually trigger the Azure Functions Web App to pull from the AWS CodeCommit repository. Obviously, a manual pull is not ideal. It is certainly not a Continuous Delivery.

In Part 4 we lay the groundwork for the 2nd piece of glue between Azure Functions and AWS CodeCommit. In order to automatically trigger a pull AWS CodeCommit from Azure Functions, we need an AWS Lambda. AWS Lambda and Azure Functions are somewhat analogous. They serve almost identical purposes in their respective clouds. We also need to create a KMS key that will be used for encrypting and decrypting secrets.


Series Table of Contents


Create AWS Lambda IAM Role and Attach Managed Policies

Before we create the AWS Lambda, we need to create an IAM Role for the Lambda to assume when it runs. This role will need to grant the Lambda access to AWS CodeCommit and to perform normal AWS Lambda functions. This could be done more fine-grained, but I chose to use two AWS Managed Policies because they were a good enough fit. They are bit more permissive than our purposes require, but the additional permissions are not worrisome.

As a reminder, from the settings we defined the following:

    # Managed IAM policies to apply to the IAM Role
    LambdaRolePolicyArns          = @(
        'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
        'arn:aws:iam::aws:policy/AWSCodeCommitReadOnly'
    )

These policies grant the basic Lambda permissions required for the Lambda to run and read-only access to AWS CodeCommit. Rather than give the Lambda read-only access to the specific CodeCommit repository, we grant it read-only access to all CodeCommit repositories. Since we are using one AWS CodeCommit repository per Azure Functions Web App, we will reuse a single AWS Lambda to manage the automated pull triggering.

First we create the AWS IAM Role for the Lambda to assume:

# Create an IAM role for the AWS Lambda Function to assume
$AssumeRolePolicyDocument = @"
{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "",
        "Effect": "Allow",
        "Principal": {
          "Service": "lambda.amazonaws.com"
        },
        "Action": "sts:AssumeRole"
      }
    ]
  }
"@
$Params = @{
    AssumeRolePolicyDocument = $AssumeRolePolicyDocument
    RoleName                 = $Settings.LambdaRoleName
    Description              = $Settings.LambdaRoleDescription
}
$AWSAssets['LambdaRole'] = New-IAMRole @Params

Then we attach the managed policies to the role:

# Attach the IAM managed policies to the lambda IAM Role
# The WSLambdaBasicExecutionRole and AWSCodeCommitReadOnly policies should be sufficient
foreach ($PolicyArn in $Settings.LambdaRolePolicyArns) {
    $PolicyArnsConfigured
    $Params = @{
        PolicyArn = $PolicyArn
        RoleName = $Settings.LambdaRoleName
    }
    Register-IAMRolePolicy @Params
}

Finally, we validate the role was created and the policies attached using Pester:

# Validate the IAM Role and Policies
Describe 'AWS Lambda Role' {
    BeforeAll {
        $AWSAssets['LambdaAttachedPolicies'] = Get-IAMAttachedRolePolicyList -RoleName $Settings.LambdaRoleName
    }
    It "Was successfully created" {
        $role = Get-IAMRole -RoleName $Settings.LambdaRoleName
        $policyDocument = [uri]::UnescapeDataString($role.AssumeRolePolicyDocument) | ConvertFrom-Json

        $role.RoleName | Should -BeExactly $Settings.LambdaRoleName
        $role.Description | Should -BeExactly $Settings.LambdaRoleDescription
        $policyDocument.Statement[0].Effect | Should -BeExactly 'Allow'
        $policyDocument.Statement[0].Principal.Service | Should -BeExactly 'lambda.amazonaws.com'
        $policyDocument.Statement[0].Action | Should -BeExactly "sts:AssumeRole"
    }

    It "Has the <PolicyArn> Policy attached" -TestCases @(
        foreach($PolicyArn in $Settings.LambdaRolePolicyArns){ @{PolicyArn = $PolicyArn }}
    ) {
        param($PolicyArn)
        $AWSAssets.LambdaAttachedPolicies.PolicyArn | Should -Contain $PolicyArn
    }
}

If all is well, you should see the following:

2018-03-03-01


About the C# .NET Core 2.0 AWS Lambda

Before we go further, I want to briefly go over the C# AWS Lambda this project will use. I chose C# and .NET Core 2.0 for this project because I’m more familiar with it than the other languages available in AWS Lambda. I have done a fair bit of programming in python, but that was many years ago and at this point, my C# skills outpace where I was in my python prime. I also already have a decent development environment already set up for C# and .NET Core 2.0 projects. There is nothing particular about C# and what this lambda does could just as easily be translated to other AWS Lambda languages.

This AWS Lambda does the following:

  1. Accepts a CodeCommit event
  2. Queries the calling CodeCommit repository
  3. Retrieves the cc2fa.yml file.
  4. Validates the git branch the commit was made against is the one configured to be used in Azure Functions
  5. Decodes and Decrypts the Azure Functions Web App deployment password and AWS IAM User HTTPS Git Credential Password
  6. Triggers the Azure Functions Web App Deployment
  7. Writes the environment settings and results to CloudWatch Logs

First, I want to make it clear that the AWS Lambda does not push any data to the Azure Function. It only triggers the Azure Function Web App manual deployment. This is analogous to clicking the sync button in the deployment options of the Azure Function in the Azure portal.

Second, I want to explain why I’m storing secrets in the cc2fa.yml instead of using environment variables. Remember, that the relationship of the AWS Lambda to AWS CodeCommit repositories is one-to-many. Also, in an ideal situation, the AWS Lambda maintainer is separate from the Azure Functions maintainer. That means that secrets are subject to change and rather than coordinate a hand off, we can make use of the YAML file. The secrets are encrypted with a KMS key (which you will see performed later). You can limit who (or what) can decrypt these strings with IAM policies. There are other ways to accomplish this, but I kind of wanted to emulate how AppVeyor allows for secure strings in their YAML file.

Finally, I want to share the code used for the CodeCommit event object model. Currently, this model is not available in the AWS .NET SDK, so I had to piece this together from the API documentation and from making several different types of CodeCommit events and inspecting the JSON sent to SNS. Since I had to put so much effort into this, I felt I should share so others can benefit from my efforts. If I ever get the time I will see if this is something I can get added to the SDK. Anyway, the code for the model can be found here.

The C# AWS Lambda is available as a Visual Studio solution at https://github.com/markekraus/PeanutButterChocolate/tree/master/CSLambda


Build and Deploy the C# .NET Core 2.0 AWS Lambda

This process may be documented somewhere, but I couldn’t find it.I ended up reverse engineering this from this from the AWS add-in for Visual Studio 2017. Essentially, to deploy C# .NET 2.0 AWS Lambda from source to working Lambda, you need to build and publish the .NET project, zip the publish files, then use that zip file with the CreateFunction API.

As a reminder, the prerequisites for this include installing the .NET Core SDK.

First, we build, publish, and zip the .NET Core project:

# Build, and zip the C# Lambda trigger
Push-Location $Settings.LambdaSrcDirectory
dotnet publish -c release
$PublishPath = Join-Path $pwd 'bin\release\netcoreapp2.0\publish\*'
$TempFile = New-TemporaryFile
$NewName = '{0}.zip' -f $TempFile.BaseName
$TempFile = $TempFile | Rename-Item -NewName $NewName -PassThru
Compress-Archive -Path $PublishPath -DestinationPath $TempFile -Force
Pop-Location

Next, we publish the AWS Lambda using the zip file:

# Publish the AWS Lambda Function that will trigger the Azure Deployment when a CodeCommit push occurs
$Params = @{
    FunctionName = $Settings.LambdaName
    Description  = $Settings.LambdaDescription
    ZipFilename  = $TempFile.FullName
    Handler      = $Settings.LambdaHandler
    Runtime      = $settings.LambdaRuntime
    Force        = $true
    Role         = $AWSAssets.LambdaRole.Arn
    Timeout      = $Settings.LambdaTimeOut
    MemorySize   = $Settings.LambdaMemorySize
}
$AWSAssets['PublishedLambda'] = Publish-LMFunction @Params

Then, we clean up the temporary zip file:

# Clean up the temporary zip file
Remove-Item -Path $TempFile -Force -ErrorAction 'SilentlyContinue'

Finally, we validation the AWS Lambda deployment with Pester:

# Validate that the AWS lambda function has been published
Describe "AWS Lambda Function" {
    It "was published successfully" {
        $lambda = Get-LMFunction -FunctionName $Settings.LambdaName
        $config = $lambda.Configuration

        $config.Role | Should -BeExactly $AWSAssets.LambdaRole.Arn
        $config.Runtime | Should -BeExactly $Settings.LambdaRuntime
        $config.FunctionName | Should -BeExactly $Settings.LambdaName
        $config.Handler | Should -BeExactly $Settings.LambdaHandler
        $config.Description | Should -BeExactly $Settings.LambdaDescription
        $config.Timeout | Should -Be $Settings.LambdaTimeOut
    }
}

If it was successful, you should see the following:

2018-03-03-02


Create the AWS KMS Key

We need to create a KMS Key that the AWS Lambda will use to decrypt the secrets in the cc2af.yml. We will also use this key to encrypt those same secrets.

# Create AWS KMS Key used to secure secrets in the configuration YAML
$AWSAssets['KMSKey'] = New-KMSKey -Description $Settings.KMSKeyDescription
$AWSAssets.KMSKey

I have it displaying the result, which contains the Key ID. You should note this Key ID for your records. It would probably be a good idea to use Add-KMSResourceTag to add some tags so you know what the key is being used for.

Before continuing, we validate the KMS key was created using Pester:

# Validate the key's creation
Describe "AWS KMS Key" {
    It "Was successfully created" {
        $key = Get-KMSKey -KeyId $AWSAssets.KMSKey.KeyId

        $key.Description | Should -BeExactly $Settings.KMSKeyDescription
    }
}

If successful, you should see:

2018-03-03-03


Grant AWS Lambda IAM Role Access to AWS KMS Key

For the final topic for this post, we will grant the AWS IAM Role that the AWS Lambda assumes access to the AWS KMS Key. This will allow the Lambda to use the KMS key to decrypt secrets in the cc2af.yml file.

# Grant the IAM Role assumed by the AWS Lambda Function access to the KMS Key so it can decrypt
# secrets in the configuration YAML.
$PolicyDocument = @"
{
    "Version": "2012-10-17",
    "Statement": {
      "Effect": "Allow",
      "Action": [
        "kms:Encrypt",
        "kms:Decrypt"
      ],
      "Resource": [
        "$($AWSAssets.KMSKey.Arn)"
      ]
    }
  }
"@
$Params = @{
    PolicyDocument = $PolicyDocument
    PolicyName     = $Settings.KMSRoleAccessPolicyName
    RoleName       = $Settings.LambdaRoleName
    PassThru       = $true
}
$AWSAssets['KMSRoleAccessPolicy'] =  Write-IAMRolePolicy @Params

I am giving it encrypt rights too, but honestly decrypting is the bigger risk anyway. Therefore, I see no problem giving both even though it doesn’t require encryption rights at this time.

Now we validate the access policy is in place with Pester:

# Validate the Key access policy has been applied
Describe "Lambda Role KMS Access Policy" {
    It "Was successfully created" {
        $policy = Get-IAMRolePolicy -RoleName $Settings.LambdaRoleName -PolicyName $Settings.KMSRoleAccessPolicyName
        $policyDocument = [uri]::UnEscapeDataString($policy.PolicyDocument) | ConvertFrom-Json

        $policy.RoleName | Should -BeExactly $Settings.LambdaRoleName
        $policy.PolicyName | Should -BeExactly $Settings.KMSRoleAccessPolicyName
        $policyDocument.Statement.Action | Should -HaveCount 2
        "kms:Encrypt", "kms:Decrypt" | Should -BeIn $policyDocument.Statement.Action
        $policyDocument.Statement.Effect | Should -BeExactly 'Allow'
        $policyDocument.Statement.Resource | Should -BeExactly $AWSAssets.KMSKey.Arn
    }
}

If it was successful you should see:

2018-03-03-04


Part 4 End

Now we have an AWS Lambda, an IAM role for it to assume, and a KMS key to encrypt and decrypt secrets. In the next part we will continue by creating the automatic trigger to launch the AWS lambda automatically when a commit is made tot he AWS CodeCommit repository.

It may be over a week before I can post Part 5. The Microsoft MVP Summit is next week and I will be pretty busy.So check back next weekend.