From b985a0ea53c3cab7c0c00289f34f103bfae8cac2 Mon Sep 17 00:00:00 2001 From: JK Date: Fri, 7 Aug 2026 15:13:50 +0900 Subject: [PATCH] fix: stabilize Amazon Linux 2023 AMI builds Use Session Manager with preflight checks and retain the full SSH reconnect contract for Docker group changes across legacy and current AL2023 process names. Also make first boot reproducible by installing Compose system-wide, reserving QueryPie listener ports, resetting machine identity, and validating both architectures with Spot capacity fallbacks. Co-Authored-By: Atlas --- aws/ami/99-querypie-ports.conf | 1 + aws/ami/README.md | 33 ++-- aws/ami/ami-build.pkr.hcl | 30 +++- aws/ami/ami-build.sh | 5 + aws/ami/ami-verify.pkr.hcl | 16 +- aws/ami/ami-verify.sh | 5 + aws/ami/sanitize-image-before-snapshot.sh | 6 +- aws/ami/tests/ami-build-region.bats | 169 ++++++++++++++++++ .../install-docker-on-amazon-linux-2023.sh | 3 +- 9 files changed, 244 insertions(+), 24 deletions(-) create mode 100644 aws/ami/99-querypie-ports.conf diff --git a/aws/ami/99-querypie-ports.conf b/aws/ami/99-querypie-ports.conf new file mode 100644 index 0000000..00e910e --- /dev/null +++ b/aws/ami/99-querypie-ports.conf @@ -0,0 +1 @@ +net.ipv4.ip_local_reserved_ports = 40000-40030 diff --git a/aws/ami/README.md b/aws/ami/README.md index 783ffaa..a920d8b 100644 --- a/aws/ami/README.md +++ b/aws/ami/README.md @@ -29,6 +29,9 @@ export AMI_REGION=ap-northeast-2 스크립트는 응답의 계정 ID나 IAM ARN이 위 표의 값과 일치하는지 검사하지 않습니다. `ami-verify.sh`, `ami-validate.sh`, `ami-ls.sh`도 계정이나 프로파일을 내부에서 변경하지 않습니다. +AMI 빌드와 인스턴스 검증에는 `packer`, `aws`, `session-manager-plugin` 명령이 필요합니다. +Packer는 Session Manager 플러그인을 사용해 빌드 및 검증 인스턴스에 대한 SSH 터널을 생성합니다. + ## 파일과 호출 관계 사용자가 직접 실행하는 명령은 다음 네 개입니다. @@ -101,8 +104,9 @@ AMI_REGION=ap-northeast-2 \ 1. `packer` 명령이 존재해야 합니다. 2. `aws` 명령이 존재해야 합니다. -3. 현재 AWS 자격 증명으로 `sts get-caller-identity`가 성공해야 합니다. -4. `AMI_REGION`의 `EbsEncryptionByDefault` 값이 정확히 `False`여야 합니다. +3. `session-manager-plugin` 명령이 존재해야 합니다. +4. 현재 AWS 자격 증명으로 `sts get-caller-identity`가 성공해야 합니다. +5. `AMI_REGION`의 `EbsEncryptionByDefault` 값이 정확히 `False`여야 합니다. EBS 기본 암호화가 활성화되어 있으면 Packer를 실행하지 않습니다. @@ -116,25 +120,27 @@ EBS 기본 암호화가 활성화되어 있으면 Packer를 실행하지 않습 | 베이스 AMI 이름 | `al2023-ami-2023.12.*-kernel-6.12-*` | 동일 | | 루트 장치 유형 | `ebs` | `ebs` | | 가상화 유형 | `hvm` | `hvm` | -| 빌드 인스턴스 | Spot `t3.xlarge` | Spot `t4g.xlarge` | +| 빌드 인스턴스 | Spot `t3.xlarge` | Spot `t4g.xlarge`, `m7g.xlarge`, `m6g.xlarge` 순서의 용량 대안 | 생성되는 AMI는 HVM과 ENA를 사용하고 IMDSv2를 요구합니다. 루트 볼륨은 `gp3`, 32 GiB, 16,000 IOPS, 1,000 MiB/s로 설정됩니다. 빌드 인스턴스와 AMI의 루트 볼륨은 암호화하지 않습니다. -Packer의 임시 보안 그룹은 빌드를 실행한 공인 IP에서 SSH 접속을 허용합니다. +Packer는 `ec2-session-manager` 인스턴스 프로파일과 Session Manager를 통해 SSH를 연결합니다. ### Packer 실행 순서 `ami-build.pkr.hcl`은 다음 순서로 인스턴스를 구성합니다. 1. `cloud-init status --wait`로 초기화 완료를 기다립니다. -2. `../scripts/install-docker-on-amazon-linux-2023.sh`를 실행합니다. -3. `compose/setup.v2.sh`를 `/usr/local/bin/setup.v2.sh`로 설치합니다. -4. `setup.v2.sh --install-partially-for-ami `을 실행합니다. -5. `querypie-first-boot.service`를 설치하고 활성화합니다. -6. `validate-image-runtime.sh`로 암호화된 장치와 파일시스템이 없는지 검사합니다. -7. `sanitize-image-before-snapshot.sh`로 빌드 인스턴스 상태를 정리합니다. -8. AMI 스냅샷과 `manifest.json`을 생성합니다. +2. QueryPie listener 범위 `40000-40030`을 커널 임시 포트 할당에서 제외합니다. +3. `../scripts/install-docker-on-amazon-linux-2023.sh`를 실행합니다. + Docker Compose 플러그인은 최초 부팅 systemd 서비스에서도 사용할 수 있도록 시스템 경로에 설치합니다. +4. `compose/setup.v2.sh`를 `/usr/local/bin/setup.v2.sh`로 설치합니다. +5. `setup.v2.sh --install-partially-for-ami `을 실행합니다. +6. `querypie-first-boot.service`를 설치하고 활성화합니다. +7. `validate-image-runtime.sh`로 암호화된 장치와 파일시스템이 없는지 검사합니다. +8. `sanitize-image-before-snapshot.sh`로 빌드 인스턴스 상태를 정리합니다. +9. AMI 스냅샷과 `manifest.json`을 생성합니다. 부분 설치 단계는 QueryPie 구성 파일을 배치하고 database, querypie, tools 프로파일의 컨테이너 이미지를 미리 받습니다. 부분 설치 단계는 `.env`의 `AGENT_SECRET`, `KEY_ENCRYPTION_KEY`, `DB_PASSWORD`, `REDIS_PASSWORD` 값을 비운 상태로 AMI를 생성합니다. @@ -147,7 +153,8 @@ Packer의 임시 보안 그룹은 빌드를 실행한 공인 IP에서 SSH 접속 - SSH root 로그인을 비활성화하고 root 계정을 잠급니다. - `/root`와 `/home` 아래의 `authorized_keys`를 삭제합니다. - 기존 SSH host key를 삭제합니다. -- `cloud-init clean --logs --machine-id`를 실행합니다. +- `cloud-init clean --logs`를 실행하고 `/etc/machine-id`를 `uninitialized`로 초기화합니다. +- 별도로 남은 D-Bus machine ID를 삭제합니다. - systemd random seed를 삭제합니다. - DNF 캐시, 임시 파일, 셸 히스토리 및 로그 내용을 정리합니다. - 비어 있지 않은 `authorized_keys`가 남아 있으면 빌드를 실패시킵니다. @@ -214,7 +221,7 @@ Marketplace 제출, 스캔 실행 및 제품 등록도 이 옵션의 동작 범 `ami-verify.sh`는 다음 순서로 실행됩니다. -1. `packer`와 `aws` 명령이 존재하는지 확인합니다. +1. `packer`, `aws`, `session-manager-plugin` 명령이 존재하는지 확인합니다. 2. `ami-validate.sh`로 AMI 구조를 검사합니다. 3. AWS API에서 AMI 아키텍처를 조회합니다. 4. `ami-verify.pkr.hcl`로 해당 AMI의 검증 인스턴스를 기동합니다. diff --git a/aws/ami/ami-build.pkr.hcl b/aws/ami/ami-build.pkr.hcl index 09c3212..a9a9679 100644 --- a/aws/ami/ami-build.pkr.hcl +++ b/aws/ami/ami-build.pkr.hcl @@ -120,16 +120,26 @@ source "amazon-ebs" "amazon-linux-2023" { ami_name = local.ami_name ami_description = "QueryPie Suite ${var.querypie_version} on Amazon Linux 2023" ami_virtualization_type = "hvm" - ena_support = true encrypt_boot = false imds_support = "v2.0" region = var.region ssh_username = local.ssh_username + ssh_interface = "session_manager" + iam_instance_profile = "ec2-session-manager" + associate_public_ip_address = true # ssh_private_key_file = "demo-targets.pem" # ssh_keypair_name = "demo-targets" - spot_instance_types = var.architecture == "arm64" ? ["t4g.xlarge"] : ["t3.xlarge"] + spot_instance_types = var.architecture == "arm64" ? ["t4g.xlarge", "m7g.xlarge", "m6g.xlarge"] : ["t3.xlarge"] + spot_price = "0.09" + + subnet_filter { + filters = { + "default-for-az" = "true" + } + most_free = true + } # EBS configuration ebs_optimized = true @@ -152,9 +162,6 @@ source "amazon-ebs" "amazon-linux-2023" { http_put_response_hop_limit = 1 } - # Security group configuration - temporary_security_group_source_public_ip = true - # Tags of the EC2 instance used for building the AMI run_tags = local.instance_tags @@ -178,6 +185,19 @@ build { ] } + # Keep QueryPie listener ports out of the kernel's ephemeral allocation range. + provisioner "file" { + source = "99-querypie-ports.conf" + destination = "/tmp/99-querypie-ports.conf" + } + provisioner "shell" { + inline_shebang = "/bin/bash -ex" + inline = [ + "sudo install -m 644 /tmp/99-querypie-ports.conf /etc/sysctl.d/99-querypie-ports.conf", + "sudo sysctl --system", + ] + } + provisioner "shell" { expect_disconnect = true # It will logout at the end of this provisioner. script = "../scripts/install-docker-on-amazon-linux-2023.sh" diff --git a/aws/ami/ami-build.sh b/aws/ami/ami-build.sh index 40bea0f..0c8c79d 100755 --- a/aws/ami/ami-build.sh +++ b/aws/ami/ami-build.sh @@ -66,6 +66,11 @@ function validate_environment() { exit 1 fi + if ! command -v session-manager-plugin &>/dev/null; then + log::error "AWS Session Manager plugin is not installed. Please install session-manager-plugin to continue." + exit 1 + fi + log::do aws sts get-caller-identity --output text >/dev/null local encryption_by_default diff --git a/aws/ami/ami-verify.pkr.hcl b/aws/ami/ami-verify.pkr.hcl index 0af008b..6cb8d47 100644 --- a/aws/ami/ami-verify.pkr.hcl +++ b/aws/ami/ami-verify.pkr.hcl @@ -67,7 +67,18 @@ source "amazon-ebs" "ami-verify" { region = var.region instance_type = local.instance_type - ssh_username = local.ssh_username + ssh_username = local.ssh_username + ssh_interface = "session_manager" + + iam_instance_profile = "ec2-session-manager" + associate_public_ip_address = true + + subnet_filter { + filters = { + "default-for-az" = "true" + } + most_free = true + } # EBS configuration ebs_optimized = true @@ -92,9 +103,6 @@ source "amazon-ebs" "ami-verify" { http_put_response_hop_limit = 1 } - # Security group configuration - temporary_security_group_source_public_ip = true - # Tags of the EC2 instance used for building the AMI run_tags = local.instance_tags } diff --git a/aws/ami/ami-verify.sh b/aws/ami/ami-verify.sh index 99ed21b..4f025cf 100755 --- a/aws/ami/ami-verify.sh +++ b/aws/ami/ami-verify.sh @@ -51,6 +51,11 @@ function validate_environment() { log::error "AWS CLI is not installed. Please install AWS CLI to continue." exit 1 fi + + if ! command -v session-manager-plugin &>/dev/null; then + log::error "AWS Session Manager plugin is not installed. Please install session-manager-plugin to continue." + exit 1 + fi } function main() { diff --git a/aws/ami/sanitize-image-before-snapshot.sh b/aws/ami/sanitize-image-before-snapshot.sh index 0859558..8275e0a 100755 --- a/aws/ami/sanitize-image-before-snapshot.sh +++ b/aws/ami/sanitize-image-before-snapshot.sh @@ -19,7 +19,11 @@ sudo find /root /home -xdev -type f -name authorized_keys -delete sudo find /etc/ssh -maxdepth 1 -type f -name 'ssh_host_*_key*' -delete # Force cloud-init and systemd to initialize per-instance state on the next boot. -sudo cloud-init clean --logs --machine-id +# Amazon Linux 2023's cloud-init does not expose the --machine-id flag, so +# reproduce its documented systemd behavior explicitly. +sudo cloud-init clean --logs +printf 'uninitialized\n' | sudo tee /etc/machine-id >/dev/null +sudo rm -f /var/lib/dbus/machine-id sudo rm -f /var/lib/systemd/random-seed # Remove build caches, temporary files, histories, and logs. diff --git a/aws/ami/tests/ami-build-region.bats b/aws/ami/tests/ami-build-region.bats index 1810b9b..4d0708d 100644 --- a/aws/ami/tests/ami-build-region.bats +++ b/aws/ami/tests/ami-build-region.bats @@ -40,6 +40,12 @@ printf '%s\n' "$*" >>"$PACKER_INVOCATIONS_FILE" EOF chmod +x "$MOCK_BIN/packer" + cat >"$MOCK_BIN/session-manager-plugin" <<'EOF' +#!/usr/bin/env bash +exit 0 +EOF + chmod +x "$MOCK_BIN/session-manager-plugin" + PACKER_INVOCATIONS_FILE="$TEST_ROOT/packer-invocations" export PACKER_INVOCATIONS_FILE : >"$PACKER_INVOCATIONS_FILE" @@ -72,3 +78,166 @@ teardown() { [[ "$output" == *"EBS encryption by default must be disabled"* ]] [ ! -s "$PACKER_INVOCATIONS_FILE" ] } + +@test "AMI build stops before Packer when Session Manager plugin is missing" { + rm "$MOCK_BIN/session-manager-plugin" + + run env \ + PATH="$MOCK_BIN:/usr/bin:/bin" \ + AMI_REGION=ap-northeast-2 \ + "$BATS_TEST_DIRNAME/../ami-build.sh" 11.6.0 amazon-linux-2023 x86_64 + + [ "$status" -eq 1 ] + [[ "$output" == *"AWS Session Manager plugin is not installed"* ]] + [ ! -s "$PACKER_INVOCATIONS_FILE" ] +} + +@test "AMI verification stops before Packer when Session Manager plugin is missing" { + rm "$MOCK_BIN/session-manager-plugin" + + run env \ + PATH="$MOCK_BIN:/usr/bin:/bin" \ + AMI_REGION=ap-northeast-2 \ + "$BATS_TEST_DIRNAME/../ami-verify.sh" ami-0123456789abcdef0 + + [ "$status" -eq 1 ] + [[ "$output" == *"AWS Session Manager plugin is not installed"* ]] + [ ! -s "$PACKER_INVOCATIONS_FILE" ] +} + +@test "AMI template sets a maximum price for Spot Fleet builds" { + run grep -F \ + 'spot_price = "0.09"' \ + "$BATS_TEST_DIRNAME/../ami-build.pkr.hcl" + + [ "$status" -eq 0 ] +} + +@test "arm64 Spot builds include same-memory Graviton capacity fallbacks" { + run grep -F \ + 'var.architecture == "arm64" ? ["t4g.xlarge", "m7g.xlarge", "m6g.xlarge"]' \ + "$BATS_TEST_DIRNAME/../ami-build.pkr.hcl" + + [ "$status" -eq 0 ] +} + +@test "AMI template does not request ENA modification for Spot builds" { + run grep -F \ + 'ena_support' \ + "$BATS_TEST_DIRNAME/../ami-build.pkr.hcl" + + [ "$status" -eq 1 ] +} + +@test "AMI template assigns a public IP to remotely provisioned builders" { + run grep -F \ + 'associate_public_ip_address = true' \ + "$BATS_TEST_DIRNAME/../ami-build.pkr.hcl" + + [ "$status" -eq 0 ] +} + +@test "AMI template restricts builders to default public subnets" { + run grep -F \ + '"default-for-az" = "true"' \ + "$BATS_TEST_DIRNAME/../ami-build.pkr.hcl" + + [ "$status" -eq 0 ] +} + +@test "AMI template tunnels SSH through Session Manager" { + run grep -E \ + 'ssh_interface[[:space:]]*=[[:space:]]*"session_manager"' \ + "$BATS_TEST_DIRNAME/../ami-build.pkr.hcl" + [ "$status" -eq 0 ] + + run grep -E \ + 'iam_instance_profile[[:space:]]*=[[:space:]]*"ec2-session-manager"' \ + "$BATS_TEST_DIRNAME/../ami-build.pkr.hcl" + [ "$status" -eq 0 ] + + run grep -F \ + 'temporary_security_group_source_public_ip' \ + "$BATS_TEST_DIRNAME/../ami-build.pkr.hcl" + [ "$status" -eq 1 ] +} + +@test "AMI verification tunnels SSH through Session Manager" { + run grep -E \ + 'ssh_interface[[:space:]]*=[[:space:]]*"session_manager"' \ + "$BATS_TEST_DIRNAME/../ami-verify.pkr.hcl" + [ "$status" -eq 0 ] + + run grep -E \ + 'iam_instance_profile[[:space:]]*=[[:space:]]*"ec2-session-manager"' \ + "$BATS_TEST_DIRNAME/../ami-verify.pkr.hcl" + [ "$status" -eq 0 ] + + run grep -F \ + 'temporary_security_group_source_public_ip' \ + "$BATS_TEST_DIRNAME/../ami-verify.pkr.hcl" + [ "$status" -eq 1 ] +} + +@test "Docker installation disconnects legacy and current AL2023 SSH sessions" { + local installer="$BATS_TEST_DIRNAME/../../scripts/install-docker-on-amazon-linux-2023.sh" + + run grep -F \ + 'killall sshd sshd-session' \ + "$installer" + + [ "$status" -eq 0 ] +} + +@test "Docker Compose is installed as a system-wide CLI plugin for first boot" { + local installer="$BATS_TEST_DIRNAME/../../scripts/install-docker-on-amazon-linux-2023.sh" + + run grep -F \ + 'sudo install -m 755 -D /tmp/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose' \ + "$installer" + + [ "$status" -eq 0 ] +} + +@test "AMI reserves QueryPie listener ports before first boot networking" { + local sysctl_config="$BATS_TEST_DIRNAME/../99-querypie-ports.conf" + + run grep -F \ + 'net.ipv4.ip_local_reserved_ports = 40000-40030' \ + "$sysctl_config" + [ "$status" -eq 0 ] + + run grep -F \ + 'sudo install -m 644 /tmp/99-querypie-ports.conf /etc/sysctl.d/99-querypie-ports.conf' \ + "$BATS_TEST_DIRNAME/../ami-build.pkr.hcl" + [ "$status" -eq 0 ] + + run grep -F \ + 'sudo sysctl --system' \ + "$BATS_TEST_DIRNAME/../ami-build.pkr.hcl" + [ "$status" -eq 0 ] +} + +@test "AMI sanitization resets machine identity without unsupported cloud-init flags" { + local sanitizer="$BATS_TEST_DIRNAME/../sanitize-image-before-snapshot.sh" + + run grep -F \ + 'sudo cloud-init clean --logs' \ + "$sanitizer" + [ "$status" -eq 0 ] + + run grep -F \ + "printf 'uninitialized\\n' | sudo tee /etc/machine-id >/dev/null" \ + "$sanitizer" + [ "$status" -eq 0 ] + + run grep -F \ + 'sudo rm -f /var/lib/dbus/machine-id' \ + "$sanitizer" + [ "$status" -eq 0 ] + + run grep -F \ + 'cloud-init clean --logs --machine-id' \ + "$sanitizer" + [ "$status" -eq 1 ] +} diff --git a/aws/scripts/install-docker-on-amazon-linux-2023.sh b/aws/scripts/install-docker-on-amazon-linux-2023.sh index 891b900..82c0b29 100755 --- a/aws/scripts/install-docker-on-amazon-linux-2023.sh +++ b/aws/scripts/install-docker-on-amazon-linux-2023.sh @@ -19,6 +19,7 @@ function install_docker_compose() { -o /tmp/docker-compose if file /tmp/docker-compose | grep -q "ELF 64-bit LSB executable"; then install -m 755 -D /tmp/docker-compose ~/.docker/cli-plugins/docker-compose + sudo install -m 755 -D /tmp/docker-compose /usr/local/lib/docker/cli-plugins/docker-compose sudo install -m 755 /tmp/docker-compose /usr/local/bin/docker-compose fi } @@ -31,7 +32,7 @@ function test_if_docker_installed_already { } function shutdown_ssh_session { - killall sshd + killall sshd sshd-session } function main() {