Skip to content

Commit 782a561

Browse files
Replace Elasticsearch Fargate with AWS OpenSearch
- Use OpenSearch t3.small.search instance (~/month) - Single AZ deployment for cost savings - 10GB EBS storage (plenty for 10K talks) - VPC deployment with security groups - Fine-grained access control with master user - Encryption at rest and in transit - Lambdas connect via HTTPS endpoint Total cost: ~/month OpenSearch + ~/month Lambdas = ~/month Much cheaper than Fargate ES (~/month) and simpler to manage
1 parent 2e866a2 commit 782a561

File tree

4 files changed

+78
-219
lines changed

4 files changed

+78
-219
lines changed

.github/workflows/deploy.yaml

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,6 @@ jobs:
6767
TF_VAR_assign_public_ip: false
6868
TF_VAR_allowed_cidr_blocks: ${{ secrets.VPC_CIDR_BLOCKS }}
6969
TF_VAR_elasticsearch_password: ${{ secrets.ELASTICSEARCH_PASSWORD }}
70-
TF_VAR_elasticsearch_url: http://elasticsearch.javazone.internal:9200
71-
TF_VAR_task_cpu: 1024
72-
TF_VAR_task_memory: 2048
73-
TF_VAR_heap_size: 1024
7470
TF_VAR_webhook_secret: ${{ secrets.WEBHOOK_SECRET }}
7571
TF_VAR_moresleep_url: ${{ secrets.MORESLEEP_URL }}
7672
TF_VAR_moresleep_username: ${{ secrets.MORESLEEP_USERNAME }}
@@ -105,20 +101,25 @@ jobs:
105101
terraform output -raw webhook_url
106102
echo ""
107103
echo ""
108-
echo "📊 Elasticsearch:"
109-
terraform output -raw elasticsearch_endpoint
104+
echo "📊 OpenSearch:"
105+
terraform output -raw opensearch_endpoint
110106
echo ""
111107
echo ""
112108
echo "📦 SQS Queue:"
113109
terraform output -raw sqs_queue_url
114110
echo ""
115111
echo "================================================"
116112
117-
- name: Create Elasticsearch Index
113+
- name: Wait for OpenSearch to be ready
114+
run: |
115+
echo "Waiting 5 minutes for OpenSearch domain to be active..."
116+
sleep 300
117+
118+
- name: Create OpenSearch Index
119+
working-directory: terraform
118120
run: |
119-
sleep 60
120-
ES_URL="http://elasticsearch.javazone.internal:9200"
121-
curl -X PUT "$ES_URL/javazone_talks" \
121+
OS_URL=$(terraform output -raw opensearch_endpoint)
122+
curl -X PUT "$OS_URL/javazone_talks" \
122123
-u elastic:${{ secrets.ELASTICSEARCH_PASSWORD }} \
123124
-H "Content-Type: application/json" \
124-
-d @config/index-mapping.json || echo "Index may already exist"
125+
-d @../config/index-mapping.json || echo "Index may already exist"

terraform/main.tf

