homerss services talks gpg

Aws cli v2 installation garbage

2022-07-21

Broke-ass way of shipping software

I feel like the aws-cli team is pretty ignorant of standard conventions when it comes to shipping software to end users. As usual, I have opinions.

installer fails but not really

 ---> Running in 66a1029f66b9
/aws/dist/aws: error while loading shared libraries: libdl.so.2: cannot open shared object file: No such file or directory
You can now run: /usr/local/bin/aws --version

Super cool, error but exiting zero. We can see right in the installer script that they don’t check return codes on subshells.

set_global_vars() {
  ROOT_INSTALL_DIR=${PARSED_INSTALL_DIR:-/usr/local/aws-cli}
  BIN_DIR=${PARSED_BIN_DIR:-/usr/local/bin}
  UPGRADE=${PARSED_UPGRADE:-no}

  EXE_NAME="aws"
  COMPLETER_EXE_NAME="aws_completer"
  INSTALLER_DIR="$( cd "$( dirname "$0" )" >/dev/null 2>&1 && pwd )" 
  INSTALLER_DIST_DIR="$INSTALLER_DIR/dist"
  INSTALLER_EXE="$INSTALLER_DIST_DIR/$EXE_NAME"
  AWS_EXE_VERSION=$($INSTALLER_EXE --version | cut -d ' ' -f 1 | cut -d '/' -f 2)
[snip]

shipping a dynamically linked binary wrapper

It’s a damned bit of python code so you get cross-platform for free with the notable exceptions of any c-extensions that have non-portable implementations. Despite that they ship with a wrapper that is dynamically linked.

~  file /aws/dist/aws
/aws/dist/aws: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=1
da3a1d77c7109ce6444919f4a15e7e6c63d02fa, stripped

This means that the machine you run it on has to have the same shared libraries on as the machine that built it. Requiring shared libraries in-and-of itself is no big deal, this is how operating systems have worked for years and years. The main issue with this is that they expect glibc to be available. This completely ignores one of the most popular container platforms alpine, which uses muslc. If you have an upstream image that uses alpine and you need to add the aws-cli to it you’re out of luck. You have to either rebuild the upstream image with a glibc environment as the source, or futz about and get the awscli working in alpine.

Why they couldn’t just statically link this wrapper script is beyond me. Many languages compile statically linked binaries by default1. Better yet, if they had provided a source distribution of their code we could plumb in a patch at build time to make the cli work with musl.2

Making this go

There are numerous threads and pages on this topic but variants of this SO answer seem to be the most popular. I opted for the self-hosted, poor-mans way of building this.

Remember that error above? error while loading shared libraries: libdl.so.2: cannot open shared object file: No such file or directory

~  mkdir kludge
# docker build (Dockerfile below)
# a wild error appears
~  cp /usr/lib64/libdl.so.2 kludge/
~  docker build -t test .

Wash, rinse, repeat for each shared library.

It wound up looking like this

The libraries. Not to bad, took all of 2 minutes.

~  ls kludge
ld-linux-x86-64.so.2  libc.so.6  libdl.so.2  libm.so.6  libpthread.so.0  librt.so.1  libutil.so.1  libz.so.1

And we’ll need a wrapper script so we can set LD_LIBRARY_PATH. To inform the linker of the shared library location when the binary is evoked.

~  cat bin/aws
#!/usr/bin/env bash

main(){
  export LD_LIBRARY_PATH=/lib64
  awscli "$@"
  exit $?
}

main "$@"

And we muck with the LD_LIBRARY_PATH during the install as well, taking care to unset it when done so our main application doesn’t run into a bunch of symbol-related issues itself.

~  cat Dockerfile
from alpine
run apk add curl bash
run mkdir /lib64      # musl doesn't use /lib64 by default
copy kludge/ /lib64/

run curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
		unzip awscliv2.zip && \
		rm awscliv2.zip

env LD_LIBRARY_PATH=/lib64
run /aws/install && mv /usr/local/bin/aws /usr/local/bin/awscli
copy bin/aws /usr/local/bin/aws
env LD_LIBRARY_PATH=

and there you have it

One self-hosted kludgy solution. Garbage in, garbage out.

~  docker build -t test .
[snip]
~  docker run -it test aws --version
aws-cli/2.7.17 Python/3.9.11 Linux/5.18.9-arch1-1 exe/x86_64.alpine.3 prompt/off

  1. Which is wasteful in terms of memory and disk, but portable. ↩︎

  2. This is fairly common for alpine builds ↩︎