Skip to content

Register balenaOS AWS/EC2 AMI #3

Register balenaOS AWS/EC2 AMI

Register balenaOS AWS/EC2 AMI #3

Workflow file for this run

---
# https://github.com/balena-os/balena-yocto-scripts/blob/master/.github/workflows/yocto-build-deploy.yml
# https://github.com/balena-os/balena-yocto-scripts/blob/master/automation/entry_scripts/balena-generate-ami.sh
name: aws-ami-from-balena-os-image
on: workflow_dispatch
permissions:
contents: read
id-token: write # AWS GitHub OIDC required: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.ref }}
# cancel jobs in progress for updated PRs, but not merge or tag events
cancel-in-progress: ${{ github.event.action == 'synchronize' }}
env:
AWS_DEFAULT_REGION: ${{ vars.AWS_REGION || 'us-east-1' }}
AWS_IAM_ROLE: ${{ vars.AWS_IAM_ROLE }}
AWS_REGION: ${{ vars.AWS_REGION || 'us-east-1' }}
AWS_SECURITY_GROUP_ID: ${{ vars.AWS_SECURITY_GROUP_ID }}
AWS_SUBNET_ID: ${{ vars.AWS_SUBNET_ID }}
BALENA_TEST_ORG: ${{ vars.BALENA_TEST_ORG || 'belodetek' }}
VMIMPORT_S3_BUCKET: ${{ vars.VMIMPORT_S3_BUCKET }}
jobs:
import-vm:
defaults:
run:
shell: bash --noprofile --norc -eo pipefail -x {0}
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
target:
- generic-amd64
include:
- target: generic-amd64
balena_app_name: belodetek/cloud-config-amd64 # balena_os/cloud-config-amd64
balena_app_commit: 1678f684c300862750ba9dbef1532b49
# renovate: datasource=github-releases depName=balena-os/balena-generic
balena_os_version: v6.5.53+rev4
development_mode: true
ec2_instance_type: m8g.medium
steps:
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
with:
persist-credentials: false
ref: ${{ github.event.pull_request.head.sha }}
# https://github.com/unfor19/install-aws-cli-action
- uses: unfor19/install-aws-cli-action@f5b46b7f32cf5e7ebd652656c5036bf83dd1e60c # v1
- uses: aws-actions/configure-aws-credentials@1b2b73eb6a459c3a91fde76ba4c255e5b4b8e94e
with:
aws-region: ${{ vars.AWS_REGION || 'us-east-1' }}
role-session-name: github-${{ github.job }}-${{ github.run_id }}-${{ github.run_attempt }}
# balena-io/environments-bases: aws/balenacloud/ephemeral-tests/balena-tests-iam.yml
role-to-assume: ${{ vars.AWS_IAM_ROLE }}
mask-aws-account-id: false
# https://github.com/balena-io-examples/setup-balena-action
- uses: balena-io-examples/setup-balena-action@d3ab8f4a2c2878a2ec4725ea0f7f28aec09b7cc9 # v0.0.31
env:
# renovate: datasource=github-releases depName=balena-io/balena-cli
BALENA_CLI_VERSION: v22.3.0
with:
cli-version: ${{ env.BALENA_CLI_VERSION }}
balena-token: ${{ secrets.BALENA_API_KEY }}
- name: download balena.img
run: |
balena os download ${{ matrix.target }} -o balena.img --version ${{ matrix.balena_os_version }}
- name: Set AWS/EC2/AMI image architecture string from balena device type
id: ami
run: |
if [[ '${{ matrix.target }}' =~ "amd64" ]]; then
echo 'arch=x86_64' >>"${GITHUB_OUTPUT}"
elif [[ '${{ matrix.target }}' =~ "aarch64" ]]; then
echo 'arch=arm64' >>"${GITHUB_OUTPUT}"
else
exit 1
fi
echo "name=balena-os-${{ matrix.balena_os_version }}-${{ matrix.target }}" | sed 's/+/-/g' >>"${GITHUB_ENV}"
- name: Pre-load cloud-config app to handle cloud provider metadata interfaces
run: |
balena preload --debug --pin-device-to-release balena.img \
--fleet ${{ matrix.balena_app_name }} \
--commit ${{ matrix.balena_app_commit }}
- name: Copy balenaOS image to AWS/S3 for import
id: s3
run: |
s3_key="balena-${RANDOM}.tmp"
s3_url="s3://${{ env.VMIMPORT_S3_BUCKET }}/preloaded-images/${s3_key}"
aws s3 cp --no-progress balena.img "${s3_url}"
echo "url=${s3_url}" >>"${GITHUB_OUTPUT}"
- name: Create AWS/EC2 EBS snapshot from balenaOS image
id: snapshot
run: |
import_task_id=$(aws ec2 import-snapshot \
--description 'snapshot-${{ steps.ami.outputs.name }}' \
--disk-container 'Description=balena-os,Format=RAW,UserBucket={S3Bucket=${{ env.VMIMPORT_S3_BUCKET }},S3Key=preloaded-images/${{ steps.s3.outputs.url }}}' \
| jq -r .ImportTaskId)
while true; do
response="$(aws ec2 describe-import-snapshot-tasks --import-task-ids "${import_task_id}")"
echo "${response}" | jq -re
status="$(echo "${response}" | jq -r ".ImportSnapshotTasks[].SnapshotTaskDetail.Status")"
if [[ "$status" =~ "completed" ]]; then
break
fi
if [[ "$status" =~ "deleting" ]]; then
status_message="$(aws ec2 describe-import-snapshot-tasks --import-task-ids "${import_task_id}" \
| jq -r ".ImportSnapshotTasks[].SnapshotTaskDetail.StatusMessage")"
echo "::error::${status_message}"
exit 1
fi
sleep $(( (RANDOM % 30) + 30))$
done
snapshot_id=$(aws ec2 describe-import-snapshot-tasks --import-task-ids "${import_task_id}" \
| jq -r '.ImportSnapshotTasks[].SnapshotTaskDetail.SnapshotId')
echo "id=${snapshot_id}" >>"${GITHUB_OUTPUT}"
- name: Create AWS/EC2 AMI image from EBS snapshot
id: image
env:
AMI_ARCHITECTURE: ${{ steps.ami.outputs.arch }}
AMI_BOOT_MODE: uefi
AMI_EBS_DELETE_ON_TERMINATION: true
AMI_EBS_VOLUME_SIZE: 8
AMI_EBS_VOLUME_TYPE: gp3
AMI_NAME: ${{ steps.ami.outputs.name }}
AMI_ROOT_DEVICE_NAME: /dev/sda1
AMI_SNAPSHOT_ID: ${{ steps.snapshot.outputs.id }}
run: |
existing_image_id=$(aws ec2 describe-images --output text \
--filters "Name=name,Values=${AMI_NAME}" \
--query 'Images[*].[ImageId]')
if [ -n "$existing_image_id" ]; then
exit 1
fi
# only supported on x86_64
if [ "${AMI_ARCHITECTURE}" = "x86_64" ]; then
tpm_opts="--tpm-support v2.0"
fi
block_device_mappings="DeviceName=${AMI_ROOT_DEVICE_NAME},Ebs={
DeleteOnTermination=${AMI_EBS_DELETE_ON_TERMINATION},
SnapshotId=${AMI_SNAPSHOT_ID},
VolumeSize=${AMI_EBS_VOLUME_SIZE},
VolumeType=${AMI_EBS_VOLUME_TYPE}
}"
# shellcheck disable=SC2086
image_id=$(aws ec2 register-image \
--name "${AMI_NAME}" \
--architecture "${AMI_ARCHITECTURE}" \
--virtualization-type hvm \
${tpm_opts} \
--ena-support \
--root-device-name "${AMI_ROOT_DEVICE_NAME}" \
--boot-mode "${AMI_BOOT_MODE}" \
--block-device-mappings "${block_device_mappings}" | jq -r .ImageId)
[ -z "${image_id}" ] && exit 1
aws ec2 create-tags --resources "${image_id}" --tags Key=Name,Value="${AMI_NAME}"
echo "id=${image_id}" >>"${GITHUB_OUTPUT}"
- name: Delete temporary image from AWS/S3
continue-on-error: true
if: always()
run: |
set -x
aws s3 rm '${{ steps.s3.outputs.url }}'
- name: Crete balenaCloud fleet to test the image
id: test-fleet
run: |
ami_test_fleet=$(openssl rand -hex 4)
>&2 balena fleet create "${ami_test_fleet}" \
--organization '${{ env.BALENA_TEST_ORG }}' \
--type '${{ matrix.target }}'
key_file="${HOME}/.ssh/id_ed25519"
mkdir -p "$(dirname "${_key_file}")"
ssh-keygen -t ed25519 -N "" -q -f "${key_file}"
# shellcheck disable=SC2046
>&2 eval $(ssh-agent)
>&2 ssh-add
balena ssh-key add "${ami_test_fleet}" "${key_file}.pub"
uuid="$(balena device register "${{ env.BALENA_TEST_ORG }}/${ami_test_fleet}" | awk '{print $4}')"
echo "uuid=${uuid}" >>"${GITHUB_OUTPUT}"
if [ '${{ matrix.development_mode }}' = true ]; then
_dev_mode="--dev";
else
_dev_mode="";
fi
config_json=$(mktemp)
echo "config_json=${config_json}" >>"${GITHUB_OUTPUT}"
>&2 balena config generate \
--network ethernet \
--version '${{ matrix.balena_os_version }}' \
--device "${uuid}" \
--appUpdatePollInterval 5 \
--output "${config_json}" \
${_dev_mode}
if [ ! -f "${config_json}" ]; then
exit 1
else
new_uuid=$(jq -r '.uuid' "${config_json}")
if [ "${new_uuid}" != "${uuid}" ]; then
exit 1
fi
fi
echo "name=${{ env.BALENA_TEST_ORG }}/${ami_test_fleet}" >>"${GITHUB_OUTPUT}"
- name: Test AWS/EC2 AMI image
id: test
env:
AMI_NAME: ${{ steps.ami.outputs.name }}
AWS_SECURITY_GROUP_ID: ${{ env.AWS_SECURITY_GROUP_ID }}
AWS_SUBNET_ID: ${{ env.AWS_SUBNET_ID }}
CONFIG_JSON: ${{ steps.test-fleet.outputs.config_json }}
UUID: ${{ steps.test-fleet.outputs.uuid }}
run: |
_ami_image_id=$(aws ec2 describe-images --output text \
--filters "Name=name,Values=${AMI_NAME}" \
--query 'Images[*].[ImageId]')
if [ -z "$_ami_image_id" ]; then
exit 1
fi
echo "ami_image_id=${_ami_image_id}" >>"${GITHUB_OUTPUT}"
_instance_id=$(aws ec2 run-instances --image-id "${_ami_image_id}" --count 1 \
--instance-type '${{ matrix.ec2_instance_type }}' \
--tag-specifications \
"ResourceType=instance,Tags=[{Key=Name,Value=test-${AMI_NAME}}]" \
"ResourceType=volume,Tags=[{Key=Name,Value=test-${AMI_NAME}}]" \
--subnet-id "${AWS_SUBNET_ID}" \
--security-group-ids "${AWS_SECURITY_GROUP_ID}" \
--user-data "file://${CONFIG_JSON}" | jq -r '.Instances[0].InstanceId')
if [ -z "${_instance_id}" ]; then
exit 1
fi
echo "instance_id=${_instance_id}" >>"${GITHUB_OUTPUT}"
aws ec2 wait instance-running --instance-ids "${_instance_id}"
aws ec2 wait instance-status-ok --instance-ids "${_instance_id}"
until echo 'balena ps -q -f name=balena_supervisor | xargs balena inspect \
| jq -r ".[] | select(.State.Health.Status!=null).Name + \":\" + .State.Health.Status"; exit' \
| balena device ssh "${UUID}" | grep -q ":healthy"; do
echo "::info::Waiting for balena-supervisor..."
sleep "$(( (RANDOM % 30) + 30 ))s";
done
- name: Terminate test instance
continue-on-error: true
if: always()
run: |
aws ec2 terminate-instances --instance-ids '${{ steps.test.outputs.instance_id }}'
- name: Clean up test fleet
continue-on-error: true
if: always()
env:
FLEET: ${{ steps.test-fleet.outputs.name }}
run: |
[ -z "$FLEET" ] && exit 0
_key_id=$(balena ssh-key list | grep "${FLEET#*/}" | awk '{print $1}')
balena ssh-key rm "${_key_id}" --yes || true
balena fleet rm "${FLEET}" --yes || true
- name: Make AMI public
if: ${{ steps.test.outcome == 'success' }}
env:
AMI_ARCHITECTURE: ${{ steps.ami.outputs.arch }}
AMI_IMAGE_ID: ${{ steps.test.outputs.ami_image_id }}
AMI_NAME: ${{ steps.ami.outputs.name }}
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ami-quotas.html
AWS_AMI_PUBLIC_QUOTA: 2
run: |
_ami_public_images_count=$(aws ec2 describe-images \
--owners "self" \
--filters "Name=name,Values=${AMI_NAME%%-*}" \
"Name=architecture,Values=${AMI_ARCHITECTURE}" \
"Name=is-public,Values=true" | jq '.Images | length')
if [ "$_ami_public_images_count" -ge "$AWS_AMI_PUBLIC_ARCH_QUOTA" ]; then
_ami_oldest_image_id=$(aws ec2 describe-images \
--owners "self" \
--filters "Name=name,Values=${AMI_NAME%%-*}" \
"Name=architecture,Values=${AMI_ARCHITECTURE}" \
"Name=is-public,Values=true" \
--query 'sort_by(Images, &CreationDate)[0].ImageId')
if [ -n "$_ami_oldest_image_id" ]; then
if [ "$(aws ec2 describe-images --image-ids "${_ami_oldest_image_id}" \
| jq -r '.Images[].Public')" = "true" ]; then
if aws ec2 modify-image-attribute \
--image-id "${_ami_oldest_image_id}" \
--launch-permission '{"Remove":[{"Group":"all"}]}'; then
if ! [ "$(aws ec2 describe-images --image-ids "${_ami_oldest_image_id}" \
| jq -r '.Images[].Public')" = "false" ]; then
exit 1
fi
fi
fi
fi
fi
_ami_snapshot_id=$(aws ec2 describe-images \
--region='${{ env.AWS_DEFAULT_REGION }}' \
--image-ids "${AMI_IMAGE_ID}" | jq -r '.Images[].BlockDeviceMappings[].Ebs.SnapshotId')
if [ -n "$_ami_snapshot_id" ]; then
if aws ec2 modify-snapshot-attribute --operation-type add \
--region '${{ env.AWS_DEFAULT_REGION }}' \
--snapshot-id "${_ami_snapshot_id}" \
--attribute createVolumePermission \
--group-names all; then
if ! [ "$(aws ec2 describe-snapshot-attribute \
--region '${{ env.AWS_DEFAULT_REGION }}' \
--snapshot-id "${_ami_snapshot_id}" \
--attribute createVolumePermission | jq -r '.CreateVolumePermissions[].Group')" == "all" ]; then
exit 1
fi
fi
else
exit 1
fi
if aws ec2 modify-image-attribute \
--image-id "${AMI_IMAGE_ID}" \
--launch-permission "Add=[{Group=all}]"; then
if ! [ "$(aws ec2 describe-images \
--image-ids "${AMI_IMAGE_ID}" \
| jq -r '.Images[].Public')" = "true" ]; then
exit 1
fi
fi
- name: Clean up EoL images
continue-on-error: true
if: always()
env:
AMI_NAME: ${{ steps.ami.outputs.name }}
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ami-quotas.html
PERIOD: "1 years ago"
run: |
_date=$(date +%Y-%m-%d -d "${PERIOD}")
image_ids=$(aws ec2 describe-images --output text \
--filters "Name=name,Values=${AMI_NAME%%-*}-*" \
--owners "self" \
--query 'Images[?CreationDate<`'"${_date}"'`].[ImageId]')
for image_id in ${image_ids}; do
_snapshots="$(aws ec2 describe-images --output text\
--image-ids "${image_id}" \
--query 'Images[*].BlockDeviceMappings[*].Ebs.SnapshotId')"
if aws ec2 deregister-image --image-id "${image_id}"; then
echo "De-registered AMI ${image_id}"
if [ -n "$_snapshots" ]; then
for snapshot in ${_snapshots}; do
if ! aws ec2 delete-snapshot --snapshot-id "${snapshot}"; then
echo "::warning::Could not remove snapshot ${snapshot}"
fi
done
fi
else
echo "::warning::Could not de-register image ${image_id}"
fi
done
- name: Clean up on failure
if: failure()
env:
AMI_NAME: ${{ steps.ami.outputs.name }}
run: |
image_id=$(aws ec2 describe-images --output text \
--filters "Name=name,Values=${AMI_NAME}" \
--query 'Images[*].[ImageId]')
snapshots="$(aws ec2 describe-images --output text \
--image-ids "${image_id}" \
--query 'Images[*].BlockDeviceMappings[*].Ebs.SnapshotId')"
if aws ec2 deregister-image --image-id "${image_id}"; then
if [ -n "$snapshots" ]; then
for snapshot in ${snapshots}; do
if ! aws ec2 delete-snapshot --snapshot-id "${snapshot}"; then
echo "::warning::Could not remove snapshot ${snapshot}"
fi
done
fi
else
echo "::warning::Could not de-register image ${image_id}"
fi