From 2b4e5a7bb9339ef3cdfe94eb8c9b9610efade192 Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Wed, 20 Aug 2025 12:43:23 +0200 Subject: [PATCH 01/15] make unzip quiet --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f8287e0..8ab7bbc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,7 +52,7 @@ RUN apt-get update -qq \ RUN curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m).zip" -o awscli-exe-linux.zip \ && tmpdir=$(mktemp -d) \ - && unzip awscli-exe-linux.zip -d "${tmpdir}" \ + && unzip -q awscli-exe-linux.zip -d "${tmpdir}" \ && "${tmpdir}/aws/install" \ && rm -rf awscli-exe-linux.zip "${tmpdir}" From e17db3f813cd1aeaadd5859f20a3465aae8b7831 Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Tue, 19 Aug 2025 13:25:19 +0200 Subject: [PATCH 02/15] fix build and publish task upgrade actions and fix image sign task --- .github/workflows/build-publish.yaml | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-publish.yaml b/.github/workflows/build-publish.yaml index f93e70b..2707453 100644 --- a/.github/workflows/build-publish.yaml +++ b/.github/workflows/build-publish.yaml @@ -27,19 +27,21 @@ jobs: # https://github.com/sigstore/cosign-installer - name: Install cosign if: startsWith(github.ref, 'refs/tags/v') - uses: sigstore/cosign-installer@v3 + uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0 with: - cosign-release: 'v1.4.0' + cosign-release: 'v2.2.4' - # Workaround: https://github.com/docker/build-push-action/issues/461 - - name: Setup Docker buildx - uses: docker/setup-buildx-action@v3 + # Set up BuildKit Docker container builder to be able to build + # multi-platform images and export cache + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 # Login against a Docker registry except on PR # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: startsWith(github.ref, 'refs/tags/v') - uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -49,7 +51,7 @@ jobs: # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | @@ -63,7 +65,7 @@ jobs: # https://github.com/docker/build-push-action - name: Build and push Docker image id: build-and-push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 with: context: . build-args: | @@ -80,7 +82,9 @@ jobs: - name: Sign the published Docker image if: startsWith(github.ref, 'refs/tags/v') env: - COSIGN_EXPERIMENTAL: "true" + # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build-and-push.outputs.digest }} # This step uses the identity token to provision an ephemeral certificate # against the sigstore community Fulcio instance. - run: cosign sign ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }} + run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST} From 75c2901229f14ac0955e4b4a745d46d74580c85e Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Tue, 19 Aug 2025 13:28:25 +0200 Subject: [PATCH 03/15] add build and test workflow --- .github/workflows/build-test.yaml | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/build-test.yaml diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml new file mode 100644 index 0000000..e8cc507 --- /dev/null +++ b/.github/workflows/build-test.yaml @@ -0,0 +1,50 @@ +# +# NOTE: Add Action Secrets for the following variables: +# +# - AWS_ACCESS_KEY_ID +# - AWS_SECRET_ACCESS_KEY +# - AWS_REGION +# + +name: build-test + +on: + pull_request: + branches: [ main ] + +jobs: + workflow: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install Task + uses: arduino/setup-task@v2 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Build + run: task build + + - name: Test + run: task test + + - name: Test Example + run: task example + + - name: Cleanup + run: task clean From 606628ccf2846f7b8f8ac929fb76626e0852e5a5 Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Tue, 19 Aug 2025 13:35:24 +0200 Subject: [PATCH 04/15] show container log on failure --- Taskfile.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index cb717d3..27edf59 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -27,7 +27,7 @@ tasks: COMPOSE_FILE: "compose.metadock.yml:compose.test.yml" cmds: - echo "Running tests using '{{.AWS_PROFILE}}' AWS profile..." - - docker compose up --force-recreate --detach + - docker compose up --force-recreate --detach || docker compose logs && exit 1 - docker compose logs metadock - docker compose exec -it test /test.sh @@ -39,7 +39,7 @@ tasks: COMPOSE_FILE: "compose.example.yml" cmds: - echo "Running example using '{{.AWS_PROFILE}}' AWS profile..." - - docker compose up --force-recreate --detach + - docker compose up --force-recreate --detach || docker compose logs && exit 1 - docker compose logs metadock - docker compose exec -it example aws s3 ls From 7d1ecaeeaa01ce46efa1ee43fe81326951c76337 Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Tue, 19 Aug 2025 13:35:47 +0200 Subject: [PATCH 05/15] make build-publish workflow depend on build-test workflow --- .github/workflows/build-publish.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/build-publish.yaml b/.github/workflows/build-publish.yaml index 2707453..39801f5 100644 --- a/.github/workflows/build-publish.yaml +++ b/.github/workflows/build-publish.yaml @@ -6,6 +6,10 @@ on: tags: [ 'v*.*.*' ] pull_request: branches: [ main ] + workflow_run: + workflows: ["build-test"] + types: + - completed env: REGISTRY: ghcr.io @@ -19,6 +23,10 @@ jobs: packages: write id-token: write + + # only run on success of "build-test" workflow + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: - name: Checkout repository uses: actions/checkout@v2 From e0d164143d0119044ae4ded04af59ff2ff87e09b Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Tue, 19 Aug 2025 13:46:56 +0200 Subject: [PATCH 06/15] workaround `configure-aws-credentials` issue https://github.com/aws-actions/configure-aws-credentials/issues/112 --- .github/workflows/build-test.yaml | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index e8cc507..862eaaa 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -12,6 +12,9 @@ on: pull_request: branches: [ main ] +env: + AWS_PROFILE: "default" + jobs: workflow: runs-on: ubuntu-latest @@ -30,12 +33,19 @@ jobs: version: 3.x repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ secrets.AWS_REGION }} + # - name: Configure AWS credentials + # uses: aws-actions/configure-aws-credentials@v4 + # with: + # aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + # aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + # aws-region: ${{ secrets.AWS_REGION }} + + # https://github.com/aws-actions/configure-aws-credentials/issues/112 + - name: Configure AWS credentials (HACK) + run: | + aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }} + aws configure set aws_secret_access_key ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws configure set region ${{ secrets.AWS_REGION }} - name: Build run: task build From 49ae57408fe43d7565419e163fff34a67e244a19 Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Tue, 19 Aug 2025 17:00:41 +0200 Subject: [PATCH 07/15] remove extra log output --- main.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/main.go b/main.go index b67524b..ea523fb 100644 --- a/main.go +++ b/main.go @@ -76,12 +76,6 @@ func (lrw *loggingResponseWriter) WriteHeader(code int) { func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - - log.Printf("%s %s\n", - r.Method, - r.URL.Path, - ) - start := time.Now() lrw := &loggingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK} From 17a7e719513c0d91b43a11917fb79528230b041f Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Tue, 19 Aug 2025 17:01:04 +0200 Subject: [PATCH 08/15] fix how start failure is handled --- Taskfile.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 27edf59..5486fad 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -27,7 +27,7 @@ tasks: COMPOSE_FILE: "compose.metadock.yml:compose.test.yml" cmds: - echo "Running tests using '{{.AWS_PROFILE}}' AWS profile..." - - docker compose up --force-recreate --detach || docker compose logs && exit 1 + - docker compose up --force-recreate --detach || { docker compose logs; exit 1; } - docker compose logs metadock - docker compose exec -it test /test.sh @@ -39,7 +39,7 @@ tasks: COMPOSE_FILE: "compose.example.yml" cmds: - echo "Running example using '{{.AWS_PROFILE}}' AWS profile..." - - docker compose up --force-recreate --detach || docker compose logs && exit 1 + - docker compose up --force-recreate --detach || { docker compose logs; exit 1; } - docker compose logs metadock - docker compose exec -it example aws s3 ls From a8582affca0675a5a9cb32d2cdafd6a059b4ce9c Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Wed, 20 Aug 2025 11:45:53 +0200 Subject: [PATCH 09/15] retrieve session credential at startup retrieving the credential on-demand, in the `roleCredentialsHandler`, takes too long and AWS SDK/CLI requests just timeout --- main.go | 129 +++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 85 insertions(+), 44 deletions(-) diff --git a/main.go b/main.go index ea523fb..7fcb3f5 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/gorilla/mux" ) @@ -25,7 +26,8 @@ const Version = "0.0.0" type IMDSv2Service struct { Profile string Configuration aws.Config - FakeToken string + ServiceToken string + Credential aws.Credentials } const DefaultPort = "8080" @@ -64,6 +66,49 @@ func generateToken(length int) (string, error) { return base64.URLEncoding.EncodeToString(bytes), nil } +func loadCredential(cfg aws.Config) (aws.Credentials, error) { + credential, err := cfg.Credentials.Retrieve(context.TODO()) + if err != nil { + return aws.Credentials{}, err + } + return credential, nil +} + +func createTemporaryCredential(cfg aws.Config) (aws.Credentials, error) { + stsClient := sts.NewFromConfig(cfg) + resp, err := stsClient.GetSessionToken(context.TODO(), &sts.GetSessionTokenInput{}) + if err != nil { + return aws.Credentials{}, err + } + cred := resp.Credentials + + return aws.Credentials{ + Source: "createTemporaryCredentials", + AccessKeyID: *cred.AccessKeyId, + SecretAccessKey: *cred.SecretAccessKey, + SessionToken: *cred.SessionToken, + Expires: *cred.Expiration, + CanExpire: true, + }, nil +} + +func loadOrCreateCredential(cfg aws.Config) (aws.Credentials, error) { + credential, err := loadCredential(cfg) + if err != nil { + return aws.Credentials{}, err + } + + // issue temporary credential + if !credential.CanExpire { + credential, err = createTemporaryCredential(cfg) + if err != nil { + return aws.Credentials{}, err + } + } + + return credential, nil +} + type loggingResponseWriter struct { http.ResponseWriter statusCode int @@ -92,16 +137,19 @@ func loggingMiddleware(next http.Handler) http.Handler { ) }) } + +// service entrypoint + func main() { - versionFlag := flag.Bool("version", false, "Print the version and exit") + versionFlag := flag.Bool("version", false, "Print the version and exit.") flag.Usage = func() { _, _ = fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [aws-profile]\n\n", os.Args[0]) - fmt.Println("If [aws-profile] is provided, or the AWS_PROFILE environment variable is set, the app runs with that profile.") + fmt.Println("If \"aws-profile\" is provided, or the AWS_PROFILE environment variable is set, the app runs with that profile.") } flag.Parse() if *versionFlag { - fmt.Println("Version:", Version) + fmt.Printf("Version: %s\n", Version) return } @@ -121,23 +169,30 @@ func main() { if err != nil { log.Fatalf("Unable to load AWS configuration: %v\n", err) } + log.Printf("Configured for %q AWS profile.\n", profile) - fakeToken, err := generateToken(32) + serviceToken, err := generateToken(32) if err != nil { log.Fatalf("Unable to generate token: %v\n", err) } + // retrieve the credential at startup, as it takes too long if retrieved within + // the roleCredentialsHandler function, since the callee has a very short TTFB timeout + log.Printf("Retrieving credentials...\n") + credential, err := loadOrCreateCredential(configuration) + if err != nil { + log.Fatalf("Unable to load or create temporary credential: %v\n", err) + } + log.Printf("Retrieved credentials, expires at %q.\n", credential.Expires) + + // setup service, middleware and router service := &IMDSv2Service{ Profile: profile, Configuration: configuration, - FakeToken: fakeToken, + ServiceToken: serviceToken, + Credential: credential, } - // FIXME: cache credentials - _, _ = service.loadCredentials() - - log.Printf("Configured for %q AWS profile.\n", service.Profile) - router := mux.NewRouter() router.Use(loggingMiddleware) @@ -147,13 +202,8 @@ func main() { router.HandleFunc("/latest/meta-data/iam/security-credentials/{role}", service.roleCredentialsHandler).Methods("GET") router.HandleFunc("/health/", service.healthHandler).Methods("GET") + // setup HTTP server port := getEnvOrDefault("PORT", DefaultPort) - log.Printf("Listening on all interfaces on port %s.\n", port) - - // context for background processing - // which traps SIGINT/SIGTERM - ctx := contextWithSignal(context.Background()) - server := &http.Server{ Addr: ":" + port, Handler: router, @@ -161,18 +211,22 @@ func main() { WriteTimeout: 5 * time.Second, } + // context for background processing + ctx := contextWithSignal(context.Background()) + // run the server in a goroutine so it doesn’t block go func() { + log.Printf("Listening on port %s for all network interfaces.\n", port) if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("Server error: %v\n", err) } }() - // wait for signal (ctx.done) + // wait for SIGINT/SIGTERM <-ctx.Done() log.Println("Shutting down server...") - // create a context with timeout for shutdown + // context with timeout for shutdown shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -183,14 +237,7 @@ func main() { } } -func (svc *IMDSv2Service) loadCredentials() (aws.Credentials, error) { - credentials, err := svc.Configuration.Credentials.Retrieve(context.TODO()) - if err != nil { - log.Printf("Failed to retrieve credentials: %v\n", err) - return aws.Credentials{}, err - } - return credentials, nil -} +// IDMS implementation // url: / (root) func (svc *IMDSv2Service) rootHandler(w http.ResponseWriter, r *http.Request) { @@ -203,13 +250,13 @@ func (svc *IMDSv2Service) rootHandler(w http.ResponseWriter, r *http.Request) { func (svc *IMDSv2Service) tokenHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) - _, _ = fmt.Fprintf(w, svc.FakeToken) + _, _ = fmt.Fprintf(w, svc.ServiceToken) } // url: /latest/meta-data/iam/security-credentials/ func (svc *IMDSv2Service) credentialsHandler(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("X-aws-ec2-metadata-token") - if token != svc.FakeToken { + if token != svc.ServiceToken { log.Printf("Token mismatched error.\n") http.Error(w, "", http.StatusForbidden) return @@ -217,40 +264,34 @@ func (svc *IMDSv2Service) credentialsHandler(w http.ResponseWriter, r *http.Requ w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) - _, _ = fmt.Fprintf(w, "proxy-role") + _, _ = fmt.Fprintf(w, "metadock") } // url: /latest/meta-data/iam/security-credentials/{role} func (svc *IMDSv2Service) roleCredentialsHandler(w http.ResponseWriter, r *http.Request) { - //vars := mux.Vars(r) - //role := vars["role"] + // NB: this handler has to be very fast! token := r.Header.Get("X-aws-ec2-metadata-token") - if token != svc.FakeToken { + if token != svc.ServiceToken { log.Printf("Token mismatched error.\n") http.Error(w, "", http.StatusForbidden) return } - // FIXME: use cached credentials - credentials, err := svc.loadCredentials() - if err != nil { - http.Error(w, "", http.StatusInternalServerError) - return - } - w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) + credential := svc.Credential response := map[string]string{ "Code": "Success", "LastUpdated": time.Now().Format(time.RFC3339), "Type": "AWS-HMAC", - "AccessKeyId": credentials.AccessKeyID, - "SecretAccessKey": credentials.SecretAccessKey, - "Token": credentials.SessionToken, - "Expiration": credentials.Expires.Format(time.RFC3339), + "AccessKeyId": credential.AccessKeyID, + "SecretAccessKey": credential.SecretAccessKey, + "Token": credential.SessionToken, + "Expiration": credential.Expires.Format(time.RFC3339), } + if err := json.NewEncoder(w).Encode(response); err != nil { log.Printf("roleCredentialsHandler encode error: %v\n", err) http.Error(w, "", http.StatusInternalServerError) From 2175f19091cf8cfb7aa5c3eb3a5584706861769c Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Wed, 20 Aug 2025 11:46:12 +0200 Subject: [PATCH 10/15] improve README --- README.md | 132 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 84 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index ecdf03c..bad8d83 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,15 @@ ## Overview -`MetaDock` provides a lightweight Go implementation of the [AWS Instance Metadata Service (IMDSv2)][imds]. +`MetaDock` provides a lightweight Go implementation of a subset of the [AWS Instance Metadata Service (IMDSv2)][imds] +for the purpose of providing session credentials only. It is designed to run inside a Docker container and can be included in a `docker-compose` setup to provide AWS credentials to other services, effectively emulating an EC2 environment locally. Instead of retrieving credentials from AWS, the emulator loads them from the host, via the mounted -`${HOME}/.aws` directory, using the `aws configure export-credentials` command. It then exposing -them through the same API paths that would normally be available inside an EC2 instance: +`${HOME}/.aws` directory, and then exposes them through the same API paths that would normally be +available inside an EC2 instance: * `/latest/api/token` * `/latest/meta-data/iam/security-credentials` @@ -21,88 +22,120 @@ them through the same API paths that would normally be available inside an EC2 i `MetaDock` responds with the same metadata format as a real EC2 instance, enabling AWS SDKs and CLI commands inside containers to authenticate transparently. -It relies on the developer obtaining AWS credentials on the host machine *before* running the -`metadock` service, and attaching the services which require the service to the emulators docker -network. +It relies on the developer obtaining AWS credentials on the host machine *before* running the `metadock` +service. If long-lived (static) credentials are found, the service will generate session credentials, +with a default expiry of 12 hours. ## Usage ### Prerequisites -* Host machine with: +* AWS CLI v2 installed and configured (`aws configure`, `aws sso login`, or equivalent) +* AWS configuration profile with valid credentials. +* Docker and `docker-compose` +* [Task][task] for running development tasks - - Linux - - [Task][task] - - Docker and `docker-compose` - - AWS CLI v2 installed and configured (`aws configure`, `aws sso login`, or equivalent) +### Quickstart -* An existing AWS profile with valid credentials. +#### Ensure you are logged into AWS and have valid credentials -### Quickstart +```bash +aws configure +``` -1. Ensure you are logged into AWS: +Or - ```bash - aws configure - ``` +```bash +aws sso login [--profile profile-name] +```` - Or +#### Include the `metadock` service in your Docker Compose configuration - ```bash - aws sso login [--profile profile-name] - ```` +The `MetaDock` service can be used in one of the following ways. -2. Include `compose.metadock.yml` in your `docker-compose.yml`. +1. Using the provided [`compose.metadock.yml`](compose.metadock.yml) file. - - Use `include` directive to include the supplied [`compose.metadock.yml`](compose.metadock.yml) file. - - Add the `metadock` network to the services which need AWS credentials. + - Use an [`include` directive][include] to include the [`compose.metadock.yml`](compose.metadock.yml) file. + - Add the `metadock` network to the services which need IMDS. See [`compose.example.yml`](compose.example.yml) for an example configuration. -3. Your services can now query the emulator. +2. Adding the `metadock` service and configure the `AWS_EC2_METADATA_SERVICE_ENDPOINT` environment variable. + + Edit your compose file, add the `metadock` service and configure services which need IMDS. - Optionally, from within the service (using `docker exec ...`): + ```yaml + services: + + metadock: + image: "ghcr.io/virtualstaticvoid/metadock:latest" + command: "${AWS_PROFLE:-default}" + volumes: + - "${HOME}/.aws:/root/.aws:ro" - * Use `curl` to check if the `MetaDock` service is accessible + your_service: + image: "..." + env: + AWS_EC2_METADATA_SERVICE_ENDPOINT: http://metadock/ + + ``` - ```bash - curl http://metadock/ +#### Your services can now query the emulator - # => MetaDock - ``` +Optionally, from within the respective services (using the `docker exec` command). - * Or, check by running `aws` CLI commands +* If `curl` is installed in the container, check if the `MetaDock` service is issuing credentials. + + ```bash + TOKEN=$(curl -X PUT "http://metadock/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + curl -H "X-aws-ec2-metadata-token: $TOKEN" http://metadock/latest/meta-data/iam/security-credentials/metadock + # => {"AccessKeyId":"...", ...} + ``` + + See [Use the Instance Metadata Service to access instance metadata][using-imds] documentation for details. - ```bash - aws s3 ls - ``` +* If the [`aws` CLI tool][aws-cli] is installed in the container, check by running the `sts get-caller-identity` CLI command. + + ```bash + aws sts get-caller-identity --no-cli-pager + # => {"UserId": "...", ...} + ``` ## Building and Testing This project uses [Task][task] to manage common development workflows. -* Build the service +### Build the service - ```bash - task build - ``` +```bash +task build +``` -* Run tests +#### Run tests - ```bash - task test - ``` +```bash +task test +``` -* Cleanup artifacts +#### Cleanup artifacts - ```bash - task clean - ``` +```bash +task clean +``` -Alternatively, you can build manually: +Alternatively, you can run it directly on the host. ```bash go build -o metadock . +PORT=8080 ./metadock +``` + +And connect, via `localhost` with the configured `PORT`. + +```bash +TOKEN=$(curl -X PUT "http://localhost:8080/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") +curl -H "X-aws-ec2-metadata-token: $TOKEN" http://localhost:8080/latest/meta-data/iam/security-credentials/metadock +# => {"AccessKeyId":"...", ...} ``` ## License @@ -111,5 +144,8 @@ MIT License. Copyright (c) 2025 Chris Stefano. See [LICENSE](LICENSE) for detail +[aws-cli]: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html [imds]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html +[include]: https://docs.docker.com/reference/compose-file/include/ [task]: https://taskfile.dev/ +[using-imds]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html From b7b17e51228083bb80fe6635e35d731605a68089 Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Wed, 20 Aug 2025 11:56:56 +0200 Subject: [PATCH 11/15] simplify HEALTHCHECK --- Dockerfile | 3 --- 1 file changed, 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8ab7bbc..32b8bd1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,9 +29,6 @@ COPY README.md LICENSE /opt/metadock/ ENV PORT=80 HEALTHCHECK --interval=5s \ - --timeout=10s \ - --start-period=1s \ - --retries=5 \ CMD curl -sSfL http://127.0.0.1:${PORT}/health/ > /dev/null ENTRYPOINT ["/sbin/tini", "--", "/opt/metadock/bin/metadock"] From 8b1d997a4ec091162bd9e13d32582b21fea7ac6b Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Wed, 20 Aug 2025 13:26:15 +0200 Subject: [PATCH 12/15] remove unneeded tests ~ reduce permissions needed for testing with GitHub Actions --- test.sh | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test.sh b/test.sh index c47d846..c00405d 100755 --- a/test.sh +++ b/test.sh @@ -21,11 +21,3 @@ heading "Running tests..." topic "aws sts get-caller-identity" aws sts get-caller-identity --no-cli-pager | indent - -topic "aws s3 ls" -aws s3 ls | indent - -topic "aws ec2 describe-instances" -aws ec2 describe-instances --region us-east-1 | indent - -# TODO: add others...? From 71fd0ff56cda659b8e9064be1a5c6fb60731609f Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Wed, 20 Aug 2025 13:27:06 +0200 Subject: [PATCH 13/15] update notices --- compose.example.yml | 5 +++++ compose.metadock.yml | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/compose.example.yml b/compose.example.yml index 076f9a3..edc8dd1 100644 --- a/compose.example.yml +++ b/compose.example.yml @@ -1,3 +1,8 @@ +# +# MetaDock +# https://github.com/virtualstaticvoid/metadock +# MIT License. Copyright (c) 2025 Chris Stefano +# --- include: - compose.metadock.yml diff --git a/compose.metadock.yml b/compose.metadock.yml index f068a8c..d8382c0 100644 --- a/compose.metadock.yml +++ b/compose.metadock.yml @@ -1,5 +1,6 @@ # -# From https://github.com/virtualstaticvoid/metadock +# MetaDock +# https://github.com/virtualstaticvoid/metadock # MIT License. Copyright (c) 2025 Chris Stefano # --- From 22b5f5d5b210badd20601744d71a23777dce1545 Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Wed, 20 Aug 2025 13:27:43 +0200 Subject: [PATCH 14/15] refactor compose configurations --- Taskfile.yml | 11 ++++++----- compose.metadock.yml | 1 - compose.test.yml | 8 ++++++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 5486fad..f7eb11a 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -23,13 +23,14 @@ tasks: desc: "Run tests." vars: AWS_PROFILE: '{{.AWS_PROFILE | default "default"}}' + HIDE_OUTPUT: '{{.CLI_ARGS | default ""}}' env: - COMPOSE_FILE: "compose.metadock.yml:compose.test.yml" + COMPOSE_FILE: "compose.test.yml" cmds: - echo "Running tests using '{{.AWS_PROFILE}}' AWS profile..." - - docker compose up --force-recreate --detach || { docker compose logs; exit 1; } + - docker compose up --force-recreate --detach --wait || { docker compose logs; exit 1; } - docker compose logs metadock - - docker compose exec -it test /test.sh + - docker compose exec -it test /test.sh {{if .HIDE_OUTPUT}}> /dev/null{{end}} example: desc: "Run example." @@ -39,9 +40,9 @@ tasks: COMPOSE_FILE: "compose.example.yml" cmds: - echo "Running example using '{{.AWS_PROFILE}}' AWS profile..." - - docker compose up --force-recreate --detach || { docker compose logs; exit 1; } + - docker compose up --force-recreate --detach --wait || { docker compose logs; exit 1; } - docker compose logs metadock - - docker compose exec -it example aws s3 ls + - docker compose exec -it example aws sts get-caller-identity --no-cli-pager clean: desc: "Stop services and clean up." diff --git a/compose.metadock.yml b/compose.metadock.yml index d8382c0..16c8e74 100644 --- a/compose.metadock.yml +++ b/compose.metadock.yml @@ -20,4 +20,3 @@ networks: driver: default config: - subnet: 169.254.169.0/24 - gateway: 169.254.169.1 diff --git a/compose.test.yml b/compose.test.yml index 791ea98..08fb41e 100644 --- a/compose.test.yml +++ b/compose.test.yml @@ -1,13 +1,17 @@ --- services: + metadock: + image: "ghcr.io/virtualstaticvoid/metadock:latest" + command: "${AWS_PROFILE:-default}" + volumes: + - "${HOME}/.aws:/root/.aws" + test: image: "ghcr.io/virtualstaticvoid/metadock-test:latest" init: true build: context: . target: test - networks: - - metadock environment: AWS_EC2_METADATA_SERVICE_ENDPOINT: "http://metadock/" depends_on: From fe4ecd928131a450f96a15846b30d6f6bd8f8a6b Mon Sep 17 00:00:00 2001 From: Chris Stefano Date: Wed, 20 Aug 2025 13:28:17 +0200 Subject: [PATCH 15/15] update GitHub Actions configuration --- .github/workflows/build-test.yaml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 862eaaa..c68110e 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -13,6 +13,7 @@ on: branches: [ main ] env: + HOME: ${{ github.workspace }} AWS_PROFILE: "default" jobs: @@ -33,6 +34,7 @@ jobs: version: 3.x repo-token: ${{ secrets.GITHUB_TOKEN }} + # https://github.com/aws-actions/configure-aws-credentials/issues/112 # - name: Configure AWS credentials # uses: aws-actions/configure-aws-credentials@v4 # with: @@ -40,21 +42,22 @@ jobs: # aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} # aws-region: ${{ secrets.AWS_REGION }} - # https://github.com/aws-actions/configure-aws-credentials/issues/112 - name: Configure AWS credentials (HACK) run: | - aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }} - aws configure set aws_secret_access_key ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws configure set region ${{ secrets.AWS_REGION }} + aws configure set profile.${AWS_PROFILE}.aws_access_key_id "${{ secrets.AWS_ACCESS_KEY_ID }}" + aws configure set profile.${AWS_PROFILE}.aws_secret_access_key "${{ secrets.AWS_SECRET_ACCESS_KEY }}" + aws configure set profile.${AWS_PROFILE}.region "${{ secrets.AWS_REGION }}" - name: Build run: task build - name: Test - run: task test + run: task test -- true - - name: Test Example - run: task example + # example can't run on GitHub Actions due to compose network configuration + # since access to the internet isn't possible from within the container network + # - name: Test Example + # run: task example - name: Cleanup run: task clean