From 8200962d8d2abca254ea96f8ddadf1033e71cba0 Mon Sep 17 00:00:00 2001 From: Lauri Kajan Date: Thu, 12 Feb 2026 10:42:29 +0200 Subject: [PATCH 01/12] Move var files to a separate directory --- infra/Makefile | 4 ++-- infra/README.md | 22 +++++++++---------- .../{ => var-files}/arho-dev.tfvars.enc.json | 0 infra/{ => var-files}/arho.tfvars.sample.json | 0 .../{ => var-files}/hame-test.tfvars.enc.json | 0 .../tykki-test.tfvars.enc.json | 0 6 files changed, 13 insertions(+), 13 deletions(-) rename infra/{ => var-files}/arho-dev.tfvars.enc.json (100%) rename infra/{ => var-files}/arho.tfvars.sample.json (100%) rename infra/{ => var-files}/hame-test.tfvars.enc.json (100%) rename infra/{ => var-files}/tykki-test.tfvars.enc.json (100%) diff --git a/infra/Makefile b/infra/Makefile index 874e935..65f2df9 100644 --- a/infra/Makefile +++ b/infra/Makefile @@ -70,8 +70,8 @@ load-mml: decrypt-workspace-secrets: sops -d public_keys/$(shell terraform workspace show).enc > public_keys/$(shell terraform workspace show) - sops -d $(shell terraform workspace show).tfvars.enc.json > $(shell terraform workspace show).tfvars.json + sops -d var-files/$(shell terraform workspace show).tfvars.enc.json > var-files/$(shell terraform workspace show).tfvars.json encrypt-workspace-secrets: sops -e public_keys/$(shell terraform workspace show) > public_keys/$(shell terraform workspace show).enc - sops -e $(shell terraform workspace show).tfvars.json > $(shell terraform workspace show).tfvars.enc.json + sops -e var-files/$(shell terraform workspace show).tfvars.json > var-files/$(shell terraform workspace show).tfvars.enc.json diff --git a/infra/README.md b/infra/README.md index 929bd56..6218ad0 100644 --- a/infra/README.md +++ b/infra/README.md @@ -43,15 +43,15 @@ To make changes to instances, first check that your variables and current infra ```shell terraform init -terraform plan --var-file hame-dev.tfvars.json +terraform plan --var-file var-files/hame-dev.tfvars.json ``` -This should report that terraform state is up to date with infra and configuration. You may make changes to configuration or variables and run `terraform plan --var-file hame-dev.tfvars.json` again to check what your changes would mean to the infrastructure. +This should report that terraform state is up to date with infra and configuration. You may make changes to configuration or variables and run `terraform plan --var-file var-files/hame-dev.tfvars.json` again to check what your changes would mean to the infrastructure. When you are sure that you want to change AWS infra, run ```shell -terraform apply --var-file hame-dev.tfvars.json +terraform apply --var-file var-files/hame-dev.tfvars.json ``` Please verify that the reported changes are desired, and respond `yes` to apply the changes to infrastructure. @@ -94,7 +94,7 @@ To launch new instances, running the following commands should be sufficient: ```shell terraform init -terraform apply --var-file your-deployment.tfvars.json +terraform apply --var-file var-files/your-deployment.tfvars.json ``` But in practice it is little bit more complicated, as some manual steps are required in between. `Terraform apply` would encounter some errors that need to be fixed manually before proceeding. Here is an example session for deploying a new `arho-dev` instance: @@ -112,12 +112,12 @@ cp arho.tfvars.sample.json arho-dev.tfvars.json # Edit arho-dev.tfvars.json # Test the plan first -terraform plan -var-file=arho-dev.tfvars.json +terraform plan -var-file=var-files/arho-dev.tfvars.json -terraform apply -var-file=arho-dev.tfvars.json +terraform apply -var-file=var-files/arho-dev.tfvars.json # Error is expected: Error: creating RDS DB Subnet Group (arho-dev-db): operation error RDS: CreateDBSubnetGroup, https response error StatusCode: 400, RequestID: f89c1d41-e247-48d2-9003-5a40b0e018aa, InvalidSubnet: Subnet IDs are required. -terraform apply -var-file=arho-dev.tfvars.json +terraform apply -var-file=var-files/arho-dev.tfvars.json # Error is expected: Error: creating Lambda Function (arho-dev-db_manager): operation error Lambda: CreateFunction, https response error StatusCode: 400, RequestID: acc82829-26b3-4c88-af07-c9bae6f27b81, InvalidParameterValueException: Source image 631260641272.dkr.ecr.eu-central-1.amazonaws.com/arho-dev-db_manager:latest does not exist. Provide a valid source image. # Set AWS_REGION and AWS_ACCOUNT_ID environment variables for Makefile @@ -127,12 +127,12 @@ export prefix=arho-dev # Build and push lambda images make push-lambdas -terraform apply -var-file=arho-dev.tfvars.json +terraform apply -var-file=var-files/arho-dev.tfvars.json # Error is expected: Error: creating Lambda Provisioned Concurrency Config (arho-dev-ryhti_client,live): operation error Lambda: PutProvisionedConcurrencyConfig, https response error StatusCode: 400, RequestID: 284038e3-650d-4ccb-951f-764e6cd9161d, InvalidParameterValueException: Provisioned Concurrency Configs cannot be applied to unpublished function versions. # Update lambda functions make update-lambdas -terraform apply -var-file=arho-dev.tfvars.json +terraform apply -var-file=var-files/arho-dev.tfvars.json # Now the infra should be deployed, but the database is still empty. Initialize the database with: make create-db @@ -155,7 +155,7 @@ This is because you need to apply for a separate permit for your subsystem to be When your application is accepted, you are provided with the configuration anchor file needed later. 2. Create an SSH key and add the public key to `bastion_ec2_user_public_keys` in `your-deployment.tfvars.json`. 3. Fill in the desired admin username and password in `x-road_secrets`, your desired `x-road_db_password` (password for x-road database) and your desired `x-road_token_pin` (for accessing authentication tokens), in `your-deployment.tfvars.json`. -4. Apply the variables to AWS with `terraform apply --var-file your-deployment.tfvars.json`. +4. Apply the variables to AWS with `terraform apply --var-file var-files/your-deployment.tfvars.json`. 5. Check the private IP address of your `your-deployment-x-road_securityserver` service task under your AWS Elastic Container Service `your-deployment-x-road_securityserver` cluster in your AWS web console. 6. Open an SSH tunnel to the X-Road server admin interface (e.g. `ssh -N -L4001::4000 -i "~/.ssh/arho-ec2-user.pem" ec2-user@your-deployment..`, where `arho-ec2-user.pem` contains your SSH key created in step 2, and `bastion_subdomain` and `aws_hosted_domain` are the settings in your `your-deployment.tfvars.json`). 7. Point your web browser to [https://localhost:4001](https://localhost:4001). The connection @@ -226,7 +226,7 @@ Congratulations! You now have access to X-Road Ryhti APIs! ## Teardown of instances -Shut down and destroy the instances with `terraform destroy --var-file your-deployment.tfvars.json` +Shut down and destroy the instances with `terraform destroy --var-file var-files/your-deployment.tfvars.json` ## Manual interactions diff --git a/infra/arho-dev.tfvars.enc.json b/infra/var-files/arho-dev.tfvars.enc.json similarity index 100% rename from infra/arho-dev.tfvars.enc.json rename to infra/var-files/arho-dev.tfvars.enc.json diff --git a/infra/arho.tfvars.sample.json b/infra/var-files/arho.tfvars.sample.json similarity index 100% rename from infra/arho.tfvars.sample.json rename to infra/var-files/arho.tfvars.sample.json diff --git a/infra/hame-test.tfvars.enc.json b/infra/var-files/hame-test.tfvars.enc.json similarity index 100% rename from infra/hame-test.tfvars.enc.json rename to infra/var-files/hame-test.tfvars.enc.json diff --git a/infra/tykki-test.tfvars.enc.json b/infra/var-files/tykki-test.tfvars.enc.json similarity index 100% rename from infra/tykki-test.tfvars.enc.json rename to infra/var-files/tykki-test.tfvars.enc.json From f24c77a70685f56a4368e1e44b14bccaff94e0f7 Mon Sep 17 00:00:00 2001 From: Lauri Kajan Date: Thu, 12 Feb 2026 10:43:21 +0200 Subject: [PATCH 02/12] Remove unused terraform variable --- infra/variables.tf | 5 ----- 1 file changed, 5 deletions(-) diff --git a/infra/variables.tf b/infra/variables.tf index d0eccb0..ddfee7c 100644 --- a/infra/variables.tf +++ b/infra/variables.tf @@ -70,11 +70,6 @@ variable "enable_route53_record" { default = false } -variable "SLACK_HOOK_URL" { - description = "Slack URL to post cloudwatch notifications to" - type = string -} - variable "bastion_ec2_user_public_keys" { description = "Public ssh key for bastion EC2 superuser" type = list From 225c92c98094da4196d7d80dcc3e47dd5e6f253c Mon Sep 17 00:00:00 2001 From: Lauri Kajan Date: Wed, 18 Feb 2026 16:13:03 +0200 Subject: [PATCH 03/12] Fix timeout to maximum allowed value --- infra/api.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/api.tf b/infra/api.tf index a4b0e77..4f150ac 100644 --- a/infra/api.tf +++ b/infra/api.tf @@ -76,7 +76,7 @@ resource "aws_api_gateway_integration" "lambda_integration" { type = "AWS_PROXY" # Our lambdas may run long if everything is processed. For a single # plan, the request will be much faster. - timeout_milliseconds = 120000 + timeout_milliseconds = 29000 uri = aws_lambda_function.ryhti_client.invoke_arn } From 1d81c8e300bcb42f755ecdff4b9838e1716bd5de Mon Sep 17 00:00:00 2001 From: Lauri Kajan Date: Wed, 18 Feb 2026 16:14:00 +0200 Subject: [PATCH 04/12] Use the latest amazon linux image --- infra/bastion.tf | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/infra/bastion.tf b/infra/bastion.tf index 69927d9..5382ba0 100644 --- a/infra/bastion.tf +++ b/infra/bastion.tf @@ -1,9 +1,13 @@ # We don't want to create the key in terraform. Otherwise the private key(s) would be saved in terraform state. # Let's save the public key(s) here as ec2 instance user data. +data "aws_ssm_parameter" "amazon_linux" { + name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64" +} + # Just the smallest arm instance available, for routing traffic to postgres resource "aws_instance" "bastion-ec2-instance" { - ami = "ami-0bf463e49ccd368ed" # Amazon Linux 2023 + ami = data.aws_ssm_parameter.amazon_linux.value instance_type = "t4g.nano" subnet_id = aws_subnet.public[0].id vpc_security_group_ids = [aws_security_group.bastion.id] From 8783de0faaaeb755e3a09029f3c657012d2db2f9 Mon Sep 17 00:00:00 2001 From: Lauri Kajan Date: Wed, 18 Feb 2026 16:16:42 +0200 Subject: [PATCH 05/12] Use aws lambda settings for logging --- infra/lambda.tf | 24 ++++++++++++++++---- lambdas/db_manager/db_manager.py | 1 - lambdas/koodistot_loader/koodistot_loader.py | 1 - lambdas/mml_loader/mml_loader.py | 1 - lambdas/ryhti_client/lambda_function.py | 1 - 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/infra/lambda.tf b/infra/lambda.tf index 0213d39..b4a6fa5 100644 --- a/infra/lambda.tf +++ b/infra/lambda.tf @@ -9,7 +9,11 @@ resource "aws_lambda_function" "db_manager" { subnet_ids = data.aws_subnets.private.ids security_group_ids = [aws_security_group.lambda.id] } - + logging_config { + log_format = "JSON" + application_log_level = "INFO" + system_log_level = "INFO" + } environment { variables = { AWS_REGION_NAME = var.AWS_REGION_NAME @@ -47,7 +51,11 @@ resource "aws_lambda_function" "koodistot_loader" { subnet_ids = data.aws_subnets.private.ids security_group_ids = [aws_security_group.lambda.id] } - + logging_config { + log_format = "JSON" + application_log_level = "INFO" + system_log_level = "INFO" + } environment { variables = { AWS_REGION_NAME = var.AWS_REGION_NAME @@ -92,7 +100,11 @@ resource "aws_lambda_function" "ryhti_client" { subnet_ids = data.aws_subnets.private.ids security_group_ids = [aws_security_group.lambda.id] } - + logging_config { + log_format = "JSON" + application_log_level = "INFO" + system_log_level = "INFO" + } environment { variables = local.ryhti_client_env } @@ -147,7 +159,11 @@ resource "aws_lambda_function" "mml_loader" { subnet_ids = data.aws_subnets.private.ids security_group_ids = [aws_security_group.lambda.id] } - + logging_config { + log_format = "JSON" + application_log_level = "INFO" + system_log_level = "INFO" + } environment { variables = { AWS_REGION_NAME = var.AWS_REGION_NAME diff --git a/lambdas/db_manager/db_manager.py b/lambdas/db_manager/db_manager.py index 4f9d636..768121b 100644 --- a/lambdas/db_manager/db_manager.py +++ b/lambdas/db_manager/db_manager.py @@ -20,7 +20,6 @@ """ LOGGER = logging.getLogger() -LOGGER.setLevel(logging.INFO) class Action(enum.Enum): diff --git a/lambdas/koodistot_loader/koodistot_loader.py b/lambdas/koodistot_loader/koodistot_loader.py index 3e651f5..9fe431f 100644 --- a/lambdas/koodistot_loader/koodistot_loader.py +++ b/lambdas/koodistot_loader/koodistot_loader.py @@ -28,7 +28,6 @@ """ LOGGER = logging.getLogger() -LOGGER.setLevel(logging.INFO) user_credentials = get_user_credentials( DbUser.DBA diff --git a/lambdas/mml_loader/mml_loader.py b/lambdas/mml_loader/mml_loader.py index 13b6326..84e41aa 100644 --- a/lambdas/mml_loader/mml_loader.py +++ b/lambdas/mml_loader/mml_loader.py @@ -36,7 +36,6 @@ """ LOGGER = logging.getLogger() -LOGGER.setLevel(logging.INFO) user_credentials = get_user_credentials( DbUser.DBA diff --git a/lambdas/ryhti_client/lambda_function.py b/lambdas/ryhti_client/lambda_function.py index 231a053..5e48d54 100644 --- a/lambdas/ryhti_client/lambda_function.py +++ b/lambdas/ryhti_client/lambda_function.py @@ -40,7 +40,6 @@ # method is run. It is run with burst CPU, so we will get faster initialization. # Boto3 and db helper initialization should go here. LOGGER = logging.getLogger() -LOGGER.setLevel(logging.INFO) user_credentials = get_user_credentials( DbUser.DBA From 6ea8cdf7267593f090b52cd15c3844a045904545 Mon Sep 17 00:00:00 2001 From: Lauri Kajan Date: Thu, 19 Feb 2026 09:27:15 +0200 Subject: [PATCH 06/12] Move file --- .../after_role_refactoring.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename migrations/{manual_migration_after_role_refactoring.sql => manual_migrations/after_role_refactoring.sql} (100%) diff --git a/migrations/manual_migration_after_role_refactoring.sql b/migrations/manual_migrations/after_role_refactoring.sql similarity index 100% rename from migrations/manual_migration_after_role_refactoring.sql rename to migrations/manual_migrations/after_role_refactoring.sql From 82ae1d6f425c5cb5753b238594d480fa5b069562 Mon Sep 17 00:00:00 2001 From: Lauri Kajan Date: Thu, 19 Feb 2026 09:28:00 +0200 Subject: [PATCH 07/12] Add sql statements to change srid for reference --- .../manual_migrations/coordinate_change.sql | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 migrations/manual_migrations/coordinate_change.sql diff --git a/migrations/manual_migrations/coordinate_change.sql b/migrations/manual_migrations/coordinate_change.sql new file mode 100644 index 0000000..f8d1092 --- /dev/null +++ b/migrations/manual_migrations/coordinate_change.sql @@ -0,0 +1,29 @@ +create or replace procedure change_srid(relation name, srid int) language plpgsql AS +$$ +DECLARE + view_def text; + geom_type text; +BEGIN + view_def = pg_get_viewdef(('hame.'||relation||'_v')::regclass::oid, true); + EXECUTE format('DROP VIEW hame.%I', relation||'_v'); + SELECT "type" INTO geom_type FROM geometry_columns WHERE f_table_schema = 'hame' and f_table_name = relation; + EXECUTE format('ALTER TABLE hame.%I ALTER COLUMN geom TYPE geometry(%s, %s) USING st_transform(geom, %s)', relation, geom_type, srid, srid); + EXECUTE format('CREATE VIEW hame.%I AS %s', relation||'_v', view_def); + EXECUTE format('GRANT SELECT ON hame.%I TO arho_read_only', relation||'_v'); + EXECUTE format('GRANT SELECT, DELETE, UPDATE, INSERT ON hame.%I TO arho_read_write', relation||'_v'); + EXECUTE format('CREATE TRIGGER %I + INSTEAD OF INSERT OR DELETE OR UPDATE + ON hame.%I + FOR EACH ROW + EXECUTE FUNCTION hame.trgf_iiud() + ', 'trg_iiud_'||relation||'_v', relation||'_v'); +END +$$; + +call change_srid('point', 3879); +call change_srid('land_use_area', 3879); +call change_srid('other_area', 3879); +call change_srid('line', 3879); +ALTER TABLE hame.plan ALTER COLUMN geom TYPE geometry(multipolygon, 3879) USING st_transform(geom, 3879); + +drop procedure change_srid(name, int); From 1bb802f43ad4bfc0692616d2c204c129feabff6f Mon Sep 17 00:00:00 2001 From: Lauri Kajan Date: Thu, 19 Feb 2026 12:48:38 +0200 Subject: [PATCH 08/12] Refactor to use terraform policy document --- infra/iam.tf | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/infra/iam.tf b/infra/iam.tf index bccfaf7..3228b50 100644 --- a/infra/iam.tf +++ b/infra/iam.tf @@ -192,25 +192,21 @@ resource "aws_iam_role_policy_attachment" "api-gateway-cloudwatch" { # Bastion # +data "aws_iam_policy_document" "bastion_trust" { + statement { + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } +} + # Adding a role for the EC2 machine allows making AWS service APIs available via IAM policies resource "aws_iam_role" "ec2-role" { name = "${var.prefix}-ec2-iam-role" path = "/" - assume_role_policy = < Date: Thu, 19 Feb 2026 22:20:39 +0200 Subject: [PATCH 09/12] Refactor cloud-init config to use cloudinit provider --- infra/.terraform.lock.hcl | 19 ++++++++++++ infra/bastion.tf | 17 ++++++++++- infra/bastion_config/cloud-config.yaml.tftpl | 10 ++++++ infra/bastion_user_data.tpl | 32 -------------------- infra/variables.tf | 6 ---- 5 files changed, 45 insertions(+), 39 deletions(-) create mode 100644 infra/bastion_config/cloud-config.yaml.tftpl delete mode 100644 infra/bastion_user_data.tpl diff --git a/infra/.terraform.lock.hcl b/infra/.terraform.lock.hcl index 6afdcb7..b0a280d 100644 --- a/infra/.terraform.lock.hcl +++ b/infra/.terraform.lock.hcl @@ -23,3 +23,22 @@ provider "registry.terraform.io/hashicorp/aws" { "zh:fdad558b1c41aa68123d0da82cc0d65bc86d09eaa1ab1d3a167ec3bce0fc0c66", ] } + +provider "registry.terraform.io/hashicorp/cloudinit" { + version = "2.3.7" + hashes = [ + "h1:iZ27qylcH/2bs685LJTKOKcQ+g7cF3VwN3kHMrzm4Ow=", + "zh:06f1c54e919425c3139f8aeb8fcf9bceca7e560d48c9f0c1e3bb0a8ad9d9da1e", + "zh:0e1e4cf6fd98b019e764c28586a386dc136129fef50af8c7165a067e7e4a31d5", + "zh:1871f4337c7c57287d4d67396f633d224b8938708b772abfc664d1f80bd67edd", + "zh:2b9269d91b742a71b2248439d5e9824f0447e6d261bfb86a8a88528609b136d1", + "zh:3d8ae039af21426072c66d6a59a467d51f2d9189b8198616888c1b7fc42addc7", + "zh:3ef4e2db5bcf3e2d915921adced43929214e0946a6fb11793085d9a48995ae01", + "zh:42ae54381147437c83cbb8790cc68935d71b6357728a154109d3220b1beb4dc9", + "zh:4496b362605ae4cbc9ef7995d102351e2fe311897586ffc7a4a262ccca0c782a", + "zh:652a2401257a12706d32842f66dac05a735693abcb3e6517d6b5e2573729ba13", + "zh:7406c30806f5979eaed5f50c548eced2ea18ea121e01801d2f0d4d87a04f6a14", + "zh:7848429fd5a5bcf35f6fee8487df0fb64b09ec071330f3ff240c0343fe2a5224", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + ] +} diff --git a/infra/bastion.tf b/infra/bastion.tf index 5382ba0..04783a7 100644 --- a/infra/bastion.tf +++ b/infra/bastion.tf @@ -5,6 +5,21 @@ data "aws_ssm_parameter" "amazon_linux" { name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64" } +data "cloudinit_config" "bastion_config" { + gzip = true # Compresses data to fit more in the 16KB limit + base64_encode = true # AWS requires base64 + + part { + content_type = "text/cloud-config" + content = templatefile( + "bastion_config/cloud-config.yaml.tftpl", + { + ec2_user_public_keys = var.bastion_ec2_user_public_keys, + } + ) + } +} + # Just the smallest arm instance available, for routing traffic to postgres resource "aws_instance" "bastion-ec2-instance" { ami = data.aws_ssm_parameter.amazon_linux.value @@ -14,7 +29,7 @@ resource "aws_instance" "bastion-ec2-instance" { iam_instance_profile = aws_iam_instance_profile.ec2-iam-profile.name tenancy = "default" user_data_replace_on_change = true # This is needed to update user data *and* ip address - user_data = local.bastion_user_data + user_data_base64 = data.cloudinit_config.bastion_config.rendered tags = merge(local.default_tags, { Name = "${var.prefix}-bastion" }) diff --git a/infra/bastion_config/cloud-config.yaml.tftpl b/infra/bastion_config/cloud-config.yaml.tftpl new file mode 100644 index 0000000..78a460d --- /dev/null +++ b/infra/bastion_config/cloud-config.yaml.tftpl @@ -0,0 +1,10 @@ +#cloud-config +users: + - name: ec2-user + sudo: ALL=(ALL) NOPASSWD:ALL + ssh-authorized-keys: +%{ for key in ec2_user_public_keys ~} + - ${key} +%{ endfor ~} + - name: ec2-tunnel + sudo: False diff --git a/infra/bastion_user_data.tpl b/infra/bastion_user_data.tpl deleted file mode 100644 index 6c3af9a..0000000 --- a/infra/bastion_user_data.tpl +++ /dev/null @@ -1,32 +0,0 @@ -Content-Type: multipart/mixed; boundary="//" -MIME-Version: 1.0 - ---// -Content-Type: text/cloud-config; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit -Content-Disposition: attachment; filename="cloud-config.txt" - -#cloud-config -cloud_final_modules: -- [users-groups, once] -users: - - name: ec2-user - sudo: ALL=(ALL) NOPASSWD:ALL - ssh-authorized-keys: -%{ for key in ec2_user_public_keys ~} - - ${key} -%{ endfor ~} - - name: ec2-tunnel - sudo: False - ---// -Content-Type: text/x-shellscript; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit -Content-Disposition: attachment; filename="userdata.sh" - -#!/bin/bash -sudo dnf update -sudo dnf install postgresql15 -y ---//-- diff --git a/infra/variables.tf b/infra/variables.tf index ddfee7c..7c3c113 100644 --- a/infra/variables.tf +++ b/infra/variables.tf @@ -200,10 +200,4 @@ locals { "Name" = var.prefix "Terraform" = "true" }) - bastion_user_data = templatefile( - "bastion_user_data.tpl", - { - ec2_user_public_keys = var.bastion_ec2_user_public_keys, - } - ) } From fb5c3fd2aff19c7cfc68a7a712af8da7c32be643 Mon Sep 17 00:00:00 2001 From: Lauri Kajan Date: Thu, 19 Feb 2026 22:33:12 +0200 Subject: [PATCH 10/12] Add bastion host key setup and IAM policy for SSM access --- infra/README.md | 47 +++++++++++++++++--------- infra/bastion.tf | 13 +++++++ infra/bastion_config/host_key_setup.sh | 37 ++++++++++++++++++++ infra/iam.tf | 28 +++++++++++++++ 4 files changed, 109 insertions(+), 16 deletions(-) create mode 100644 infra/bastion_config/host_key_setup.sh diff --git a/infra/README.md b/infra/README.md index 6218ad0..3d644be 100644 --- a/infra/README.md +++ b/infra/README.md @@ -90,6 +90,24 @@ ansible-playbook ansible/playbook.yml \ ## Deploying instances +Change to the `infra` directory and set your AWS MFA session variables: +``` +cd infra +# Set AWS MFA session variables +. get-mfa-vars.sh +``` + +Generate the Host Key for the bastion host and add to AWS SSM Parameter Store. This is required for the bastion host to be able to use the same host key across reboots, and to avoid SSH warnings about changed host key when the bastion host is restarted. +``` +ssh-keygen -t ed25519 -f bastion_key -N "" + +aws ssm put-parameter \ + --name "/infra/-bastion/host_key_ed25519" \ + --value "$(cat bastion_key)" \ + --type "SecureString" \ + --region eu-central-1 +``` + To launch new instances, running the following commands should be sufficient: ```shell @@ -97,42 +115,39 @@ terraform init terraform apply --var-file var-files/your-deployment.tfvars.json ``` -But in practice it is little bit more complicated, as some manual steps are required in between. `Terraform apply` would encounter some errors that need to be fixed manually before proceeding. Here is an example session for deploying a new `arho-dev` instance: +**But in practice it is little bit more complicated**, as some manual steps are required in between. `Terraform apply` would encounter some errors that need to be fixed manually before proceeding. Here is the complete list of steps to deploy new instances, including the manual steps to fix the errors: ```shell -cd infra -# Set AWS MFA session variables -. get-mfa-vars.sh # Create new terraform workspace -terraform workspace new arho-dev +terraform workspace new # Copy sample variables and edit them -cp arho.tfvars.sample.json arho-dev.tfvars.json -# Edit arho-dev.tfvars.json +cp arho.tfvars.sample.json .tfvars.json +# Edit .tfvars.json # Test the plan first -terraform plan -var-file=var-files/arho-dev.tfvars.json +terraform plan -var-file=var-files/.tfvars.json -terraform apply -var-file=var-files/arho-dev.tfvars.json -# Error is expected: Error: creating RDS DB Subnet Group (arho-dev-db): operation error RDS: CreateDBSubnetGroup, https response error StatusCode: 400, RequestID: f89c1d41-e247-48d2-9003-5a40b0e018aa, InvalidSubnet: Subnet IDs are required. +terraform apply -var-file=var-files/.tfvars.json +# Error is expected: Error: creating RDS DB Subnet Group (-db): operation error RDS: CreateDBSubnetGroup, https response error StatusCode: 400, RequestID: f89c1d41-e247-48d2-9003-5a40b0e018aa, InvalidSubnet: Subnet IDs are required. -terraform apply -var-file=var-files/arho-dev.tfvars.json -# Error is expected: Error: creating Lambda Function (arho-dev-db_manager): operation error Lambda: CreateFunction, https response error StatusCode: 400, RequestID: acc82829-26b3-4c88-af07-c9bae6f27b81, InvalidParameterValueException: Source image 631260641272.dkr.ecr.eu-central-1.amazonaws.com/arho-dev-db_manager:latest does not exist. Provide a valid source image. +terraform apply -var-file=var-files/.tfvars.json +# Error is expected: Error: creating Lambda Function (-db_manager): operation error Lambda: CreateFunction, https response error StatusCode: 400, RequestID: acc82829-26b3-4c88-af07-c9bae6f27b81, InvalidParameterValueException: Source image 631260641272.dkr.ecr.eu-central-1.amazonaws.com/-db_manager:latest does not exist. Provide a valid source image. # Set AWS_REGION and AWS_ACCOUNT_ID environment variables for Makefile export AWS_REGION= export AWS_ACCOUNT_ID= -export prefix=arho-dev +export prefix= # Build and push lambda images make push-lambdas -terraform apply -var-file=var-files/arho-dev.tfvars.json -# Error is expected: Error: creating Lambda Provisioned Concurrency Config (arho-dev-ryhti_client,live): operation error Lambda: PutProvisionedConcurrencyConfig, https response error StatusCode: 400, RequestID: 284038e3-650d-4ccb-951f-764e6cd9161d, InvalidParameterValueException: Provisioned Concurrency Configs cannot be applied to unpublished function versions. +terraform apply -var-file=var-files/.tfvars.json +# Error is expected: Error: creating Lambda Provisioned Concurrency Config (-ryhti_client,live): operation error Lambda: PutProvisionedConcurrencyConfig, https response error StatusCode: 400, RequestID: 284038e3-650d-4ccb-951f-764e6cd9161d, InvalidParameterValueException: Provisioned Concurrency Configs cannot be applied to unpublished function versions. # Update lambda functions make update-lambdas -terraform apply -var-file=var-files/arho-dev.tfvars.json +terraform apply -var-file=var-files/.tfvars.json # Now the infra should be deployed, but the database is still empty. Initialize the database with: make create-db diff --git a/infra/bastion.tf b/infra/bastion.tf index 04783a7..16dff34 100644 --- a/infra/bastion.tf +++ b/infra/bastion.tf @@ -9,6 +9,7 @@ data "cloudinit_config" "bastion_config" { gzip = true # Compresses data to fit more in the 16KB limit base64_encode = true # AWS requires base64 + # Part 1: Cloud-Config (Runs first by default) part { content_type = "text/cloud-config" content = templatefile( @@ -18,6 +19,18 @@ data "cloudinit_config" "bastion_config" { } ) } + + # Part 2: Shell Script (Runs after) + part { + content_type = "text/x-shellscript" + content = templatefile( + "bastion_config/host_key_setup.sh", + { + ssm_parameter_name = "/infra/${var.prefix}-bastion/host_key_ed25519" + aws_region = data.aws_region.current.name + } + ) + } } # Just the smallest arm instance available, for routing traffic to postgres diff --git a/infra/bastion_config/host_key_setup.sh b/infra/bastion_config/host_key_setup.sh new file mode 100644 index 0000000..6642ff3 --- /dev/null +++ b/infra/bastion_config/host_key_setup.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -e + +# Parameters passed from Terraform +SSM_PARAM_NAME="${ssm_parameter_name}" +AWS_REGION="${aws_region}" + +echo "Configuring exclusive Ed25519 Host Key..." + +# Fetch and setup key +service sshd stop + +echo "Fetching Host Key from SSM..." +aws ssm get-parameter \ + --name "$SSM_PARAM_NAME" \ + --with-decryption \ + --region "$AWS_REGION" \ + --query "Parameter.Value" \ + --output text > /etc/ssh/ssh_host_ed25519_key + +chmod 640 /etc/ssh/ssh_host_ed25519_key +chown root:ssh_keys /etc/ssh/ssh_host_ed25519_key +ssh-keygen -y -f /etc/ssh/ssh_host_ed25519_key > /etc/ssh/ssh_host_ed25519_key.pub +chmod 644 /etc/ssh/ssh_host_ed25519_key.pub + + +# Comment out any existing HostKey definitions +sed -i 's/^HostKey/#HostKey/g' /etc/ssh/sshd_config + +# Append our specific Ed25519 key as the only valid host key +echo "HostKey /etc/ssh/ssh_host_ed25519_key" >> /etc/ssh/sshd_config + +# 5. Destroy the default OS-generated keys to ensure they are never used +rm -f /etc/ssh/ssh_host_rsa_key* +rm -f /etc/ssh/ssh_host_ecdsa_key* + +service sshd start diff --git a/infra/iam.tf b/infra/iam.tf index 3228b50..0a468e8 100644 --- a/infra/iam.tf +++ b/infra/iam.tf @@ -202,6 +202,29 @@ data "aws_iam_policy_document" "bastion_trust" { } } +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} +data "aws_iam_policy_document" "bastion_ssm_read" { + statement { + sid = "AllowReadingBastionHostKey" + actions = ["ssm:GetParameter"] + + resources = [ + "arn:aws:ssm:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:parameter/infra/${var.prefix}-bastion/host_key_rsa" + ] + } +} + +resource "aws_iam_policy" "bastion_ssm_read_policy" { + name = "${var.prefix}-bastion-ssm-read-policy" + description = "Allows bastion to read its own host key from SSM" + policy = data.aws_iam_policy_document.bastion_ssm_read.json + + tags = merge(local.default_tags, { + Name = "${var.prefix}-bastion_ssm_read_policy" + }) +} + # Adding a role for the EC2 machine allows making AWS service APIs available via IAM policies resource "aws_iam_role" "ec2-role" { name = "${var.prefix}-ec2-iam-role" @@ -213,6 +236,11 @@ resource "aws_iam_role" "ec2-role" { }) } +resource "aws_iam_role_policy_attachment" "bastion_attach" { + role = aws_iam_role.ec2-role.name + policy_arn = aws_iam_policy.bastion_ssm_read_policy.arn +} + resource "aws_iam_instance_profile" "ec2-iam-profile" { name = "${var.prefix}-ec2-iam-profile" role = aws_iam_role.ec2-role.name From da2990cf0f5a2ee0bf36dfe041026f4e2fdaa295 Mon Sep 17 00:00:00 2001 From: Lauri Kajan Date: Fri, 20 Feb 2026 09:56:30 +0200 Subject: [PATCH 11/12] Add variables for a new test instance --- infra/var-files/arho-test.tfvars.enc.json | 44 +++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 infra/var-files/arho-test.tfvars.enc.json diff --git a/infra/var-files/arho-test.tfvars.enc.json b/infra/var-files/arho-test.tfvars.enc.json new file mode 100644 index 0000000..f45cbd7 --- /dev/null +++ b/infra/var-files/arho-test.tfvars.enc.json @@ -0,0 +1,44 @@ +{ + "AWS_REGION_NAME": "ENC[AES256_GCM,data:HRYRKWXBull2VQ==,iv:NUTudGxBT7RnOaKhu04wMpn9wGIHt6hBe2Q70EftdNc=,tag:SiSfWJACeiPMB7yMjx9Ymw==,type:str]", + "AWS_LAMBDA_USER": "ENC[AES256_GCM,data:wY0MxI+/RKDkvwOG9fz3XfQ8nFVQNS/WtqzRAQ==,iv:afuxJMDTk6MECX77lic3o8XbcLAp7Z+PBRy4XCQ3MFY=,tag:L9gLqK2Bc0H4jfujhDiQHg==,type:str]", + "AWS_HOSTED_DOMAIN": "ENC[AES256_GCM,data:9lklgoAbv++xRaPZHbY=,iv:Ko2ZCFghEfIisbLDvRAHnkR/dSawvhyODmVgoR/Nv9U=,tag:ke0AuNCi8wQltDfSGnP/lA==,type:str]", + "bastion_subdomain": "ENC[AES256_GCM,data:Yhm9WxsR7g==,iv:B0E5330YnGuRZO3aJjgyGq9cWQdCKUDcelQnAXNVEkw=,tag:9rk0UYlifGHJ3KDqJ8JySw==,type:str]", + "enable_x_road": "ENC[AES256_GCM,data:vKMDFJI=,iv:+K4/u12806uvAVI1XaEIMPzyUKZDwtJdXvCuIH5DDTE=,tag:lLkElPQLHQ12twdGnquxmQ==,type:bool]", + "enable_route53_record": "ENC[AES256_GCM,data:FVyiVw==,iv:qDinBSzRdhel0IvmQv8eNlPNVIBvETFxR71F/DI+pC4=,tag:7CSS06UXS2Wwsdn9Mjj9Rw==,type:bool]", + "hame_db_name": "ENC[AES256_GCM,data:g7dBncjTeSq3,iv:ks56VZO4usx8v1ez09KSgm2EXXLSw6LmIx+NZImSKxM=,tag:WYvbRXoDAHCOkKmIaNsd5Q==,type:str]", + "arho_su_secrets": { + "username": "ENC[AES256_GCM,data:R4XYDdaN2Q==,iv:LdDOGc8DRnWwgDdb+6lJZEqi747Zw3T0Qyz2TU3vqlY=,tag:m2WLU+zksm4B0L4sUqQDIw==,type:str]", + "password": "ENC[AES256_GCM,data:cUvRYf6RPIUuUvj3acVMUoE/Rh92EqCxjQq3C4R3eyKqO8eDIT3SPXiaD0mX06DS2F1muvjOhnWopdMBP6n1hw==,iv:Env0/maVIB7nLy59SWk9n/w9zjKQeQxekwhnxwwAM14=,tag:VP98Vn699lvgSKB64HrCQQ==,type:str]" + }, + "arho_dba_secrets": { + "username": "ENC[AES256_GCM,data:VkT9u6MUl4k=,iv:IaNIvXlFXM9T+m0TVn20kZKsDAZC8LjSfUDuCsI/Axc=,tag:meJx/RhurodSA44Vo+Jrtg==,type:str]", + "password": "ENC[AES256_GCM,data:J+DEj3FIVP/YkdJqiNAB5Y1HqNTU3W88Y9eH1lBQO4yfeQintuutDHpds/NDGAZTzJgSKbbjsnUGuwoLKzMqmA==,iv:KjPmF0YyNcqY9YFobbVBeOSh8d+h8ehq4W0EwhXu5pw=,tag:yqwMc7yT03/6L0Q6gujCAA==,type:str]" + }, + "prefix": "ENC[AES256_GCM,data:PM19udoERIJa,iv:WG67tJzpEwnlJS6LQ5hQoDDJYtdAj7QHF/rW1u2rzKs=,tag:u6gbfzjugFFLC/fGSKOt8w==,type:str]", + "project_srid": "ENC[AES256_GCM,data:3pBAcw==,iv:8xUBEDAoUhiqIKAUiCVsAU4FUIvBNx6QmaxTBMkadko=,tag:e7DR81F8fKMiGrnNTHlSNA==,type:float]", + "extra_tags": { + "Customer": "ENC[AES256_GCM,data:Z3Fxhw==,iv:aU/eo8zF3o2Ko0/C3uZ9+WoWDMmlMfZkIoqwKHnikgw=,tag:CXpF2BIHFSODddRDib7i4w==,type:str]", + "Project": "ENC[AES256_GCM,data:ra2DYHTCJp6DZg==,iv:50dJtugw6SXB0dCHfkKSi7o6ccKi1zHRiaf6cYplZEg=,tag:oI9ueFj7m8e5cjgEpMx1hw==,type:str]", + "wrike_task_id": "ENC[AES256_GCM,data:QdKf0DVwjdyqPw==,iv:fwdv+C+TviwMS8Y2sP48FpJoKl4pJX9DnmvPkwkMR60=,tag:0QYd+2ry0c4Ciz9ezoY39g==,type:str]" + }, + "syke_apikey": "ENC[AES256_GCM,data:tdrQA1UfYr3/pRB6nrfctsxMS2D8G7OOgKVuOgrTkx8=,iv:spN8RseXqRbv7aCN3+sknSXFhaH06OyskjJHWMRzrGs=,tag:E1+BLdboPitupsa3T4UcQw==,type:str]", + "mml_apikey": "ENC[AES256_GCM,data:CajkCPE72PsYMe8aY2pH+/+VNV8++PwBVmJekZRE1vKVdgYz,iv:AQ+GaLsm5jZ3W1ELxRBjSx8ieD9HuOQ/HaFB0W71Vu8=,tag:O/PyoJQOe+5X55C+zUWWWg==,type:str]", + "bastion_ec2_user_public_keys": [ + "ENC[AES256_GCM,data:uXVvzaJPfP6Dh3jTtnyAvG3Un2vJKrmroW64O0seGhqAvFu2TFLmLP9j9hDEWGNsU2a4oMRcDhvFO07eEkbIO/kkgyymN4MtwGbuwB/29oDK8pQBxBggccW9Bv4/2xLPZEP8tNg4qA==,iv:pd64HtoEFvYGpe8ODw9Y6+YCP0jSNCzAvfWumz1zRdI=,tag:dozrWekar+Bne7hQEvZqVQ==,type:str]", + "ENC[AES256_GCM,data:4qtQfwnDuAVJp82rqOlPMp+L+wb4Nta9tRKiZZ7Fi9G7563H5x5CuVw9da6M7sVzHOwey3ah+W7nFqFBI0BIJeLGCxaAH3PXQfIJV5T0lEE=,iv:U9aTYixRzJfhXEpgEbvBue9ud6DDUgn+UQObiv4+LA4=,tag:P3zoWvrgvvk9MiHBMSxHSg==,type:str]" + ], + "sops": { + "kms": [ + { + "arn": "arn:aws:kms:eu-central-1:631260641272:alias/Hame-ryhti", + "created_at": "2026-02-20T07:54:00Z", + "enc": "AQICAHiE41A2/p59xbFWiUt+oC17wSyQhQ4tvk+1zIy71M/m3QEitaFpEbwsEG7n8MRM48UCAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMEDIh+7HL8/M71avoAgEQgDuWVrKZFuw428Lv4QjyhzerpAPGpwqzyDJBwe3q9nZM7FQYr8a21x8JGJHShxl2bBK3ayQTqubAHzgATg==", + "aws_profile": "" + } + ], + "lastmodified": "2026-02-20T07:54:00Z", + "mac": "ENC[AES256_GCM,data:WA07wN6tvqYRoWKxcKwwrtPoHBuvh1TUtXFPRgDwiS6RJslfL6/hhvazx2uDR0eX+af1ocI7HRm45YJg1Z8aeQ1gbSPngQLuqSCRfgyE/0lLR0fSYhI+slnpuWK8WiRW+YvLpmbarXIaPu3rNQWCaYYNtHMn9aL7pVAzqkO3hKw=,iv:M9jhNdFLxX4qdc9HLj57/PYfj6AR4D2Ed5Yc//gZyo0=,tag:A82UIVWBBIpBY6/h87b8gg==,type:str]", + "unencrypted_suffix": "_unencrypted", + "version": "3.10.2" + } +} From 5db3be55e95d8415789b8375b3190e3ab54dce87 Mon Sep 17 00:00:00 2001 From: Lauri Kajan Date: Fri, 20 Feb 2026 12:23:46 +0200 Subject: [PATCH 12/12] Add the new instance to cicd --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 11e7f92..1dbf020 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -37,7 +37,7 @@ jobs: if: github.event_name == 'workflow_dispatch' || needs.check-skip-deploy.outputs.skip-deploy == 'false' strategy: matrix: - environment: [vsl-test, espoo-test] + environment: [vsl-test, espoo-test, arho-test] environment: ${{ matrix.environment }} steps: - uses: actions/checkout@v4