Updated terraform config for newer aws provider, deleted old lambdas

This commit is contained in:
Jocelyn Badgley (Twipped) 2023-02-19 12:58:46 -08:00
parent bb52a23297
commit 77bd70819a
20 changed files with 238 additions and 2997 deletions

1
.gitignore vendored
View File

@ -23,3 +23,4 @@ node_modules
/analytics/database.sqlite
/analytics/RAW
/public/gdb.zip
/terraform/secret.auto.tfvars

View File

@ -2,39 +2,82 @@
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/archive" {
version = "2.1.0"
version = "2.3.0"
hashes = [
"h1:Rjd4bHMA69V+16tiriAUTW8vvqoljzNLmEaRBCgzpUs=",
"h1:f3WXKM/FBu5EMY6j2BGt982hzVMNicrxTyEAz5EsrOU=",
"zh:033279ecbf60f565303222e9a6d26b50fdebe43aa1c6e8f565f09bb64d67c3fd",
"zh:0af998e42eb421c92e87202df5bfee436b3cfe553214394f08d786c72a9e3f70",
"zh:1183b661c692f38409a61eefb5d412167c246fcd9e49d4d61d6d910012d206ba",
"zh:5febb66f4a8207117f71dcd460fb9c81d3afb7b600b5e598cf517cf6e27cf4b2",
"zh:66135ce46d29d0ccf0e3b6a119423754ca334dbf4266bc989cce5b0b667b5fde",
"zh:6b9dc1a4f0a680bb650a7191784927f99675a8c8dd3c155ba821185f630db604",
"zh:91e249482c016ecf6bf8b83849964005cd2d0b4396688419cd1752809b46b23e",
"zh:a6a2e5f2f010c511e66174cb84ea18899e8bcfc1354c4b9fed972fdb131ffffc",
"zh:bb1f6abc76552a883732caff897ff7b07a91977a9b4bb97915f6aac54116bb65",
"zh:f05a9a63607f85719fde705f58d82ee16fa67f9158a5c3424c0216507631eddf",
"zh:fc603a05a06814387ffa4a054d1baee8ea6b5ab32c53cb73e90a5bf9a2616777",
"h1:NaDbOqAcA9d8DiAS5/6+5smXwN3/+twJGb3QRiz6pNw=",
"zh:0869128d13abe12b297b0cd13b8767f10d6bf047f5afc4215615aabc39c2eb4f",
"zh:481ed837d63ba3aa45dd8736da83e911e3509dee0e7961bf5c00ed2644f807b3",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:9f08fe2977e2166849be24fb9f394e4d2697414d463f7996fd0d7beb4e19a29c",
"zh:9fe566deeafd460d27999ca0bbfd85426a5fcfcb40007b23884deb76da127b6f",
"zh:a1bd9a60925d9769e0da322e4523330ee86af9dc2e770cba1d0247a999ef29cb",
"zh:bb4094c8149f74308b22a87e1ac19bcccca76e8ef021b571074d9bccf1c0c6f0",
"zh:c8984c9def239041ce41ec8e19bbd76a49e74ed2024ff736dad60429dee89bcc",
"zh:ea4bb5ae73db1de3a586e62f39106f5e56770804a55aa5e6b4f642df973e0e75",
"zh:f44a9d596ecc3a8c5653f56ba0cd202ad93b49f76767f4608daf7260b813289e",
"zh:f5c5e6cc9f7f070020ab7d95fcc9ed8e20d5cf219978295a71236e22cbb6d508",
"zh:fd2273f51dcc8f43403bf1e425ba9db08a57c3ddcba5ad7a51742ccde21ca611",
]
}
provider "registry.terraform.io/hashicorp/aws" {
version = "3.30.0"
version = "4.55.0"
hashes = [
"h1:PmKa3uxO2mDA5FJfGmpX+4e0x70vFLV5Ka9NxkuMpUo=",
"h1:z9kdXY2A/+dIZrPy9hNlg/B5I/AuETQsp0jz9EgprIQ=",
"zh:01f562a6a31fe46a8ca74804f360e3452b26f71abc549ce1f0ab5a8af2484cdf",
"zh:25bacc5ed725051f0ab1f7d575e45c901e5b8e1d50da4156a31dda92b2b7e481",
"zh:349b79979d9169db614d8ebd1bc2e0caeb7a38dc816e261b8b2b4b5204615519",
"zh:5e41446acc54c6fc15e82c3fa14b72174b30eba81e0711ede297e5620c55a628",
"zh:68ad98f6d612bdc35a65d48950abc8e75c69decb49db28258ce8eeb5458586b7",
"zh:704603d65e8bac17d203b57c2db142c3134a91076e1b4a31c40f75eb3257dde8",
"zh:a362c700032b2db047d16007d52f28b3f216d32671b6b355d23bdaa082c66a4b",
"zh:bd197797b41268de3c93cad02b7c655dc0c4d8661abb37544ca049e6b1eccae6",
"zh:deb12ef0e3396a71d485977ddc14b695775f7937097ebf2b2f53ed348a4365e7",
"zh:ec8a7d0f02738f290107d39bf401d68ddce82a95cd9d998003f7e04b3a196411",
"zh:ffcc43b6c5e7f26c55e2a8c539d7370fca8042722400a3e06bdce4240bd7088a",
"h1:vSVjfh4GIrca2Z3YPjWMaac5hOEBc1U3xOOwFmo7HZc=",
"zh:0866f25575bad3b9c313cd778c94fc65e79d335af2d20a3480f79d7731d93b7b",
"zh:2c05c16155cbc054622cf83e4b6614fef35935b00b238e4c21ee225e6c896770",
"zh:2efba66649fb12af0492c6cce4e2361fe9139df648734264f61a9a1ef754df53",
"zh:3c60bb53e3b65d7f86699fae0797a55a9aa41b8ba377aaff4daf23d1661393a9",
"zh:41f6dcd90b54b623d523df8fb4a30779cfe22e9ab59516bc05b29291a7af0946",
"zh:4b8330b154e9e2d035dd5488abcac25efec1fa6055d3a70894a8c0384f0579d6",
"zh:595f263706cf1fb6b8447e2ec343638de4360841a15e6bff6ccbb0ff86c7ce74",
"zh:5dfc5b858a43cf45fde5542eb673f6104c14cdc3d73843d1b87a9e44545cbad4",
"zh:7bbe05cf30521f0110603bb84995a4025ce7810626010276600e4b402143df27",
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
"zh:a490e68c63504d3301d6dcb700c95778d93bb2baa6632a46c5a1d62862a7067c",
"zh:c4f9f6659148528375c8a822163925c9aae490ccce2e6301cefbbab009531971",
"zh:ef66070f957408f1c924ddfd5dbd0d34bce16efd9e36ccecbf699de72beb131f",
"zh:f7ba5e3e62a2b51b24e326797a89fdd86bafaea7d1912738d514c9903c14d7f2",
"zh:ffc20b7d9f7bd331fb6451d0fc92c68196383d7115e69380de6566cc268cb9b9",
]
}
provider "registry.terraform.io/hashicorp/http" {
version = "3.2.1"
hashes = [
"h1:Q2YQZzEhHQVlkQCQVpMzFVs0Gg+eXzISbOwaOYqpflc=",
"zh:088b3b3128034485e11dff8da16e857d316fbefeaaf5bef24cceda34c6980641",
"zh:09ed1f2462ea4590b112e048c4af556f0b6eafc7cf2c75bb2ac21cd87ca59377",
"zh:39c6b0b4d3f0f65e783c467d3f634e2394820b8aef907fcc24493f21dcf73ca3",
"zh:47aab45327daecd33158a36c1a36004180a518bf1620cdd5cfc5e1fe77d5a86f",
"zh:4d70a990aa48116ab6f194eef393082c21cf58bece933b63575c63c1d2b66818",
"zh:65470c43fda950c7e9ac89417303c470146de984201fff6ef84299ea29e02d30",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:842b4dd63e438f5cd5fdfba1c09b8fdf268e8766e6690988ee24e8b25bfd9e8d",
"zh:a167a057f7e2d80c78d4b4057538588131fceb983d5c93b07675ad9eb1aa5790",
"zh:d0ba69b62b6db788cfe3cf8f7dc6e9a0eabe2927dc119d7fe3fe6573ee559e66",
"zh:e28d24c1d5ff24b1d1cc6f0074a1f41a6974f473f4ff7a37e55c7b6dca68308a",
"zh:fde8a50554960e5366fd0e1ca330a7c1d24ae6bbb2888137a5c83d83ce14fd18",
]
}
provider "registry.terraform.io/namecheap/namecheap" {
version = "2.1.0"
constraints = ">= 2.0.0"
hashes = [
"h1:wYjp1Gs7wf2J+VtkLmTRO6mALieCFhS0HYsQUwAbAUI=",
"zh:3731f5f14a0958cd27a589ef7daa9be786b6490f2309c429eb2e9862aa4ac5f7",
"zh:3cbceb12ec3521d9dfbd890eee731a40f4e1f42de30d28fc1d1e524091148caa",
"zh:44095af1b1d1ee6d4b930e21e3c5bf0f81d9df65fe04f6f1e55d46713c240b21",
"zh:693e169228fe0c5fb1989425b1ad42c1206f8187c9932b4daee5a5c5e851a28e",
"zh:6b04c3c2666db3050f49bc85151496fe33cf852db9ad8fc6f455d1daf0a2bba6",
"zh:85fd126a573cc468f8d5d1b90f4a94f5977ea40623b1c5cd7c799bb95ef233bd",
"zh:99014437ef4e96161b0029efa12f05fa1ab63ff9bc0a255b0a249e17b4f8587a",
"zh:a4d8288ef01d4002a5aa07d1e64e4504757f07d6ada24fbf7d3670ceb24d2871",
"zh:d27f7798cbe1957294bb08459b1fbabe68721cc9cc50afee80bda87ce674dab8",
"zh:d85483f90380829d05b8a2725ce50bf2ee766d6c1cbef223b388d19c5a92dce2",
"zh:ddfecfbefd32e40386b482a2610e4173a52591afea3861f041041439d51d34da",
"zh:f9a10edfe11dbc4947cbb2f0db8935050693d5fff3b6559096288c689c2dd847",
"zh:fae14a74781a94bcaac07b6d533dd9eb1e40c1d152eb6ee49b2a44cdf5740cfe",
]
}

