Showing posts with label cicd. Show all posts
Showing posts with label cicd. Show all posts

Tuesday, April 13, 2021

AWS codebuild for scala, docker (ecr) CI

Problem and Audience

A continuous integration (CI) process that builds and tests our code, then publishes versioned deployable artifacts (docker images) is a prerequisite for deploying stable software services in the cloud. There are a wide variety of good, inexpensive CI services available, but we decided to build littleware's CI system on AWS codebuild, because it provides an easy to use serverless solution that supports the technology we build on (nodejs, java, scala, docker), and integrates well with AWS. It was straight forward for us to setup a codebuild CI process (buildspec.yml) for our little scala project given the tools we already have in place to deploy cloudformation stacks that define the codebuild project and ecr docker repository.

Overview

There were two steps to setting up our CI build: create the infrastructure, then debug and deploy the build script. The first step was easy, since we already have cloudformation templates for codebuild projects and ecr repositories that our little stack tool can deploy. For example, we deployed the codebuild project to build the littlware github repo by running:

little stack create ./stackParams.json

with this parameters file (stackParams.json):

{
    "StackName": "build-littleware",
    "Capabilities": [
        "CAPABILITY_NAMED_IAM"
    ],
    "TimeoutInMinutes": 10,
    "EnableTerminationProtection": true,
    "Parameters" : [
        {
            "ParameterKey": "PrivilegedMode",
            "ParameterValue": "true"
        },
        {
            "ParameterKey": "ProjectName",
            "ParameterValue": "cicd-littleware"
        },
        {
            "ParameterKey": "ServiceRole",
            "ParameterValue": "arn:aws:iam::027326493842:role/littleCodeBuild"
        },
        {
            "ParameterKey": "GithubRepo",
            "ParameterValue": "https://github.com/frickjack/littleware.git"
        }
    ],
    "Tags": [
            {
                "Key": "org",
                "Value": "applications"
            },
            {
                "Key": "project",
                "Value": "cicd-littleware"
            },
            {
                "Key": "stack",
                "Value": "frickjack.com"
            },
            {
                "Key": "stage",
                "Value": "dev"
            },
            {
              "Key": "role",
              "Value": "build"
            }
    ],
    "Littleware": {
        "TemplatePath": "lib/cloudformation/cicd/nodeBuild.json"
    }
}

With our infrastructure in place, we can add our build script to our github repository. There a few things to notice about our build script. First, the littleware git repo holds multiple interrelated projects - java and scala libraries and applications that build on top of them. We are currently interested in building and packaging the littleAudit/ folder (that will probably be renamed), so the build begins by moving to that folder:

  build:
    commands:
      - cd littleAudit

Next, we setup our codebuild project to run the build container in privileged mode, so our build can start a docker daemon, and build docker images:

phases:
  install:
    runtime-versions:
      # see https://github.com/aws/aws-codebuild-docker-images/blob/master/ubuntu/standard/5.0/Dockerfile
      java: corretto11
    commands:
      # see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-environment.html#cfn-codebuild-project-environment-privilegedmode
      - nohup /usr/local/bin/dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2375 --storage-driver=overlay &
      - timeout 15 sh -c "until docker info; do echo .; sleep 1; done"

We use gradle to compile our code and run the unit test suite. The org.owasp.dependencycheck gradle plugin adds a dependencyCheckAnalyze task that checks our maven dependencies against public databases of known vulnerabilities:

  build:
    commands:
      - cd littleAudit
      - gradle build
      - gradle dependencyCheckAnalyze
      - docker build -t codebuild:frickjack .

