{"id":13521,"date":"2020-11-19T07:55:19","date_gmt":"2020-11-19T07:55:19","guid":{"rendered":"https:\/\/slack.engineering\/?p=13521"},"modified":"2020-11-19T23:00:11","modified_gmt":"2020-11-19T23:00:11","slug":"migrating-slack-airflow-to-python-3-without-disruption","status":"publish","type":"post","link":"https:\/\/slack.engineering\/migrating-slack-airflow-to-python-3-without-disruption\/","title":{"rendered":"Migrating Slack Airflow to Python 3 Without Disruption"},"content":{"rendered":"<p>Last year, we migrated Airflow from 1.8 to 1.10 at Slack (see <a href=\"https:\/\/slack.engineering\/reliably-upgrading-apache-airflow-at-slacks-scale\/\">here<\/a>) and we did a \u201cBig bang\u201d upgrade because of the constraints we had. This year, due to Python 2 reaching end of life, we again had a major migration of Airflow from Python 2 to 3 and we wanted to put our learnings from 2019 into practice. This post describes how we moved our Apache Airflow infrastructure, and hundreds of DAGs, from Python 2 to Python 3 in a more reliable \u201cRed-black\u201d deployment using <a href=\"https:\/\/airflow.apache.org\/docs\/stable\/executor\/celery.html\">Celery<\/a> queues, resulting in the migration being completely transparent to our users.<\/p>\n<p><a href=\"https:\/\/airflow.apache.org\/\">Apache Airflow<\/a> is a tool for describing, executing, and monitoring workflows. At Slack, we use Airflow to schedule our data warehouse workflows, which includes product metrics, business metrics, and also other use-cases like search and offline indexing. We run hundreds of workflows and tens of thousands of airflow tasks every day.<\/p>\n<h2>High-level steps for Airflow Python 3 migration<\/h2>\n<p>Here are the list of things we did for the migration:<\/p>\n<ol>\n<li><b>Set up a virtual environment:<\/b> Created a working Python 3 virtual environment with Airflow and all the dependencies.<\/li>\n<li><b>Set up Python 3 workers:<\/b> Launched Python 3 celery workers running the virtual environment from step 1, and make them pick tasks from a \u201cpython3\u201d queue on Redis.<\/li>\n<li><b>Clean up phase 1:<\/b> Removed stale or unused DAGs to make the migration easier.<\/li>\n<li><b>Fix Python 3 incompatibilities in DAGs<\/b>: Updated DAGs to be Python 2\/3 compatible but let them continue to run on Python 2 workers.<\/li>\n<li><b>Move the DAGs to Python 3 workers: <\/b>Tested and switched DAGs from Python 2 to Python 3 workers. We started with a small set and kept increasing depending on success rate. If DAGs failed, we fixed forward or rolled back to Python 2 workers.<\/li>\n<li><b>Migrate the Airflow services to Python 3:<\/b> Switched web server, scheduler, and flower to Python 3.<\/li>\n<li><b>Clean up phase 2:<\/b> Cleaned up Python 2 references and virtual environments, and terminated Python 2 celery workers.<\/li>\n<\/ol>\n<p>We did these steps in a dev environment first and then in prod. Let&#8217;s delve into each step in the following sections (except for the clean-up steps, which are self-explanatory).<\/p>\n<p><b>Pro tip:<\/b> The above order of migration is important; migrating scheduler or webserver to Python 3 before fixing DAG incompatibilities will result in errors because the scheduler in Python 3 will be unable to parse the DAG.<\/p>\n<h3>Set up a virtual environment<\/h3>\n<p>To set up a working python3 virtual environment, we did the following:<\/p>\n<ul>\n<li><b>Decouple Python system installation using <\/b><a href=\"https:\/\/github.com\/pyenv\/pyenv\"><b>pyenv<\/b><\/a><b>: <\/b>This is not a necessary step but we noticed that we were depending on system Python on Airflow boxes which had version 3.5.2 installed, and which don\u2019t work with the more recent versions of Airflow. We resolved this by installing pyenv to manage Python installations and decouple from system installations. Pyenv comes with a <a href=\"https:\/\/github.com\/pyenv\/pyenv-virtualenv\">virtual environment plugin<\/a> and we used that for creating virtual environments.<\/li>\n<\/ul>\n<ul>\n<li><b>Use <\/b><a href=\"https:\/\/python-poetry.org\/\"><b>Poetry<\/b><\/a><b> for dependency management of Python 2 and 3:<\/b> We use Poetry to help install the right versions of dependencies for Python 2 and 3 needed for Airflow to work. These dependencies can be defined in one file (pyproject.toml). For example:<\/li>\n<\/ul>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1450\" height=\"255\" class=\"size-medium wp-image-13533 aligncenter\" src=\"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic1.png?w=640\" alt=\"\" srcset=\"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic1.png 1450w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic1.png?resize=640,113 640w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic1.png?resize=768,135 768w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic1.png?resize=1280,225 1280w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic1.png?resize=380,67 380w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic1.png?resize=800,141 800w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic1.png?resize=1160,204 1160w\" sizes=\"auto, (max-width: 1450px) 100vw, 1450px\" \/><\/p>\n<ul>\n<li><b>Enable virtual environment relocation:<\/b> We build virtual environments, ship them to Airflow boxes and we make use of the \u201c&#8211;relocatable\u201d option in virtualenv to make them \u201cshippable\u201d. However, in Python 3, this option is deprecated. To get past this, we updated the virtual environment\u2019s files with the following command (basically replacing the shebang in the files with the final location of the virtual environment). This step is not needed if the virtual environment is not shipped, or if one is using containers.<\/li>\n<\/ul>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1451\" height=\"143\" class=\"size-medium wp-image-13534 aligncenter\" src=\"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic2.png?w=640\" alt=\"\" srcset=\"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic2.png 1451w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic2.png?resize=640,63 640w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic2.png?resize=768,76 768w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic2.png?resize=1280,126 1280w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic2.png?resize=380,37 380w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic2.png?resize=800,79 800w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic2.png?resize=1160,114 1160w\" sizes=\"auto, (max-width: 1451px) 100vw, 1451px\" \/><\/p>\n<h3>Setup Python 3 workers<img loading=\"lazy\" decoding=\"async\" width=\"1415\" height=\"420\" class=\"size-medium wp-image-13565 aligncenter\" src=\"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic10.png?w=640\" alt=\"\" srcset=\"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic10.png 1415w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic10.png?resize=640,190 640w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic10.png?resize=768,228 768w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic10.png?resize=1280,380 1280w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic10.png?resize=380,113 380w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic10.png?resize=800,237 800w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic10.png?resize=1160,344 1160w\" sizes=\"auto, (max-width: 1415px) 100vw, 1415px\" \/><\/h3>\n<ul>\n<li><b>Launch instances:<\/b> In this step, we launched a fleet of python3 celery workers that runs the Airflow worker process using the Python 3 virtual environment that we built in step 1. These instances run alongside the existing python2 worker fleet.<\/li>\n<li><b>Configure queues:<\/b> We configured the workers to listen to the python2 and python3 queues from Redis (the queue creation on Redis is automatic when airflow scheduler submits a task to Redis with a queue configured).<img loading=\"lazy\" decoding=\"async\" width=\"1446\" height=\"88\" class=\"size-medium wp-image-13536 aligncenter\" src=\"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic4.png?w=640\" alt=\"\" srcset=\"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic4.png 1446w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic4.png?resize=640,39 640w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic4.png?resize=768,47 768w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic4.png?resize=1280,78 1280w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic4.png?resize=380,23 380w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic4.png?resize=800,49 800w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic4.png?resize=1160,71 1160w\" sizes=\"auto, (max-width: 1446px) 100vw, 1446px\" \/><\/li>\n<\/ul>\n<ul>\n<li><b>Patch sqlalchemy:<\/b> Airflow uses sqlalchemy\u2014which is a Python SQL toolkit and object relational mapper\u2014to read\/write to the metadata DB. Some fields in the Airflow tables are written out in serialized or pickle format and are read by other Airflow services. Unfortunately, sqlalchemy writes out using a higher protocol version in Python 3 and this creates deserialization errors on the scheduler running on Python 2, resulting in celery failure task states not getting propagated to scheduler (Note: schedule\/web servers move to Python 3 towards the end, and until then we need to support it on Python 2). To resolve this, we patched sqlalchemy to serialize using protocol version 2 on Python 3 workers, which can then be deserialized on Python 2. The change has to be made <a href=\"https:\/\/github.com\/sqlalchemy\/sqlalchemy\/blob\/master\/lib\/sqlalchemy\/sql\/sqltypes.py#L1717\">here<\/a>. We can remove this once we migrate everything to Python 3.<img loading=\"lazy\" decoding=\"async\" width=\"1447\" height=\"296\" class=\"size-medium wp-image-13537 aligncenter\" src=\"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic5.png?w=640\" alt=\"\" srcset=\"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic5.png 1447w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic5.png?resize=640,131 640w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic5.png?resize=768,157 768w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic5.png?resize=1280,262 1280w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic5.png?resize=380,78 380w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic5.png?resize=800,164 800w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic5.png?resize=1160,237 1160w\" sizes=\"auto, (max-width: 1447px) 100vw, 1447px\" \/><\/li>\n<\/ul>\n<h3>Fix Python 3 incompatibilities in DAGs<\/h3>\n<p>Now that we have the infrastructure in place, the next step is to make the DAGs Python 2\/3 compatible. It&#8217;s important that it&#8217;s Python 2\/3 compatible for two reasons; Firstly, the services still on Python 2, like the scheduler, should still be able to parse the DAGs. Secondly, when we migrate to Python 3 workers, the rollback is simpler since there will be no code change in the DAGs except for the queue configuration. Here are some details about fixing incompatibilities:<\/p>\n<ul>\n<li><b>Automatic conversion of Python 2 to 3 code: <\/b>Fortunately we didn\u2019t have to go file by file and change Python 3 incompatibilities manually. We evaluated two automatic Python 2 to 3 converters: <a href=\"https:\/\/docs.python.org\/2\/library\/2to3.html\">2to3<\/a> and <a href=\"https:\/\/python-future.org\/futurize.html\">futurize<\/a>. We chose futurize because it produces Python 2 and 3 compatible code, while 2to3 is more suitable for one way porting.<\/li>\n<li><b>Using <\/b><a href=\"https:\/\/python-future.org\/futurize.html\"><b>futurize<\/b><\/a><b>: <\/b>futurize applies fixers (think of a fixer as something that fixes a specific Python 3 incompatibility, like converting print function to use parenthesis). futurize divides the fixers into stage 1 (safe changes) and stage 2 (potentially unsafe changes). We applied fixers carefully (especially in stage 2), one by one, and then tested and deployed them over the course of a few days so that it&#8217;s easy to narrow down issues. Fixers can be applied one by one like this:<\/li>\n<\/ul>\n<p><img loading=\"lazy\" decoding=\"async\" width=\"1450\" height=\"133\" class=\"size-medium wp-image-13538 aligncenter\" src=\"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic6.png?w=640\" alt=\"\" srcset=\"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic6.png 1450w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic6.png?resize=640,59 640w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic6.png?resize=768,70 768w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic6.png?resize=1280,117 1280w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic6.png?resize=380,35 380w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic6.png?resize=800,73 800w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic6.png?resize=1160,106 1160w\" sizes=\"auto, (max-width: 1450px) 100vw, 1450px\" \/><\/p>\n<ul>\n<li><b>Incompatibility detection in CI tests: <\/b>As we applied fixers, we wanted to make sure that users don\u2019t add the same type of incompatibility again in the future that we just fixed. To avoid that, we added a CI test in PRs to catch Python 3 incompatibilities that have already been fixed. Here is the <a href=\"https:\/\/gist.github.com\/ashwinshankar77\/7d5e5ef2f23e612fce349525989abbae\">script<\/a>.<\/li>\n<\/ul>\n<h3>Move DAGs to Python 3 workers<\/h3>\n<ul>\n<li><b>Team by team migration:<\/b> We needed to migrate hundreds of DAGs. Among the various ways we can pick DAGs to move over to Python 3 workers, we chose to move \u201cteam by team\u201d. We contacted teams, got a point of contact in case of issues, prioritized the DAGs, tested DAGs locally, and then migrated them based on criticality (low criticality first). We fixed forward any runtime issues that we faced and barely had to rollback any DAGs. Once we migrated a few hundred DAGs and got enough confidence, we did a mass switch over of all the remaining ones.<\/li>\n<li><b>Usage of Python 3 DAG list:<\/b> To move a DAG or task to Python 3, we need to set the \u201cqueue\u201d attribute to \u201cpython3\u201d. Instead of setting this config manually for every DAG we move, we maintained a file which had all the migrated DAGs. We then wrote code to read from that file and set the configuration automatically. This makes adding DAGs or rolling back easier because we needed to touch just one file.<\/li>\n<\/ul>\n<h3>Final steps<\/h3>\n<p>Once all the DAGs were moved to Python 3 workers, we switched over the Airflow scheduler, web server, and flower to Python 3 by shipping the Python 3 virtual environment built in step 1. Finally, we cleaned up all Python 2 related code and terminated the python2 workers. Now, we can call the migration done!<\/p>\n<h2>Common issues and solutions<\/h2>\n<ol>\n<li><strong>TypeError: a bytes-like object is required, not &#8216;str&#8217;<\/strong><br \/>\n<strong>Solution<\/strong>: This is the most frequent error we observed, and happens because of type mismatch of bytes and strings due to Python 3 treating all strings as unicode. To fix this, we need to explicitly convert string to bytes using encode().<\/li>\n<li><strong>Snowflake connector issues<\/strong><br \/>\n<strong>Solution<\/strong>: We notice errors like \u201cERROR &#8211; asn1crypto.keys.PublicKeyInfo().unwrap() has been removed, please use oscrypto.asymmetric.PublicKey().unwrap() instead\u201d with Snowflake operators. We resolved this by upgrading the snowflake-connector-python library to 2.x.<img loading=\"lazy\" decoding=\"async\" width=\"1447\" height=\"236\" class=\"size-medium wp-image-13539 aligncenter\" src=\"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic7.png?w=640\" alt=\"\" srcset=\"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic7.png 1447w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic7.png?resize=640,104 640w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic7.png?resize=768,125 768w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic7.png?resize=1280,209 1280w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic7.png?resize=380,62 380w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic7.png?resize=800,130 800w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic7.png?resize=1160,189 1160w\" sizes=\"auto, (max-width: 1447px) 100vw, 1447px\" \/><\/li>\n<li><strong>Dictionary random ordering of entries<\/strong><br \/>\n<strong>Solution<\/strong>: In Python 2, we had predictable ordering of keys in dictionaries, while in Python 3 this wasn\u2019t the case. We solved this by using OrderedDict, which preserves the keys in insertion order.<\/li>\n<li><strong>Broken DAG: &#8216;ascii&#8217; codec can&#8217;t decode byte 0xe2 in position 3825: ordinal not in range(128)<\/strong><br \/>\n<strong>Solution<\/strong>: This issue happened when we tried to open files in Python 3 without specifying the encoding (it defaults to ASCII). To sort this out, we explicitly specified the encoding while opening files.<img loading=\"lazy\" decoding=\"async\" width=\"1438\" height=\"95\" class=\"size-medium wp-image-13540 aligncenter\" src=\"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic8.png?w=640\" alt=\"\" srcset=\"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic8.png 1438w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic8.png?resize=640,42 640w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic8.png?resize=768,51 768w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic8.png?resize=1280,85 1280w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic8.png?resize=380,25 380w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic8.png?resize=800,53 800w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic8.png?resize=1160,77 1160w\" sizes=\"auto, (max-width: 1438px) 100vw, 1438px\" \/><\/li>\n<li><strong>futurize missed converting Python code in SQL files<\/strong><br \/>\n<strong>Solution<\/strong>: We have a small set of SQL scripts with embedded Python code and futurize skipped it because it only looks at Python files for porting. This caused issues and we had to manually convert those files to be Python 2\/3 compatible.<\/li>\n<li><strong>Openssl issue with Mac OS Catalina<\/strong><br \/>\n<strong>Solution<\/strong>: We noticed issues when we moved to Python 3 where running Airflow locally gave errors like \u201cAbort trap: 6\u201d. This was due to an OpenSSL issue that specifically happens on Mac OS Catalina. We fixed this by running the following commands:<img loading=\"lazy\" decoding=\"async\" width=\"1445\" height=\"323\" class=\"size-medium wp-image-13541 aligncenter\" src=\"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic9.png?w=640\" alt=\"\" srcset=\"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic9.png 1445w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic9.png?resize=640,143 640w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic9.png?resize=768,172 768w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic9.png?resize=1280,286 1280w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic9.png?resize=380,85 380w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic9.png?resize=800,179 800w, https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/pic9.png?resize=1160,259 1160w\" sizes=\"auto, (max-width: 1445px) 100vw, 1445px\" \/><\/li>\n<\/ol>\n<h2>Conclusion<\/h2>\n<p>This Airflow migration proved to be reliable, completely transparent to users, and had zero SLA misses since we did a \u201cred black\u201d deployment. This was done by running both Python 2 and 3 infrastructure side-by-side and eventually moving all the traffic to Python 3. In conclusion, this migration went well, and we are excited that it enables us to use Python 3 features, not to mention the ability to upgrade to Airflow 2.0, in the future.<\/p>\n<p>Big thanks to Prateek Kakirwar, Diana Pojar, Travis Cook, Joohee Yoo, Ajay Bhonsule for their contributions to the project and to Ross Harmes for reviewing this tech blog.<\/p>\n<p><i>We are always looking for engineers to join our Data Engineering team. If you\u2019d like to help us, please check out <\/i><a href=\"https:\/\/slack.com\/careers\"><i>https:\/\/slack.com\/careers<\/i><\/a><\/p>\n","protected":false},"excerpt":{"rendered":"Last year, we migrated Airflow from 1.8 to 1.10 at Slack (see here) and we did a \u201cBig bang\u201d upgrade because of the constraints we had. This year, due to Python 2 reaching end of life, we again had a major migration of Airflow from Python 2 to 3 and we wanted to put our&hellip;","protected":false},"author":176,"featured_media":13528,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[],"tags":[],"class_list":["post-13521","post","type-post","status-publish","format-standard","has-post-thumbnail","ts-entry"],"acf":{"subtitle":"","author_group":{"configure_author":"wordpress","authors":[{"ID":12100,"post_author":"3","post_date":"2020-04-27 17:29:00","post_date_gmt":"2020-04-27 17:29:00","post_content":"","post_title":"Ashwin Shankar","post_excerpt":"","post_status":"publish","comment_status":"closed","ping_status":"closed","post_password":"","post_name":"ashwin-shankar","to_ping":"","pinged":"","post_modified":"2020-05-23 01:46:58","post_modified_gmt":"2020-05-23 01:46:58","post_content_filtered":"","post_parent":0,"guid":"https:\/\/slackhq.com\/engineering\/?p=12100","menu_order":0,"post_type":"author","post_mime_type":"","comment_count":"0","filter":"raw"}],"custom_author":""},"series":false,"tags":false},"jetpack_featured_media_url":"https:\/\/slack.engineering\/wp-content\/uploads\/sites\/7\/2020\/11\/lukasz-szmigiel-jgFBXnK4F_g-unsplash-2.jpg","_links":{"self":[{"href":"https:\/\/slack.engineering\/wp-json\/wp\/v2\/posts\/13521","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/slack.engineering\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/slack.engineering\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/slack.engineering\/wp-json\/wp\/v2\/users\/176"}],"replies":[{"embeddable":true,"href":"https:\/\/slack.engineering\/wp-json\/wp\/v2\/comments?post=13521"}],"version-history":[{"count":28,"href":"https:\/\/slack.engineering\/wp-json\/wp\/v2\/posts\/13521\/revisions"}],"predecessor-version":[{"id":13582,"href":"https:\/\/slack.engineering\/wp-json\/wp\/v2\/posts\/13521\/revisions\/13582"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/slack.engineering\/wp-json\/wp\/v2\/media\/13528"}],"wp:attachment":[{"href":"https:\/\/slack.engineering\/wp-json\/wp\/v2\/media?parent=13521"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/slack.engineering\/wp-json\/wp\/v2\/categories?post=13521"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/slack.engineering\/wp-json\/wp\/v2\/tags?post=13521"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}