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

Saturday, November 16, 2019

Publish Web Content to S3

I host a little website at https://apps.frickjack.com by publishing static web files to an S3 bucket behind a cloudfront CDN. One way to improve the performance of an S3 web site is to configure each file's (s3 object) content encoding (gzip'ing the file if required), content type, and cache control at upload time (S3 natively specifies an Etag).

Here's a little script that makes that easy (also at https://github.com/frickjack/misc-stuff/blob/master/AWS/bin/s3web.sh), but rather than copy the script - you can install it like this:

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

little help s3web
little s3web publish localFolder s3Path [--dryrun]

Given a local source folder with html and other web content, then little s3web publish localFolder s3://destiation does the following:

  • html files are gzipped, and annotated with headers content-encoding gzip, mime-type, cache for one hour
  • css, js, svg, map, json, md files are gzipped, and annotated with content-encoding, mime-type, and cache for 1000 hours
  • png, jpg, wepb files are not gzipped, and annotated with mime-type and cache for 1000 hours

This caching policy assumes that html files may periodically change to reference new assets (javascript, css, etc), but the referenced assets are immutable. For example, https://apps.frickjack.com loads javascript and CSS assets from a versioned subpath, and every update to https://apps.frickjack.com publishes a new folder of assets.

#
# helpers for publishing web content to S3
#

source "${LITTLE_HOME}/lib/bash/utils.sh"

# lib ------------------------

#
# Determine mime type for a particular file name
#
# @param path
# @return echo mime type
#
s3webPath2ContentType() {
    local path="$1"
    shift
    case "$path" in
        *.html)
            echo "text/html; charset=utf-8"
            ;;
        *.json)
            echo "application/json; charset=utf-8"
            ;;
        *.js)
            echo "application/javascript; charset=utf-8"
            ;;
        *.mjs)
            echo "application/javascript; charset=utf-8"
            ;;
        *.css)
            echo "text/css; charset=utf-8"
            ;;
        *.svg)
            echo "image/svg+xml; charset=utf-8"
            ;;
        *.png)
            echo "image/png"
            ;;
        *.jpg)
            echo "image/jpeg"
            ;;
        *.webp)
            echo "image/webp"
            ;;
        *.txt)
            echo "text/plain; charset=utf-8"
            ;;
        *.md)
            echo "text/markdown; charset=utf-8"
            ;;
        *)
            echo "text/plain; charset=utf-8"
            ;;
    esac
}