Finally, our post-build command tags and pushes the docker image to an ecr repository. The tagging rules align with the lifecycle rules on the repository (described here and here).

  post_build:
    commands:
      - BUILD_TYPE="$(echo "$CODEBUILD_WEBHOOK_TRIGGER" | awk -F / '{ print $1 }')"
      - echo "BUILD_TYPE is $BUILD_TYPE"
      - |
        (
          little() {
              bash "$CODEBUILD_SRC_DIR_HELPERS/AWS/little.sh" "$@"
          }

          scanresult=""
          scan_in_progress() {
            local image
            image="$1"
            if ! shift; then
                echo "invalid scan image"
                exit 1
            fi
            local tag
            local repo
            tag="$(echo "$image" | awk -F : '{ print $2 }')"
            repo="$(echo "$image" | awk -F : '{ print $1 }' | cut -d / -f 2-)"
            scanresult="$(little ecr scanreport "$repo" "$tag")"
            test "$(echo "$scanresult" | jq -e -r .imageScanStatus.status)" = IN_PROGRESS
          }

          TAGSUFFIX="$(echo "$CODEBUILD_WEBHOOK_TRIGGER" | awk -F / '{ suff=$2; gsub(/[ @/]+/, "_", suff); print suff }')"
          LITTLE_REPO_NAME=little/session_mgr
          LITTLE_DOCKER_REG="$(little ecr registry)" || exit 1
          LITTLE_DOCKER_REPO="${LITTLE_DOCKER_REG}/${LITTLE_REPO_NAME}"

          little ecr login || exit 1
          if test "$BUILD_TYPE" = pr; then
            TAGNAME="${LITTLE_DOCKER_REPO}:gitpr_${TAGSUFFIX}"
            docker tag codebuild:frickjack "$TAGNAME"
            docker push "$TAGNAME"
          elif test "$BUILD_TYPE" = branch; then
            TAGNAME="${LITTLE_DOCKER_REPO}:gitbranch_${TAGSUFFIX}"
            docker tag codebuild:frickjack "$TAGNAME"
            docker push "$TAGNAME"
          elif test "$BUILD_TYPE" = tag \
            && (echo "$TAGSUFFIX" | grep -E '^[0-9]{1,}\.[0-9]{1,}\.[0-9]{1,}$' > /dev/null); then
            # semver tag
            TAGNAME="${LITTLE_DOCKER_REPO}:gitbranch_${TAGSUFFIX}"
            if ! docker tag codebuild:frickjack "$TAGNAME"; then
              echo "ERROR: failed to tag image with $TAGNAME"
              exit 1
            fi
            ...

If the CI build was triggered by a semver git tag, then it waits for the ecr image scan to complete successfully before tagging the docker image for production use:

       ...
          elif test "$BUILD_TYPE" = tag \
            && (echo "$TAGSUFFIX" | grep -E '^[0-9]{1,}\.[0-9]{1,}\.[0-9]{1,}$' > /dev/null); then
            # semver tag
            TAGNAME="${LITTLE_DOCKER_REPO}:gitbranch_${TAGSUFFIX}"
            if ! docker tag codebuild:frickjack "$TAGNAME"; then
              echo "ERROR: failed to tag image with $TAGNAME"
              exit 1
            fi
            # see https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_ImageScanStatus.html
            docker push "$TAGNAME" || exit 1
            count=0
            sleep 10

            while scan_in_progress "$TAGNAME" && test "$count" -lt 50; do
              echo "Waiting for security scan - sleep 10"
              count=$((count + 1))
              sleep 10
            done
            echo "Got image scan result: $scanresult"
            if ! test "$(echo "$scanresult" | jq -e -r .imageScanStatus.status)" = COMPLETE \
               || ! test "$(echo "$scanresult" | jq -e -r '.imageScanFindingsSummary.findingSeverityCounts.HIGH // 0')" = 0 \
               || ! test "$(echo "$scanresult" | jq -e -r '.imageScanFindingsSummary.findingSeverityCounts.CRITICAL // 0')" = 0; then
               echo "Image $TAGNAME failed security scan - bailing out"
               exit 1
            fi
            SEMVER="${LITTLE_DOCKER_REPO}:${TAGSUFFIX}"
            docker tag "$TAGNAME" "$SEMVER"
            docker push "$SEMVER"
          else
            echo "No docker publish for build: $BUILD_TYPE $TAGSUFFIX"
          fi

Summary

A continuous integration (CI) process that builds and tests our code, then publishes versioned deployable artifacts (docker images) is a prerequisite for deploying stable software services in the cloud. Our codebuild CI project builds and publishes the docker images that we will use to deploy our "little session manager" service as a lambda behind an API gateway (but we're still working on that).

Thursday, November 21, 2019

Simple CICD with CodeBuild

CICD is awesome. It automates the tasks we want to perform whenever we push code to our repository:

  • test linting
  • audit third party dependencies for security
  • compile the code
  • run unit tests
  • publish artifacts

There are a lot of great options for implementing CICD too. On-prem systems like Jenkins and Team City, and SAAS systems like Travis, CircleCI, and Github Actions. For my personal projects I wanted a SAAS solution that triggers on github PR's and integrates with AWS, so I setup a CodeBuild process that I'm pretty happy with.

Codebuild Setup

Setting up codebuild is straight forward if you're comfortable with cloudformation.

  • save credentials
  • setup IAM roles for codebuild
  • setup a build for each github repository
  • add a build conifiguration to each github repository

save credentials

Codebuild can access credentials in AWS secrets manager for interacting with non-AWS systems like github and npm.org. The misc-stuff github repo has helpers for common operations tasks like managing secrets.

git clone https://github.com/frickjack/misc-stuff.git
export LITTLE_HOME="$(pwd)/misc-stuff"
alias little="bash '$(pwd)/misc-stuff/AWS/little.sh'"

little help secret
little secret create the/secret/key "$(cat secretValue.json)"

codebuild IAM setup

Codebuild needs IAM credentials to allocate build resources. This cloudformation template sets up a standard role that we'll pass to our builds:

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Parameters": {
      "GithubToken": {
        "Type": "String",
        "Description": "arn of secretsmanager secret for access token",
        "ConstraintDescription": "secret arn",
        "AllowedPattern": "arn:.+"
      }
    },
    "Resources": {
      "CodeBuildRole": {
        "Type": "AWS::IAM::Role",
        "Properties": {
          "RoleName" : "littleCodeBuild",
          "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [{
              "Effect": "Allow",
              "Principal": { "Service": ["codebuild.amazonaws.com"] },
              "Action": ["sts:AssumeRole"]
            }]
          },
          "Policies": [{
            "PolicyName": "CodebuildPolicy",
            "PolicyDocument": {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Sid": "CloudWatchLogsPolicy",
                  "Effect": "Allow",
                  "Action": [
                    "logs:CreateLogGroup",
                    "logs:CreateLogStream",
                    "logs:PutLogEvents"
                  ],
                  "Resource": [
                    "*"
                  ]
                },
                {
                  "Sid": "CodeCommitPolicy",
                  "Effect": "Allow",
                  "Action": [
                    "codecommit:GitPull"
                  ],
                  "Resource": [
                    "*"
                  ]
                },
                {
                  "Sid": "S3Policy",
                  "Effect": "Allow",
                  "Action": [
                    "s3:GetObject",
                    "s3:GetObjectVersion",
                    "s3:PutObject",
                    "s3:GetBucketAcl",
                    "s3:GetBucketLocation"
                  ],
                  "Resource": [
                    "*"
                  ]
                },
                {
                  "Sid": "SsmPolicy",
                  "Effect": "Allow",
                  "Action": [
                    "ssm:GetParameters",
                    "secretsmanager:Get*"
                  ],
                  "Resource": [
                    "*"
                  ]
                },
                {
                  "Sid": "ECRPolicy",
                  "Effect": "Allow",
                  "Action": [
                    "ecr:BatchCheckLayerAvailability",
                    "ecr:GetDownloadUrlForLayer",
                    "ecr:BatchGetImage",
                    "ecr:GetAuthorizationToken"

                  ],
                  "Resource": [
                    "*"
                  ]
                },
                {
                  "Sid": "CFPolicy",
                  "Effect": "Allow",
                  "Action": [
                    "cloudformation:ValidateTemplate"
                  ],
                  "Resource": [
                    "*"
                  ]
                },
                {
                  "Sid": "LambdaLayerPolicy",
                  "Effect": "Allow",
                  "Action": [
                    "lambda:PublishLayerVersion",
                    "lambda:Get*",
                    "lambda:List*",
                    "lambda:DeleteLayerVersion",
                    "iam:List*"
                  ],
                  "Resource": [
                    "*"
                  ]
                }
              ]
            }
          }
        ]}
      },
      "GithubCreds": {
        "Type" : "AWS::CodeBuild::SourceCredential",
        "Properties" : {
            "AuthType" : "PERSONAL_ACCESS_TOKEN",
            "ServerType" : "GITHUB",
            "Token": { "Fn::Join" : [ "", [ "{{resolve:secretsmanager:", { "Ref": "GithubToken" }, ":SecretString:token}}" ]] }
          }
      }
    },

    "Outputs": {
    }
}