View File

@ -1,90 +0,0 @@
/* eslint no-console:0 */
const AWS = require('aws-sdk');
const zlib = require('zlib');
const util = require('util');
const path = require('path');
const { URL } = require('url');
const s3 = new AWS.S3();
const { parse: parseLog } = require('cloudfront-log-parser');
const parseUA = require('ua-parser-js');
const format = require('date-fns/format');
const gunzip = util.promisify(zlib.gunzip);
const gzip = util.promisify(zlib.gzip);
function url (input) {
const { hash, host, hostname, href, origin, password, pathname, port, protocol, search, searchParams, username } = new URL(input);
return { hash, host, hostname, href, origin, password, pathname, port, protocol, search, searchParams, username };
}
exports.handler = async (event) => {
// Read options from the event.
console.log('Reading options from event:\n', JSON.stringify(event, null, 2));
const Bucket = event.Records[0].s3.bucket.name;
const inputKey = event.Records[0].s3.object.key;
const file = path.parse(inputKey);
const outputKey = path.format({ ...file, dir: 'Converted', ext: '.json.gz' });
const response = await s3.getObject({ Bucket, Key: inputKey }).promise();
const input = (await gunzip(response.Body)).toString('utf8');
const entries = parseLog(input, { format: 'web' });
console.log(`Found ${entries.length} rows`);
const results = entries.map((row) => {
// filter out OPTIONS calls
if (row['cs-method'] === 'OPTIONS') return null;
// I only care about the pixel hits, nothing else.
if (row['cs-uri-stem'] !== '/i') return null;
// this isn't an analytics event
if (row['cs-referer'] === '-') return null;
row = Object.fromEntries(Object.entries(row).map(([ k, v ]) => [ k.replace(/-/g, '_'), v ]));
const query = (row.cs_uri_query === '-')
? {}
: Object.fromEntries(new URLSearchParams(row.cs_uri_query))
;
// we didn't get analytics data from this load, ignore it
if (!query.start) return null;
const useragent = parseUA(row.cs_user_agent);
const sessionStart = Number(query.start);
const sessionEnd = query.end === 'null' ? 0 : Number(query.end);
const duration = sessionEnd > sessionStart ? Math.floor((sessionEnd - sessionStart) / 1000) : null;
return JSON.stringify({
dts: `${row.date} ${row.time}`,
url: url(row.cs_referer),
referer: url(query.referer),
client_start: format(new Date(sessionStart), 'yyyy-MM-dd HH:mm:ss'),
client_end: sessionEnd ? format(new Date(sessionStart), 'yyyy-MM-dd HH:mm:ss') : null,
duration,
useragent,
query,
original: row,
});
}).filter(Boolean);
if (!results.length) {
console.log('No results to save');
return;
}
console.log('Writing new file to ' + outputKey);
await s3.putObject({
Bucket,
Key: outputKey,
Body: await gzip(Buffer.from(results.join('\n'))),
ContentType: 'application/gzip',
}).promise();
};

