Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions aws/ami/99-querypie-ports.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
net.ipv4.ip_local_reserved_ports = 40000-40030
33 changes: 20 additions & 13 deletions aws/ami/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 터널을 생성합니다.

## 파일과 호출 관계

사용자가 직접 실행하는 명령은 다음 네 개입니다.
Expand Down Expand Up @@ -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를 실행하지 않습니다.

Expand All @@ -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 <querypie_version>`을 실행합니다.
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 <querypie_version>`을 실행합니다.
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를 생성합니다.
Expand All @@ -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`가 남아 있으면 빌드를 실패시킵니다.
Expand Down Expand Up @@ -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의 검증 인스턴스를 기동합니다.
Expand Down
30 changes: 25 additions & 5 deletions aws/ami/ami-build.pkr.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Comment thread
jk-kim0 marked this conversation as resolved.
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
Expand All @@ -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

Expand All @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions aws/ami/ami-build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 12 additions & 4 deletions aws/ami/ami-verify.pkr.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down
5 changes: 5 additions & 0 deletions aws/ami/ami-verify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
6 changes: 5 additions & 1 deletion aws/ami/sanitize-image-before-snapshot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
169 changes: 169 additions & 0 deletions aws/ami/tests/ami-build-region.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 ]
}
Loading