-
Notifications
You must be signed in to change notification settings - Fork 17.4k
Various fixes on ECS run task operator #31838
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
a3afef6
f96c0fa
a899019
176cc6a
3347548
7ea0126
e1c9268
9c2a3b5
ce01b0c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -480,6 +480,17 @@ def __init__( | |
| self.waiter_delay = waiter_delay | ||
| self.waiter_max_attempts = waiter_max_attempts | ||
|
|
||
| if self._aws_logs_enabled() and not self.wait_for_completion: | ||
| self.log.warning( | ||
| "Trying to get logs without waiting for the task to complete is undefined behavior." | ||
| ) | ||
|
|
||
| @staticmethod | ||
| def _get_ecs_task_id(task_arn: str | None) -> str | None: | ||
| if task_arn is None: | ||
| return None | ||
| return task_arn.split("/")[-1] | ||
|
|
||
| @provide_session | ||
| def execute(self, context, session=None): | ||
| self.log.info( | ||
|
|
@@ -506,25 +517,24 @@ def execute(self, context, session=None): | |
|
|
||
| @AwsBaseHook.retry(should_retry_eni) | ||
| def _start_wait_check_task(self, context): | ||
|
|
||
| if not self.arn: | ||
| self._start_task(context) | ||
|
|
||
| if not self.wait_for_completion: | ||
| return | ||
|
|
||
|
Comment on lines
+523
to
+525
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whatever logs users were getting for the short period of time without a wait_for_completion they will no longer get. So we're calling it a bug fix with no deprecation?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, idk, we may want to keep the existing behavior, but what I don't like about it is that it made the operator slower just for the sake of maybe getting a couple of logs...
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd agree with Raph, I dont think this is a desired behavior but more an forgotten edge case. I would call it a bug fix
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ack, I'll call that quorum then, let's call it a bug fix 👍 |
||
| if self._aws_logs_enabled(): | ||
| self.log.info("Starting ECS Task Log Fetcher") | ||
| self.task_log_fetcher = self._get_task_log_fetcher() | ||
| self.task_log_fetcher.start() | ||
|
|
||
| try: | ||
| if self.wait_for_completion: | ||
| self._wait_for_task_ended() | ||
| self._wait_for_task_ended() | ||
| finally: | ||
| self.task_log_fetcher.stop() | ||
|
|
||
| self.task_log_fetcher.join() | ||
| else: | ||
| if self.wait_for_completion: | ||
| self._wait_for_task_ended() | ||
| self._wait_for_task_ended() | ||
|
|
||
| self._check_success_task() | ||
|
|
||
|
|
@@ -566,8 +576,7 @@ def _start_task(self, context): | |
| self.log.info("ECS Task started: %s", response) | ||
|
|
||
| self.arn = response["tasks"][0]["taskArn"] | ||
| self.ecs_task_id = self.arn.split("/")[-1] | ||
| self.log.info("ECS task ID is: %s", self.ecs_task_id) | ||
| self.log.info("ECS task ID is: %s", self._get_ecs_task_id(self.arn)) | ||
|
|
||
| if self.reattach: | ||
| # Save the task ARN in XCom to be able to reattach it if needed | ||
|
|
@@ -590,7 +599,6 @@ def _try_reattach_task(self, context): | |
| ) | ||
| if previous_task_arn in running_tasks: | ||
| self.arn = previous_task_arn | ||
| self.ecs_task_id = self.arn.split("/")[-1] | ||
| self.log.info("Reattaching previously launched task: %s", self.arn) | ||
| else: | ||
| self.log.info("No active previously launched task found to reattach") | ||
|
|
@@ -620,7 +628,7 @@ def _aws_logs_enabled(self): | |
| def _get_task_log_fetcher(self) -> EcsTaskLogFetcher: | ||
| if not self.awslogs_group: | ||
| raise ValueError("must specify awslogs_group to fetch task logs") | ||
| log_stream_name = f"{self.awslogs_stream_prefix}/{self.ecs_task_id}" | ||
| log_stream_name = f"{self.awslogs_stream_prefix}/{self._get_ecs_task_id(self.arn)}" | ||
|
|
||
| return EcsTaskLogFetcher( | ||
| aws_conn_id=self.aws_conn_id, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,7 @@ | |
| from airflow import DAG | ||
| from airflow.decorators import task | ||
| from airflow.models.baseoperator import chain | ||
| from airflow.providers.amazon.aws.hooks.ecs import EcsClusterStates, EcsTaskStates | ||
| from airflow.providers.amazon.aws.hooks.ecs import EcsClusterStates | ||
| from airflow.providers.amazon.aws.operators.ecs import ( | ||
| EcsCreateClusterOperator, | ||
| EcsDeleteClusterOperator, | ||
|
|
@@ -34,7 +34,6 @@ | |
| from airflow.providers.amazon.aws.sensors.ecs import ( | ||
| EcsClusterStateSensor, | ||
| EcsTaskDefinitionStateSensor, | ||
| EcsTaskStateSensor, | ||
| ) | ||
| from airflow.utils.trigger_rule import TriggerRule | ||
| from tests.system.providers.amazon.aws.utils import ENV_ID_KEY, SystemTestContextBuilder | ||
|
|
@@ -67,6 +66,15 @@ def get_region(): | |
| return boto3.session.Session().region_name | ||
|
|
||
|
|
||
| @task(trigger_rule=TriggerRule.ALL_DONE) | ||
| def clean_logs(group_name: str): | ||
| client = boto3.client("logs") | ||
| # A bit brutal to delete the whole group, I know, | ||
| # but we don't have the access to the arn of the task which is used in the stream name | ||
| # and also those logs just contain "hello world", which is not very interesting. | ||
| client.delete_log_group(logGroupName=group_name) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is going to fail if the group does not exist, so in a way it makes sure the log configuration stays correct. |
||
|
|
||
|
|
||
| with DAG( | ||
| dag_id=DAG_ID, | ||
| schedule="@once", | ||
|
|
@@ -85,6 +93,7 @@ def get_region(): | |
| asg_name = f"{env_id}-asg" | ||
|
|
||
| aws_region = get_region() | ||
| log_group_name = "/ecs/hello-world" | ||
|
vandonr-amz marked this conversation as resolved.
Outdated
|
||
|
|
||
| # [START howto_operator_ecs_create_cluster] | ||
| create_cluster = EcsCreateClusterOperator( | ||
|
|
@@ -114,7 +123,16 @@ def get_region(): | |
| "workingDirectory": "/usr/bin", | ||
| "entryPoint": ["sh", "-c"], | ||
| "command": ["ls"], | ||
| } | ||
| "logConfiguration": { | ||
| "logDriver": "awslogs", | ||
| "options": { | ||
| "awslogs-group": log_group_name, | ||
| "awslogs-region": aws_region, | ||
| "awslogs-create-group": "true", | ||
| "awslogs-stream-prefix": "ecs", | ||
| }, | ||
| }, | ||
| }, | ||
| ], | ||
| register_task_kwargs={ | ||
| "cpu": "256", | ||
|
|
@@ -140,38 +158,19 @@ def get_region(): | |
| "containerOverrides": [ | ||
| { | ||
| "name": container_name, | ||
| "command": ["echo", "hello", "world"], | ||
| "command": ["echo hello world"], | ||
| }, | ||
| ], | ||
| }, | ||
| network_configuration={"awsvpcConfiguration": {"subnets": existing_cluster_subnets}}, | ||
| # [START howto_awslogs_ecs] | ||
| awslogs_group="/ecs/hello-world", | ||
| awslogs_group=log_group_name, | ||
| awslogs_region=aws_region, | ||
| awslogs_stream_prefix="ecs/hello-world-container", | ||
| awslogs_stream_prefix=f"ecs/{container_name}", | ||
| # [END howto_awslogs_ecs] | ||
| # You must set `reattach=True` in order to get ecs_task_arn if you plan to use a Sensor. | ||
| reattach=True, | ||
| ) | ||
| # [END howto_operator_ecs_run_task] | ||
|
|
||
| # EcsRunTaskOperator waits by default, setting as False to test the Sensor below. | ||
| run_task.wait_for_completion = False | ||
|
|
||
| # [START howto_sensor_ecs_task_state] | ||
| # By default, EcsTaskStateSensor waits until the task has started, but the | ||
| # demo task runs so fast that the sensor misses it. This sensor instead | ||
| # demonstrates how to wait until the ECS Task has completed by providing | ||
| # the target_state and failure_states parameters. | ||
| await_task_finish = EcsTaskStateSensor( | ||
| task_id="await_task_finish", | ||
| cluster=existing_cluster_name, | ||
| task=run_task.output["ecs_task_arn"], | ||
| target_state=EcsTaskStates.STOPPED, | ||
| failure_states={EcsTaskStates.NONE}, | ||
| ) | ||
| # [END howto_sensor_ecs_task_state] | ||
|
|
||
| # [START howto_operator_ecs_deregister_task_definition] | ||
| deregister_task = EcsDeregisterTaskDefinitionOperator( | ||
| task_id="deregister_task", | ||
|
|
@@ -209,10 +208,10 @@ def get_region(): | |
| register_task, | ||
| await_task_definition, | ||
| run_task, | ||
| await_task_finish, | ||
| deregister_task, | ||
| delete_cluster, | ||
| await_delete_cluster, | ||
| clean_logs(log_group_name), | ||
| ) | ||
|
|
||
| from tests.system.utils.watcher import watcher | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doing this because I thought that remembering to update the ecs_task_id every time the arn gets changed was a bit brittle. This method makes the dependency arn->task_id explicit.
Yes it means we're going to recompute it each time we need it, but I think it's negligible.