#
# Get the npm package name from the `package.json`
# in the current directory
#
s3webPublish() {
    local srcFolder
    local destPrefix
    local dryRun=off

    if [[ $# -lt 2 ]]; then
        gen3_log_err "s3web publish takes 2 arguments"
        little help s3web
        return 1
    fi
    srcFolder="$1"
    shift
    destPrefix="$1"
    shift
    if [[ $# -gt 0 && "$1" =~ ^-*dryrun ]]; then
        shift
        dryRun=on
    fi
    if [[ ! -d "$srcFolder" ]]; then
        gen3_log_err "invalid source folder: $srcFolder"
        return 1
    fi
    if ! [[ "$destPrefix" =~ ^s3://.+ ]]; then
        gen3_log_err "destination prefix should start with s3:// : $destPrefix"
        return 1
    fi
    local filePath
    local ctype
    local encoding
    local cacheControl
    local errCount=0
    local gzTemp
    local commandList
    (
        commandList=()
        cd "$srcFolder"
        gzTemp="$(mktemp "$XDG_RUNTIME_DIR/gzTemp_XXXXXX")"
        find . -type f -print | while read -r filePath; do
            ctype="$(s3webPath2ContentType "$filePath")"
            encoding="gzip"
            cacheControl="max-age=3600000, public"
            destPath="${destPrefix%/}/${filePath#./}"
            if [[ "$ctype" =~ ^image/ && ! "$ctype" =~ ^image/svg ]]; then
                encoding=""
            fi
            if [[ "$ctype" =~ ^text/html || "$ctype" =~ ^application/json ]]; then
                cacheControl="max-age=3600, public"
            fi

            if [[ "$encoding" == "gzip" ]]; then
                gzip -c "$filePath" > "$gzTemp"
                commandList=(aws s3 cp "$gzTemp" "$destPath" --content-type "$ctype" --content-encoding "$encoding" --cache-control "$cacheControl")
            else
                commandList=(aws s3 cp "$filePath" "$destPath" --content-type "$ctype" --cache-control "$cacheControl")
            fi
            gen3_log_info "dryrun=$dryRun - ${commandList[@]}"
            if [[ "$dryRun" == "off" ]]; then
                "${commandList[@]}" 1>&2
            fi
            errCount=$((errCount + $?))
            if [[ -f "$gzTemp" ]]; then
                /bin/rm "$gzTemp"
            fi
        done
        [[ $errCount == 0 ]]
    )
}

# main ---------------------

# GEN3_SOURCE_ONLY indicates this file is being "source"'d
if [[ -z "${GEN3_SOURCE_ONLY}" ]]; then
    command="$1"
    shift

    case "$command" in
        "content-type")
            s3webPath2ContentType "$@"
            ;;
        "publish")
            s3webPublish "$@"
            ;;
        *)
            little help s3web
            ;;
    esac
fi

Friday, October 11, 2019

Executable Test Plans for BDD

There are many good libraries for developing automated software tests, but we also want to allow non developers to specify test plans and acceptance tests early in the development life cycle.

  • we want test plans and acceptance tests to help guide feature development
  • we want to track test plans as artifacts in git
  • we want to use Gherkin or some similar "living documentation" technology that allows us to link test automation with human readable test plans

Test automation frameworks like jasminejs can support manual tests via interactive helpers.

Test Plans and QA

A test plan is a living document that describes how a system should fulfill different use cases and how the system behaves under various scenarios. The system or feature design process should produce an initial test plan which guides the work of developers and testers, and helps ensure that the development process yields a system that matches its requirements. The test plan changes as the requirements for a system change.

Unit tests and integration tests are both important parts of the quality assurance process. During system development the developer team produce unit tests that run as part of the system's continuous integration process.

The QA team deploys each new build of the feature to a test environment, and caries out a test cycle that tests the behavior of the system against the system's test plan, and generates a report. The level of automation applied to a test cycle generally increases as a feature matures. A CICD system can run a fully automated test cycle that deploys each new build to a test environment, runs the test plan, and publish a report without human intervention.

Test Automation

A test plan should naturally evolve from manual checks carried out by the QA team to a fully automated test suite execute by a CICD pipeline. Test frameworks like jasminejs support automated tests, and can be leveraged to execute and generate reports for manual test cycles.

The testHelper.ts helpers allow us to write jasminejs test suites that present instructions to a human tester, then interactively collect the results of the test. This facility allows us to intermix manual and automated tests, and also support partially automated tests.

For example, one test in the testHelperSpec looks like this:

    it("allows interactive tests", ... ifInteractive(async () => {
        let result = await interactive("press enter at the prompt");
        expect(result.didPass).toBe(true);
        result = await interactive("enter \"y\" at the prompt");
        expect(result.didPass).toBe(true);
        result = await interactive("enter \"n\" at the prompt, then some explanation at the next prompt");
        expect(result.didPass).toBe(false);
        expect(result.details.length > 0).toBe(true);
    }, 3600000) as any,
    );

What's going on here?

  • ifInteractive tests the LITTLE_INTERACTIVE environment variable. If it's not false, then ifInteractive just returns its arguments (a lambda and timeout for jasmine's it method); otherwise ifInteractive returns a do-nothing test lambda for non-interactive environments
  • interactive prints the given instructions to the console for a human tester, and prompts the tester for whether the test passed or failed
  • there's also an isInteractive helper that just returns true if the LITTLE_INTERACTIVE environment variable is not false, and allows an automated test case to include optional manual elements

That's it! A simple test driven development process has a project manager specify acceptance tests in the feature definition, then a developer or tester translates those tests directly to code that fits into existing tooling for tracking changes, running tests, and generating reports. If you use jasminejs, then you can get started with the following code, otherwise it's straight forward to adapt the code to other frameworks. Good luck!


npm install @littleware/little-elements
const { interactive, ifInteractive } = require('@littleware/little-elements/commonjs/bin/testHelper.js');

Sunday, June 30, 2019

Hydrate an AWS Account

You just got your brand spankin' new AWS account, and your team is launching like a rocket into the cloud. How should you set this account thing up? Before playing with EC2 instances and S3 buckets and all the other toys at the application layer of the stack; you should figure out how you want to authenticate, authorize, and monitor users manipulating your base cloud infrastructure.

The following process describes one approach to inflate an AWS account before beginning application development. Although the steps taken here are only suitable for a single account setup for an individual or small team, the approach (setting up authentication, roles for authorization, monitoring, a simple budget, and basic alerts) generalizes for use with a multi-account AWS organization.

The Plan

Here's what we're going to do

  • login as the account's root user, and setup an IAM bootstrap user with admin privileges, so we can acquire credentials to run through a suite of account-bootstrap scripts.

  • run an accountBootstrap script to setup some basic infrastructure for deploying cloudformation stacks.

  • deploy a series of cloudformation stacks

    • IAM groups and roles for:
      • administrators - with write access to IAM and cloudtrail as well as operator permissions
      • operators - with write access to whatever services you want available for use in the account, except only read access to cloudtrail and IAM
      • developers - with access required to develop, test, deploy, and monitor applications that use infrastructure put into place by administrators and operators
    • an SNS topic for publishing alert notifications
    • a cloudtrail that logs updates to your account
    • a set of cloudwatch alarms that publish notifications to administrators (via the SNS topic described above) when various events occur:
      • IAM policy changes
      • root account access
      • budget limit exceeded
      • guard duty event
  • finally - setup an initial administrator account using the new infrastructure, and delete the temporary bootstrap user

Setup an AWS bootstrap user

Login to the root account, and do the following:

  • enable MFA on the root account
  • setup a bootstrap IAM user with MFA enabled and admin privileges:
{
    "Effect": "Allow",
    "Action": "*",
    "Resource": "*",
    "Condition": {
        "Bool": {
            "aws:MultiFactorAuthPresent": "true"
        }
    }
}
  • download the access key pair for the new account, and setup ~/.aws/credentials and ~/.aws/config - ex:
[profile bootstrap-ohio]
region = us-east-2
output = json
mfa_serial = arn:aws:iam::123456789:mfa/bootstrap

Install software tools

These instructions assume your command shell has access to the following tools: bash, the jq json tool, git, and the aws cli.

  • download the cloudformation templates and helper scripts from our git repository:

    git clone https://github.com/frickjack/misc-stuff.git
  • add the little tool to your command path

    # assuming you're running a bash shell or similar
    alias little="bash $(pwd)/misc-stuff/AWS/little.sh"
    export LITTLE_HOME="$(pwd)/misc-stuff/AWS"
  • run the bootstrap script - it does the following:

    • deploys a block on s3 public access
    • creates an s3 bucket for cloudformation templates
    • sets a password policy for IAM users

ex:

export AWS_PROFILE="bootstrap-ohio"
little accountBootstrap
  • finally - prepare the inputs to our cloudformation stacks.
    • make a copy of the account-specific stack-parameters:
      cp AWS/misc-stuff/db/frickjack AWS/misc-stuff/db/YOUR-ACCOUNT
    • make whatever changes are appropriate for your account. For example - change the SNS notify e-mail in AWS/db/cloudformation/YourAccount/accountSetup/snsNotify.json
    • customize the cloudformation templates under misc-stuff/AWS/lib/cloudformation/ for your account. For example - the IamSetup.json template sets up an IAM policy that allows access to S3 and lambda and APIGateway API's, because I'm interested in those serverless technologies, but you may want to add permissions for accessing the EC2 and VPC API's.

What's the idea?

Before we start deploying stacks let's talk about the ideas we're implementing.

Authentication

The Right Way to Authenticate

First, authentication - how should a user prove who he is? AWS IAM has primitives for setting up users and groups, but that's not your best option for establishing a user's identity, because it's one more thing you need to maintain.

Instead of administering identity with users and groups in IAM under an AWS account - it's better to setup federated authentication with Google Apps or Office365 or some other identity provider that you already maintain with multi-factor auth and a password policy and all that other good stuff. If you don't already have an identity provider, then AWS has its own service, AWS SSO

While you're at it - you might setup an organization, because whatever you're doing it is bound to be wildly successful, and you'll wind up setting up multiple accounts for your galloping unicorn, and an organization helps simplify that administration.

Not the Right Way to Authenticate

If you don't already have an SSO identity provider, and you don't have someone to do it for you, then setting up an SSO and AWS federation and an AWS organization may seem like a lot of work just to manage a small team's access to AWS API's. So let's not do things the right way, but let's not be completely wrong either. We can emulate the right way.

  • require MFA for user authentication
  • enforce a password length and rotation policy
  • require rotation of user access keys
  • associate each user with at least one group
  • associate each group with an IAM role, so that a group member gains access to AWS API's by acquiring a temporary credentials via a multifactor-signed call to sts

This authentication setup ensures that access to AWS API's comes either from AWS managed temporary credentials passed directly to AWS resources like EC2 or labmda via something like the AWS metadata service, or a user must pass mutlifactor authentication to acquire a temporary token directly. Hopefully this authentication setup will protect our account from being compromised due to an exposed secret.

Authorization

Now that we have a mechanism to securely authenticate users and services that want to access AWS API's, how should we decide which privileges to grant different users? Our iamSetup cloudformation stack sets up three groups of users each associated with its own IAM role:

  • administrator
  • operator
  • developer

We restrict write permission to IAM policies to the administrators that are trained to enforce least privilege access. We want to restrict which users can disable cloudtrail, because there's no reason to do that.

The administrator group shares permissions to create other (non-IAM) AWS resources (whatever we want to allow in our account) with the group of operators. I'm not sure if it makes sense to have both an administrator group and an operator group, but one scenario might be that an administrator can setup IAM policies conditional on resource tags for a particular application or whatever, and an operator (maybe a devops specialist on a team) can then create and delete resources with the appropriate tags.

The developer group members cannot create new resources directly, but they do have permissions to deploy new versions of an application (udpate a lambda, or change the backend on an api gateway, or upgrade an EC2 AMI, or modify S3 objects - that kind of thing).

Finally - each application service has its own IAM role attached to its ECS container or EC2 instances or lambda or whatever. The administrator, operator, and developer roles should only be available to human users; each application's role grants the minimum privilege that service requires.

Tagging

A consistent tagging strategy allows everyone to easily determine the general purpose of a resource and who is responsible for the resource's allocation and billing. Something like this works, but there are many ways to do it.

"Tagging": {
        "TagSet": [
            {
                "Key": "org",
                "Value": "devops"
            },
            {
                "Key": "project",
                "Value": "infrastructure"
            },
            {
                "Key": "stack",
                "Value": "main"
            },
            {
                "Key": "stage",
                "Value": "prod"
            },
            {
              "Key": "role",
              "Value": "cloudformation-bucket"
            }
        ]
    }
  • org: which billing organization owns this resource
  • project: which project or application
  • stack: a project or application may have multiple deployments - for different clients or whatever
  • stage: a particular application stack may have multiple deployments for different stages of the development process (qa, staging, production)
  • role: what is the purpose of the resource?

Logs, Metrics, Monitoring, and Alerts

I was slow to understand what's up with cloudwatch and sns, but it's not that complicated. SNS is a pub-sub system - a client publishes something to a topic, and subscribers (lambda functions, e-mail, SQS, queues, ...) immediately receive whatever was published - no queueing or flow control - just a way to decouple systems.

Cloudwatch logs is a place to save log ("event") streams. Cloudwatch events lets listeners subscribe for notificates of various events from the AWS control plane. Cloudwatch metrics lets applications publish metrics like load, response time, number of requests, whatever. Cloudwatch alarms fire actions (lambda, SNS publication, ...) triggered by rules applied to metrics, events, and logs.

For example - our cloudformation stack sets up a notifications topic in SNS that our cloudwatch alarms publish to; and we setup alarms to send notifications when changes are made to IAM, or when the root account is accessed, or when an account approaches its budget limit, or when AWS guard duty detects something ... that kind of thing.

Deploy the stacks

Ok - let's do this thing. As the bootstrap user:

  • setup IAM groups and roles
little stack create "$LITTLE_HOME/db/cloudformation/YourAccountNameHere/accountSetup/iamSetup.json"

Check if the stack came up successfully:

little stack events "$LITTLE_HOME/db/cloudformation/YourAccountNameHere/accountSetup/iamSetup.json"

If not, then you can delete the stack, fix whatever the problem is, and try again:

little stack delete "$LITTLE_HOME/db/cloudformation/YourAccountNameHere/accountSetup/iamSetup.json"

Similarly, you can modify a successfully deployed stack later:

little stack update "$LITTLE_HOME/db/cloudformation/YourAccountNameHere/accountSetup/iamSetup.json"
  • setup a real user for yourself

The iamSetup stack creates administrator, operator, and developer groups and roles - where members of each group can assume the corresponding role. Use the AWS web console to create a user (with MFA, etc) for yourself via the console, and add the user to the administrator group. Download an access key for the new user, and configure your local ~/.aws/config, so that you can run commands with an administrator token - something like this:

[default]
region = us-east-1
output = json
mfa_serial = arn:aws:iam::012345678901:mfa/yourUser

[profile admin-ohio]
region = us-east-2
role_arn = arn:aws:iam::012345678901:role/littleware/account/user/littleAdmin
source_profile = default
mfa_serial = arn:aws:iam::012345678901:mfa/yourUser

With these credentials in place, you can run commands like the following. These tools will prompt you for an MFA code when necessary to acquire a fresh access token:

export AWS_PROFILE=admin-ohio
aws s3 ls
little env | grep AWS_

You can now deploy the following stacks as the new administrator user, and delete the bootstrap user.

  • setup cloudtrail

Update the cloudtrail parameters (AWS/db/cloudformation/YourAccount/accountSetup/cloudTrail.json) with a bucket name unique to your account - something like cloudtrail-management-$YourAccountName. You can retrieve the name of your account with aws iam list-account-aliases.

little stack create "$LITTLE_HOME/db/cloudformation/YourAccountNameHere/accountSetup/cloudTrail.json"
  • setup an SNS topic

Remember to set the notification e-mail in the parameters before deploying the SNS stack; or customize the template with a subscriber for whatever notification channel (Slack, SMS, ...) you prefer. You can always add more subscribers to the topic later.

little stack create "$LITTLE_HOME/db/cloudformation/YourAccountNameHere/accountSetup/snsNotify.json"
  • setup alarms

Update the stack parameter files for the alaram stacks (AWS/db/YourAccount/accountSetup/*Alarm.json) to reference the new SNS topic (aws sns list-topics) before deploying the following stacks:

little stack create "$LITTLE_HOME/db/cloudformation/YourAccountNameHere/accountSetup/guardDuty.json"

little stack create "$LITTLE_HOME/db/cloudformation/YourAccountNameHere/accountSetup/budgetAlarm.json"

little stack create "$LITTLE_HOME/db/cloudformation/YourAccountNameHere/accountSetup/rootAccountAlarm.json"

little stack create "$LITTLE_HOME/db/cloudformation/YourAccountNameHere/accountSetup/iamAlarm.json"

Summary

We presented a simple way to secure API access in a new AWS account with authentication, authorization, a tagging strategy, a notification topic in SNS, basic cloudtrail logging, guard duty monitoring, and a few alarms. This simple setup is just a first step for a small team's journey into cloud security. A more sophisticated deployment would leverage AWS organizations and SSO. A larger organization may setup configuration rules, administrative accounts for centralized logging and alerts, and the journey goes on and on (we haven't even deployed an application yet).

Monday, December 24, 2018

EKS and AWS CNI IP Space management

AWS recently introduced its EKS managed kubernetes service which manages the kubernetes control plane (API and Etcd services), while the cluster owner administers the cluster's worker nodes in a VPC.

One of the features of EKS is the VPC CNI networking plugin that tightly integrates with AWS VPC networking, so that each kubernetes pod is assigned an IP address from the VPC CIDR range allocated to the worker node subnets. It's important to remember that EKS pods draw from the VPC IP pool when designing the VPC subnets for an EKS worker pool. When we transitioned our kubernetes infrastructure to EKS we initially allocated three /24 CIDR subnets for the EKS worker nodes. We assumed that design would allow a cluster of up to 768 nodes (256 nodes per subnet) where each node runs up to 5 pods, so up to 3840 pods with the calico CNI plugin that manages a separate IP space for pods in an overlay network; but with the AWS CNI the pods and nodes both draw IP addresses from the VPC pool of 768 IP addresses, so the cluster only supports about 625 pods on 125 nodes.

JWT Parser

I just published a new JSON web token (JWT) parser app under apps.frickjack.com. There's nothing special about this app - it's just another little vanilla-js plus custom-elements app that continues the evolution of the little-elements and little-apps code.

Thursday, October 11, 2018

little-elements webapp shell

I recently updated apps.frickjack.com to use unbundled javascript modules to load custom elements and style rules. It worked great, except I soon noticed that initial page load would present an un-styled, no-custom-element rendering of the page until the javascript modules finished loading asynchronously... ugh.

I worked around the problem by implementing a simple application shell. The html pages under https://apps.frickjack.com now extend a compile-time nunjucks template, basicShell.html.njk (npm install @littleware/little-elements). The shell hides the page content with a style rule, maybe loads the webcomponentsjs polyfill, and renders a "Loading ..." message if the main.js javascript module doesn't load fast enough. I also extended the main javascript module on each page to signal the shell when it has finished rendering the initial page content. For example - indexMain.js for https://apps.frickjack.com/index.html looks like this:

import './headerSimple/headerSimple.js';

if ( window['littleShell'] ) {
    window['littleShell'].clear();
}

The index.html nunjucks template looks like this:

{% extends "little-elements/lib/styleGuide/shell/basicShell.html.njk" %}

{% block head %}
<title>frickjack.com</title>
{% endblock %}

{% block content %}
    <lw-header-simple title="Home">
    </lw-header-simple>
    ...
   <script src="{{ jsroot }}/@littleware/little-apps/lib/indexMain.js" type="module"></script>
{% endblock %}

A visitor to https://apps.frickjack.com may now briefly see a "Loading" screen for the first page load, but subsequent page loads generally load fast enough from cache that the shell doesn't bother to show the "Loading" screen. I can improve on this by extending the shell with a service worker - that will be my next project.

I envy gmail's loading screen that renders a crazy animation of a googley envelope. Fancy!

Friday, August 03, 2018

jasminejs tests with es2015 modules

Running jasminejs tests of javascript modules in a browser is straight forward, but requires small adjustments, since a module loads asynchronously, while jasmine's default runtime assumes code loads synchronously. The approach I take is the same as the one used to test requirejs AMD modules:

  • customize jasmine boot, so that it does not automatically launch test execution on page load
  • write a testMain.js script that imports all the spec's for your test suite, then launches jasmine's test execution

For example, this is the root test suite (testMain.ts) for the @littleware/little-elements module:

import './test/spec/utilSpec.js';
import './arrivalPie/spec/arrivalPieSpec.js';
import './styleGuide/spec/styleGuideSpec.js';
import {startTest} from './test/util.js';

startTest();

The startTest() function is a little wrapper that detects whether karmajs is the test runtime, since the test bootstrap process is a little different in that scenario. The karmajs config file should also annotate javascript module files with a module type. For example, here is an excerpt from little-element's karma.conf.js:

    files: [
      'lib/test/karmaAdapter.js',
      { pattern: 'lib/arrivalPie/**/*.js', type: 'module', included: false },
      { pattern: 'lib/styleGuide/**/*.js', type: 'module', included: false },
      { pattern: 'lib/test/**/*.js', type: 'module', included: false },
      { pattern: 'lib/testMain.js', type: 'module', included: true },
      { pattern: 'node_modules/lit-html/*.js', type: 'module', included: false }
    ],

Feel free to import the @littleware/little-elements module into your project if it will be helpful!

Tuesday, July 31, 2018

little-elements

Last week I rolled out an update to apps.frickjack.com that changes the plumbing of the site.

  • The javascript is now organized as unbundled es2015 modules
  • The site also leverages javascript modules to manage html templates and CSS with lit-html

I also setup my first module on npm (here) to track UX components that could be shared between projects - which was fun :-)

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.

Saturday, April 07, 2018

nginx reverse-proxy tricks for jwt and csrf

I've been doing devops work over the last few months at CDIS. One of the tasks I worked on recently was to extend the nginx configuration (on github as a kubernetes configmap here) for the gen3 data commons stack.

First, we added end-user data to the proxy log by extracting user information from the JWT token in the auth cookie. We took advantage of the nginscript (nginscript is a subset of javascript) to import some standard javascript code (also in the kubernetes configmap) to parse the JWT.

function userid(req, res) {
  var token = req.variables["access_token"];
  var user = "uid:null,unknown@unknown";

  if (token) {
    // note - raw token is secret, so do not expose in userid
    var raw = atob((token.split('.')[1] || "").replace('-', '+').replace('_', '/'));
    if (raw) {
      try {
        var data = JSON.parse(raw);
        if (data) {
          if (data.context && data.context.user && data.context.user.name) {
            user = "uid:" + data.sub + "," + data.context.user.name;
          }
        }
      } catch (err) {}
    }
  }
  return user;
}

Next, we added a CSRF guard that verifies that if a CSRF cookie is present, then it matches a CSRF header. The logic is a little clunky (in github here), because of the restrictions on conditionals in nginx configuration, but it basically looks like the sample configuration below - where $access_token is the JWT auth token (either from a header from non-web clients, or a cookie for web browser clients), and an "ok-SOMETHING" $csrf_check is only required for browser clients. Finally - we do not enforce the HTTP-header based CSRF check in the proxy for endpoints that may be accessed by traditional HTML form submissions (that embed the token in the form body), or for endpoints that are accessed by third party browser frontends (like jupyterhub) that may implement a CSRF guard in a different way. Finally, there's also a little bit of logic that auto-generates a CSRF cookie if it's not present - a javascript client in the same domain can read the cookie to get the value to put in the X-CSRF header.

          set $access_token "";
          set $csrf_check "ok-tokenauth";
          if ($cookie_access_token) {
              set $access_token "bearer $cookie_access_token";
              # cookie auth requires csrf check
              set $csrf_check "fail";
          }
          if ($http_authorization) {
              # Authorization header is present - prefer that token over cookie token
              set $access_token "$http_authorization";
          }

          #
          # CSRF check
          # This block requires a csrftoken for all POST requests.
          #
          if ($cookie_csrftoken = $http_x_csrf_token) {
            # this will fail further below if cookie_csrftoken is empty
            set $csrf_check "ok-$cookie_csrftoken";
          }
          if ($request_method != "POST") {
            set $csrf_check "ok-$request_method";
          }
          if ($cookie_access_token = "") {
            # do this again here b/c empty cookie_csrftoken == empty http_x_csrf_token - ugh  
            set $csrf_check "ok-tokenauth";
          }
          ...
          location /index/ {
              if ($csrf_check !~ ^ok-\S.+$) {
                return 403 "failed csrf check";
              }
              if ($cookie_csrftoken = "") {
                add_header Set-Cookie "csrftoken=$request_id$request_length$request_time$time_iso8601;Path=/";
              }

              proxy_pass http://indexd-service/;
          }
          ...

Thursday, November 23, 2017

Jenkins Backup Pipeline

I recently setup a Jenkins CICD service to complement the Travis based automation already in use where I work. I worked with job-based Jenkins workflows in the past - where we setup chains of interdependent jobs (ex: build, publish assets, deploy to dev environment, ...), but I took this opportunity to adopt Jenkins' new (to me) Pipeline pattern, and I'm glad I did.

A Jenkins pipeline defines (via a groovy DSL) a sequence of steps that execute together in a build under a single Jenkins job. Here's an example pipeline we use to backup our Jenkins configuration to S3 every night.

#!groovy

pipeline {
  agent any

  stages {
    stage('BuildArchive'){
      steps {
        echo "BuildArchive $env.JENKINS_HOME"
        sh "tar cvJf backup.tar.xz --exclude '$env.JENKINS_HOME/jobs/[^/]*/builds/*' --exclude '$env.JENKINS_HOME/jobs/[^/]*/last*' --exclude '$env.JENKINS_HOME/workspace' --exclude '$env.JENKINS_HOME/war' --exclude '$env.JENKINS_HOME/jobs/[^/]*/workspace/'  $env.JENKINS_HOME"
      }
    }
    stage('UploadToS3') {
      steps {
        echo 'Upload to S3!'
        sh 'aws s3 cp --sse AES256 backup.tar.xz s3://cdis-terraform-state/JenkinsBackup/backup.$(date +%u).tar.xz'
      }
    }
    stage('Cleanup') {
      steps {
        echo 'Cleanup!'
        sh 'rm -f backup.tar.xz'
      }
    }
  }
  post {
    success {
      slackSend color: 'good', message: 'Jenkins backup pipeline succeeded'
    }
    failure {
      slackSend color: 'bad', message: 'Jenkins backup pipeline failed'
    }
    unstable {
      slackSend color: 'bad', message: 'Jenkins backup pipeline unstable'
    }
  }
}

If you are a Jenkins user, then take the time to give Pipelines a try. If you're also using github or bitbucket - then look into Jenkins' support for organizations that nicely support pull-request based workflows. Also try the new Blue Ocean UI - it's designed with pipelines in mind.

Saturday, August 19, 2017

stuff an app into a docker image

I had some fun over the last week setting up a docker image for an S3-copy utility, and integrating it into the gulp build for https://apps.frickjack.com. I pushed the image for the s3cp app up to hub.docker.com. I use s3cp to sync a local build of https://apps.frickjack.com up to an S3 bucket. I could probably coerce the AWS CLI (aws s3 sync) into doing the same job, but I wrote this app a while ago, and it includes functionality to automatically gzip-compress each asset before upload, and setup various HTTP headers on the asset (content-encoding, cache-control, content-type, and etag). For example - inspect the headers (using a browser's developer tools) on an arbitrary apps.frickjack.com asset like https://apps.frickjack.com/resources/css/littleware/styleGuide/guide.css.


A few s3cp details - it's a simple scala application with two command line flags (-config aws-key aws-secret, -copy source destination). The '-config' command saves the AWS credential under ~/.littleware/aws/. The code could use a little love - it is hard-coded to use the AWS Virginia region, it could use a '-force' option with '-copy', and its error messages are annoying - but it works for what I need. The source is on github (git clone ...; cd littleware/webapp/littleApps/s3Copy; gradle build; gradle copyToLib; cli/s3cp.sh --help).


I pushed a binary build of s3cp to dockerhub. I run it like this. First, I register my AWS credentials with the app. We only need to configure s3cp once, but we'll want to mount a docker volume to save the configuration to (I need to wire up a more secure way to save and pass the AWS secrets):


docker volume create littleware

docker run -it -v littleware:/root/.littleware \
    -v /home/reuben:/mnt/reuben \
    --name s3cp --rm frickjack/s3cp:1.0.0 \
    -config aws-key aws-secret

After the configuration is done I use s3cp with '-copy' commands like this:


docker run -it -v littleware:/root/.littleware \
    -v /home/reuben:/mnt/reuben \
    --name s3cp --rm frickjack/s3cp:1.0.0 \
    -copy /mnt/reuben/Code/littleware-html5Client/build/ s3://apps.frickjack.com/

I added a gulp task to the apps.frickjack.com code to simplify the S3 deploy - from gulpfile.js:


gulp.task( 'deploy', [ 'compileclean' ], function(cb) {
    const pwdPath = process.cwd();
    const imageName = "frickjack/s3cp:1.0.0";
    const commandStr = "yes | docker run --rm --name s3gulp -v littleware:/root/.littleware -v '" +
        pwdPath + ":/mnt/workspace' " + imageName + " -copy /mnt/workspace/build/ s3://apps.frickjack.com/";

    console.log( "Running: " + commandStr );

    exec( commandStr, 
        function (err, stdout, stderr) {
            console.log(stdout);
            console.log(stderr);
            if ( err ) {
                //reject( err );
            } else {
                cb();
            }
        }
    );
});

Sunday, August 13, 2017

versioning js and css with gulp-rev and gulp-revReplace

I've discussed before how I run https://apps.frickjack.com on S3, but one issue I did not address was how to version updates, so that each visitor loads a consistent set of assets. Here's the problem.

  • on Monday I publish v1 assets to the S3 bucket behind apps.frickjack.com
  • later on Monday Fred visits apps.frickjack.com, and his browser caches several v1 javascript and CSS files - a.js, b.js, x.css, y.css, ...
  • on Tuesday I publish v2 assets to S3 - just changing a few files
  • on Wednesday Fred visits apps.frickjack.com, but for whatever reason his browser cache updates b.js to v2, but loads v1 of the other assets from cache

On Wednesday Fred loads an inconsistent set of assets that might not work together. There are several approaches people take to avoid this problem. Surma gave a good overview of HTTP cache headers and thinking in this talk on YouTube, and Jake Archibald goes into more detail in this bLog (we set cache-headers directly on our apps.frickjack.com S3 objects).

Long story short - I finally wired up my gulpfile with gulp-rev and gulp-rev-replace to add a hash to the javascript and css file names. Each visitor to apps.frickjack.com is now guaranteed to load a consistent set of assets, because an asset's name changes when its content changes. I was really happy to find gulp-rev - it just takes care of things for me. The only gotcha is that gulp-rev-rewrite does not like to work with relative asset paths (ex - <script src="511.js"), so I had to update a few files to use absolute paths (src="/511/511.js") - otherwise things worked great.

Monday, August 07, 2017

use docker's overlay2 storage driver on Ubuntu 16.0+

Docker defaults to using its aufs storage driver on Ubuntu 16.0.4, but the system has a 4.4 kernel, so it's probably a good idea to switch over to the overlay2 driver.


$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 16.04.2 LTS
Release: 16.04
Codename: xenial

$ uname -r
4.4.0-83-generic

$ sudo cat /etc/docker/daemon.json
{
  "storage-driver":"overlay2",
  "log-driver": "journald"
}


Monday, June 05, 2017

S3+CloudFront+ACM+Route53 = serverless http2 https site with CDN

The other night I logged into the AWS web console, and upgraded my little S3 hosted site at https://apps.frickjack.com to http2 with TLS - which opens the door for adding a service worker to the site. As a side benefit the site is behind the CloudFront CDN. I also moved the domain's authoritative DNS to Route 53 earlier in the week - just to consolidate that functionality under AWS.

It's ridiculous how easy it was to do this upgrade - I should have done it a while ago:

  • create the Cloud Front network
  • configure the CDN with a certificate setup with the AWS Certificate Manager
  • update DNS for the domain 'apps.frickjack.com' to reference the CDN hostname - Route53 supports an 'alias' mechanism that exposes an A record, but you can also just use a CNAME if you have another DNS provider

Anyway - this is fun stuff to play with, and my AWS bill will still be less than $2 a month.

Monday, May 29, 2017

Content Security Policy and S3 hosted site

When I went to implement a content security policy on http://apps.frickjack.com I was disappointed to realize that the 'Content-Security-Policy' header is not one of the standard headers supported by S3's metadata API. This bLog
explains an approach using lambda on the CloudFront edge, but that looks crazy.

Fortunately - it turns out we can add a basic security policy to a page with a meta tag, so I added a <meta http-equiv="Content-Security-Policy" ...> tag to the nunjucks template that builds apps.frickjack.com's html pages at gulp compile time. The full code is on github:

<meta http-equiv="Content-Security-Policy" 
content="
  default-src 'none'; 
  img-src 'self' data: https://www.google-analytics.com; 
  script-src 'self' https://www.google-analytics.com; 
  style-src 'self' https://unpkg.com https://fonts.googleapis.com; 
  object-src 'none'; 
  font-src 'self' https://fonts.googleapis.com https://fonts.gstatic.com
"
>

Saturday, May 27, 2017

Update apps.frickjack.com - new 511 app

I finally updated my little S3-hosted web sandbox at http://apps.frickjack.com. The update has a few parts. First - I took down some old javascript apps and CSS code that I had developed several years ago using the YUI framework (now defunct), and tried to cleanup and simplify the landing page.

Next, I posted some new code exploring app development and testing with vanillajs, custom elements, and CSS. The new code includes an update to the 511 app for timing labor contractions with the 511 rule. The app uses a simple custom element to show the distribution of contractions over the last hour on a SVG pie-chart. The code is on github.

Finally, I switched the code over to an ISC license. I don't want an LGPL license to discourage people for copying something they find useful.

There's still a lot I'd like to do with apps.littleware - starting with upgrading the site to https by putting the S3 bucket behind cloud front, and wiring up a service worker. We'll see how long it takes me to make time for that ...

Thursday, January 26, 2017

VPN blocking Docker routes on Windows Workaround

Here's the situation. You're stuck in 2017 running Windows 7 with a Cisco VPN client. You're also a Docker evangelist, and run local developer environments using Docker Toolbox on that Windows 7 laptop. Docker Toolbox runs the Docker daemon on a Virtual Box VM running the boot2docker Linux distribution. One of the cool tricks Docker Toolbox manages for you is it sets up a virtual network (VirtualBox host-only network), so the Boot2Docker VM has its own IP address (192.168.0.100 or whatever), and you alias that IP address in \Windows\System32\drivers\etc\hosts, so that you can connect to https://my.docker.vm/services, and everything is super cool - until you connect to that damn Cisco VPN, because the VPN is configured by some bonehead IT Windows group policy to hijack all routes to private network IP addresses, and somehow they wired it so that you can't "route add" new routes to your Docker VM.

Fortunately - there's an easy workaround to this mess. First, identify a block of public IP addresses that you know you don't need to communicate with (I chose the 55.0.0.0/8 block assigned to the DOD Network Information Center), and reconfigure the Virtual Box host-only network to assign addresses from that block rather than the default private network it was originally configured with (the Virtual Box GUI has a tool under File -> Preferences -> Network). I had to reboot to get the boot2docker VM to stick to the new IP address, and screw around with 'docker-machine regenerate-certs', but it eventually worked. Good luck!

Monday, January 23, 2017

Debugging Dockerfile builds

I often find myself in a situation where I'm building an image from some Dockerfile, and the build fails 10 or 15 lines in, and I want to dive in and debug what's going wrong with that failing line. Fortunately - that's easy to do.
Let's suppose you're trying to build an image with a Dockerfile like this:


$ cat Dockerfile
FROM alpine:3.5
RUN echo "Step 2"
RUN echo "Step 3" && exit 1
RUN echo "Step 4"


Of course the build fails on 'exit 1' like this:


$ docker build -t demo:1.0.0 .Sending build context to Docker daemon  60.6 MB
Step 1 : FROM alpine:3.5
 ---> 88e169ea8f46
Step 2 : RUN echo "Step 2"
 ---> Running in 7ec0de04622c
Step 2
 ---> 281d8cac4e45
Removing intermediate container 7ec0de04622c
Step 3 : RUN echo "Step 3" && exit 1
 ---> Running in a8a16cb6d591
Step 3
The command '/bin/sh -c echo "Step 3" && exit 1' returned a non-zero code: 1

Fortunately, the docker build saves an intermediate image after each command in the Dockerfile, and outputs the id of that image (---> 281d8cac4e45), so it's easy to do something like this to debug that failing command:


$ docker run --name debug -v '/home/reuben:/mnt/reuben' -it 281d8cac4e45 /bin/sh
/ #