We can use the little tool mentioned earlier to deploy the stack:

little stack create "$LITTLE_HOME/AWS/db/cloudformation/YourAccount/cell0/cicd/cicdIam/stackParams.json"

register a new build

This cloudformation template registers a new build that runs for pull requests and tag events on a github repository. In addition to the primary repo the build also pulls in a secondary support repository that hosts our automation scripts.

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Parameters": {
      "ProjectName": {
        "Type": "String",
        "Description": "name of the build project - also tag",
        "ConstraintDescription": "should usually be the domain of the github repository"
      },
      "ServiceRole": {
        "Type": "String",
        "Description": "arn of IAM role for codebuild to assume",
        "ConstraintDescription": "IAM role arn",
        "AllowedPattern": "arn:.+"
      },
      "GithubRepo": {
        "Type": "String",
        "Description": "url of github source repo",
        "ConstraintDescription": "https://github.com/ repo url",
        "AllowedPattern": "https://github.com/.+"
      },
      "SupportRepo": {
        "Type": "String",
        "Description": "url of github secondary repo - default https://github.com/frickjack/misc-stuff.git",
        "Default": "https://github.com/frickjack/misc-stuff.git",
        "ConstraintDescription": "https://github.com/ repo url",
        "AllowedPattern": "https://github.com/.+"
      }
    },

    "Resources": {
        "CodeBuild": {
            "Type" : "AWS::CodeBuild::Project",
            "Properties" : {
                "Artifacts" : {
                  "Type": "NO_ARTIFACTS"
                },
                "BadgeEnabled" : true,
                "Description" : "build and test little-elements typescript project",
                "Environment" : {
                  "ComputeType" : "BUILD_GENERAL1_SMALL",
                  "EnvironmentVariables" : [ 
                    {
                      "Name" : "LITTLE_EXAMPLE",
                      "Type" : "PLAINTEXT",
                      "Value" : "ignore"
                    }
                   ],
                  "Image" : "aws/codebuild/standard:2.0",
                  "Type" : "LINUX_CONTAINER"
                },
                "Name" : { "Ref": "ProjectName" },
                "QueuedTimeoutInMinutes" : 30,
                "SecondaryArtifacts" : [],
                "ServiceRole" : { "Ref": "ServiceRole" },
                "Source" : {
                  "Type": "GITHUB",
                  "Location": { "Ref" : "GithubRepo" },
                  "GitCloneDepth": 2,
                  "ReportBuildStatus": true
                },
                "SecondarySources": [
                  {
                    "Type": "GITHUB",
                    "Location": { "Ref" : "SupportRepo" },
                    "GitCloneDepth": 1,
                    "SourceIdentifier": "HELPERS"
                  }
                ],
                "Tags": [
                    {
                        "Key": "org",
                        "Value": "applications"
                    },
                    {
                        "Key": "project",
                        "Value": { "Ref": "ProjectName" }
                    },
                    {
                        "Key": "stack",
                        "Value": "cell0"
                    },
                    {
                        "Key": "stage",
                        "Value": "dev"
                    },
                    {
                      "Key": "role",
                      "Value": "codebuild"
                    }
                ],
                "TimeoutInMinutes" : 10,
                "Triggers" : {
                  "FilterGroups" : [ 
                    [
                      {
                        "ExcludeMatchedPattern" : false,
                        "Pattern" : "PULL_REQUEST_CREATED, PULL_REQUEST_UPDATED, PULL_REQUEST_REOPENED, PULL_REQUEST_MERGED",
                        "Type" : "EVENT"
                      }
                    ],
                    [
                        {
                          "ExcludeMatchedPattern" : false,
                          "Pattern" : "PUSH",
                          "Type" : "EVENT"
                        },
                        {
                          "ExcludeMatchedPattern" : false,
                          "Pattern" : "^refs/tags/.*",
                          "Type" : "HEAD_REF"
                        }
                    ]
                   ],
                  "Webhook" : true
                }
            }
        }
    },

    "Outputs": {
    }
}

