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() {