Continuous Integration (CI) and Continuous Deployment (CD) for databases is a lot harder than those things are for stateless applications. I still regularly meet teams who are deploying their database by hand because automated CD was too hard. Not coincidentally, those teams also need help from a data engineering consultant.

image: FabrĂzio Mello CC Attribution License
You can and should build your data structures from source control. This post will show you how to do so with Amazon Web Services’ Codestar, Redshift, and flyway. The goal is for your database structures to be in a known state at all times, and for all upgrades to be one-click.
A Virtual Private Cloud (VPC), with private subnets and Network Address Translation (NAT) gateways. I presume that most people already have one of these. If not, the following scripts should help:
You’ll need codestar, cloud formation, IAM, and redshift permissions.
There are no special requirements for Code Commit. I used a command line `aws codecommit create-repository cfn-flyway` call.
Next we use a Cloudformation script to create the Code Pipeline and its Code Build step.
---
AWSTemplateFormatVersion: "2010-09-09"
Description: Create a flyway CodePipeline for database migrations
Parameters:
  DBMasterUsername:
    Type: String
    Description: Name for the DB User
    Default: pipeline_is_dba
  DBMasterPassword:
    Type: String
    Description: Password for the DB
    NoEcho: true
  DBPortNumber:
    Description: The port number on which the database accepts incoming connections.
    Type: Number
    Default: 3306
  PipelineBucket:
    Type: String
    Description: Name of the S3 bucket to store CodePipeline artifacts.
  RepoName:
    Type: String
    Description: Name of the repo being deployed
  PrivateSubnet:
    Type: AWS::EC2::Subnet::Id
    Description: Subnet for codebuild to access DB
  VPC:
    Type: AWS::EC2::VPC::Id
    Description: Existing VPC in which to deploy
