この記事は、弁護士ドットコム Advent Calendar 2024 の 22 日目の記事です。
こんにちは。弁護士ドットコム 技術戦略本部でSREをしています山崎です。
今年の初めに3ヶ月間の育休から復帰しました。長期休暇は社会人として珍しい経験だったため、新鮮な気持ちでした。
背景
復帰後に、子どもが生まれる前と比べて仕事に集中できる時間(工数)が大きく異なることを実感しました。
当社のエンジニアは、チームの働き方に合わせて柔軟にリモートワークを選択できる環境が整っています。
しかし、家で仕事をしていると、子どもがいつ自分を求めてくるか分からない状況があります。 このため、できるだけ作業を自動化・効率化したいと考えるようになりました。
また、運用上の課題として、メンテナンスが早朝作業になることが挙げられます。 早起きや夜間作業が苦手な方も多いのではないでしょうか。
今回は、その一環として取り組んだAWSサービス更新の自動化について紹介します。
マネージドサービス更新
当チームが管理しているAWSリソースには、RDS、OpenSearch、ElastiCacheなどがあります。 これらのリソースに月次でアップデートがないか確認し、必要に応じて更新しています。
この作業は、新機能を利用するためというよりも、セキュリティ向上のための対応です。
AWSコンソールから確認することもあれば、Slackと連携して通知させる仕組みも活用しています。
元々はAWSコンソール上から作業していましたが、作業事故防止や手順書作成の工数削減の観点から、AWS CLIを使った作業に移行しました。
例えば、以下のようなコマンドを利用します。
aws opensearch upgrade-domain --domain-name <ドメイン名> --target-version OpenSearch_2.7
しかし、手動実行は手間がかかり、手順書作成や作業ミスのリスクも伴います。
そこで、チーム内で自動化する方針を立てました。
OpenSerchのバージョン
OpenSearchには次の2種類のバージョンがあります。
・バージョン:
例として、OpenSearch 2.15
のような形式で、更新頻度は少ないです。
・サービスソフトウェアのバージョン:
例として 、OpenSearch_2_15_R20240904-P4
のような形式で、パッチ更新があり、1ヶ月に1回程度の頻度で更新されます。
どちらか片方のみ対応しても不十分なため、今回の自動化では両方のバージョンに対応するようにしました。
OpenSerchマネージドサービス更新の自動化
当社のシステム上、影響が少ないOpenSearchのアップデートを優先的に自動化しました。
検討した実装方法の例として、以下のものが挙げられます。
- Health -> EventBridge -> Codebuild
- Health -> EventBridge -> Lambda
- Health -> EventBridge -> CDK
しかし、AWSサポートに問い合わせたところ、Healthを契機にアップデートを実施するのは難しいとの回答を得ました。
また、完全自動化するとSREチーム以外への周知が必要になるため、以下の構成で実現しました。
EventBridgeのスケジュール機能を使うことで、任意のタイミングで実行可能にしています。
アップデート実行時の通知について
Lambdaを使ったアップデート処理には、会社で利用している「Slack」への通知機能を実装しました。
Slack処理はこちら
SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL'] # ログ設定 logger = logging.getLogger() logger.setLevel(logging.INFO) my_config = Config( region_name='ap-northeast-1' ) client = boto3.client('opensearch', config=my_config) def lambda_handler(event, context): if 'DomainName' in event: domain_name = event['DomainName'] update_with_version_check(domain_name) else: print('対象のドメインが存在しません') def slack(title, details): slack_message = { 'username': "Lambda Execution Result", 'text': title, 'attachments': [ { "color": "#36a64f", "text": details } ] } req = Request(SLACK_WEBHOOK_URL, json.dumps(slack_message).encode('utf-8')) try: response = urlopen(req) response.read() logger.info("Message posted to %s", SLACK_WEBHOOK_URL) except HTTPError as e: logger.error("Request failed: %d %s", e.code, e.reason) except URLError as e: logger.error("Server connection failed: %s", e.reason)
アップデート実行時に以下の情報が通知されます。
- 対象ドメイン
- アップデート前のバージョン
- アップデート後のバージョン
通知の例を以下に示します。
- バージョン更新がある場合: 対象のバージョンを含むメッセージ
- バージョン更新がない場合: 「更新なし」のメッセージ
Slackを利用していない場合は、他の通知方法を適宜検討してください。
アップデート処理の内容
バージョンアップされると、ソフトウェアバージョンも最新となる。 深く考える必要はないですが、バージョン更新された場合ソフトウェアバージョンの更新は必要ないです。
アップデート処理はこちら
# バージョン確認と更新処理 def update_with_version_check(domain_name): try: # バージョン確認 response = invoke_opensearch_compatible_versions(domain_name) target_versions = response.get('CompatibleVersions', [{}])[0].get('TargetVersions') version = response.get('CompatibleVersions', [{}])[0].get('SourceVersion') if target_versions: target_version = target_versions[0] # バージョンがあるときバージョンアップ upgrade_opensearch_version(domain_name, target_version) print(f"OpenSearchのバージョンを {target_version} にアップグレードしています") message = f"OpenSearch 自動更新実行. 対象ドメイン: {domain_name} " message1 = f"Version: ['{version}'] -> {target_versions}" slack(message, message1) else: # バージョンがないとき、サービスソフトウェアの更新を実行 get_update_status(domain_name) print(f"サービスソフトウェアの更新を確認しています") except Exception as e: print(f"エラーが発生しました: {e}") # バージョン確認 def invoke_opensearch_compatible_versions(domain_name): client = boto3.client('opensearch') response = client.get_compatible_versions(DomainName=domain_name) return response # バージョンアップグレード def upgrade_opensearch_version(domain_name, new_version): client = boto3.client('opensearch') response = client.upgrade_domain( DomainName=domain_name, TargetVersion=new_version ) print('Upgrading domain to ' + new_version + '...') # 更新対象のドメインのサービスソフトウェア更新 def get_update_status(domain_name): try: response = client.describe_domain(DomainName=domain_name) sso = response['DomainStatus']['ServiceSoftwareOptions'] if sso['UpdateStatus'] == 'ELIGIBLE': print('ドメイン [' + domain_name + '] は、バージョン ' + sso['CurrentVersion'] + ' からバージョン ' + sso['NewVersion'] + ' にサービスソフトウェアを更新することが可能です') update_domain(domain_name) message = f"OpenSearch 自動更新実行. 対象ドメイン: {domain_name} " message1 = f'SoftwareVersion: [' + sso['CurrentVersion'] + '] -> [' + sso['NewVersion'] + ']' slack(message, message1) else: print('現在、ドメインは更新の対象ではありません。') message = f"OpenSearch 自動更新実行. 対象ドメイン: {domain_name} " message1 = f"現在、ドメインは更新の対象ではありません" slack(message, message1) except ClientError as e: if e.response['Error']['Code'] == 'ResourceNotFoundException': print('指定されたドメインが見つかりませんでした。') else: print('エラーが発生しました: {}'.format(e)) # サービスソフトウェア更新 def update_domain(domain_name): response = client.start_service_software_update(DomainName=domain_name) print('ドメイン [' + domain_name + '] をバージョン ' + response['ServiceSoftwareOptions']['NewVersion'] + ' に更新しています...')
実行にはEventBridgeを採用しました。
定期実行として、月1回や3ヶ月に1回の頻度で設定するのが適切だと考えています。
自動化して感じたこと
SREに限らず、手動作業を自動化できることは大きな喜びです。
たとえ数分で終わる作業でも、毎日繰り返していると面倒に感じてしまうものです。
自動化を進める際には、エラー時の影響が少ない作業や、人が監視しなくてもよいタスクを選定する必要があります。
今回、早朝の手動作業が1つ減ったことで、非常に快適になりました。
おわりに
今回は毎月実施していた手作業を自動化した事例として、OpenSearchのサービス更新自動化について紹介しました。
RDSなど他のリソースも同様に自動化したい気持ちはありますが、バグや切り戻しを考慮すると、慎重な対応が必要だと感じています。
引き続き改善を進め、より洗練された解決策を追求し続けています。
この内容が少しでも参考になれば幸いです。