From 312eccbf79ce092b370ba06668facd997e50bb7f Mon Sep 17 00:00:00 2001 From: frankcash Date: Tue, 26 Mar 2019 19:59:59 -0400 Subject: [PATCH] initial commit --- .dockerignore | 127 ++++ .flake8 | 4 + .github/PULL_REQUEST_TEMPLATE | 16 + .gitignore | 124 ++++ Dockerfile | 81 +++ Makefile | 2 + README.md | 50 ++ airflow_home/dags/hackerqueue_etl_dag.py | 12 + .../plugins/postgres_to_s3_operator.py | 50 ++ config/airflow.cfg | 578 ++++++++++++++++++ docker-compose.yml | 32 + requirements-dev.txt | 2 + requirements.txt | 59 ++ script/entrypoint.sh | 64 ++ 14 files changed, 1201 insertions(+) create mode 100644 .dockerignore create mode 100644 .flake8 create mode 100644 .github/PULL_REQUEST_TEMPLATE create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 airflow_home/dags/hackerqueue_etl_dag.py create mode 100644 airflow_home/plugins/postgres_to_s3_operator.py create mode 100644 config/airflow.cfg create mode 100644 docker-compose.yml create mode 100644 requirements-dev.txt create mode 100644 requirements.txt create mode 100755 script/entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f413251 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,127 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# fuck osx +.DS_Store + +airflow_home/logs +airflow_home/airflow.cfg +airflow_home/airflow.db +airflow_home/unittests.cfg +temp/output.csv + +.vscode + +temp/*.csv + +*.pid + + +.git +temp/*.csv +test/* +venv/* + +__pycache__ \ No newline at end of file diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..3873d63 --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +ignore = D203,W293,W191,E305,E501,E251,E302,E115,E402,E266,E128,E261,E265,E101,E202,E231,E222,E303,E124,E293,E226,W503,E722,E201,E225,W292,W291,E127,E201,E126,E123,E121,E203,E221,E122,E262, +exclude = .git, __pycache__, venv +max-complexity = 35 \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE new file mode 100644 index 0000000..b168422 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE @@ -0,0 +1,16 @@ + + +# Related Tickets + + + +# Changes proposed + + + +#### Expected Output + + +#### Caveats + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e7a079 --- /dev/null +++ b/.gitignore @@ -0,0 +1,124 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +temp/* + +*.db-journal + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# fuck osx +.DS_Store + +airflow_home/logs +airflow_home/airflow.cfg +airflow_home/airflow.db +airflow_home/unittests.cfg +temp/output.csv + +.vscode + +temp/*.csv + +*.pid +.data/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7966735 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,81 @@ +FROM python:3.6-slim + +# Never prompts the user for choices on installation/configuration of packages +ENV DEBIAN_FRONTEND noninteractive +ENV TERM linux + +# Airflow +ARG AIRFLOW_VERSION=1.10.0 +ARG AIRFLOW_HOME=/usr/local/airflow +ENV AIRFLOW_GPL_UNIDECODE yes +ENV AIRFLOW_HOME=/usr/local/airflow + +# Define en_US. +ENV LANGUAGE en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LC_ALL en_US.UTF-8 +ENV LC_CTYPE en_US.UTF-8 +ENV LC_MESSAGES en_US.UTF-8 +ENV locale-gen en_US.UTF-8 +ENV locale-gen es_CO.UTF-8 + + +COPY requirements.txt ${AIRFLOW_HOME}/requirements.txt + +RUN set -ex \ + && buildDeps=' \ + python3-dev \ + libkrb5-dev \ + libsasl2-dev \ + libssl-dev \ + libffi-dev \ + libblas-dev \ + liblapack-dev \ + libpq-dev \ + git \ + ' \ + && apt-get update -yqq \ + && apt-get upgrade -yqq \ + && apt-get install -yqq --no-install-recommends \ + $buildDeps \ + build-essential \ + python3-pip \ + python3-requests \ + mysql-client \ + mysql-server \ + default-libmysqlclient-dev \ + apt-utils \ + curl \ + rsync \ + netcat \ + locales \ + && sed -i 's/^# en_US.UTF-8 UTF-8$/en_US.UTF-8 UTF-8/g' /etc/locale.gen \ + && locale-gen \ + && update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 \ + && useradd -ms /bin/bash -d ${AIRFLOW_HOME} airflow \ + && pip install -U pip setuptools wheel \ + && pip install -r ${AIRFLOW_HOME}/requirements.txt \ + && apt-get purge --auto-remove -yqq $buildDeps \ + && apt-get autoremove -yqq --purge \ + && apt-get clean \ + && rm -rf \ + /var/lib/apt/lists/* \ + /tmp/* \ + /var/tmp/* \ + /usr/share/man \ + /usr/share/doc \ + /usr/share/doc-base + +COPY script/entrypoint.sh /entrypoint.sh +COPY config/airflow.cfg ${AIRFLOW_HOME}/airflow.cfg +COPY airflow_home/ ${AIRFLOW_HOME} +RUN mkdir ${AIRFLOW_HOME}/temp + +RUN chown -R airflow: ${AIRFLOW_HOME} + +EXPOSE 8080 5555 8793 + +USER airflow +WORKDIR ${AIRFLOW_HOME} +ENTRYPOINT ["/entrypoint.sh"] +CMD ["webserver"] # set default arg for entrypoint diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..eb22307 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +build: + docker build --no-cache --rm -t airflow_compose:latest . \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d69b381 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# airflow_compose + +A project for maintaining ETLs with Apache's Airflow. + +### Getting dependencies installed + +``` +cd /path/to/my/airflow_compose/ +virtualenv -p `which python3` venv +source venv/bin/activate +``` + +From inside the virtualenv: +``` +export SLUGIFY_USES_TEXT_UNIDECODE=yes +pip install -r requirements.txt +airflow initdb +``` + +### Running the web server + +``` +cd /path/to/my/airflow/workspace +source venv/bin/activate + +export AIRFLOW_HOME=`pwd`/airflow_home +airflow webserver +``` + +### Running the scheduler + +``` +cd /path/to/my/airflow/workspace +export AIRFLOW_HOME=`pwd`/airflow_home + +source venv/bin/activate +airflow scheduler +``` + +### Adding database +Go to the configuration tab underneath admin to add a database connection. + + +# Resources for Learning Apache Airflow + +- http://michal.karzynski.pl/blog/2017/03/19/developing-workflows-with-apache-airflow/ +- http://tech.marksblogg.com/airflow-postgres-redis-forex.html +- https://cloud.google.com/blog/products/gcp/how-to-aggregate-data-for-bigquery-using-apache-airflow +- https://www.dataengineeringpodcast.com/airflow-in-production-with-james-meickle-episode-43/ +- https://www.astronomer.io/guides/dag-best-practices/ diff --git a/airflow_home/dags/hackerqueue_etl_dag.py b/airflow_home/dags/hackerqueue_etl_dag.py new file mode 100644 index 0000000..54ef67b --- /dev/null +++ b/airflow_home/dags/hackerqueue_etl_dag.py @@ -0,0 +1,12 @@ +from datetime import datetime +from airflow.models import DAG +from postgres_to_s3_operator import PostgresToS3Operator # https://stackoverflow.com/questions/43907813/cant-import-airflow-plugins + +dag = DAG('hackerqueue_etl_dag_v0_0_1', + description='etl_hackerqueue_info_once_a_day', + schedule_interval='0 12 * * *', + start_date=datetime(2019, 3, 26), catchup=False) + +export = PostgresToS3Operator(task_id="hackerqueue_export_task", dag=dag, postgres_conn_id ="hackerqueue_prod", s3_conn_id = 'demo_bucket', query="SELECT story_url, source, title, comments FROM public.crawls;") + +export \ No newline at end of file diff --git a/airflow_home/plugins/postgres_to_s3_operator.py b/airflow_home/plugins/postgres_to_s3_operator.py new file mode 100644 index 0000000..79481e5 --- /dev/null +++ b/airflow_home/plugins/postgres_to_s3_operator.py @@ -0,0 +1,50 @@ +import logging +import os +import unicodecsv as csv +import io +import uuid +from datetime import timedelta, datetime +from airflow.models import BaseOperator +from airflow.plugins_manager import AirflowPlugin +from airflow.utils.decorators import apply_defaults +from airflow.hooks import PostgresHook, S3Hook + + +logger = logging.getLogger(__name__) +airflow_path = os.environ["AIRFLOW_HOME"] + + +class PostgresToS3Operator(BaseOperator): + @apply_defaults + def __init__(self, postgres_conn_id, s3_conn_id, query, *args, **kwargs): + super().__init__(*args, **kwargs) + self.postgres_conn_id = postgres_conn_id + self.s3_conn_id = s3_conn_id + self.query = query + + def execute(self, context): + postgres_hook = PostgresHook(postgres_conn_id=self.postgres_conn_id) + s3_hook = S3Hook(aws_conn_id=self.s3_conn_id) + res = self.query_db(self.query, postgres_hook) + res.seek(0) + s3_hook.load_file_obj(res, key="egress/sources.airflow.csv", bucket_name="demo-bucket-temp-977338899", replace=True) + return True + + def query_db(self, query, hook): + content = hook.get_records(sql=query) + return self.gen_file(content) + + def gen_file(self, content): + print(f"gen_file {content}") + output = io.BytesIO() + writer = csv.writer(output, delimiter='|', encoding='utf-8') + writer.writerows(content) + return output + + def cleanup(self, fname): + os.remove(fname) + + +class PostgresToS3Operators(AirflowPlugin): + name = "postgres_to_s3_operator" + operators = [PostgresToS3Operator] \ No newline at end of file diff --git a/config/airflow.cfg b/config/airflow.cfg new file mode 100644 index 0000000..5075363 --- /dev/null +++ b/config/airflow.cfg @@ -0,0 +1,578 @@ +[core] +# The home folder for airflow, default is ~/airflow +airflow_home = /usr/local/airflow + +# The folder where your airflow pipelines live, most likely a +# subfolder in a code repository +# This path must be absolute +dags_folder = /usr/local/airflow/dags + +# The folder where airflow should store its log files +# This path must be absolute +base_log_folder = /usr/local/airflow/logs + +# Airflow can store logs remotely in AWS S3, Google Cloud Storage or Elastic Search. +# Users must supply an Airflow connection id that provides access to the storage +# location. If remote_logging is set to true, see UPDATING.md for additional +# configuration requirements. +remote_logging = False +remote_log_conn_id = S3_LOGS +remote_base_log_folder = +encrypt_s3_logs = False + +# Logging level +logging_level = INFO +fab_logging_level = WARN + +# Logging class +# Specify the class that will specify the logging configuration +# This class has to be on the python classpath +# logging_config_class = my.path.default_local_settings.LOGGING_CONFIG +logging_config_class = + +# Log format +# we need to escape the curly braces by adding an additional curly brace +log_format = [%%(asctime)s] {{%%(filename)s:%%(lineno)d}} %%(levelname)s - %%(message)s +simple_log_format = %%(asctime)s %%(levelname)s - %%(message)s + +# Log filename format +# we need to escape the curly braces by adding an additional curly brace +log_filename_template = {{ ti.dag_id }}/{{ ti.task_id }}/{{ ts }}/{{ try_number }}.log +log_processor_filename_template = {{ filename }}.log + +# Hostname by providing a path to a callable, which will resolve the hostname +hostname_callable = socket:getfqdn + +# Default timezone in case supplied date times are naive +# can be utc (default), system, or any IANA timezone string (e.g. Europe/Amsterdam) +default_timezone = est + +# The executor class that airflow should use. Choices include +# SequentialExecutor, LocalExecutor, CeleryExecutor, DaskExecutor +executor = LocalExecutor + +# The SqlAlchemy connection string to the metadata database. +# SqlAlchemy supports many different database engine, more information +# their website +# sql_alchemy_conn = sqlite:////tmp/airflow.db + +# If SqlAlchemy should pool database connections. +sql_alchemy_pool_enabled = True + +# The SqlAlchemy pool size is the maximum number of database connections +# in the pool. 0 indicates no limit. +sql_alchemy_pool_size = 10 + +# The SqlAlchemy pool recycle is the number of seconds a connection +# can be idle in the pool before it is invalidated. This config does +# not apply to sqlite. If the number of DB connections is ever exceeded, +# a lower config value will allow the system to recover faster. +sql_alchemy_pool_recycle = 1800 + +# How many seconds to retry re-establishing a DB connection after +# disconnects. Setting this to 0 disables retries. +sql_alchemy_reconnect_timeout = 300 + +# The amount of parallelism as a setting to the executor. This defines +# the max number of task instances that should run simultaneously +# on this airflow installation +parallelism = 8 + +# The number of task instances allowed to run concurrently by the scheduler +dag_concurrency = 4 + +# Are DAGs paused by default at creation +dags_are_paused_at_creation = True + +# When not using pools, tasks are run in the "default pool", +# whose size is guided by this config element +non_pooled_task_slot_count = 128 + +# The maximum number of active DAG runs per DAG +max_active_runs_per_dag = 16 + +# Whether to load the examples that ship with Airflow. It's good to +# get started, but you probably want to set this to False in a production +# environment +load_examples = False + +# Where your Airflow plugins are stored +plugins_folder = /usr/local/airflow/plugins + +# Secret key to save connection passwords in the db +fernet_key = $FERNET_KEY + +# Whether to disable pickling dags +donot_pickle = False + +# How long before timing out a python file import while filling the DagBag +dagbag_import_timeout = 30 + +# The class to use for running task instances in a subprocess +task_runner = BashTaskRunner + +# If set, tasks without a `run_as_user` argument will be run with this user +# Can be used to de-elevate a sudo user running Airflow when executing tasks +default_impersonation = + +# What security module to use (for example kerberos): +security = + +# If set to False enables some unsecure features like Charts and Ad Hoc Queries. +# In 2.0 will default to True. +secure_mode = False + +# Turn unit test mode on (overwrites many configuration options with test +# values at runtime) +unit_test_mode = False + +# Name of handler to read task instance logs. +# Default to use task handler. +task_log_reader = task + +# Whether to enable pickling for xcom (note that this is insecure and allows for +# RCE exploits). This will be deprecated in Airflow 2.0 (be forced to False). +enable_xcom_pickling = True + +# When a task is killed forcefully, this is the amount of time in seconds that +# it has to cleanup after it is sent a SIGTERM, before it is SIGKILLED +killed_task_cleanup_time = 60 + +# Whether to override params with dag_run.conf. If you pass some key-value pairs through `airflow backfill -c` or +# `airflow trigger_dag -c`, the key-value pairs will override the existing ones in params. +dag_run_conf_overrides_params = False + +[cli] +# In what way should the cli access the API. The LocalClient will use the +# database directly, while the json_client will use the api running on the +# webserver +api_client = airflow.api.client.local_client + +# If you set web_server_url_prefix, do NOT forget to append it here, ex: +# endpoint_url = http://localhost:8080/myroot +# So api will look like: http://localhost:8080/myroot/api/experimental/... +endpoint_url = http://localhost:8080 + +[api] +# How to authenticate users of the API +auth_backend = airflow.api.auth.backend.default + +[lineage] +# what lineage backend to use +backend = + +[atlas] +sasl_enabled = False +host = +port = 21000 +username = +password = + +[operators] +# The default owner assigned to each new operator, unless +# provided explicitly or passed via `default_args` +default_owner = Airflow +default_cpus = 1 +default_ram = 1024 +default_disk = 1024 +default_gpus = 0 + +[hive] +# Default mapreduce queue for HiveOperator tasks +default_hive_mapred_queue = + +[webserver] +# The base url of your website as airflow cannot guess what domain or +# cname you are using. This is used in automated emails that +# airflow sends to point links to the right web server +base_url = http://localhost:8080 + +# The ip specified when starting the web server +web_server_host = 0.0.0.0 + +# The port on which to run the web server +web_server_port = 8080 + +# Paths to the SSL certificate and key for the web server. When both are +# provided SSL will be enabled. This does not change the web server port. +web_server_ssl_cert = +web_server_ssl_key = + +# Number of seconds the webserver waits before killing gunicorn master that doesn't respond +web_server_master_timeout = 120 + +# Number of seconds the gunicorn webserver waits before timing out on a worker +web_server_worker_timeout = 120 + +# Number of workers to refresh at a time. When set to 0, worker refresh is +# disabled. When nonzero, airflow periodically refreshes webserver workers by +# bringing up new ones and killing old ones. +worker_refresh_batch_size = 1 + +# Number of seconds to wait before refreshing a batch of workers. +worker_refresh_interval = 30 + +# Secret key used to run your flask app +secret_key = temporary_key + +# Number of workers to run the Gunicorn web server +workers = 4 + +# The worker class gunicorn should use. Choices include +# sync (default), eventlet, gevent +worker_class = sync + +# Log files for the gunicorn webserver. '-' means log to stderr. +access_logfile = - +error_logfile = - + +# Expose the configuration file in the web server +expose_config = False + +# Set to true to turn on authentication: +# https://airflow.incubator.apache.org/security.html#web-authentication +authenticate = False + +# Filter the list of dags by owner name (requires authentication to be enabled) +filter_by_owner = False + +# Filtering mode. Choices include user (default) and ldapgroup. +# Ldap group filtering requires using the ldap backend +# +# Note that the ldap server needs the "memberOf" overlay to be set up +# in order to user the ldapgroup mode. +owner_mode = user + +# Default DAG view. Valid values are: +# tree, graph, duration, gantt, landing_times +dag_default_view = tree + +# Default DAG orientation. Valid values are: +# LR (Left->Right), TB (Top->Bottom), RL (Right->Left), BT (Bottom->Top) +dag_orientation = LR + +# Puts the webserver in demonstration mode; blurs the names of Operators for +# privacy. +demo_mode = False + +# The amount of time (in secs) webserver will wait for initial handshake +# while fetching logs from other worker machine +log_fetch_timeout_sec = 5 + +# By default, the webserver shows paused DAGs. Flip this to hide paused +# DAGs by default +hide_paused_dags_by_default = False + +# Consistent page size across all listing views in the UI +page_size = 100 + +# Use FAB-based webserver with RBAC feature +rbac = False + +# Define the color of navigation bar +navbar_color = #007A87 + +# Default dagrun to show in UI +default_dag_run_display_number = 25 + +[email] +email_backend = airflow.utils.email.send_email_smtp + +[smtp] +# If you want airflow to send emails on retries, failure, and you want to use +# the airflow.utils.email.send_email_smtp function, you have to configure an +# smtp server here +smtp_host = localhost +smtp_starttls = True +smtp_ssl = False +# Uncomment and set the user/pass settings if you want to use SMTP AUTH +# smtp_user = airflow +# smtp_password = airflow +smtp_port = 25 +smtp_mail_from = airflow@example.com + +[celery] +# This section only applies if you are using the CeleryExecutor in +# [core] section above + +# The app name that will be used by celery +celery_app_name = airflow.executors.celery_executor + +# The concurrency that will be used when starting workers with the +# "airflow worker" command. This defines the number of task instances that +# a worker will take, so size up your workers based on the resources on +# your worker box and the nature of your tasks +worker_concurrency = 16 + +# When you start an airflow worker, airflow starts a tiny web server +# subprocess to serve the workers local log files to the airflow main +# web server, who then builds pages and sends them to users. This defines +# the port on which the logs are served. It needs to be unused, and open +# visible from the main web server to connect into the workers. +worker_log_server_port = 8793 + +# The Celery broker URL. Celery supports RabbitMQ, Redis and experimentally +# a sqlalchemy database. Refer to the Celery documentation for more +# information. +broker_url = redis://redis:6379/1 + +# Another key Celery setting +result_backend = db+postgresql://airflow:airflow@postgres/airflow + +# Celery Flower is a sweet UI for Celery. Airflow has a shortcut to start +# it `airflow flower`. This defines the IP that Celery Flower runs on +flower_host = 0.0.0.0 + +# The root URL for Flower +# Ex: flower_url_prefix = /flower +flower_url_prefix = + +# This defines the port that Celery Flower runs on +flower_port = 5555 + +# Default queue that tasks get assigned to and that worker listen on. +default_queue = default + +# Import path for celery configuration options +celery_config_options = airflow.config_templates.default_celery.DEFAULT_CELERY_CONFIG + +# In case of using SSL +ssl_active = False +ssl_key = +ssl_cert = +ssl_cacert = + +[celery_broker_transport_options] +# This section is for specifying options which can be passed to the +# underlying celery broker transport. See: +# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-broker_transport_options + +# The visibility timeout defines the number of seconds to wait for the worker +# to acknowledge the task before the message is redelivered to another worker. +# Make sure to increase the visibility timeout to match the time of the longest +# ETA you're planning to use. +# +# visibility_timeout is only supported for Redis and SQS celery brokers. +# See: +# http://docs.celeryproject.org/en/master/userguide/configuration.html#std:setting-broker_transport_options +# +#visibility_timeout = 21600 + +[dask] +# This section only applies if you are using the DaskExecutor in +# [core] section above + +# The IP address and port of the Dask cluster's scheduler. +cluster_address = 127.0.0.1:8786 +# TLS/ SSL settings to access a secured Dask scheduler. +tls_ca = +tls_cert = +tls_key = + +[scheduler] +# Task instances listen for external kill signal (when you clear tasks +# from the CLI or the UI), this defines the frequency at which they should +# listen (in seconds). +job_heartbeat_sec = 5 + +# The scheduler constantly tries to trigger new tasks (look at the +# scheduler section in the docs for more information). This defines +# how often the scheduler should run (in seconds). +scheduler_heartbeat_sec = 5 + +# after how much time should the scheduler terminate in seconds +# -1 indicates to run continuously (see also num_runs) +run_duration = -1 + +# after how much time a new DAGs should be picked up from the filesystem +min_file_process_interval = 0 + +# How many seconds to wait between file-parsing loops to prevent the logs from being spammed. +min_file_parsing_loop_time = 1 + +dag_dir_list_interval = 300 + +# How often should stats be printed to the logs +print_stats_interval = 30 + +child_process_log_directory = /usr/local/airflow/logs/scheduler + +# Local task jobs periodically heartbeat to the DB. If the job has +# not heartbeat in this many seconds, the scheduler will mark the +# associated task instance as failed and will re-schedule the task. +scheduler_zombie_task_threshold = 300 + +# Turn off scheduler catchup by setting this to False. +# Default behavior is unchanged and +# Command Line Backfills still work, but the scheduler +# will not do scheduler catchup if this is False, +# however it can be set on a per DAG basis in the +# DAG definition (catchup) +catchup_by_default = True + +# This changes the batch size of queries in the scheduling main loop. +# This depends on query length limits and how long you are willing to hold locks. +# 0 for no limit +max_tis_per_query = 512 + +# Statsd (https://github.com/etsy/statsd) integration settings +statsd_on = False +statsd_host = localhost +statsd_port = 8125 +statsd_prefix = airflow + +# The scheduler can run multiple threads in parallel to schedule dags. +# This defines how many threads will run. +max_threads = 2 + +authenticate = False + +[ldap] +# set this to ldaps://: +uri = +user_filter = objectClass=* +user_name_attr = uid +group_member_attr = memberOf +superuser_filter = +data_profiler_filter = +bind_user = cn=Manager,dc=example,dc=com +bind_password = insecure +basedn = dc=example,dc=com +cacert = /etc/ca/ldap_ca.crt +search_scope = LEVEL + +[mesos] +# Mesos master address which MesosExecutor will connect to. +master = localhost:5050 + +# The framework name which Airflow scheduler will register itself as on mesos +framework_name = Airflow + +# Number of cpu cores required for running one task instance using +# 'airflow run --local -p ' +# command on a mesos slave +task_cpu = 1 + +# Memory in MB required for running one task instance using +# 'airflow run --local -p ' +# command on a mesos slave +task_memory = 256 + +# Enable framework checkpointing for mesos +# See http://mesos.apache.org/documentation/latest/slave-recovery/ +checkpoint = False + +# Failover timeout in milliseconds. +# When checkpointing is enabled and this option is set, Mesos waits +# until the configured timeout for +# the MesosExecutor framework to re-register after a failover. Mesos +# shuts down running tasks if the +# MesosExecutor framework fails to re-register within this timeframe. +# failover_timeout = 604800 + +# Enable framework authentication for mesos +# See http://mesos.apache.org/documentation/latest/configuration/ +authenticate = False + +# Mesos credentials, if authentication is enabled +# default_principal = admin +# default_secret = admin + +# Optional Docker Image to run on slave before running the command +# This image should be accessible from mesos slave i.e mesos slave +# should be able to pull this docker image before executing the command. +# docker_image_slave = puckel/docker-airflow + +[kerberos] +ccache = /tmp/airflow_krb5_ccache +# gets augmented with fqdn +principal = airflow +reinit_frequency = 3600 +kinit_path = kinit +keytab = airflow.keytab + + +[github_enterprise] +api_rev = v3 + +[admin] +# UI to hide sensitive variable fields when set to True +hide_sensitive_variable_fields = True + +[elasticsearch] +elasticsearch_host = +# we need to escape the curly braces by adding an additional curly brace +elasticsearch_log_id_template = {dag_id}-{task_id}-{execution_date}-{try_number} +elasticsearch_end_of_log_mark = end_of_log + +[kubernetes] +# The repository and tag of the Kubernetes Image for the Worker to Run +worker_container_repository = +worker_container_tag = + +# If True (default), worker pods will be deleted upon termination +delete_worker_pods = True + +# The Kubernetes namespace where airflow workers should be created. Defaults to `default` +namespace = default + +# The name of the Kubernetes ConfigMap Containing the Airflow Configuration (this file) +airflow_configmap = + +# For either git sync or volume mounted DAGs, the worker will look in this subpath for DAGs +dags_volume_subpath = + +# For DAGs mounted via a volume claim (mutually exclusive with volume claim) +dags_volume_claim = + +# For volume mounted logs, the worker will look in this subpath for logs +logs_volume_subpath = + +# A shared volume claim for the logs +logs_volume_claim = + +# Git credentials and repository for DAGs mounted via Git (mutually exclusive with volume claim) +git_repo = +git_branch = +git_user = +git_password = +git_subpath = + +# For cloning DAGs from git repositories into volumes: https://github.com/kubernetes/git-sync +git_sync_container_repository = gcr.io/google-containers/git-sync-amd64 +git_sync_container_tag = v2.0.5 +git_sync_init_container_name = git-sync-clone + +# The name of the Kubernetes service account to be associated with airflow workers, if any. +# Service accounts are required for workers that require access to secrets or cluster resources. +# See the Kubernetes RBAC documentation for more: +# https://kubernetes.io/docs/admin/authorization/rbac/ +worker_service_account_name = + +# Any image pull secrets to be given to worker pods, If more than one secret is +# required, provide a comma separated list: secret_a,secret_b +image_pull_secrets = + +# GCP Service Account Keys to be provided to tasks run on Kubernetes Executors +# Should be supplied in the format: key-name-1:key-path-1,key-name-2:key-path-2 +gcp_service_account_keys = + +# Use the service account kubernetes gives to pods to connect to kubernetes cluster. +# It's intended for clients that expect to be running inside a pod running on kubernetes. +# It will raise an exception if called from a process not running in a kubernetes environment. +in_cluster = True + +[kubernetes_secrets] +# The scheduler mounts the following secrets into your workers as they are launched by the +# scheduler. You may define as many secrets as needed and the kubernetes launcher will parse the +# defined secrets and mount them as secret environment variables in the launched workers. +# Secrets in this section are defined as follows +# = : +# +# For example if you wanted to mount a kubernetes secret key named `postgres_password` from the +# kubernetes secret object `airflow-secret` as the environment variable `POSTGRES_PASSWORD` into +# your workers you would follow the following format: +# POSTGRES_PASSWORD = airflow-secret:postgres_credentials +# +# Additionally you may override worker airflow settings with the AIRFLOW__
__ +# formatting as supported by airflow normally. \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1e9b380 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3' +services: + postgres: + container_name: airflowmeta_example + image: postgres:9.6.3 + environment: + POSTGRES_PASSWORD: airflow123 + POSTGRES_USER: airflow + POSTGRES_DB: airflow + ports: + - "5432:5432" + volumes: + - ./.data/pg:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5432"] + interval: 30s + timeout: 10s + retries: 5 + airflow: + build: . + image: airflow_compose:latest + restart: on-failure + environment: + AIRFLOW__CORE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow123@postgres:5432/airflow + ports: + - "8080:8080" + - "5555:5555" + - "8793:8793" + depends_on: + - postgres + links: + - postgres:postgres \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..036d8c5 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +pytest +flake8 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c06c8f0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,59 @@ +apache-airflow[celery,crypto,emr,hive,hdfs,ldap,mysql,postgres,redis,slack,s3,kubernetes]==1.10.2 +# git+git://github.com/apache/airflow.git@de75b7a2bd7f5bef6a1d09942e0b43c17a3fbb95#egg=master # https://stackoverflow.com/questions/20101834/pip-install-from-git-repo-branch/20101940#20101940 +celery[redis]>=4.1.1,<4.2.0 +cython==0.29 +alembic==0.8.10 +certifi==2018.4.16 +chardet==3.0.4 +click==6.7 +croniter==0.3.24 +dill==0.2.8.2 +docutils==0.14 +Flask==0.12.4 +flask-admin==1.5.2 +Flask-Cache==0.13.1 +Flask-Login<0.5,>=0.3 +flask-swagger==0.2.13 +Flask-WTF==0.14.2 +funcsigs==1.0.0 +future==0.16.0 +gitdb2==2.0.4 +GitPython==2.1.11 +gunicorn==19.5.0 +idna==2.7 +itsdangerous==0.24 +Jinja2==2.8.1 +lockfile==0.12.2 +lxml>=4.0.0 +Mako==1.0.7 +Markdown==2.6.11 +MarkupSafe==1.0 +numpy==1.15.0 +ordereddict==1.1 +pandas==0.23.3 +psutil==4.4.2 +Pygments==2.2.0 +python-daemon==2.1.2 +python-dateutil==2.7.3 +python-editor==1.0.3 +python-nvd3==0.15.0 +python-slugify==1.2.5 +pytz==2018.5 +PyYAML==3.13 +requests<3,>=2.20.0 +setproctitle==1.1.10 +six==1.11.0 +smmap2==2.0.4 +SQLAlchemy==1.1.15 +tabulate==0.7.7 +thrift==0.9.3 +Unidecode==1.0.22 +urllib3==1.23 +Werkzeug==0.14.1 +WTForms==2.2.1 +zope.deprecation==4.3.0 +pyOpenSSL==18.0.0 +ndg-httpsclient==0.5.1 +pyasn1==0.4.4 +newrelic==4.6.0.106 +newrelic-api \ No newline at end of file diff --git a/script/entrypoint.sh b/script/entrypoint.sh new file mode 100755 index 0000000..9e32685 --- /dev/null +++ b/script/entrypoint.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash + +TRY_LOOP="20" + +# Defaults and back-compat +: "${AIRFLOW__CORE__FERNET_KEY:=${FERNET_KEY:=$(python -c "from cryptography.fernet import Fernet; FERNET_KEY = Fernet.generate_key().decode(); print(FERNET_KEY)")}}" +: "${AIRFLOW__CORE__EXECUTOR:=${EXECUTOR:-Local}Executor}" + +export \ + AIRFLOW__CELERY__BROKER_URL \ + AIRFLOW__CELERY__RESULT_BACKEND \ + AIRFLOW__CORE__EXECUTOR \ + AIRFLOW__CORE__FERNET_KEY \ + AIRFLOW__CORE__LOAD_EXAMPLES \ + AIRFLOW__CORE__SQL_ALCHEMY_CONN \ + + +# Load DAGs exemples (default: Yes) +if [[ -z "$AIRFLOW__CORE__LOAD_EXAMPLES" && "${LOAD_EX:=n}" == n ]] +then + AIRFLOW__CORE__LOAD_EXAMPLES=False +fi + +wait_for_port() { + local name="$1" host="$2" port="$3" + local j=0 + while ! nc -z "$host" "$port" >/dev/null 2>&1 < /dev/null; do + j=$((j+1)) + if [ $j -ge $TRY_LOOP ]; then + echo >&2 "$(date) - $host:$port still not reachable, giving up" + exit 1 + fi + echo "$(date) - waiting for $name... $j/$TRY_LOOP" + sleep 5 + done +} + +export NEW_RELIC_CONFIG_FILE + +case "$1" in + webserver) + newrelic-admin run-program airflow upgradedb + if [ "$AIRFLOW__CORE__EXECUTOR" = "LocalExecutor" ]; then + # With the "Local" executor it should all run in one container. + newrelic-admin run-program airflow scheduler & + fi + if [ "$AIRFLOW__CORE__EXECUTOR" = "SequentialExecutor" ]; then + newrelic-admin run-program airflow scheduler & + # newrelic-admin run-program airflow webserver & + fi + # exec newrelic-admin run-program airflow scheduler + + exec newrelic-admin run-program airflow webserver + ;; + worker|scheduler|flower|version) + # To give the webserver time to run initdb. + sleep 10 + exec newrelic-admin run-program airflow "$@" + ;; + *) + # The command is something like bash, not an airflow subcommand. Just run it in the right environment. + exec newrelic-admin run-program "$@" + ;; +esac