Resources:
  CodePipelineRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName:
        Fn::Sub: CodePipelineRole-${AWS::StackName}
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Principal:
              Service:
                - "codepipeline.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Path: /
      Policies:
        -
          PolicyName: "CodePipelineNestedCFNAccessPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              -
                Effect: "Allow"
                Action:
                  - "s3:DeleteObject"
                  - "s3:GetObject"
                  - "s3:GetObjectVersion"
                  - "s3:ListBucket"
                  - "s3:PutObject"
                  - "s3:GetBucketPolicy"
                Resource:
                  - Fn::Sub: arn:aws:s3:::${PipelineBucket}
                  - Fn::Sub: arn:aws:s3:::${PipelineBucket}/*
              -
                Effect: "Allow"
                Action:
                  - "codecommit:ListBranches"
                  - "codecommit:ListRepositories"
                  - "codecommit:BatchGetRepositories"
                  - "codecommit:Get*"
                  - "codecommit:GitPull"
                  - "codecommit:UploadArchive"
                Resource:
                  - Fn::Sub: arn:aws:codecommit:${AWS::Region}:${AWS::AccountId}:${RepoName}
              -
                Effect: "Allow"
                Action:
                  - "codebuild:StartBuild"
                  - "codebuild:BatchGetBuilds"
                Resource:
                  - Fn::Sub: arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/FlywayCodebuild
                  - Fn::Sub: arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:build/FlywayCodebuild:*
              -
                Effect: "Allow"
                Action:
                  - "iam:PassRole"
                Resource:
                  - Fn::Sub: arn:aws:iam::${AWS::AccountId}:role/CloudFormationRole-${AWS::StackName}
  CodeBuildRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName:
        Fn::Sub: CodeBuildRole-${AWS::StackName}
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Principal:
              Service:
                - "codebuild.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Path: /service-role/
      Policies:
        -
          PolicyName: "CodeBuildAccessPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Sid: CodeCommitReadPermissions
                Effect: "Allow"
                Action:
                  - "codecommit:ListBranches"
                  - "codecommit:ListRepositories"
                  - "codecommit:BatchGetRepositories"
                  - "codecommit:Get*"
                  - "codecommit:GitPull"
                Resource:
                  - Fn::Sub: arn:aws:codecommit:${AWS::Region}:${AWS::AccountId}:${RepoName}
              - Sid: GeneralReadPermissions
                Effect: "Allow"
                Action:
                  - "ec2:Describe*"
                  - "cloudformation:ValidateTemplate"
                  - "iam:Get*"
                  - "iam:List*"
                  - "logs:Describe*"
                  - "logs:Get*"
                  - "tag:Get*"
                Resource:
                  - "*"
              - Sid: WriteLogsPermissions
                Effect: "Allow"
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                Resource:
                  - "*"
              - Sid: PipelineBucketPermissions
                Effect: "Allow"
                Action:
                  - "s3:PutObject"
                  - "s3:GetObject"
                  - "s3:GetObjectVersion"
                  - "s3:ListBucket"
                Resource:
                  - Fn::Sub: arn:aws:s3:::codepipeline-${AWS::Region}-*
                  - Fn::Sub: arn:aws:s3:::${PipelineBucket}/*
                  - Fn::Sub: arn:aws:s3:::${PipelineBucket}
  CodeBuildInVPCPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: codebuild-in-vpc
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action:
              - ec2:CreateNetworkInterface
              - ec2:DescribeDhcpOptions
              - ec2:DescribeNetworkInterfaces
              - ec2:DeleteNetworkInterface
              - ec2:DescribeSubnets
              - ec2:DescribeSecurityGroups
              - ec2:DescribeVpcs
            Resource: "*"
          - Effect: Allow
            Action:
              - ec2:CreateNetworkInterfacePermission
            Resource: !Sub arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:network-interface/*
            Condition:
              StringEquals:
                ec2:AuthorizedService: codebuild.amazonaws.com
      Roles:
        - !Ref CodeBuildRole
  DBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Group for DB
      VpcId:
        Ref: VPC
  AccessDBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow access to DB
      VpcId:
        Ref: VPC
  ToDBRule:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      IpProtocol: tcp
      FromPort: !Ref DBPortNumber
      ToPort: !Ref DBPortNumber
      DestinationSecurityGroupId:
        Fn::GetAtt:
          - DBSecurityGroup
          - GroupId
      GroupId:
        Fn::GetAtt:
          - AccessDBSecurityGroup
          - GroupId
  DBListenRule:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      IpProtocol: tcp
      FromPort: !Ref DBPortNumber
      ToPort: !Ref DBPortNumber
      SourceSecurityGroupId:
        Fn::GetAtt:
          - AccessDBSecurityGroup
          - GroupId
      GroupId:
        Fn::GetAtt:
          - DBSecurityGroup
          - GroupId
  HTTPSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow outbound HTTPS, for S3, CodeCommit, etc
      VpcId:
        Ref: VPC
  HTTPSOutRule:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      IpProtocol: tcp
      FromPort: 443
      ToPort: 443
      CidrIp: 0.0.0.0/0
      GroupId:
        Fn::GetAtt:
          - HTTPSSecurityGroup
          - GroupId
  RedshiftSubnetGroup:
    Type: 'AWS::Redshift::ClusterSubnetGroup'
    Properties:
      Description: My ClusterSubnetGroup
      SubnetIds:
        - !Ref PrivateSubnet
  Redshift:
    Type: AWS::Redshift::Cluster
    Properties:
      ClusterSubnetGroupName:
        Ref: RedshiftSubnetGroup
      DBName: "mydb"
      MasterUsername:
        Ref: DBMasterUsername
      MasterUserPassword:
        Ref: DBMasterPassword
      NodeType: "dc2.large"
      ClusterType: "single-node"
      Port:
        Ref: DBPortNumber
      VpcSecurityGroupIds:
        - !GetAtt DBSecurityGroup.GroupId
  FlywayCodeBuild:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: FlywayCodebuild
      Description: build a database
      ServiceRole:
        Fn::GetAtt: [CodeBuildRole, Arn]
      Artifacts:
        Type: NO_ARTIFACTS
      Environment:
        Type: LINUX_CONTAINER
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/nodejs:7.0.0
        EnvironmentVariables:
          - Name: FLYWAY_URL
            Value: !Join ["", ["jdbc:redshift://", !GetAtt Redshift.Endpoint.Address, ":", !Ref DBPortNumber, "/mydb"]]
          - Name: FLYWAY_USER
            Value: !Ref DBMasterUsername
          - Name: FLYWAY_PASSWORD
            Value: !Ref DBMasterPassword
      Source:
        Location:
          Fn::Sub: https://git-codecommit.${AWS::Region}.amazonaws.com/v1/repos/${RepoName}
        Type: CODECOMMIT
      TimeoutInMinutes: 15
      EncryptionKey:
        Fn::Sub: arn:aws:kms:${AWS::Region}:${AWS::AccountId}:alias/aws/s3
      Tags:
        - Key: Name
          Value:
            Fn::Sub: FlywayCFN-${AWS::StackName}
      VpcConfig:
        SecurityGroupIds:
          - !Ref AccessDBSecurityGroup
          - !Ref HTTPSSecurityGroup
        Subnets:
          - !Ref PrivateSubnet
        VpcId: !Ref VPC
  DeployPipeline:
    Type: "AWS::CodePipeline::Pipeline"
    Properties:
      Name:
        Fn::Sub: Flyway-demo-${AWS::StackName}
      RoleArn:
        Fn::GetAtt: [CodePipelineRole, Arn]
      Stages:
        -
          Name: CodeCommitStage
          Actions:
            -
              Name: CodeCommitAction
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: "1"
                Provider: CodeCommit
              OutputArtifacts:
                -
                  Name: GitOut
              Configuration:
                BranchName: master
                RepositoryName:
                  Ref: RepoName
              RunOrder: 1
        -
          Name: Deploy
          Actions:
            -
              Name: CodeBuild
              InputArtifacts:
                - Name: GitOut
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: "1"
                Provider: CodeBuild
              Configuration:
                ProjectName:
                  Ref: FlywayCodeBuild
              RunOrder: 1
      ArtifactStore:
        Type: S3
        Location:
          Ref: PipelineBucket
There is a lot going on here, but its not too much to understand.
Code Build has been left at its defaults, expecting a buildspec.yml file. I use the same YAML most of the time:
---
version: 0.1
phases:
  install:
    commands:
      - echo "Configuring build files"
      # Allow execute on the files, -v is verbose, -v -v is more verbose
      - chmod -v -v +x ./*.sh
      - ./install.sh
  build:
    commands:
      - ./build.sh
This script does very little. It delegates the work to `install.sh` and `build.sh` and makes sure that they are executable. Those two shell scripts do the deployment.
#!/bin/bash
source ./build.env
wget "https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline/${FLYWAY_VERSION}/flyway-commandline-${FLYWAY_VERSION}-linux-x64.tar.gz"
tar -xvzf "flyway-commandline-${FLYWAY_VERSION}-linux-x64.tar.gz"
wget "https://s3.amazonaws.com/redshift-downloads/drivers/jdbc/${JDBC_VERSION}/RedshiftJDBC42-${JDBC_VERSION}.jar"
mv "RedshiftJDBC42-${JDBC_VERSION}.jar" "./flyway-${FLYWAY_VERSION}/drivers/"
mv ./sql "./flyway-${FLYWAY_VERSION}/sql/"
Install downloads and installs flyway and the redshift JDBC driver. It sources `build.env` for shared configuration:
#!/bin/bash
if [ "${BASH_SOURCE[0]}" -ef "$0" ]
then
    echo "Source this script, do not execute it!"
    exit 1
fi
export FLYWAY_VERSION="5.2.4"
export JDBC_VERSION="1.2.16.1027"
All that is left is to call flyway and let it build the database:
#!/bin/bash
source ./build.env
ls -R "./flyway-${FLYWAY_VERSION}/"
"./flyway-${FLYWAY_VERSION}/flyway" migrate -X
The flyway migrate command does the actual work of creating the database objects. The ls command is for debugging. Its output is written to the Code Build log.
So far we’re migrating no content. We need to create a `sql` folder, and start putting flyway compatibly named SQL scripts into it, for example:
CREATE TABLE first_table (id int primary key ,name varchar(100) );
And now you’re done. You have a record of what changed, when, why, and by whom. You can build a correct database from scratch, reliably and with almost no effort. When you’re comfortable, take away everyone’s DDL permissions on the database. And bask in the resulting calm and productivity.
I often hear that people believe that they need permission to type untested commands into production in order to resolve incidents. Typically in an untraceable way. I phrased it differently when I had this permission, but typing untested commands into production is what we’re talking about here. And I’ve never met a database that I didn’t configure where DDL commands were being logged in a way that the auditors can be confident that the log is complete. We don’t typically type untested commands into production because we need to test, because the source control commit message provides the motivation for change, and because we require a reliable audit log. Right?
As for how incidents are fixed, they are fixed the same way that everything else happens. The “ALTER DATABASE SET smoke=back_in” commands that are desperately needed to resolve incidents are committed into source control and deployed by the pipeline. With a test case that demonstrates if they worked or not.
Be the first to comment.