diff --git a/.gitignore b/.gitignore index 0fa9b79..bc42e71 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ .terraform/ *.tfstate *.tfstate* +*.lock.hcl secret.tfvars diff --git a/data.tf b/data.tf index 3b4fce6..a9ed804 100644 --- a/data.tf +++ b/data.tf @@ -21,3 +21,94 @@ data "aws_ami" "this" { most_recent = true } + +data "aws_iam_policy_document" "lambda" { + version = "2012-10-17" + + statement { + effect = "Allow" + + principals { + type = "Service" + + identifiers = [ + "lambda.amazonaws.com", + ] + } + + actions = [ + "sts:AssumeRole", + ] + } +} + +data "aws_iam_policy_document" "instance" { + version = "2012-10-17" + + statement { + effect = "Allow" + + resources = [ + "*", + ] + + actions = [ + "ec2:Start*", + "ec2:Stop*", + ] + } +} + +data "aws_iam_policy_document" "cloudwatch" { + version = "2012-10-17" + + statement { + effect = "Allow" + + resources = [ + "arn:aws:logs:*:*:*", + ] + + actions = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ] + } +} + +data "archive_file" "start_instance" { + type = "zip" + output_path = "${path.module}/start_instance.zip" + source_content_filename = "${path.module}/start_instance.js" + source_content = < await ec2.startInstances({ + InstanceIds: [ + '${aws_instance.this.id}', + ], +}).promise(); + EOF +} + +data "archive_file" "stop_instance" { + type = "zip" + output_path = "${path.module}/stop_instance.zip" + source_content_filename = "${path.module}/stop_instance.js" + source_content = < await ec2.stopInstances({ + InstanceIds: [ + '${aws_instance.this.id}', + ], +}).promise(); + EOF +} diff --git a/main.tf b/main.tf index 8353aef..e5cba49 100644 --- a/main.tf +++ b/main.tf @@ -140,6 +140,136 @@ resource "aws_instance" "this" { } } +resource "aws_iam_role" "lambda" { + name = "tailscale" + assume_role_policy = data.aws_iam_policy_document.lambda.json +} + +resource "aws_iam_policy" "lambda" { + name = "tailscale-lambda" + path = "/" + policy = data.aws_iam_policy_document.instance.json +} + +resource "aws_iam_policy" "cloudwatch" { + name = "tailscale-cloudwatch" + path = "/" + policy = data.aws_iam_policy_document.cloudwatch.json +} + +resource "aws_iam_role_policy_attachment" "lambda" { + role = aws_iam_role.lambda.name + policy_arn = aws_iam_policy.lambda.arn +} + +resource "aws_iam_role_policy_attachment" "cloudwatch" { + role = aws_iam_role.lambda.name + policy_arn = aws_iam_policy.cloudwatch.arn +} + +resource "aws_cloudwatch_log_group" "start_instance" { + name = "/aws/lambda/${var.server_hostname}-${var.aws_region}-start-instance" + retention_in_days = var.log_retention +} + +resource "aws_cloudwatch_log_group" "stop_instance" { + name = "/aws/lambda/${var.server_hostname}-${var.aws_region}-stop-instance" + retention_in_days = var.log_retention +} + +resource "aws_cloudwatch_event_rule" "start_instance" { + name = "${var.server_hostname}-${var.aws_region}-start-instance" + schedule_expression = var.server_start_expression +} + +resource "aws_cloudwatch_event_rule" "stop_instance" { + name = "${var.server_hostname}-${var.aws_region}-stop-instance" + schedule_expression = var.server_stop_expression +} + +resource "aws_s3_bucket" "this" { + bucket = "${var.server_hostname}-${var.aws_region}" + force_destroy = true +} + +resource "aws_s3_object" "start_instance" { + bucket = aws_s3_bucket.this.id + key = "start_instance.zip" + source = data.archive_file.start_instance.output_path +} + +resource "aws_s3_object" "stop_instance" { + bucket = aws_s3_bucket.this.id + key = "stop_instance.zip" + source = data.archive_file.stop_instance.output_path +} + +resource "aws_lambda_function" "start_instance" { + function_name = "${var.server_hostname}-${var.aws_region}-start-instance" + role = aws_iam_role.lambda.arn + s3_bucket = aws_s3_bucket.this.id + s3_key = aws_s3_object.start_instance.id + handler = "start_instance.handler" + runtime = "nodejs16.x" + memory_size = 128 + timeout = 60 + + architectures = [ + "arm64", + ] + + depends_on = [ + aws_cloudwatch_log_group.start_instance, + ] +} + +resource "aws_lambda_function" "stop_instance" { + function_name = "${var.server_hostname}-${var.aws_region}-stop-instance" + role = aws_iam_role.lambda.arn + s3_bucket = aws_s3_bucket.this.id + s3_key = aws_s3_object.stop_instance.id + handler = "stop_instance.handler" + runtime = "nodejs16.x" + memory_size = 128 + timeout = 60 + + architectures = [ + "arm64", + ] + + depends_on = [ + aws_cloudwatch_log_group.stop_instance, + ] +} + +resource "aws_cloudwatch_event_target" "start_instance" { + rule = aws_cloudwatch_event_rule.start_instance.name + target_id = aws_cloudwatch_event_rule.start_instance.name + arn = aws_lambda_function.start_instance.arn +} + +resource "aws_cloudwatch_event_target" "stop_instance" { + rule = aws_cloudwatch_event_rule.stop_instance.name + target_id = aws_cloudwatch_event_rule.stop_instance.name + arn = aws_lambda_function.stop_instance.arn +} + +resource "aws_lambda_permission" "start_instance" { + statement_id = "AllowExecutionFromCloudWatch" + principal = "events.amazonaws.com" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.start_instance.function_name + source_arn = aws_cloudwatch_event_rule.start_instance.arn +} + +resource "aws_lambda_permission" "stop_instance" { + statement_id = "AllowExecutionFromCloudWatch" + principal = "events.amazonaws.com" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.stop_instance.function_name + source_arn = aws_cloudwatch_event_rule.stop_instance.arn +} + resource "tailscale_tailnet_key" "this" { reusable = true preauthorized = true diff --git a/variables.tf b/variables.tf index f8c643d..d896e97 100644 --- a/variables.tf +++ b/variables.tf @@ -50,6 +50,24 @@ variable "server_hostname" { default = "vpn" } +variable "server_start_expression" { + description = "Server start schedule expression" + type = string + default = "cron(0 10 * * ? *)" +} + +variable "server_stop_expression" { + description = "Server stop schedule expression" + type = string + default = "cron(0 1 * * ? *)" +} + +variable "log_retention" { + description = "CloudWatch log retention" + type = number + default = 14 +} + variable "tailscale_api_key" { description = "Tailscale API access token" type = string