We can use the little tool mentioned earlier to deploy the stack:

little stack create "$LITTLE_HOME/AWS/db/cloudformation/YourAccount/cell0/cicd/nodeBuild/little-elements/stackParams.json"

configure the repository

Codebuild expects a buildspec.yaml file in the code repository to contain the commands for a build. This build file runs a typescript compile, unit tests, dependency security audit, and linting check. If the build trigger is a tagging event, then the build goes on to publish the build's assets as a lambda layer and https://npm.org package.

# see https://docs.aws.amazon.com/codepipeline/latest/userguide/tutorials-codebuild-devicefarm.html
version: 0.2

env:
  variables:
    LITTLE_INTERACTIVE: "false"
  parameter-store:
    NPM_TOKEN: "/aws/reference/secretsmanager/applications/cicd/cell0/dev/npmjs-token"
phases:
  install:
    runtime-versions:
      nodejs: 10
    commands:
      - echo "Entered the install phase - jq already installed ..."
      #- apt-get update -y
      #- apt-get install -y jq
  pre_build:
    commands:
      - echo "HOME is $HOME, CODEBUILD_SRC_DIR is $CODEBUILD_SRC_DIR, CODEBUILD_SRC_DIR_HELPERS is $CODEBUILD_SRC_DIR_HELPERS, pwd is $(pwd)"
      - echo "//registry.npmjs.org/:_authToken=$(echo "$NPM_TOKEN" | jq -e -r .token)" > "$HOME/.npmrc"
      - mkdir -p "$HOME/.aws"; /bin/echo -e "[default]\nregion = us-east-2\noutput = json\ncredential_source = Ec2InstanceMetadata\n" | tee "$HOME/.aws/config"
      - npm ci
      - pip install yq --upgrade
  build:
    commands:
      - npm run build
      - npm run lint
      - npm audit --audit-level=high
      - npm run test
  post_build:
    commands:
      - echo "CODEBUILD_WEBHOOK_TRIGGER == $CODEBUILD_WEBHOOK_TRIGGER"
      # checkout a branch, so lambda publish goes there
      - git checkout -b "cicd-$CODEBUILD_WEBHOOK_TRIGGER"
      - BUILD_TYPE="$(echo $CODEBUILD_WEBHOOK_TRIGGER | awk -F / '{ print $1 }')"
      - echo "BUILD_TYPE is $BUILD_TYPE"
      # publish lambda layers for pr's and tags
      - if test "$BUILD_TYPE" = pr || test "$BUILD_TYPE" = tag; then bash "$CODEBUILD_SRC_DIR_HELPERS/AWS/little.sh" lambda update "$CODEBUILD_SRC_DIR"; fi
      # publish on tag events - see https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html
      - if test "$BUILD_TYPE" = tag; then npm publish . --tag cicd; fi