View File

@ -1,117 +0,0 @@
{
"name": "decorate",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"aws-sdk": {
"version": "2.975.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.975.0.tgz",
"integrity": "sha512-cmRcM+gU+rfW1RZjca3bdbRTQV6WHuCI6NuqnDgd5HoqufylCrNI/wtysPK1IMJLsAhrcsY0U0YRx+Y/hxQNoQ==",
"requires": {
"buffer": "4.9.2",
"events": "1.1.1",
"ieee754": "1.1.13",
"jmespath": "0.15.0",
"querystring": "0.2.0",
"sax": "1.2.1",
"url": "0.10.3",
"uuid": "3.3.2",
"xml2js": "0.4.19"
}
},
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"buffer": {
"version": "4.9.2",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
"integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4",
"isarray": "^1.0.0"
}
},
"cloudfront-log-parser": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cloudfront-log-parser/-/cloudfront-log-parser-1.1.0.tgz",
"integrity": "sha512-g1lxh8aW5ZrQ7/loX2/vLzz4SWefQhSvZw++wgoIx3aEugXHKyfYaWOXGS4pNNp9hi7JcXITxLYqBI2FY+jtgA=="
},
"date-fns": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.9.0.tgz",
"integrity": "sha512-khbFLu/MlzLjEzy9Gh8oY1hNt/Dvxw3J6Rbc28cVoYWQaC1S3YI4xwkF9ZWcjDLscbZlY9hISMr66RFzZagLsA=="
},
"events": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
"integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
},
"ieee754": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"jmespath": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz",
"integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc="
},
"punycode": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
},
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
},
"sax": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
"integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o="
},
"ua-parser-js": {
"version": "0.7.21",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz",
"integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ=="
},
"url": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
"integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=",
"requires": {
"punycode": "1.3.2",
"querystring": "0.2.0"
}
},
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
},
"xml2js": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
"requires": {
"sax": ">=0.6.0",
"xmlbuilder": "~9.0.1"
}
},
"xmlbuilder": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
"integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0="
}
}
}

