airflow를 잘 쓰고 있었는데 갑자기 UI에서 Connections 메뉴에 들어가면 아래와 같은 오류가 발생하기 시작했다.
Can't decrypt encrypted password for login=ADMIN, FERNET_KEY configuration is missing
처음에는 config에 FERNET_KEY 가 없어서 생긴 문제인 줄 알고 AIRFLOW__CORE__FERNET_KEY
를 환경변수로 추가했는데도 비슷한 오류가 발생했다.
cryptography.fernet.InvalidToken
이리저리 검색했는데 해결 방법은 airflow db reset
뿐이었고, QA 환경에서는 AIRFLOW__CORE__FERNET_KEY
를 추가하고 db reset을 해 해결했지만,
운영 환경에서는 db reset을 하면 모든 수행기록이 날아가 곤란한 상황이었다.
그래서 도대체 문제가 뭔지 코드를 열어봤다.
오류가 발생하는 곳은 이 곳이었다.
# airflow/models/connection.py
...
def get_password(self) -> Optional[str]:
"""Return encrypted password."""
if self._password and self.is_encrypted:
fernet = get_fernet()
if not fernet.is_encrypted:
raise AirflowException(
"Can't decrypt encrypted password for login={}, \
FERNET_KEY configuration is missing".format(
self.login
)
)
return fernet.decrypt(bytes(self._password, 'utf-8')).decode()
else:
return self._password
self._password
와 self.is_encrypted
가 True이면 fernet
을 가져오는데, 이 때 fernet.is_encrypted
가 False이면 raise한다.
그럼 어떤 상황에서 fernet.is_encrypted
가 False인지 확인해보자.
get_fernet()
은 여기 있다.
# airflow/models/crypto.py
...
def get_fernet():
"""
Deferred load of Fernet key.
This function could fail either because Cryptography is not installed
or because the Fernet key is invalid.
:return: Fernet object
:raises: airflow.exceptions.AirflowException if there's a problem trying to load Fernet
"""
global _fernet # pylint: disable=global-statement
if _fernet:
return _fernet
try:
fernet_key = conf.get('core', 'FERNET_KEY')
if not fernet_key:
log.warning("empty cryptography key - values will not be stored encrypted.")
_fernet = NullFernet()
else:
_fernet = MultiFernet(
[Fernet(fernet_part.encode('utf-8')) for fernet_part in fernet_key.split(',')]
)
_fernet.is_encrypted = True
except (ValueError, TypeError) as value_error:
raise AirflowException(f"Could not create Fernet object: {value_error}")
return _fernet
config로 따로 설정한 FERNET_KEY가 없는 경우 fernet.is_encrypted
가 False가 되는 것 같다. (FERNET_KEY가 있는 경우 명시적으로 True가 세팅되므로)
하지만 우리는 FERNET_KEY를 설정하지 않고도 그동안 잘 사용하고 있었다. 그럼 self._password
와 self.is_encrypted
가 True가 되는 부분이 문제 아닐까?
이 생각이 들고 나서야 get_password
메소드가 있는 Connection 클래스가 뭔지 자세히 봤다.
# airflow/models/connection.py
...
class Connection(Base, LoggingMixin): # pylint: disable=too-many-instance-attributes
"""
Placeholder to store information about different database instances
connection information. The idea here is that scripts use references to
database instances (conn_id) instead of hard coding hostname, logins and
passwords when using operators or hooks.
...
Connection은 metadata db의 connection 테이블에서 커넥션 정보를 들고오는 클래스이며, self._password
와 self.is_encrypted
에는 테이블의 각 row에 들어있는 정보가 그대로 담긴다.
그리고 운영에서 사용중인 metadata db의 connection 테이블에는 airflow가 예시로 넣어놓은 (쓸모없는) 커넥션 중 password
도 존재하면서 is_encrypted
가 True인 커넥션이 몇 개 있었다.
이것들을 모두 is_encrypted=False
로 바꿔주니 connections 메뉴를 정상적으로 조회할 수 있었다.
와...멋지네요!!