Lines changed: 52 additions & 188 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ resource "aws_lambda_function" "es_indexer" {
196196
MORESLEEP_API_URL = var.moresleep_url
197197
MORESLEEP_USERNAME = var.moresleep_username
198198
MORESLEEP_PASSWORD = var.moresleep_password
199-
ELASTICSEARCH_URL = var.elasticsearch_url
199+
ELASTICSEARCH_URL = "https://${aws_opensearch_domain.javazone.endpoint}"
200200
ELASTICSEARCH_USERNAME = var.elasticsearch_username
201201
ELASTICSEARCH_PASSWORD = var.elasticsearch_password
202202
ELASTICSEARCH_INDEX = var.elasticsearch_index
@@ -213,28 +213,21 @@ resource "aws_lambda_event_source_mapping" "sqs_trigger" {
213213
}
214214

215215
################################################################################
216-
# Elasticsearch on Fargate
216+
# OpenSearch Domain
217217
################################################################################
218218

219-
# SSM Parameter for ES password
220-
resource "aws_ssm_parameter" "elasticsearch_password_ssm" {
221-
name = "/javazone/elasticsearch/password"
222-
type = "SecureString"
223-
value = var.elasticsearch_password
224-
}
225-
226-
# Security Group for Elasticsearch
227-
resource "aws_security_group" "elasticsearch" {
228-
name = "elasticsearch-javazone"
229-
description = "Elasticsearch for JavaZone"
219+
# Security Group for OpenSearch
220+
resource "aws_security_group" "opensearch" {
221+
name = "opensearch-javazone"
222+
description = "OpenSearch for JavaZone"
230223
vpc_id = var.vpc_id
231224

232225
ingress {
233-
from_port = 9200
234-
to_port = 9200
226+
from_port = 443
227+
to_port = 443
235228
protocol = "tcp"
236229
cidr_blocks = var.allowed_cidr_blocks
237-
description = "Elasticsearch HTTP"
230+
description = "HTTPS for OpenSearch"
238231
}
239232

240233
egress {
@@ -245,193 +238,64 @@ resource "aws_security_group" "elasticsearch" {
245238
}
246239
}
247240

248-
# IAM Role for ECS Task Execution
249-
resource "aws_iam_role" "es_execution_role" {
250-
name = "elasticsearch-javazone-execution"
251-
252-
assume_role_policy = jsonencode({
253-
Version = "2012-10-17"
254-
Statement = [{
255-
Action = "sts:AssumeRole"
256-
Effect = "Allow"
257-
Principal = {
258-
Service = "ecs-tasks.amazonaws.com"
259-
}
260-
}]
261-
})
262-
}
263-
264-
resource "aws_iam_role_policy_attachment" "es_execution_role_policy" {
265-
role = aws_iam_role.es_execution_role.name
266-
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
267-
}
268-
269-
resource "aws_iam_role_policy" "es_execution_ssm_policy" {
270-
name = "ssm-access"
271-
role = aws_iam_role.es_execution_role.id
241+
# OpenSearch Domain
242+
resource "aws_opensearch_domain" "javazone" {
243+
domain_name = "javazone-talks"
244+
engine_version = "OpenSearch_2.11"
272245

273-
policy = jsonencode({
274-
Version = "2012-10-17"
275-
Statement = [{
276-
Effect = "Allow"
277-
Action = ["ssm:GetParameters", "ssm:GetParameter"]
278-
Resource = aws_ssm_parameter.elasticsearch_password_ssm.arn
279-
}]
280-
})
281-
}
282-
283-
# IAM Role for ECS Task
284-
resource "aws_iam_role" "es_task_role" {
285-
name = "elasticsearch-javazone-task"
286-
287-
assume_role_policy = jsonencode({
288-
Version = "2012-10-17"
289-
Statement = [{
290-
Action = "sts:AssumeRole"
291-
Effect = "Allow"
292-
Principal = {
293-
Service = "ecs-tasks.amazonaws.com"
294-
}
295-
}]
296-
})
297-
}
298-
299-
# EFS for persistent storage
300-
resource "aws_efs_file_system" "elasticsearch_data" {
301-
creation_token = "elasticsearch-javazone-data"
302-
encrypted = true
303-
304-
lifecycle_policy {
305-
transition_to_ia = "AFTER_30_DAYS"
246+
cluster_config {
247+
instance_type = "t3.small.search" # ~$0.036/hour = ~$26/month
248+
instance_count = 1
249+
zone_awareness_enabled = false
306250
}
307-
}
308-
309-
resource "aws_efs_mount_target" "elasticsearch_data" {
310-
for_each = toset(var.es_subnet_ids)
311251

312-
file_system_id = aws_efs_file_system.elasticsearch_data.id
313-
subnet_id = each.value
314-
security_groups = [aws_security_group.efs.id]
315-
}
316-
317-
resource "aws_security_group" "efs" {
318-
name = "elasticsearch-efs"
319-
description = "EFS for Elasticsearch data"
320-
vpc_id = var.vpc_id
321-
322-
ingress {
323-
from_port = 2049
324-
to_port = 2049
325-
protocol = "tcp"
326-
security_groups = [aws_security_group.elasticsearch.id]
327-
description = "NFS from ES tasks"
252+
ebs_options {
253+
ebs_enabled = true
254+
volume_size = 10 # GB - plenty for 10K talks
255+
volume_type = "gp3"
328256
}
329257

330-
egress {
331-
from_port = 0
332-
to_port = 0
333-
protocol = "-1"
334-
cidr_blocks = ["0.0.0.0/0"]
258+
vpc_options {
259+
subnet_ids = [var.es_subnet_ids[0]] # Single AZ for cost savings
260+
security_group_ids = [aws_security_group.opensearch.id]
335261
}
336-
}
337-
338-
# ECS Cluster
339-
resource "aws_ecs_cluster" "es_cluster" {
340-
name = "elasticsearch-javazone"
341-
}
342262

343-
# ECS Task Definition
344-
resource "aws_ecs_task_definition" "elasticsearch" {
345-
family = "elasticsearch-javazone"
346-
requires_compatibilities = ["FARGATE"]
347-
network_mode = "awsvpc"
348-
cpu = var.task_cpu
349-
memory = var.task_memory
350-
execution_role_arn = aws_iam_role.es_execution_role.arn
351-
task_role_arn = aws_iam_role.es_task_role.arn
352-
353-
volume {
354-
name = "elasticsearch-data"
355-
efs_volume_configuration {
356-
file_system_id = aws_efs_file_system.elasticsearch_data.id
357-
transit_encryption = "ENABLED"
263+
advanced_security_options {
264+
enabled = true
265+
internal_user_database_enabled = true
266+
master_user_options {
267+
master_user_name = var.elasticsearch_username
268+
master_user_password = var.elasticsearch_password
358269
}
359270
}
360271

361-
container_definitions = jsonencode([{
362-
name = "elasticsearch"
363-
image = "docker.elastic.co/elasticsearch/elasticsearch:8.11.0"
364-
365-
portMappings = [{
366-
containerPort = 9200
367-
protocol = "tcp"
368-
}]
369-
370-
mountPoints = [{
371-
sourceVolume = "elasticsearch-data"
372-
containerPath = "/usr/share/elasticsearch/data"
373-
}]
374-
375-
environment = [
376-
{ name = "discovery.type", value = "single-node" },
377-
{ name = "xpack.security.enabled", value = "true" },
378-
{ name = "ES_JAVA_OPTS", value = "-Xms${var.heap_size}m -Xmx${var.heap_size}m" },
379-
{ name = "cluster.name", value = "javazone-cluster" }
380-
]
381-
382-
secrets = [{
383-
name = "ELASTIC_PASSWORD"
384-
valueFrom = aws_ssm_parameter.elasticsearch_password_ssm.arn
385-
}]
386-
387-
healthCheck = {
388-
command = ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"]
389-
interval = 30
390-
timeout = 5
391-
retries = 3
392-
startPeriod = 120
393-
}
394-
}])
395-
}
396-
397-
# ECS Service
398-
resource "aws_ecs_service" "elasticsearch" {
399-
name = "elasticsearch-javazone"
400-
cluster = aws_ecs_cluster.es_cluster.id
401-
task_definition = aws_ecs_task_definition.elasticsearch.arn
402-
desired_count = 1
403-
launch_type = "FARGATE"
404-
platform_version = "1.4.0"
405-
406-
network_configuration {
407-
subnets = var.es_subnet_ids
408-
security_groups = [aws_security_group.elasticsearch.id]
409-
assign_public_ip = var.assign_public_ip
272+
encrypt_at_rest {
273+
enabled = true
410274
}
411275

412-
service_registries {
413-
registry_arn = aws_service_discovery_service.elasticsearch.arn
276+
node_to_node_encryption {
277+
enabled = true
414278
}
415-
}
416-
417-
# Service Discovery
418-
resource "aws_service_discovery_private_dns_namespace" "main" {
419-
name = "javazone.internal"
420-
vpc = var.vpc_id
421-
}
422279

423-
resource "aws_service_discovery_service" "elasticsearch" {
424-
name = "elasticsearch"
425-
426-
dns_config {
427-
namespace_id = aws_service_discovery_private_dns_namespace.main.id
428-
dns_records {
429-
ttl = 10
430-
type = "A"
431-
}
280+
domain_endpoint_options {
281+
enforce_https = true
282+
tls_security_policy = "Policy-Min-TLS-1-2-2019-07"
432283
}
433284

434-
health_check_custom_config {
435-
failure_threshold = 1
285+
access_policies = jsonencode({
286+
Version = "2012-10-17"
287+
Statement = [{
288+
Effect = "Allow"
289+
Principal = {
290+
AWS = "*"
291+
}
292+
Action = "es:*"
293+
Resource = "arn:aws:es:${var.aws_region}:*:domain/javazone-talks/*"
294+
}]
295+
})
296+
297+
tags = {
298+
Name = "javazone-talks"
436299
}
437300
}
301+

terraform/outputs.tf

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@ output "sqs_dlq_url" {
1111
value = aws_sqs_queue.dlq.url
1212
}
1313

14-
output "elasticsearch_endpoint" {
15-
value = "http://elasticsearch.javazone.internal:9200"
16-
description = "Elasticsearch endpoint (via service discovery)"
14+
output "opensearch_endpoint" {
15+
value = "https://${aws_opensearch_domain.javazone.endpoint}"
16+
description = "OpenSearch endpoint URL"
17+
}
18+
19+
output "opensearch_domain_name" {
20+
value = aws_opensearch_domain.javazone.domain_name
1721
}
1822

1923
output "webhook_receiver_lambda" {
@@ -23,7 +27,3 @@ output "webhook_receiver_lambda" {
2327
output "es_indexer_lambda" {
2428
value = aws_lambda_function.es_indexer.function_name
2529
}
26-
27-
output "elasticsearch_cluster" {
28-
value = aws_ecs_cluster.es_cluster.name
29-
}

terraform/variables.tf

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,16 @@ variable "allowed_cidr_blocks" {
2525
type = list(string)
2626
}
2727

28-
variable "task_cpu" {
29-
description = "CPU units for Elasticsearch"
30-
type = number
31-
default = 1024
32-
}
33-
34-
variable "task_memory" {
35-
description = "Memory for Elasticsearch"
36-
type = number
37-
default = 2048
28+
variable "opensearch_instance_type" {
29+
description = "OpenSearch instance type"
30+
type = string
31+
default = "t3.small.search" # ~$26/month
3832
}
3933

40-
variable "heap_size" {
41-
description = "Java heap size in MB"
34+
variable "opensearch_volume_size" {
35+
description = "EBS volume size in GB"
4236
type = number
43-
default = 1024
37+
default = 10
4438
}
4539

4640
variable "elasticsearch_password" {

0 commit comments

Comments
 (0)