Repository navigation
[SVLS-9300] add ecs-fargate instrument command - #2463
ojproductions merged 7 commits into
Conversation
|
✅ All CI checks and tests passed. 🎉 All green!🧪 All tests passed 🔗 Commit SHA: 56e9fc9 | Docs | View more details | Give us feedback! |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ecdd3181c0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (settings.apiKeySecretArn && !taskDefinition.executionRoleArn) { | ||
| warnings.push( | ||
| `Task definition ${family} has no executionRoleArn, which ECS needs to read ${settings.apiKeySecretArn}. Tasks started from this revision will fail until the task definition has an execution role granting secretsmanager:GetSecretValue on that secret.` | ||
| ) |
There was a problem hiding this comment.
Stop deployment when the secret has no execution role
When --api-key-secret-arn is used on a task definition without executionRoleArn, this only adds a warning even though the code itself establishes that tasks from the resulting revision cannot start. If --ecs-service was also supplied, the command proceeds to register and deploy that unusable revision, potentially replacing healthy tasks with tasks that fail at startup; this condition must block registration or at least the service update.
Useful? React with 👍 / 👎.
| const service = await describeService(client, cluster, name) | ||
| const family = taskDefinitionFamily(service.taskDefinition) | ||
| const target = instrumented.find((candidate) => candidate.family === family) |
There was a problem hiding this comment.
Reject duplicate task-definition families before deployment
When multiple --task-definition values resolve to different revisions of the same family, find() silently selects whichever result was listed first. For example, if an already-instrumented older revision is listed before a newer revision that the service currently runs, deployment points the service back to the older ARN instead of the newly registered revision; reject duplicate families or otherwise disambiguate the revision rather than selecting the first match.
Useful? React with 👍 / 👎.
OliviaShoup
left a comment
There was a problem hiding this comment.
this looks great! left a few comments but nothing blocking
|
|
||
| #### `ecs-fargate` | ||
|
|
||
| <sub>**README:** [📚](/packages/plugin-ecs-fargate) | **Documentation:** [🔗](https://docs.datadoghq.com/integrations/ecs_fargate/) | **Plugin:** `@datadog/datadog-ci-plugin-ecs-fargate`</sub> |
There was a problem hiding this comment.
this link 301-redirects to /integrations/aws-fargate/ (confirmed via curl). it still works, but it's not the canonical URL
| <sub>**README:** [📚](/packages/plugin-ecs-fargate) | **Documentation:** [🔗](https://docs.datadoghq.com/integrations/ecs_fargate/) | **Plugin:** `@datadog/datadog-ci-plugin-ecs-fargate`</sub> | |
| <sub>**README:** [📚](/packages/plugin-ecs-fargate) | **Documentation:** [🔗](https://docs.datadoghq.com/integrations/aws-fargate/) | **Plugin:** `@datadog/datadog-ci-plugin-ecs-fargate`</sub> |
| <!-- BEGIN_USAGE:instrument --> | ||
| | Argument | Shorthand | Description | Default | | ||
| | -------- | --------- | ----------- | ------- | | ||
| | `--dry` or `--dry-run` | `-d` | Preview changes running command would apply | `false` | |
There was a problem hiding this comment.
| | `--dry` or `--dry-run` | `-d` | Preview changes running command would apply | `false` | | |
| | `--dry` or `--dry-run` | `-d` | Preview the changes the command would apply | `false` | |
|
|
||
| #### Deploying the new revision | ||
|
|
||
| Pass `--ecs-service` for each service that should run the revision the command just registered, and `--cluster` if those services are not in the `default` cluster. A service named by its full ARN already says which cluster it runs in, so `--cluster` can be left off; passing one that the ARN contradicts is an error rather than a silent choice between them. A run updates services in a single cluster, so ARNs naming more than one are reported too. Each service is matched to the task definition family it currently runs, so a run over several task definitions points each service at its own new revision, and a service already running the instrumented revision is left alone rather than redeployed. Updating a service starts an ECS deployment: the command returns as soon as ECS accepts it, and the rollout follows your service's deployment configuration. |
There was a problem hiding this comment.
"a silent choice between them" is a little hard to parse on first read. the antecedent for "them" (ARN's cluster vs. flag's cluster) isn't obvious. so suggesting this but no pressure
| Pass `--ecs-service` for each service that should run the revision the command just registered, and `--cluster` if those services are not in the `default` cluster. A service named by its full ARN already says which cluster it runs in, so `--cluster` can be left off; passing one that the ARN contradicts is an error rather than a silent choice between them. A run updates services in a single cluster, so ARNs naming more than one are reported too. Each service is matched to the task definition family it currently runs, so a run over several task definitions points each service at its own new revision, and a service already running the instrumented revision is left alone rather than redeployed. Updating a service starts an ECS deployment: the command returns as soon as ECS accepts it, and the rollout follows your service's deployment configuration. | |
| Pass `--ecs-service` for each service that should run the revision the command just registered, and `--cluster` if those services are not in the `default` cluster. A service named by its full ARN already says which cluster it runs in, so `--cluster` can be left off; passing a --cluster that contradicts the ARN's cluster is an error, not a silent override. A run updates services in a single cluster, so ARNs naming more than one are reported too. Each service is matched to the task definition family it currently runs, so a run over several task definitions points each service at its own new revision, and a service already running the instrumented revision is left alone rather than redeployed. Updating a service starts an ECS deployment: the command returns as soon as ECS accepts it, and the rollout follows your service's deployment configuration. |
ava-silver
left a comment
There was a problem hiding this comment.
Automated AI Review (human curated)
The command needs stronger safeguards around deployable task definitions, MFA-backed AWS profiles, and duplicate task-definition families.
|
|
||
| // ECS resolves secrets through the task's execution role, so a reference without a role in place | ||
| // registers a revision whose tasks cannot start. | ||
| if (settings.apiKeySecretArn && !taskDefinition.executionRoleArn) { |
There was a problem hiding this comment.
P1: Reject this configuration instead of warning. ECS resolves container secrets through the task execution role, so this revision cannot start without executionRoleArn; with --ecs-service, the command proceeds to deploy that unusable revision.
| /** | ||
| * Returns the credentials loaded from the given AWS named profile. | ||
| */ | ||
| export const getAWSProfileCredentials = async (profile: string): Promise<AwsCredentialIdentity | undefined> => { |
There was a problem hiding this comment.
P1: Support MFA-backed named profiles here. The analogous Lambda credential helper supplies mfaCodeProvider, but this fromIni setup does not, so --profile fails for profiles configured with mfa_serial. Please share or reproduce the Lambda credential setup, including its MFA callback.
| try { | ||
| const service = await describeService(client, cluster, name) | ||
| const family = taskDefinitionFamily(service.taskDefinition) | ||
| const target = instrumented.find((candidate) => candidate.family === family) |
There was a problem hiding this comment.
P2: Do not select the first result when multiple targets resolve to the same family. For example, --task-definition app:1 --task-definition app:2 registers both revisions but deploys the first one, which can roll a service back. Reject duplicate resolved families or define an explicit selection rule.
ava-silver
left a comment
There was a problem hiding this comment.
Automated AI Review (human curated)
Two additional parity gaps surfaced when comparing this command with the corresponding Terraform module and CDK construct.
| // The Agent's own trace intake, which is a separate switch from the tracers' `DD_TRACE_ENABLED`. | ||
| [DD_APM_ENABLED_ENV_VAR]: 'true', | ||
| [DD_USE_DOGSTATSD_ENV_VAR]: 'true', | ||
| [DD_ECS_TASK_COLLECTION_ENABLED_ENV_VAR]: 'true', |
There was a problem hiding this comment.
P2: Enabling ECS task collection requires permissions that this command neither supplies nor checks. The Terraform module adds ecs:ListClusters, ecs:ListContainerInstances, and ecs:DescribeContainerInstances to the task role, and the CDK construct adds the same policy. Without those permissions, ECS task collection is incomplete. Avoid enabling it automatically, or make the required task-role policy explicit to the user.
| [SITE_ENV_VAR]: settings.site, | ||
| // The Agent's own trace intake, which is a separate switch from the tracers' `DD_TRACE_ENABLED`. | ||
| [DD_APM_ENABLED_ENV_VAR]: 'true', | ||
| [DD_USE_DOGSTATSD_ENV_VAR]: 'true', |
There was a problem hiding this comment.
P2: Match the DogStatsD origin-tagging defaults used by the other ECS instrumentation tools. The Terraform module defaults origin detection to enabled and cardinality to orchestrator (configuration, Agent environment); the CDK construct injects the same variables. Add DD_DOGSTATSD_ORIGIN_DETECTION=true, DD_DOGSTATSD_ORIGIN_DETECTION_CLIENT=true, and DD_DOGSTATSD_TAG_CARDINALITY=orchestrator so custom metrics retain equivalent origin tags and cardinality.
stzou
left a comment
There was a problem hiding this comment.
Thanks for adding ECS fargate support - just a couple comments around logging and traces.
APM/DogStatsD: right now app containers reach the Agent over the implicit localhost:8126 tracer default, which only works because awsvpc shares one network namespace. Can we follow the Terraform module's pattern here?
- mount a shared dd-sockets volume
- set
DD_TRACE_AGENT_URL/DD_DOGSTATSD_URLto the unix socket by default and fall back to UDP (DD_AGENT_HOST=127.0.0.1) only when the user opts out of the socket.
Logs: there's currently no path to get application logs into Datadog. Could we add an opt-in Fluent Bit sidecar (datadog-log-router) using the awsfirelens log driver, matching the Terraform module's dd_log_collection config, so app containers can route logs through it to Datadog's log intake.
Could also be worth adding DD_INSTALL_INFO_TOOL / _TOOL_VERSION / _INSTALLER_VERSION so we can track usage of this cli. If there's another way this is being tracked, feel free to leave it out.
Where possible, we should try to have parity between this onboarding tool and existing ones so the onboarding experience is consistent regardless of method used.
03d44ec to
dd3edda
Compare
|
|
||
| import {AWS_SHARED_CREDENTIALS_FILE_ENV_VAR, EXPONENTIAL_BACKOFF_RETRY_STRATEGY} from './constants' | ||
|
|
||
| // TODO: the two credential helpers below are duplicated from plugin-lambda's `functions/commons.ts`. |
There was a problem hiding this comment.
yeah the tricky thing is we don't want to pull the AWS SDK into the base package -- we can revisit this if need be in the future
| return await credentialsProvider() | ||
| } catch (err) { | ||
| if (err instanceof Error) { | ||
| throw Error(`Couldn't set AWS profile credentials. ${err.message}`) |
There was a problem hiding this comment.
| throw Error(`Couldn't set AWS profile credentials. ${err.message}`) | |
| throw Error(`Couldn't get AWS profile credentials. ${err.message}`) |
| return 1 | ||
| } | ||
|
|
||
| const region = config.region ?? AWS_REGION_ENV_VARS.map((envVar) => process.env[envVar]).find((value) => !!value) |
There was a problem hiding this comment.
would it be worth supporting multiple regions here (based on the task definition ARN)? although either way that'd be better as a follow-up PR
There was a problem hiding this comment.
yup I think followup PR is better
|
|
||
| if (failed) { | ||
| // Deploying changes what is running, so a run that could not instrument every task definition | ||
| // stops here rather than pointing some of the services at a new revision. |
There was a problem hiding this comment.
I'm not sure I understand why we'd want to do this rather than roll out the successful ones -- lets say they have 99 task definitions succeed, but one fails, we'd fail the entire fleet and nothing would get rolled out
I feel like we should probably be in line with the other commands and roll out the successful ones, so we could do each instrumentation in parallel and just wait for all the operations at the same time.
| const instrumented: InstrumentedRevisions = new Map() | ||
| let failed = false | ||
| for (const taskDefinition of config.taskDefinitions ?? []) { | ||
| const revision = await this.instrument(client, taskDefinition, settings, services.length > 0) |
There was a problem hiding this comment.
this should probably use a Promise.all() to do these in parallel, right? (Assuming the ECS client has throttling/retry handling builtin)
| instrumented: InstrumentedRevisions | ||
| ): Promise<boolean> { | ||
| let deployed = true | ||
| for (const name of services) { |
There was a problem hiding this comment.
same with this for loop, it'd be great to parallelize these, although if we refactor to make each app individual then we probably get this for free
|
@stzou I've added the dd-sockets change, though I think the logging changes would be better added to this PR (the next PR in the stack) where I add other configuration flags. |
stzou
left a comment
There was a problem hiding this comment.
LGTM
For the install info vars, dd_sls_ci covers our need to track CLI-driven installs so we're fine leaving those out.
bf06453 to
8c8913c
Compare
There was a problem hiding this comment.
a couple last things i missed in my initial review now after solidifying on command guidance: #2500
| [DD_ECS_TASK_COLLECTION_ENABLED_ENV_VAR]: 'true', | ||
| ...(settings.apiKey ? {[API_KEY_ENV_VAR]: settings.apiKey} : {}), | ||
| }, | ||
| defaults: { |
There was a problem hiding this comment.
The lifecycle guidance says omitted configuration must resolve to an explicit default or absence rather than preserve remote Datadog state. These values -- along with DD_TRACE_ENABLED and DD_LOGS_INJECTION below -- are defaults, so the same invocation can retain an earlier false or low value and yield a different Datadog configuration. Please either manage these values or add explicit options whose omitted values resolve deterministically.
| ...(settings.apiKey ? {[API_KEY_ENV_VAR]: settings.apiKey} : {}), | ||
| }, | ||
| defaults: { | ||
| [DD_DOGSTATSD_ORIGIN_DETECTION_ENV_VAR]: 'true', |
There was a problem hiding this comment.
For ECS Fargate, Datadog's DogStatsD UDS documentation requires pidMode: "task" when DD_DOGSTATSD_ORIGIN_DETECTION=true. This transform enables origin detection but leaves pidMode unchanged, so metrics may not receive the task/container origin tags described in the README. Please apply pidMode: "task" as part of this desired state.
|
|
||
| const segments = service.split('/') | ||
|
|
||
| return segments.length === 3 ? segments[1] : undefined |
There was a problem hiding this comment.
This returns no cluster for a legacy short service ARN (...:service/name). ECS then resolves it in the default cluster, so a service in another cluster cannot be found despite the README saying a full ARN does not need --cluster. Please require --cluster for short-format ARNs and reserve automatic cluster extraction for long-format ARNs.
20d0db4 to
ea77750
Compare
42d33cd to
b9298ab
Compare
b9298ab to
6ed66ca
Compare
6ed66ca to
5e2bbd7
Compare
5e2bbd7 to
790c5e6
Compare
83e1dca to
9e8877e
Compare
a2d7c20 to
b640b09
Compare
… app individually + parallelism
b640b09 to
56e9fc9
Compare
What and why?
Adds
datadog-ci ecs-fargate instrument, which instruments an AWS ECS Fargate taskdefinition with Datadog: it adds the
datadog-agentsidecar, gives the applicationcontainers the environment their tracers read, and registers the result as a new
revision. Container images are left untouched.
Fargate users currently have to hand-edit task definition JSON to add the sidecar, which
is easy to get wrong and hard to keep consistent across services.
How?
New
plugin-ecs-fargatepackage plus the base command definition, following the usualbase/plugin split. A run describes each task definition, transforms it, and registers
the new revision.
--task-definition(family,family:revision, or ARN),--region,--profile, and adatadog-ci.jsonconfig file.--ecs-servicepoints services at the revision that was just registered, sothe change reaches running tasks without a manual deployment.
--clusteris onlyneeded when the services are not named by full ARN. Services are updated only after
every task definition in the run is instrumented, so a partial failure does not leave
half the fleet on a new revision.
--api-key-secret-arnkeeps the key out of the task definition; falling backto
DD_API_KEYwrites it in plain text, which is warned about and validated first.dd_sls_civersion tag isexcluded from the comparison, so re-running (or upgrading the CLI) does not burn a
revision.
--dry-runprints the diff and registers nothing.Unified service tagging and the product toggles follow in the next PR; Windows tasks in
the one after. The
@aws-sdk/client-ecsversion is pinned to the one the other AWSpackages in the repo already use, so the lockfile addition stays small.
Review checklist