Nix <3 AWS

NixCon 2019, Brno

DeckDeckGo

  • ​Tool for writing presentations
  • Frontend: WebComponents + typescript
  • Backend: Haskell
  • Deployment: Nix + Terraform
  • Platform: AWS (Lambda, S3, SQS, DynamoDB, RDS)
  • github.com/deckgo/deckdeckgo

λ -> AWS Lambda

  • ​fully static Haskell executables
  • package as zip, upload with Terraform
  • nh2/static-haskell-nix

  • # nix/default.nix let sources = import ./sources.nix; pkgs = import sources.nixpkgs { overlays = [ (_: pkgs: pkgs.lib.recursiveUpdate pkgs { haskell = ...; # use callCabal2Nix to add DDG packages } ) ]; }; survey = import "${sources.static-haskell-nix}/survey" { normalPkgs = pkgs; }; haskellPackages = survey.pkgsWithStaticHaskellBinaries.haskellPackages; in pkgs // { inherit haskellPackages; } # default.nix { function = pkgs.runCommand "build-lambda" {} '' cp ${pkgs.wai-lambda.wai-lambda-js-wrapper} main.js cp "${pkgs.haskellPackages.deckdeckgo-handler;}/bin/handler" main_hs mkdir $out ${pkgs.zip}/bin/zip -r $out/function.zip main.js main_hs dist.tar ''; }
    # ./default.nix { function = pkgs.runCommand "build-lambda" ...; function-handler-path = { path = builtins.seq (builtins.readDir function) "${function}/function.zip"; } ; } # main.tf resource "aws_lambda_function" "api" { function_name = "deckdeckgo-handler-lambda" filename = data.external.build-function.result.path runtime = ... } data "external" "build-function" { program = [ "nix", "eval", "(import ./default.nix).function-handler-path", "--json", ] }

    AWS services

    • docker-based environments
    • better: open-source alternatives + proprietary jars
    • redirect URLs
    • wash, rinse, repeat

    S3

    • Used for storing published presentations
    • https://min.io - "High Performance Object Storage"

    S3 - setup

    pkgs.runCommand "tests"
    { buildInputs =
    [ ...
    pkgs.minio
    ];
    }
    ''
    MINIO_ACCESS_KEY=dummy \ MINIO_SECRET_KEY=dummy_key \ minio server --address localhost:9000 $(mktemp -d) &
    ... integration tests
    ''

    S3 - fixup

    rerouteS3 :: HTTPClient.Request -> HTTPClient.Request
    rerouteS3 req =
    case HTTPClient.host req of
    "s3.amazonaws.com" ->
    req
    { HTTPClient.host = "127.0.0.1"
    , HTTPClient.port = 9000
    , HTTPClient.secure = False
    }
    _ -> req

    SQS

    • https://aws.amazon.com/sqs/
    • inter-lambda communication 
    • softwaremill/elasticmq: In-memory message queue with an Amazon SQS-compatible interface.
    ''
    java -jar ${pkgs.sources.elasticmq} &

    ... integration tests
    ''

    {
    "elasticmq": {
    "sha256": "1cp2pmkc6gx7gr6109jlcphlky5rr6s1wj528r6hyhzdc01sjhhz",
    "type": "file",
    "version": "0.14.6",
    "url": "https://s3-eu-west-1.amazonaws.com/softwaremill-public/elasticmq-server-0.14.6.jar",
    "url_template": "https://s3-eu-west-1.amazonaws.com/softwaremill-public/elasticmq-server-<version>.jar"
    }
    }

    SQS - fixup

    rerouteSQS :: HTTPClient.Request -> HTTPClient.Request rerouteSQS req = case HTTPClient.host req of "queue.amazonaws.com" -> req { HTTPClient.host = "127.0.0.1" , HTTPClient.port = 9324 , HTTPClient.secure = False } _ -> req

    DynamoDB

    • Used early on to store presentation data 
    • https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.DownloadingAndRunning.html

    DynamoDB - setup

    dynamoJar = pkgs.runCommand "dynamodb-jar" { buildInputs = [ pkgs.gnutar ]; } '' mkdir -p $out cd $out tar -xvf ${pkgs.sources.dynamodb} ''; test = pkgs.runCommand "my-test" {} '' # Set up DynamoDB java \ -Djava.library.path=${dynamoJar}/DynamoDBLocal_lib \ -jar ${dynamoJar}/DynamoDBLocal.jar \ -sharedDb -port 8123 & ... integration tests '';

    DynamoDB - fixup

    rerouteDynamoDB :: HTTPClient.Request -> HTTPClient.Request
    rerouteDynamoDB req =
    case HTTPClient.host req of
    "dynamodb.us-east-1.amazonaws.com" ->
    req
    { HTTPClient.host = "127.0.0.1"
    , HTTPClient.port = 8123
    , HTTPClient.secure = False
    }
    _ -> req

    RDS/PostgreSQL

    • Store much of user data (though not that much)
    • No system-wide install

    RDS/PostgreSQL

    initdb -D .pgdata

    echo "unix_socket_directories = '$(mktemp -d)'" >> .pgdata/postgresql.conf

    pg_ctl -D ".pgdata" -w start || (echo pg_ctl failed; exit 1)

    until psql postgres -c "SELECT 1" > /dev/null 2>&1 ; do
    echo waiting for pg
    sleep 0.5
    done

    psql postgres -w -c "CREATE DATABASE $LOCAL_PGDATABASE"
    psql postgres -w -c "CREATE ROLE $LOCAL_PGUSER WITH LOGIN PASSWORD '$LOCAL_PGPASSWORD'"
    psql postgres -w -c "GRANT ALL PRIVILEGES ON DATABASE $LOCAL_PGDATABASE TO $LOCAL_PGUSER" ... integration tests pg_ctl -D .pgdata -w -m immediate stop

    Development Shell

    • ​code check/build
    • integration test services
    • deploy
    • https://github.com/deckgo/deckdeckgo/blob/f7053d57daf30607f1b4034170af6c41c5fe3cd5/infra/default.nix#L78

    thanks nixcon!!