Tuesday, July 31, 2018

wait for kubernetes pods

First, there's probably a better way to do this with kubectl rollout, but I didn't know that existed till last week.

Anyway ... kubernetes' API is asynchronous, so if an operator that issues a series of kubernetes commands to update various deployments wants to wait till all the pods in those deployments are up and running, then he may take advantage of a script like: kube-wait4-pods

(
    # If new pods are still rolling/starting up, then wait for that to finish
    COUNT=0
    OK_COUNT=0
    # Don't exit till we get 2 consecutive readings with all pods running.
    while [[ "$OK_COUNT" -lt 2 ]]; do
      g3kubectl get pods
      if [[ 0 == "$(g3kubectl get pods -o json |  jq -r '[.items[] | { name: .metadata.generateName, phase: .status.phase, waitingContainers: [ try .status.containerStatuses[] | { waiting:.state|has("waiting"), ready:.ready}|(.waiting==true or .ready==false)|select(.) ]|length }] | map(select(.phase=="Pending" or .phase=="Running" and .waitingContainers > 0)) | length')" ]]; then
        let OK_COUNT+=1
      else
        OK_COUNT=0
      fi
      
      if [[ "$OK_COUNT" -lt 2 ]]; then
        echo ------------
        echo "INFO: Waiting for pods to exit Pending state"
        let COUNT+=1
        if [[ COUNT -gt 30 ]]; then
          echo -e "$(red_color "ERROR:") pods still not ready after 300 seconds"
          exit 1
        fi
        sleep 10
      fi
    done
)

For example - this Jenkins pipeline deploys a new stack of services to a QA environment, then waits till the new versions of pods are deployed before running through an integration test suite.

...
    stage('K8sDeploy') {
      steps {
        withEnv(['GEN3_NOPROXY=true', "vpc_name=$env.KUBECTL_NAMESPACE", "GEN3_HOME=$env.WORKSPACE/cloud-automation"]) {
          echo "GEN3_HOME is $env.GEN3_HOME"
          echo "GIT_BRANCH is $env.GIT_BRANCH"
          echo "GIT_COMMIT is $env.GIT_COMMIT"
          echo "KUBECTL_NAMESPACE is $env.KUBECTL_NAMESPACE"
          echo "WORKSPACE is $env.WORKSPACE"
          sh "bash cloud-automation/gen3/bin/kube-roll-all.sh"
          sh "bash cloud-automation/gen3/bin/kube-wait4-pods.sh || true"
        }
      }
    }
...

BTW - the code referenced above is a product of the great team of developers at the Center for Data Intensive Science.