View File

@ -1,20 +0,0 @@
{
"name": "decorate",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Jocelyn Badgley <joc@twipped.com> (http://twipped.com/)",
"license": "MIT",
"dependencies": {
"aws-sdk": "~2.975.0",
"cloudfront-log-parser": "~1.1.0",
"date-fns": "~2.9.0",
"ua-parser-js": "~0.7.21"
},
"engines": {
"node": ">=12.14.0"
}
}

View File

@ -26,62 +26,73 @@ resource "aws_iam_role" "lambda_redirect" {
}
}
#######################################################
# LOGGING POLICY
# -----------------------------------------------------------------------------------------------------------
# IAM Role for Log Parsing Lambda
data "aws_iam_policy_document" "s3_bucket_access" {
data "aws_iam_policy_document" "lambda_logging" {
statement {
actions = [
"s3:*",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:CreateLogGroup"
]
resources = [
aws_s3_bucket.ipixel_logs.arn,
"${aws_s3_bucket.ipixel_logs.arn}/*",
]
resources = [ "arn:aws:logs:*:*:*" ]
}
}
data "aws_iam_policy_document" "lambda_assume_role" {
statement {
actions = ["sts:AssumeRole"]
resource "aws_iam_policy" "lambda_logging" {
name = "${var.site}_lambda_logging"
path = "/"
description = "IAM policy for logging from a lambda"
principals {
type = "Service"
identifiers = [
"edgelambda.amazonaws.com",
"lambda.amazonaws.com"
]
policy = data.aws_iam_policy_document.lambda_logging.json
}
resource "aws_iam_role_policy_attachment" "lambda_logs" {
role = aws_iam_role.lambda_redirect.name
policy_arn = aws_iam_policy.lambda_logging.arn
}
#######################################################
# REPLICATION POLICY
# aws_iam_policy_document.lambda_replication
data "aws_iam_policy_document" "lambda_replication" {
statement {
actions = [
"lambda:EnableReplication*",
]
resources = [
"*"
]
}
statement {
actions = [
"iam:CreateServiceLinkedRole"
]
resources = [
"arn:aws:iam::*:role/aws-service-role/events.amazonaws.com/AWSServiceRoleForCloudWatchEvents*"
]
condition {
test = "StringLike"
variable = "iam:AWSServiceName"
values = ["events.amazonaws.com"]
}
}
}
resource "aws_iam_role" "ipixel_parser" {
name = "lambda-${var.site}-ipixel"
resource "aws_iam_policy" "lambda_replication" {
name = "${var.site}_lambda_replication"
path = "/"
description = "IAM policy for replication by lambda@edge"
assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json
tags = {
Site = var.site,
Role = "ipixel"
}
policy = data.aws_iam_policy_document.lambda_replication.json
}
resource "aws_iam_role_policy_attachment" "ipixel_parser" {
role = aws_iam_role.ipixel_parser.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
resource "aws_iam_role_policy_attachment" "lambda_replication" {
role = aws_iam_role.lambda_redirect.name
policy_arn = aws_iam_policy.lambda_replication.arn
}
resource "aws_iam_role_policy" "ipixel_parser_cloudwatch_log_group" {
name = "cloudwatch-log-group"
role = aws_iam_role.ipixel_parser.name
policy = data.aws_iam_policy_document.ipixel_parser_cloudwatch_log_group.json
}
resource "aws_iam_role_policy" "lambda_s3_bucket_readonly" {
name = "s3-bucket-readonly"
role = aws_iam_role.ipixel_parser.name
policy = data.aws_iam_policy_document.s3_bucket_access.json
}

View File

@ -1,9 +0,0 @@
{
"extends": "@twipped/eslint-config/node-cjs",
"rules": {
"node/no-unpublished-require": 0,
"indent": [ 2, 2, {
"MemberExpression": 1
} ]
}
}

View File

@ -1 +0,0 @@
12.13

View File

@ -1,2 +0,0 @@
module.exports = exports = require('./src/index');

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +0,0 @@
{
"name": "cloudfront-logs",
"version": "0.0.1",
"dependencies": {
"aws-sdk": "~2.966.0",
"date-fns": "~2.23.0",
"ua-parser-js": "~0.7.28"
},
"devDependencies": {
"eslint": "~7.32.0",
"eslint-config-airbnb-base": "*",
"eslint-plugin-import": "~2.24.0"
},
"scripts": {
"lint": "eslint ."
},
"engines": {
"node": ">=12.14.0"
}
}

View File

@ -1,165 +0,0 @@
/* eslint no-console: 0 */
const { gunzip } = require('zlib');
const { promisify } = require('util');
const { S3 } = require('aws-sdk');
const { unescape } = require('querystring');
const parseUA = require('ua-parser-js');
const format = require('date-fns/format');
const { URL } = require('url');
const gunzipAsync = promisify(gunzip);
function url (input) {
try {
const { hash, host, hostname, href, origin, password, pathname, port, protocol, search, searchParams, username } = new URL(input); // eslint-disable-line max-len
return { hash, host, hostname, href, origin, password, pathname, port, protocol, search, searchParams, username };
} catch (e) {
return null;
}
}
// Parsing the line containing the version.
//
// Format:
//
// #Version: 1.0
//
const parseVersion = (line) => {
if (!line.startsWith('#Version:')) {
throw new Error(`Invalid version line '${line}'`);
} else {
return line.match(/[\d.]+$/);
}
};
// Parsing the line containinge the fields format and use kebab case.
// https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html#LogFileFormat
//
// Format:
// eslint-disable-next-line max-len
// #Fields: date time x-edge-location sc-bytes c-ip cs-method cs(Host) cs-uri-stem sc-status cs(Referer) cs(User-Agent) cs-uri-query cs(Cookie) x-edge-result-type x-edge-request-id x-host-header cs-protocol cs-bytes time-taken x-forwarded-for ssl-protocol ssl-cipher x-edge-response-result-type cs-protocol-version fle-status fle-encrypted-fields
//
const parseFields = (line) => {
if (!line.startsWith('#Fields:')) {
throw new Error(`Invalid fields line '${line}'`);
} else {
return line.match(/[\w()-]+(\s|$)/g).map((field) => (
// Strip parentheses and remove unecessary abbreviations in field names
field.replace(/\(([^)]+)\)/, '-$1').replace(/^(c-|cs-|sc-)/, '').trim().toLowerCase()
));
}
};
// Unescape value twice (because fuck you that's why).
// https://forums.aws.amazon.com/thread.jspa?threadID=134017
//
const decode = (value) => unescape(unescape(value));
// Split up line and assign to corresponding field.
//
const parseLine = (line, fields) => {
if (line.startsWith('#')) {
throw new Error(`Invalid log line '${line}'`);
} else {
let row = line.split('\t').reduce((object, section, index) => {
const result = object;
if (section !== '-') result[fields[index]] = decode(section); // Skip missing fields
return result;
}, {});
// filter out OPTIONS calls
if (row.method === 'OPTIONS') return;
// I only care about the pixel hits, nothing else.
if (row['uri-stem'] !== '/i') return;
// this isn't an analytics event
if (!row.referer) return;
row = Object.fromEntries(Object.entries(row).map(([ k, v ]) => [ k.replace(/-/g, '_'), v ]));
const query = (row.uri_query)
? Object.fromEntries(new URLSearchParams(row.uri_query))
: {}
;
const useragent = parseUA(row.user_agent);
const sessionStart = Number(query.start);
const sessionEnd = query.end === 'null' ? 0 : Number(query.end);
const duration = sessionEnd > sessionStart ? Math.floor((sessionEnd - sessionStart) / 1000) : null;
let {
language,
viewed,
max_scroll,
page_height,
viewport_height,
} = query;
max_scroll = parseInt(max_scroll, 10) || 0;
page_height = parseInt(page_height, 10) || 0;
viewport_height = parseInt(viewport_height, 10) || 0;
const { pathname } = url(row.referer) || {};
const { hostname: referrer_host, href: referrer } = url(query.referrer) || {};
const result = {
dts: `${row.date} ${row.time}`,
ip: row.ip,
tid: query.tid !== 'false' ? query.tid : null,
url: pathname,
referrer,
referrer_host,
client_start: format(new Date(sessionStart), 'yyyy-MM-dd HH:mm:ss'),
client_end: sessionEnd ? format(new Date(sessionStart), 'yyyy-MM-dd HH:mm:ss') : null,
duration,
language,
viewed,
max_scroll,
page_height,
viewport_height,
browser: useragent.browser.name,
browser_version: useragent.browser.major,
os: useragent.os.name + ' ' + useragent.os.version,
device_type: useragent.device && useragent.device.type || null,
device: useragent.device && useragent.device.vendor && useragent.device.vendor + ' ' + useragent.device.model || null,
useragent,
query,
original: row,
};
return result;
}
};
// Get log file from S3 and unzip it.
//
const getLogFile = async ({ bucket, key, region }) => {
const s3 = new S3({ region });
const zippedObject = await s3.getObject({ Bucket: bucket, Key: key }).promise();
const logFile = await gunzipAsync(zippedObject.Body);
return logFile.toString().trim();
};
// Parse log file and return a list of log events.
//
exports.parseLogFile = async ({ bucket, key, region }) => {
const file = await getLogFile({ bucket, key, region });
const lines = file.split('\n');
// Shift first line which contains the version and parse it for validation
parseVersion(lines.shift());
// Shift next line containing fields format and parse it for validation
const fields = parseFields(lines.shift());
console.log(`Found ${lines.length} rows to parse`); // eslint-disable-line no-console
const rows = lines.map((line) => parseLine(line, fields)).filter(Boolean);
console.log(`Produced ${rows.length} results`);
console.log('Sample', rows[0]);
return rows;
};

View File

@ -1,88 +0,0 @@
const { CloudWatchLogs } = require('aws-sdk');
// Split up ARN like "arn:aws:logs:eu-west-1:123456789012:log-group:example-group:*"
const [ ,,, region,,, logGroupName ] = process.env.CLOUDWATCH_LOGS_GROUP_ARN.split(':');
const cloudwatchlogs = new CloudWatchLogs({ region });
// Group array of hashes by defined key.
//
const groupBy = (array, key) => (
array.reduce((object, item) => {
const result = object;
if (result[item[key]]) {
result[item[key]].push(item);
} else if (item[key]) {
result[item[key]] = [ item ];
}
return result;
}, {})
);
// Find log stream by prefix.
//
const findLogStream = async (logStreamNamePrefix) => {
const params = { logGroupName, logStreamNamePrefix };
const { logStreams } = await cloudwatchlogs.describeLogStreams(params).promise();
if (logStreams.length > 1) {
throw new Error(`Found '${logStreams.length}' matching CloudWatch Logs streams but expected only one.`);
}
return logStreams[0];
};
// Get log stream or creting it if not present yet.
//
// Name format:
// 2000-01-01
//
const describeLogStream = async (logStreamName) => {
let logStream = await findLogStream(logStreamName);
if (!logStream) {
await cloudwatchlogs.createLogStream({ logGroupName, logStreamName }).promise();
logStream = await findLogStream(logStreamName);
}
return logStream;
};
// Extend the original record with some additional fields
// and encapsule records into CloudWatch Logs event.
//
const buildlogEvents = (records) => (
records.map((record) => {
const payload = record;
payload.name = 'logs:cloudfront';
return {
message: JSON.stringify(payload),
timestamp: new Date(`${payload.date} ${payload.time} UTC`).getTime(),
};
}).sort((a, b) => a.timestamp - b.timestamp) // Events in a request must be chronological ordered
);
// Send the given documents to CloudWatch Logs group.
//
exports.putLogEvents = async (records) => {
const groupedRecords = groupBy(records, 'date');
const putLogEventsCalls = Object.keys(groupedRecords).map(async (key) => {
const logStream = await describeLogStream(key);
const params = {
logEvents: buildlogEvents(groupedRecords[key]),
logGroupName,
logStreamName: logStream.logStreamName,
sequenceToken: logStream.uploadSequenceToken,
};
return cloudwatchlogs.putLogEvents(params).promise();
});
return Promise.all(putLogEventsCalls);
};

View File

@ -1,18 +0,0 @@
const { parseLogFile } = require('./cloudfront');
const { putLogEvents } = require('./cloudwatch-logs');
// Lambda handler.
//
exports.handler = async (event) => {
if (event.Records.length !== 1) {
throw new Error(`Wrong length of events.Records, expected: '1', got: '${event.Records.length}'`);
} else {
const params = {
bucket: event.Records[0].s3.bucket.name,
key: decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' ')),
region: event.Records[0].awsRegion,
};
return putLogEvents(await parseLogFile(params));
}
};

View File

@ -1,66 +0,0 @@
# -----------------------------------------------------------------------------------------------------------
# Grant the log parsing lambda access to the logs bucket
resource "aws_lambda_permission" "s3_bucket_invoke_function" {
statement_id = "AllowExecutionFromS3Bucket"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.ipixel_parser.arn
principal = "s3.amazonaws.com"
source_arn = aws_s3_bucket.ipixel_logs.arn
}
# -----------------------------------------------------------------------------------------------------------
# Log Parsing Lambda
resource "aws_s3_bucket_notification" "ipixel_logs" {
bucket = aws_s3_bucket.ipixel_logs.bucket
lambda_function {
lambda_function_arn = aws_lambda_function.ipixel_parser.arn
events = ["s3:ObjectCreated:*"]
filter_prefix = "RAW/"
filter_suffix = ".gz"
}
depends_on = [aws_lambda_permission.s3_bucket_invoke_function]
}
data "archive_file" "ipixel_parser" {
type = "zip"
source_dir = "${path.module}/lambda"
output_path = ".terraform/tmp/lambda/ipixel_parser.zip"
}
resource "aws_lambda_function" "ipixel_parser" {
function_name = "ipixel-parser-${var.site}"
runtime = "nodejs12.x"
handler = "index.handler"
timeout = 5
reserved_concurrent_executions = 3
environment {
variables = {
CLOUDWATCH_LOGS_GROUP_ARN = aws_cloudwatch_log_group.ipixel_results.arn
}
}
role = aws_iam_role.ipixel_parser.arn
filename = data.archive_file.ipixel_parser.output_path
source_code_hash = data.archive_file.ipixel_parser.output_base64sha256
tags = {
Site = var.site,
Role = "ipixel"
}
depends_on = [
aws_cloudwatch_log_group.ipixel_parser_logs,
aws_cloudwatch_log_group.ipixel_results,
]
}

View File

@ -60,12 +60,6 @@ resource "aws_iam_access_key" "s3" {
resource "aws_s3_bucket" "src" {
bucket = var.domain
acl = "public-read"
website {
index_document = "index.html"
error_document = "404.html"
}
tags = {
Name = "Site Source"
@ -73,19 +67,23 @@ resource "aws_s3_bucket" "src" {
}
}
resource "aws_s3_bucket" "uat" {
bucket = "uat.${var.domain}"
resource "aws_s3_bucket_acl" "src" {
bucket = aws_s3_bucket.src.id
acl = "public-read"
}
website {
index_document = "index.html"
error_document = "404.html"
resource "aws_s3_bucket_website_configuration" "src" {
bucket = aws_s3_bucket.src.bucket
index_document {
suffix = "index.html"
}
tags = {
Name = "Site Source UAT"
Site = var.site
error_document {
key = "404.html"
}
}
@ -131,58 +129,3 @@ resource "aws_s3_bucket_policy" "src" {
POLICY
}
resource "aws_s3_bucket_policy" "uat" {
bucket = aws_s3_bucket.uat.bucket
policy = <<POLICY
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "${aws_iam_user.s3.arn}"
},
"Action": "s3:ListBucket",
"Resource": "${aws_s3_bucket.uat.arn}"
},
{
"Effect": "Allow",
"Principal": {
"AWS": "${aws_iam_user.s3.arn}"
},
"Action": [
"s3:PutObject",
"s3:PutObjectAcl",
"s3:GetObject",
"s3:GetObjectAcl",
"s3:DeleteObject",
"s3:ListMultipartUploadParts",
"s3:AbortMultipartUpload"
],
"Resource": "${aws_s3_bucket.uat.arn}/*"
},
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "${aws_s3_bucket.uat.arn}/*"
}
]
}
POLICY
}
resource "aws_route53_record" "uat" {
name = "uat.${var.domain}"
zone_id = aws_route53_zone.zone.zone_id
type = "A"
alias {
name = aws_s3_bucket.uat.website_domain
zone_id = aws_s3_bucket.uat.hosted_zone_id
evaluate_target_health = false
}
}

View File

@ -5,7 +5,20 @@
resource "aws_s3_bucket" "pixel" {
bucket = "t.${var.domain}"
tags = {
Name = "Tracking Pixel"
Site = var.site
}
}
resource "aws_s3_bucket_acl" "pixel" {
bucket = aws_s3_bucket.pixel.id
acl = "public-read"
}
resource "aws_s3_bucket_cors_configuration" "pixel" {
bucket = aws_s3_bucket.pixel.id
cors_rule {
allowed_headers = ["*"]
@ -15,13 +28,9 @@ resource "aws_s3_bucket" "pixel" {
max_age_seconds = 3000
}
tags = {
Name = "Tracking Pixel"
Site = var.site
}
}
resource "aws_s3_bucket_object" "ipixel" {
resource "aws_s3_object" "ipixel" {
bucket = aws_s3_bucket.pixel.bucket
key = "i"
source = "${path.module}/files/i.gif"
@ -30,49 +39,67 @@ resource "aws_s3_bucket_object" "ipixel" {
content_type = "image/gif"
}
data "aws_canonical_user_id" "current" {}
resource "aws_s3_bucket" "ipixel_logs" {
bucket = "${var.site}-analytics"
grant {
id = data.aws_canonical_user_id.current.id
permissions = ["FULL_CONTROL"]
type = "CanonicalUser"
tags = {
Name = "iPixel Logs Storage"
Site = var.site
}
}
grant {
# Grant CloudFront awslogsdelivery logs access to your Amazon S3 Bucket
# https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html#AccessLogsBucketAndFileOwnership
id = "c4c1ede66af53448b93c283ce9448c4ba468c9432aa01d700d3878632f77d2d0"
permissions = ["FULL_CONTROL"]
type = "CanonicalUser"
resource "aws_s3_bucket_acl" "ipixel_logs" {
bucket = aws_s3_bucket.ipixel_logs.id
access_control_policy {
owner {
id = data.aws_canonical_user_id.current.id
}
grant {
grantee {
id = data.aws_canonical_user_id.current.id
type = "CanonicalUser"
}
permission = "FULL_CONTROL"
}
grant {
grantee {
uri = "http://acs.amazonaws.com/groups/s3/LogDelivery"
type = "Group"
}
permission = "FULL_CONTROL"
}
}
}
lifecycle_rule {
id = "logfiles"
enabled = true
resource "aws_s3_bucket_lifecycle_configuration" "example" {
bucket = aws_s3_bucket.ipixel_logs.id
prefix = "RAW/"
rule {
id = "logfiles"
filter {
prefix = "RAW/"
}
transition {
days = 30
storage_class = "STANDARD_IA" # or "ONEZONE_IA"
}
# transition {
# days = 30
# storage_class = "GLACIER"
# }
# ... other transition/expiration actions ...
# expiration {
# days = 90
# }
}
tags = {
Name = "iPixel Logs Storage"
Site = var.site
status = "Enabled"
}
}

View File

@ -1,4 +1,10 @@
variable "region" {
type = string
description = "AWS Hosting Region"
default = "us-east-1"
}
variable "site" {
type = string
description = "The name of the site"
@ -19,7 +25,16 @@ variable "subdomains" {
]
}
provider "aws" {
profile = "default"
region = "us-east-1"
}
variable "namecheap" {
type = map
description = "Namecheap Credentials"
default = {
username = ""
apikey = ""
}
validation {
condition = length(var.namecheap.username) > 0
error_message = "Must provide a namecheap configuration."
}
}

View File

@ -6,6 +6,26 @@ terraform {
aws = {
source = "hashicorp/aws"
}
namecheap = {
source = "namecheap/namecheap"
version = ">= 2.0.0"
}
}
required_version = ">= 0.13"
}
provider "aws" {
region = var.region
}
data "http" "externalip" {
url = "http://ipv4.icanhazip.com"
}
provider "namecheap" {
user_name = var.namecheap.username
api_user = var.namecheap.username
api_key = var.namecheap.apikey
client_ip = chomp(data.http.externalip.body)
use_sandbox = false
}

View File

@ -1,4 +1,7 @@
resource "aws_cloudfront_origin_access_identity" "origin_access_identity" {
}
# -----------------------------------------------------------------------------------------------------------
# Cloudfront Configuration
@ -112,10 +115,10 @@ resource "aws_lambda_function" "index_redirect" {
filename = "${path.module}/files/index_redirect.js.zip"
function_name = "${var.site}-index-redirect"
handler = "index_redirect.handler"
source_code_hash = data.archive_file.index_redirect.output_base64sha256
# source_code_hash = data.archive_file.index_redirect.output_base64sha256
publish = true
role = aws_iam_role.lambda_redirect.arn
runtime = "nodejs10.x"
runtime = "nodejs16.x"
tags = {
Name = "${var.site}-index-redirect"