main.tf
#################
# TERRAFORM #
#################
terraform {
required_version = "~> 1.0"
required_providers {
archive = {
source = "hashicorp/archive"
version = "~> 2.0"
}
aws = {
source = "hashicorp/aws"
version = "~> 5.31"
}
}
}
##############
# LOCALS #
##############
locals {
event_rule = {
description = var.event_rule_description
name = var.event_rule_name
}
iam_role = {
description = var.iam_role_description
name = var.iam_role_name
policy_name = var.iam_role_policy_name
tags = var.iam_role_tags
}
lambda = {
filename = data.archive_file.package.output_path
runtime = var.lambda_runtime
source_code_hash = data.archive_file.package.output_base64sha256
}
lambda_api = {
description = var.lambda_api_description
function_name = var.lambda_api_function_name
memory_size = var.lambda_api_memory_size
fallback_index_url = var.lambda_api_fallback_index_url
tags = var.lambda_api_tags
timeout = var.lambda_api_timeout
}
lambda_reindex = {
description = var.lambda_reindex_description
function_name = var.lambda_reindex_function_name
memory_size = var.lambda_reindex_memory_size
tags = var.lambda_reindex_tags
timeout = var.lambda_reindex_timeout
}
log_group_api = {
retention_in_days = var.log_group_api_retention_in_days
tags = var.log_group_api_tags
}
log_group_reindex = {
retention_in_days = var.log_group_reindex_retention_in_days
tags = var.log_group_reindex_tags
}
rest_api = {
authorization_type = var.api_authorization_type
authorizer_id = var.api_authorizer_id
execution_arn = var.api_execution_arn
id = var.api_id
root_resource_id = var.api_root_resource_id
}
routes = {
"GET /" = { http_method : "GET", resource_id : local.rest_api.root_resource_id }
"HEAD /" = { http_method : "HEAD", resource_id : local.rest_api.root_resource_id }
"POST /" = { http_method : "POST", resource_id : local.rest_api.root_resource_id }
"GET /{package+}" = { http_method : "GET", resource_id : aws_api_gateway_resource.proxy.id }
"HEAD /{package+}" = { http_method : "HEAD", resource_id : aws_api_gateway_resource.proxy.id }
}
s3 = {
bucket_name = var.s3_bucket_name
bucket_tags = var.s3_bucket_tags
presigned_url_ttl = var.s3_presigned_url_ttl
}
}
####################
# S3 :: BUCKET #
####################
resource "aws_s3_bucket" "pypi" {
bucket = local.s3.bucket_name
tags = local.s3.bucket_tags
}
resource "aws_s3_bucket_public_access_block" "pypi" {
block_public_acls = true
block_public_policy = true
bucket = aws_s3_bucket.pypi.id
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_object" "index" {
bucket = aws_s3_bucket.pypi.id
key = "index.html"
content = <<-EOT
<!DOCTYPE html>
<html>
<head>
<meta name="pypi:repository-version" content="1.0">
<title>Simple index</title>
</head>
<body>
<h1>Simple index</h1>
</body>
</html>
EOT
lifecycle { ignore_changes = [content] }
}
####################
# S3 :: EVENTS #
####################
data "aws_caller_identity" "current" {
}
resource "aws_s3_bucket_notification" "reindex" {
bucket = aws_s3_bucket.pypi.id
eventbridge = true
}
###################
# EVENTBRIDGE #
###################
resource "aws_cloudwatch_event_rule" "reindex" {
description = local.event_rule.description
name = local.event_rule.name
event_pattern = jsonencode({
source = ["aws.s3"]
detail-type = ["Object Created", "Object Deleted"]
detail = {
bucket = { name = [aws_s3_bucket.pypi.id] }
object = { key = [{ anything-but = ["index.html"] }] }
}
})
}
resource "aws_cloudwatch_event_target" "reindex" {
arn = aws_lambda_function.reindex.arn
input_path = "$.detail"
rule = aws_cloudwatch_event_rule.reindex.name
target_id = "reindex"
}
###########
# IAM #
###########
resource "aws_iam_role" "role" {
description = local.iam_role.description
name = local.iam_role.name
tags = local.iam_role.tags
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "AssumeRole"
Effect = "Allow"
Action = "sts:AssumeRole"
Principal = { Service = "lambda.amazonaws.com" }
}]
})
}
resource "aws_iam_role_policy" "policy" {
name = local.iam_role.policy_name
role = aws_iam_role.role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "ListBucket"
Effect = "Allow"
Action = "s3:ListBucket"
Resource = aws_s3_bucket.pypi.arn
},
{
Sid = "GetObjects"
Effect = "Allow"
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.pypi.arn}/*"
},
{
Sid = "PutIndex"
Effect = "Allow"
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.pypi.arn}/index.html"
},
{
Sid = "WriteLambdaLogs"
Effect = "Allow"
Resource = "*"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
]
}
]
})
}
#########################
# LAMBDA :: PACKAGE #
#########################
data "archive_file" "package" {
source_file = "${path.module}/python/index.py"
output_path = "${path.module}/python/package.zip"
type = "zip"
}
###########################
# LAMBDA :: API PROXY #
###########################
resource "aws_cloudwatch_log_group" "api" {
name = "/aws/lambda/${aws_lambda_function.api.function_name}"
retention_in_days = local.log_group_api.retention_in_days
tags = local.log_group_api.tags
}
resource "aws_lambda_function" "api" {
architectures = ["arm64"]
description = local.lambda_api.description
filename = local.lambda.filename
function_name = local.lambda_api.function_name
handler = "index.proxy_request"
memory_size = local.lambda_api.memory_size
role = aws_iam_role.role.arn
runtime = local.lambda.runtime
source_code_hash = local.lambda.source_code_hash
tags = local.lambda_api.tags
timeout = local.lambda_api.timeout
environment {
variables = {
FALLBACK_INDEX_URL = local.lambda_api.fallback_index_url
S3_BUCKET = aws_s3_bucket.pypi.bucket
S3_PRESIGNED_URL_TTL = local.s3.presigned_url_ttl
}
}
}
resource "aws_lambda_permission" "api" {
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.api.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${local.rest_api.execution_arn}/*/*/*"
}
###########################
# LAMBDA :: REINDEXER #
###########################
resource "aws_cloudwatch_log_group" "reindex" {
name = "/aws/lambda/${aws_lambda_function.reindex.function_name}"
retention_in_days = local.log_group_reindex.retention_in_days
tags = local.log_group_reindex.tags
}
resource "aws_lambda_function" "reindex" {
architectures = ["arm64"]
description = local.lambda_reindex.description
filename = local.lambda.filename
function_name = local.lambda_reindex.function_name
handler = "index.reindex_bucket"
memory_size = local.lambda_reindex.memory_size
role = aws_iam_role.role.arn
runtime = local.lambda.runtime
source_code_hash = local.lambda.source_code_hash
tags = local.lambda_reindex.tags
timeout = local.lambda_reindex.timeout
environment {
variables = {
S3_BUCKET = aws_s3_bucket.pypi.bucket
}
}
}
resource "aws_lambda_permission" "reindex" {
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.reindex.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.reindex.arn
}
###########################
# API GATEWAY :: REST #
###########################
resource "aws_api_gateway_resource" "proxy" {
rest_api_id = local.rest_api.id
parent_id = local.rest_api.root_resource_id
path_part = "{package+}"
}
resource "aws_api_gateway_method" "methods" {
for_each = local.routes
authorization = local.rest_api.authorization_type
authorizer_id = local.rest_api.authorizer_id
http_method = each.value.http_method
resource_id = each.value.resource_id
rest_api_id = local.rest_api.id
}
resource "aws_api_gateway_integration" "integrations" {
depends_on = [aws_api_gateway_method.methods]
for_each = local.routes
rest_api_id = local.rest_api.id
resource_id = each.value.resource_id
http_method = each.value.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.api.invoke_arn
}