Add secrets adapter for AWS SSM Parameter Store#1791
Add secrets adapter for AWS SSM Parameter Store#1791davafons wants to merge 1 commit intobasecamp:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new Kamal secrets adapter that fetches encrypted secrets from AWS SSM Parameter Store via the AWS CLI, mirroring the existing AWS Secrets Manager adapter’s CLI-driven approach.
Changes:
- Introduce
aws_ssm_parameter_storesecrets adapter implementation that reads parameters viaaws ssm get-parameters --with-decryption. - Add test coverage for fetch behavior, missing-parameter handling, missing AWS CLI, and optional
--account/--profilebehavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
lib/kamal/secrets/adapters/aws_ssm_parameter_store.rb |
New adapter implementation using AWS CLI to fetch and decrypt SSM parameters. |
test/secrets/aws_ssm_parameter_store_adapter_test.rb |
New test suite validating adapter fetch and error scenarios. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| args = [ "aws", "ssm", "get-parameters", "--names" ] + names.map(&:shellescape) | ||
| args += [ "--with-decryption" ] | ||
| args += [ "--profile", account.shellescape ] if account | ||
| args += [ "--output", "json" ] | ||
| cmd = args.join(" ") | ||
|
|
||
| `#{cmd}`.tap do |response| | ||
| raise RuntimeError, "Could not read from AWS SSM Parameter Store" unless $?.success? | ||
|
|
||
| response = JSON.parse(response) | ||
|
|
||
| return response["Parameters"] unless response["InvalidParameters"].present? | ||
|
|
||
| raise RuntimeError, response["InvalidParameters"].map { |name| "#{name}: not found" }.join(" ") | ||
| end |
There was a problem hiding this comment.
AWS SSM get-parameters has a hard limit on how many parameter names can be requested per call (10 per request). As written, passing more than the limit will cause the AWS CLI call to fail even though the inputs are otherwise valid. Consider chunking names (e.g., in slices of 10), calling get-parameters per chunk, and concatenating the returned Parameters (while still surfacing any InvalidParameters).
| args = [ "aws", "ssm", "get-parameters", "--names" ] + names.map(&:shellescape) | |
| args += [ "--with-decryption" ] | |
| args += [ "--profile", account.shellescape ] if account | |
| args += [ "--output", "json" ] | |
| cmd = args.join(" ") | |
| `#{cmd}`.tap do |response| | |
| raise RuntimeError, "Could not read from AWS SSM Parameter Store" unless $?.success? | |
| response = JSON.parse(response) | |
| return response["Parameters"] unless response["InvalidParameters"].present? | |
| raise RuntimeError, response["InvalidParameters"].map { |name| "#{name}: not found" }.join(" ") | |
| end | |
| all_parameters = [] | |
| invalid_parameters = [] | |
| names.each_slice(10) do |chunk| | |
| args = [ "aws", "ssm", "get-parameters", "--names" ] + chunk.map(&:shellescape) | |
| args += [ "--with-decryption" ] | |
| args += [ "--profile", account.shellescape ] if account | |
| args += [ "--output", "json" ] | |
| cmd = args.join(" ") | |
| response = `#{cmd}` | |
| raise RuntimeError, "Could not read from AWS SSM Parameter Store" unless $?.success? | |
| parsed = JSON.parse(response) | |
| all_parameters.concat(parsed["Parameters"] || []) | |
| invalid_parameters.concat(parsed["InvalidParameters"] || []) | |
| end | |
| unless invalid_parameters.empty? | |
| raise RuntimeError, invalid_parameters.map { |name| "#{name}: not found" }.join(" ") | |
| end | |
| all_parameters |
There was a problem hiding this comment.
Implemented in a different way in the PR. The current Secrets Manager adapter also has a batch limit of 20 which is not enforced, so it would fail when passing >20 secret names.
SSM Parameter Store has a limit of 10 which I feel is easier to reach by mistake if not enforced, so that's why I decided it would be good to add the batch fetch even if it wouldn't match the Secrets Manager current behavior (we might want to also add batch fetch for Secrets Manager in another PR)
6302897 to
b234761
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| return response["Parameters"] unless response["InvalidParameters"].present? | ||
|
|
||
| raise RuntimeError, response["InvalidParameters"].map { |name| "#{name}: SSM Parameter Store can't find the specified secret." }.join(" ") |
There was a problem hiding this comment.
Sadly, get-parameters doesn't return a proper Error field, just an InvalidParameters: ["secret1", "secret2"]. So we can't only provide a generic error message here.
This PR adds AWS SSM Parameter Store as a secrets adapter. I was struggling to find a service that:
Normal password manager SaaS require a per-seat payment for team access, which I don't want to pay for a small project. And AWS Secrets Manager is expensive at $0.40 per stored secret.
On the other hand, Parameter Store has a generous 10k free tier and can be scoped with IAM roles and SSO login. I really think it's the perfect tool for small/medium sized projects that don't need automated rotation and all the bells and whistles that Secrets Manager provides.
For the implementation, I followed #1141 as a reference to keep the interface and functionality close to the existing Secrets Manger adapter.
Also created a docs PR here
I haven't discussed anything previous to open this PR. I really though that this adapter was different enough from other existing ones that it deserved a place here. Happy to open an Issue first if a proper discussion is needed before adding it.