Posts
TL;DR Context: internalスコープのSwiftソース定義を一括でpublic化したい場合(モジュール切出しなどの文脈) Problem: 正規表現によるコード変換では対応しきるのは難しい Solution: SwiftSyntaxのSyntaxRewriterをつかうことで、セマンティックなコード変換を比較的容易に実装できる Result: https://github.com/hrfmmr/swift-public-rewriter Context Swiftプロジェクトで、モデル類のソースを別のモジュールに切り出すといった作業が嵩むことがあり、効率的に行いたいというモチベーションがあった
単体のソースとしてはいずれも以下のようなもの
struct Foo { let x: Int let bar: Bar init(x: Int, bar: Bar) { self.x = x self.bar = bar } func doSomething() { let a = ... var b = ... } } extension Foo { struct InnerFoo {} } enum Bar { case x var hoge: Int { switch self { case .x: ... } } } class Baz { private(set) var x: Int = ... lazy var y: Int = ... } こういったソースをモジュールに切り出した場合、外部のモジュールから参照できるようにするには、public修飾子を付与する必要がある
Read moreContext スケジュール管理のあり方 比較的規模のある開発計画を引くとき、全体像と着地予想を立てる上でガントチャートを引く 全体像や依存関係/クリティカルパスを把握する可視化ツールとして有用 一方で、これをベースに進行管理していくと「間に合うか/間に合わないか」でしか状況評価できない側面がある 想定通りに推移しない場合、「間に合わない」時点で問題が発覚しがち 「テコ入れの必要性」「抱え込んでいる不確実性/不安量」を観測/検査したい 2つの不確実性の見える化
a.) スケジュール不安 b.) プロジェクトの不確実性の所在 スケジュール不安の可視化 順調さをどう検査するか
バッファ消費率という考え方
CCPMにおけるバッファマネジメント 不確実性管理のために、タスクの終了期限ではなくプロジェクトバッファの管理にフォーカスを当てる タスク毎にバッファを設定せず、プロジェクト全体で1つのバッファをもつ 進捗に応じて、バッファ消費率を観測する バッファ消費率の観測のなにがうれしいか バッファ消費率の見える化によって、進行状況がgreenかyellowかredかを検査できる 赤線: redゾーン境界のバッファ消費率(完了時点90%消費)
これより上にプロットされていると、危険域 テコ入れの検討を行う 黄線: greenゾーン境界のバッファ消費率(完了時点40%消費)
これより下にプロットされていると、安全域 ガントチャートとの違い
ガントでも「遅れている」状況自体は把握できる 早期検知できるかどうか
ガントだとバッファが消費されきった時点で「遅れ」に気付く バッファ消費率ベースの観測では、消費しきる前の段階で「遅れ」具合を検知してリカバリのアクションがとれる バッファ消費率の算出
消費バッファ量 / 全体のバッファ量 * 100 消費バッファ量 あるタスクが計画では2日で完了する予定だったが、実際には3日かかった場合、1日分の遅れが発生し、これがバッファ消費とみなされる プロジェクト全体の工数が10日で25%のバッファを設定していた場合、バッファ消費率は(1/(10*0.25)) * 100 = 40% 進捗率の算出
どう進捗率を評価するか 完成度(「どこまでできているか」)ベースの評価 「10の内7までdoneしているので70%」? 70%に至るまでの推移と残りの推移は果たしてリニアな関係性(逆算可能といえる)か 結局のところ「残り3」がどれくらいかに依る 「残り3」の係数がわからないことには着地予想がつかない(70%という数字を正しく評価できない) 完成度ベースで定量的に「残り3」だとしても、「(全体工数10日タスクに対して)日数的にはあと1週間かかりそう」という状況なら、70%と捉えてよいものか? 完成度で進捗率評価しても、スケジュール管理する上ではミスリードを招きかねない 不安量を可視化/管理する上では、「どこまでできているか」より「あとどれくらいかかるか」の方が都合が良い 都合が良いwhat 織り込まれている情報量が違う 「どこまでできているか」ベースの進捗評価は、およそ当初計画に対しての完成度にとどまる モノづくりにおいては、完成度が高まるにつれ、作り変える余地や直しどころもまた必然的に発生してくるもの 進行途中に生まれた当初計画外の「やるべきこと」は、完成度ベースの進捗評価には織り込まれていない(がち) 「あとどれくらいかかるか」ベースの進捗評価では、最新のステータスがすべて反映された上で出されるので、その分信頼が置ける 「あとどれくらいかかるか」ベースの進捗評価方法 予定工数10日に対して「あと3日」という状況なら70%と解釈できる (完了の定義の話はここでは割愛) バイアスがかかりづらく且つ答えやすいように、楽観/平均/悲観の3点で見積るとよい
楽観と悲観の差が「不安量」といえる 差が顕著に大きい場合、「何が明らかになると、差が縮まるか」という問いから、抱え込んでいる不確実性をブレークダウンしていける プロジェクトの不確実性の所在 前提として、「プロジェクトの不確実性をなるたけ早期に解決できていることが望ましい」という考え方をもつとして (「どう転ぶかわからない」要素がなく、「やるだけ。作業。」なタスクのみで構成されていればいるほど、見通しが立てやすく、着地させやすい)
Read morenginxのトレース情報を収集できるDatadog nginxモジュールのv1.0.0が最近リリースされていたので試した
ref. https://docs.datadoghq.com/tracing/trace_collection/proxy_setup/?tab=nginx#nginx-with-datadog-module
docker-compose.yml
version: "3.7" services: api: build: context: ./api dockerfile: Dockerfile args: DD_API_KEY: ${DD_API_KEY} container_name: sample-api ports: - 8080:8080 environment: - DD_ENV=dev - DD_HOSTNAME=local nginx: build: context: ./nginx dockerfile: Dockerfile container_name: sample-nginx image: sample-nginx labels: com.datadoghq.tags.env: 'dev' com.datadoghq.tags.service: 'sample-nginx' com.datadoghq.tags.version: '0.1.0' com.datadoghq.ad.check_names: '["nginx"]' com.datadoghq.ad.init_configs: '[{}]' com.datadoghq.ad.instances: '[{"nginx_status_url": "http://%%host%%:81/nginx_status/"}]' com.datadoghq.ad.logs: '[{"source": "nginx", "service": "sample-nginx"}]' volumes: - './nginx/nginx.conf:/etc/nginx/nginx.conf:ro' ports: - "8888:80" environment: DD_AGENT_HOST: datadog DD_TRACE_AGENT_PORT: 8126 datadog: image: datadog/agent:7 container_name: sample-ddagent environment: - DD_API_KEY - DD_SITE=datadoghq.com - DD_ENV=dev - DD_HOSTNAME=local - DD_LOGS_ENABLED=true - DD_LOGS_CONFIG_CONTAINER_COLLET_ALL=true - DD_APM_NON_LOCAL_TRAFFIC=true volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - /proc/:/host/proc/:ro - /sys/fs/cgroup/:/host/sys/fs/cgroup:ro ports: - "8126:8126/tcp" 構成 nginx、バックエンドapi、Datadog Agent用コンテナの3つからなる構成 nginx nginxコンテナにはDatadogのAutodiscovery機能を有効にするために、Dockerオブジェクトラベルを付与することでdd-agentとの疎通エンドポイントやDatadog Logsに関する設定を施している ラベルを付与することで単なるメタデータとしてでなく、一種の設定値として意味をもたせる扱い方はなるほど..となった(こうした活用方法をみたことがなかった) (上記設定自体はDatadog Logsに関するもので、nginx-datadogモジュールによるトレーシングとは無関係) nginx/Dockerfile
Read moreという巷によくありそうなモノをつくった。
kidsを保育園に預けていて、定期的に園からのおたよりで「mm/ddまでにxxxをもってきてほしい/つくってきてほしい」といった要求が届く。「歯磨きするから歯ブラシをビニールケースに入れて」「箸を練習していきます。箸を箸袋に入れてそれを昼食袋に」「プール開きに伴いxxx, yyy, zzzを入れたプールバッグを斯々然々..」といったもの。
弊家は家庭ナレッジをすべてScrapboxに集約していて、園からのおたよりもまた例によってScrapboxにアーカイブされている。というところで、記録は問題なくできている一方で、記憶がトぶことがしばしばあり、dueまでに指定のモノを用意できない/持参し忘れるということがしばしばあった。
カレンダに予定を入れておいてもなにかと忘れ、都度Slackのリマインダ通知を設定するのも作業的に面倒くさくてやってられない…と業を煮やした末、「対応を忘れないようにするための通知」を自動化しようとおもった。 (タスク管理といえばタスク管理だから専用のツールで管理という選択肢はあるのだけれども、家庭で扱うツールはなるたけ増やしたくないという事情もあった。ツール増やした先でコメント機能なんかがあるとコンテキストやナレッジが分散しがちでそれは避けたいなど)
それ系のGoogleカレンダ+Slackなインテグレーションはおあつらえのものがきっとあるとおもって探したが、ほしかったものはなさそうだったので自前でつくることにした。
つくったもの AWS Lambda+EventBridgeベースのスケジューラ EventBridgeでLambdaを定期実行するスケジューラを定義 LambdaはGoogleCalendar APIを叩いて向こう1週間分の指定カレンダの予定を取得 1日後/3日後/1週間後にあたるイベントを選別し、指定Slack channelに通知 Slack通知サンプル Slackアプリのアイコン画像は以下のような呪文でStable Diffusionで生成したらかわいいのが出来た。生成AI便利。 Cute robot holding a calendar in its hand and reading it out in Monet style Lambda(Pythonランタイム)本体のコードとしては以下のようなもの
import datetime import json import logging import os import re from collections import defaultdict from dataclasses import dataclass from enum import Enum from typing import Optional import boto3 import slack_sdk import slack_sdk.errors from google.oauth2 import service_account from googleapiclient.discovery import build as build_google_service logging.basicConfig() logger = logging.getLogger() SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"] GOOGLE_CALENDAR_ID = os.environ["GOOGLE_CALENDAR_ID"] PARAM_KEY_SERVICE_ACCOUNT_JSON = os.environ["PARAM_KEY_SERVICE_ACCOUNT_JSON"] PARAM_KEY_SLACK_API_TOKEN = os.environ["PARAM_KEY_SLACK_API_TOKEN"] SLACK_CHANNEL_ID = os.environ["SLACK_CHANNEL_ID"] REX_EVENT_DESCRIPTION_LINK = re.compile(r'href="([^"]*)"') class ISO8601Formatter(logging.Formatter): def formatTime(self, record, datefmt=None): tz_jst = datetime.timezone(datetime.timedelta(hours=+9), "JST") dt = datetime.datetime.fromtimestamp(record.created, tz_jst) return dt.isoformat() @dataclass class CalendarEvent: date: str summary: str description: str @property def link_url(self) -> Optional[str]: match = REX_EVENT_DESCRIPTION_LINK.search(self.description) if match: return match.group(1) return None class RemainingDays(Enum): ONE_DAY = 1 THREE_DAYS = 3 ONE_WEEK = 7 @property def date(self): target_date = datetime.datetime.now() + datetime.timedelta(days=self.value) return target_date.strftime("%Y-%m-%d") class GoogleCalendarClient: def __init__(self): service_account_json = get_ssm_param(PARAM_KEY_SERVICE_ACCOUNT_JSON) service_account_info = json.loads(service_account_json) creds = service_account.Credentials.from_service_account_info( service_account_info, scopes=SCOPES ) self.service = build_google_service("calendar", "v3", credentials=creds) def get_events(self): now = datetime.datetime.utcnow().isoformat() + "Z" # 'Z' indicates UTC time events_result = ( self.service.events() .list( calendarId=GOOGLE_CALENDAR_ID, timeMin=now, maxResults=10, singleEvents=True, orderBy="startTime", ) .execute() ) events = events_result.get("items", []) if not events: logger.info("No upcoming events found.") for event in events: date = event["start"].get("dateTime", event["start"].get("date")) summary = event.get("summary", "") description = event.get("description", "") yield CalendarEvent(date, summary, description) class SlackClient: def __init__(self): api_token = get_ssm_param(PARAM_KEY_SLACK_API_TOKEN) self.client = slack_sdk.WebClient(token=api_token) def post_schedules(self, channel_id: str, events: list[CalendarEvent]): try: message = self._build_schedule_reminder_message(events) logger.info(f"✉️ Posting message:{message}") self.client.chat_postMessage(channel=channel_id, text=message) except slack_sdk.errors.SlackApiError as e: err = e.response.get("error", "") logger.error(f"❗error:{err}") def _build_schedule_reminder_message(self, events: list[CalendarEvent]) -> str: d = {d_r.date: d_r for d_r in RemainingDays} remaining_days_event_map = defaultdict(list) for event in events: if event.date in d: d_r = d[event.date] remaining_days_event_map[d_r.value].append(event) lines: list[str] = [] lines.append(self._build_schedule_reminder_title_text()) for k in sorted(remaining_days_event_map.keys()): events = remaining_days_event_map[k] lines.append(self._build_schedule_reminder_section_text(k)) for event in events: lines.append( self._build_list_item_text( self._build_schedule_reminder_event_text(event) ) ) lines.append("\n") return "\n".join(lines) def _build_schedule_reminder_title_text(self) -> str: return "Hey <!channel>, here is the upcoming schedules🗓️" def _build_schedule_reminder_section_text(self, days_remaining: int) -> str: match days_remaining: case 1: title = "tomorrow" case 7: title = "in a week" case _: title = f"in {days_remaining} days" padding = "=" * 20 return self._build_bold_text(padding + f"{title}" + padding) def _build_schedule_reminder_event_text(self, event: CalendarEvent) -> str: date_text = f"{event.date}({get_weekday(event.date)}):" if event.link_url: return ( f"{date_text}" f"{self._build_link_text(event.summary, event.link_url)}" ) else: return f"{date_text}" f"{event.summary} {event.description}" def _build_link_text(self, text: str, url: str) -> str: return f"<{url}|{text}>" def _build_bold_text(self, text) -> str: return f"*{text}*" def _build_list_item_text(self, text) -> str: return f"• {text}" def get_ssm_param(param_key): ssm = boto3.client("ssm") response = ssm.get_parameters( Names=[ param_key, ], WithDecryption=True, ) return response["Parameters"][0]["Value"] def get_weekday(date_string: str): date = datetime.datetime.strptime(date_string, "%Y-%m-%d") weekdays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] return weekdays[date.weekday()] def init_logging(): logformatter = ISO8601Formatter( ( "[%(levelname)s] @%(asctime)s " "- %(name)s.%(filename)s#%(funcName)s():L%(lineno)s " "- %(message)s" ), "%Y-%m-%dT%H:%M:%S%z", ) for handler in logger.handlers: handler.setFormatter(logformatter) handler.setLevel(os.environ.get("LOG_LEVEL", "INFO")) logger.setLevel(os.environ.get("LOG_LEVEL", "INFO")) logger.addHandler(handler) def main(): init_logging() calendar_client = GoogleCalendarClient() events = list(calendar_client.get_events()) for event in events: logger.info(event) slack_client = SlackClient() slack_client.post_schedules(channel_id=SLACK_CHANNEL_ID, events=events) def lambda_handler(event, context): main() 動かすだけならLambdaだしロジック書いておしまいだが、折角なので実務運用でも耐えうるであろうワークフローにしてみた(=「IaCちゃんとやるとしたらどうするか」についての検討)
Read moreContext
AWSコンテナ設計・構築入門を読んだ続きとして、 ECSリソースについては他AWSリソースとライフサイクルが異なるとして、ECSはTerraformではなくecspressoによる構成管理を行うとよさそう モチベーション
TerraformでECSリソースを管理しようとすると、以下のような課題がある
a.) TerraformでECSタスク定義を管理する上での課題
aws_ecs_task_definitionを更新すると、タスクリビジョンの積み上げではなく差替(replacement)の差分が生じる リビジョンを戻すということができない rollback相当のことをしたい場合、前回のタスク定義でまたreplacementする差分をapplyする 課題 リビジョンを遡ることができない 過去のリビジョンを(gitから)探し出すのが手間 迅速なrollbackができない rollbackは思考を自動化した手順で行いたいが、それを妨げる b.) TerraformでECSサービスを管理する上での課題
Auto Scalingによってdesired_countの変更が起き得る aws_ecs_serviceで定義しているdesired_countは、たとえばaws_appautoscaling_target定義によるAuto Scaling発動によって、terraform applyを介さずに状態が変わり得る -> Terraform定義と稼働しているECSサービスとの間に差分が生まれる 課題 定義と実態で差分がある状態でterraform applyが実行されると(定義自体に差分は生じず)、Auto Scalingによるスケールアウトを意図せず打ち消してしまうなどが起きる ignore_changesに含めて意図しない「修正」を行わないようにすることは可能だが、本来のdesired_countを変更したいときなどはignore_changes外す必要があり、トリッキーな対応を要する いずれにせよ、applyが意図した収斂になるか判別できない(tfstateが最新状態を反映しているかが不明である以上は) ecspressoによるECS管理
ecspressoは何を解決するか a.) Terraform ECSタスク定義のリビジョンが残らない/rollback問題 タスク定義の登録(register)や実行(run)が個別で行える ECSサービスのデプロイ(deploy)で、“最新リビジョンのタスク定義を参照(新規登録しない)“オプション(--latest-task-definition)の付与が可能 -> タスク定義登録前のリビジョンへのロールバックが可能(rollback ) b.) ECSサービスのTerraform定義と実態の乖離と誤applyリスク -> 定義と現在のECS(=実際にデプロイされているECS)状態との差分の確認(diff)ができる ECSリソースのTerraform -> ecspresso移行 TL;DR version
https://github.com/hrfmmr/sbcntr-resources/compare/32463e6...c6ce77b before:TerraformでのECSリソース定義 ecs.tf ## Backend container resource "aws_ecs_task_definition" "sbcntr_backend" { family = local.task_def.backend.name container_definitions = templatefile(local.task_def.backend.container_def_file, { container_name = local.task_def.backend.container_name image = local.task_def.backend.image_url awslogs-group = local.task_def.backend.cwlogs_group awslogs-region = var.aws_region awslogs-prefix = local.task_def.backend.cwlogs_prefix }) requires_compatibilities = ["FARGATE"] cpu = "256" memory = "512" network_mode = "awsvpc" task_role_arn = aws_iam_role.sbcntr_task.arn execution_role_arn = aws_iam_role.sbcntr_task_exec.arn } resource "aws_ecs_cluster" "sbcntr_backend" { name = var.cluster_def.backend.name } resource "aws_ecs_service" "sbcntr_backend" { name = local.service_def.backend.name cluster = local.service_def.backend.cluster_name task_definition = aws_ecs_task_definition.sbcntr_backend.arn desired_count = local.service_def.common.desired_count launch_type = "FARGATE" enable_execute_command = true network_configuration { subnets = var.subnet_ids security_groups = local.service_def.backend.security_groups } load_balancer { target_group_arn = local.service_def.backend.lb_target_group_arn container_name = local.service_def.backend.container_name container_port = 80 } deployment_controller { type = "CODE_DEPLOY" } } CodeDeployによるBlue/GreenデプロイタイプのECS定義 resource "aws_codedeploy_app" "sbcntr_backend" { name = "sbcntr-ecs-backend-codedeploy" compute_platform = "ECS" } resource "aws_codedeploy_deployment_group" "sbcntr" { app_name = aws_codedeploy_app.sbcntr_backend.name deployment_config_name = "CodeDeployDefault.ECSAllAtOnce" deployment_group_name = "sbcntr-ecs-backend-blue-green-deployment-group" service_role_arn = aws_iam_role.ecs_codedeploy.arn auto_rollback_configuration { enabled = true events = ["DEPLOYMENT_FAILURE"] } blue_green_deployment_config { deployment_ready_option { //action_on_timeout = "CONTINUE_DEPLOYMENT" action_on_timeout = "STOP_DEPLOYMENT" wait_time_in_minutes = 5 } terminate_blue_instances_on_deployment_success { action = "TERMINATE" termination_wait_time_in_minutes = 0 } } deployment_style { deployment_option = "WITH_TRAFFIC_CONTROL" deployment_type = "BLUE_GREEN" } ecs_service { cluster_name = aws_ecs_cluster.sbcntr_backend.name service_name = aws_ecs_service.sbcntr_backend.name } load_balancer_info { target_group_pair_info { prod_traffic_route { listener_arns = [ aws_lb_listener.internal_blue.arn ] } test_traffic_route { listener_arns = [ aws_lb_listener.internal_green.arn ] } target_group { name = aws_lb_target_group.internal_blue.name } target_group { name = aws_lb_target_group.internal_green.name } } } } codedeploy.tf after:ecspresso移行後のTerraform定義 ecs.tf resource "aws_ecs_cluster" "sbcntr_backend" { name = local.backend_def.cluster_name } resource "null_resource" "ecspresso_backend" { triggers = { cluster = aws_ecs_cluster.sbcntr_backend.name task_exec_role = aws_iam_role.sbcntr_task_exec.name task_role = aws_iam_role.sbcntr_task.name alb_backend = aws_lb.internal.arn alb_target_group = aws_lb_target_group.internal_blue.arn } provisioner "local-exec" { command = "ecspresso deploy --no-wait" working_dir = "./ecspresso/backend-app" environment = { # Common AWS_ACCOUNT_ID = var.aws_account_id, ECS_CLUSTER = local.backend_def.cluster_name, ECS_SERVICE = local.backend_def.service_name, # Task CW_LOG_GROUP_ECS_TASK_BACKEND = local.backend_def.cwlogs_group, ECS_TASK_EXEC_ROLE_ARN = aws_iam_role.sbcntr_task_exec.arn, ECS_TASK_ROLE_ARN = aws_iam_role.sbcntr_task.arn, DB_HOST = var.db_host, DB_NAME = var.db_name, # Service LB_TG_INTERNAL_BLUE = aws_lb_target_group.internal_blue.arn, SG_BACKEND_APP = aws_security_group.sbcntr_sg_container.id, VPC_SUBNET_APP_AZ_A = var.subnet_ids[0], VPC_SUBNET_APP_AZ_C = var.subnet_ids[1] # CodeDeploy CODEDEPLOY_APP_NAME = local.backend_def.codedeploy_app_name, CODEDEPLOY_GROUP_NAME = local.backend_def.codedeploy_group_name, } } provisioner "local-exec" { when = destroy command = "aws ecs delete-service --cluster $ECS_CLUSTER --service $ECS_SERVICE --force" working_dir = "." environment = { ECS_CLUSTER = "sbcntr-ecs-backend-cluster", ECS_SERVICE = "sbcntr-ecs-backend-service" } } } data "aws_ecs_service" "sbcntr_backend" { cluster_arn = aws_ecs_cluster.sbcntr_backend.arn service_name = local.backend_def.service_name depends_on = [ null_resource.ecspresso_backend, ] } null_resourceのlocal-exec provisionerでecspresso deployコマンドを実行 aws_ecs_service data sourceで実際にデプロイされたECSサービスのリソースを参照 codedeploy.tf resource "aws_codedeploy_app" "sbcntr_backend" { name = local.backend_def.codedeploy_app_name compute_platform = "ECS" } resource "aws_codedeploy_deployment_group" "sbcntr" { app_name = aws_codedeploy_app.sbcntr_backend.name deployment_config_name = "CodeDeployDefault.ECSAllAtOnce" deployment_group_name = local.backend_def.codedeploy_group_name service_role_arn = aws_iam_role.ecs_codedeploy.arn auto_rollback_configuration { enabled = true events = ["DEPLOYMENT_FAILURE"] } blue_green_deployment_config { deployment_ready_option { //action_on_timeout = "CONTINUE_DEPLOYMENT" action_on_timeout = "STOP_DEPLOYMENT" wait_time_in_minutes = 5 } terminate_blue_instances_on_deployment_success { action = "TERMINATE" termination_wait_time_in_minutes = 0 } } deployment_style { deployment_option = "WITH_TRAFFIC_CONTROL" deployment_type = "BLUE_GREEN" } ecs_service { cluster_name = local.backend_def.cluster_name service_name = data.aws_ecs_service.sbcntr_backend.service_name } load_balancer_info { target_group_pair_info { prod_traffic_route { listener_arns = [ aws_lb_listener.internal_blue.arn ] } test_traffic_route { listener_arns = [ aws_lb_listener.internal_green.arn ] } target_group { name = aws_lb_target_group.internal_blue.name } target_group { name = aws_lb_target_group.internal_green.name } } } } aws_ecs_service data sourceで実際にデプロイされたECSサービスのリソースを参照して、CodeDeploy deployment groupに紐づくECSサービスを指定 CodeDeploy -> ECSサービス の依存関係を記述 after:ecspressoの定義 実際にデプロイされているECS構成のecspresso定義を生成する
Read moreAWSコンテナ設計・構築[本格]入門 を読んだ
コンテナオーケストレータとしてはKubernetesは馴染みがあったがECSはなにもわからない勢だったので、わかりたいモチベーションで本書を手にとった Terraformも何気に触れてこなかったので、その学習も兼ねて本書で扱うシステム構成をTerraformで構築していってみた Terraform化で扱った構成図(Well-Architectedにはしきれていない..) ※以下は単位AZの図(構築するものはマルチAZ構成) github repo: https://github.com/hrfmmr/sbcntr-resources/tree/terraform/terraform
書籍と異なる部分
bastion パブリックサブネットに踏み台サーバを置いている 書籍ではVPC内で行う必要のある作業(Internal ALBへの疎通確認やDB seedingなど)は、Cloud9環境で行う内容となっていた が、個人的には再現性犠牲にしてもローカルマシンから環境構築作業を行いたかったので、VPC内で行う作業用に踏み台EC2を建てる構成にした network 書籍ではコンテナからのアウトバウンド通信はNATゲートウェイを介さずすべてVPCエンドポイントで解決していた が、NATを置く構成にしてる デプロイしたコンテナが意図通りに動作しないことがあった(frontend appからbackend appへの通信が通らないなど)ので、コンテナの中に入って調査したいことがあった その過程でdns-utilsなどのパッケージインストールを行えるようにしたかったという理由(slimイメージだと何も入っていない) 運用readyになったら不要になるやつ ecs タスクロールにECS Exec用のロールを追加してる 上述の「(Fargate)コンテナに入って調査」するためにECS Exec用のポリシーをアタッチしている secrets 書籍ではAWS Secrets ManagerでRDS DBのcredentialsを設定していたが、AWS Systems Managerのパラメータストアで代用している Secrets Managerを使う一方でシークレットの自動ローテーションをOFFにしていたので、なぜパラメータストアではなくSecrets Managerを選択しているのか気になった AWSの公式FAQ をみると、両者のセキュリティモデルに違いはないとしていて、それぞれのユースケースとしてはライフサイクル管理要件ないならパラメータストア/あるならSecrets Managerをつかうという記載があった rds DBはAmazon Auroraをつかう前提だったが、学習のためとはいえさすがにコスト嵩みそうなのが怖かったので、Aurora構成は断念した 「ECSタスクからRDSにつながる」部分を最小で実現できさえすればひとまず良しとして、シングルAZのDBインスタンスを建てるのみの構成にした 感想戦
AWS学習する上ではクラウド課金をいかに最小化するかが1つテーマとしてあり、今回Terraformでインクリメンタルに構築進めていくことでわりとリーズナブルに(トータル$8ほど)行えてよかった(小さくマイルストーン達成してはterraform destroyするの繰り返しで進めた。ありがとうdestroy) はじめてHCL(HashiCorp configuration language)を触ったが、何かと凝った設定ファイル書いているときに「YAMLでプログラミングがしたい」といった欲求が芽生えるときはわりとあって、そういった需要に痒いところに手が届くといった具合でロジック記述できて書き心地としてとても良かった。LSPサポートもあったし。(CloudFormationテンプレートよりよっぽどヒューマンリーダブル。アレに戻る気にはなれない..) 次
「他AWSリソースとはライフサイクルが異なる」意味でECRについてはTerraform管理下から外していたが、 これはECSリソースについてもいえそうなことで、おなじような課題意識から生まれたとされるECS専用デプロイツールとしてecspressoがあるので、次はTerraform+ecspressoでECS管理する構成を試してみたい
Context Obsidian vaultのback up先として、privateのGitHub repoへ置いている iOSアプリとの連携用でiCloud同期は別でやっている iCloudストレージの操作をミスっておじゃんにすることもあろうかと、git repoへもsyncしておいて安心しておきたい 日々差分が生じているが、push作業は手動でおもいついたらやっている。アドホック運用。(数日だったり1週間空くこともありがち) syncを自動daily実行されるしくみがほしい git-syncをつかうとよさそう git-syncを実行する git-syncをcloneしてPATHを通す
❯ git clone https://github.com/simonthum/git-sync.git ❯ sudo ln -snfv `pwd`/git-sync /usr/local/bin/git-sync Obsidian vaultのあるディレクトリへ移動
❯ cd /path/to/ObsidianVault git-sync用のgit config設定
❯ git config --bool branch.main.sync true ❯ git config --bool branch.main.syncNewFiles true .git/configに以下が追加される
[branch "main"] remote = origin merge = refs/heads/main + sync = true + syncNewFiles = true git-sync実行
❯ git-sync git-sync: Preparing. Repo in .git ~/Library/Mobile Documents/iCloud~md~obsidian/Documents/ObsidianVault git-sync: Mode sync git-sync: Using origin/main git-sync: Committing local changes using git add -A ; git commit -m "changes from hrfmmrM1MBP.local on Sat Sep 10 16:51:46 JST 2022"; [main e31cb50] changes from hrfmmrM1MBP.local on Sat Sep 10 16:51:46 JST 2022 1 file changed, 27 insertions(+), 43 deletions(-) rewrite "Obsidian\343\201\256vault\343\203\207\343\202\243\343\203\254\343\202\257\343\203\210\343\203\252\343\202\222\344\273\273\346\204\217git repo\343\201\ 253sync\343\201\231\343\202\213.md" (68%) git-sync: Fetching from origin/main From github.com:hrfmmr/obsidian * branch main -> FETCH_HEAD git-sync: Pushing changes... Enumerating objects: 5, done. Counting objects: 100% (5/5), done. Delta compression using up to 10 threads Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 433 bytes | 433.00 KiB/s, done. Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 remote: Resolving deltas: 100% (2/2), completed with 2 local objects. To github.com:hrfmmr/obsidian.git 6f52e29..e31cb50 main -> main git-sync: In sync, all fine. よしなに差分がpushされてる🚀
Read moreというのがこの頃個人的最適解となっているのでそれについて整理してみた
TL;DR Obsidianつかうとタスク管理とナレッジ管理を圧倒的操作性の下で完結できてうれしい
前提 タスク管理について述べていく前にまず前提として「タスク」が指すものについての補足 以下で述べられる「タスク」は基本的にソフトウェアエンジニアリング的なことを行う過程で発生する「技術的なタスク(実装事/リサーチ事他)」を想定している 「タスク管理」のスコープとするところ 主体は 個人に閉じた話(チームで何らか共同でタスク管理していく話とは別) スコープ 「なにをせねばならんか」「いますべきことは」「やるなかで得られた知見の蓄積」をいかに効率的に行うか、が主眼 「いついつまでに間に合いそうか」といった進捗管理的なことは関心の対象外 タスク管理の基本要素 タスク管理どうやってますかを語る上で、避けて通れない系の基本要素を整理してみると、おおまか以下がありそう
a.) (タスクを)一覧する b.) (タスクを)選択する c.) (タスクを)開始/再開する d.) (タスクを)実行する タスク管理における基本要素毎の課題感 a.) ~ d.)の4項目について、それぞれの課題感を書き綴ってみると
a.) (タスクを)一覧する 課題感 「TODOリスト」のつらさ わすれないようにするために人は「TODOリスト」をつくる いずれ長くなってくる リストと向き合うたびに認知負荷がかかる 本来必要なのは「いま集中して取り組むやつ」だけ それ以外のも目に入ってくる。ノイズ。 「あれやんなきゃ」とおもいついたら「リスト」に移す 「リスト化」自体がコスト 「リスト」へのコンテキストスイッチが起きる 入力が面倒 複数コンテキスト(ie. プロジェクト)のTODOアイテムを扱う必要が生じた場合どうするか コンテキスト境界でTODOリスト自体分ける? 複数のTODOリストを管理する TODOリストを行き来する必要が生じる。管理コストがつらくなる。 1枚のTODOリストで複数コンテキストを区別する? 「どのコンテキストのTODOか」をどう区別するかを考える必要が生じる 管理コスト然り、TODOアイテムの追加自体がつらくなる なにがつらいのか 単にやることを忘れないようにしたいだけだが、「リスト」にすることで管理やそれに伴う認知コストが生じる 個人レベルでのタスク管理ではキレイに上から優先度順にTODOアイテムがソートされている必要はなくて「いま取り組むべきもの」を1~3つ見繕えれば事足りる なにを解決したいか -> (「やることを忘れないようにする」を担保しつつも)リスト管理から開放されたい b.) (タスクを)選択する 課題感 特になし 個人的に、「いま取り組むべきものはどれか」を決める上での課題感はない(その時々でわりと自明だったりする) ゴーストの囁き c.) (タスクを)開始/再開する 開始と再開 未着手のものを開始する場合はよいとして、えてして「再開」事と付き合う必要がある なぜ タスク開始から完了までノンストップで完遂することは稀 単純に1日がかりでは終わらなかったり 作業途中で割込の発生とそれに伴うコンテキストスイッチがあったりするのが現実 ので、「途中から再開」イベントが往々にして生じる (再開事と向き合う上での)課題感 一旦手を離れて戻ってきた際、「なにをどこまでやっていたか」を思い出すオーバヘッドが生じる 再開する上で「思い出す」オーバヘッドをできるだけ抑えたい 思い出す対象大きく2つ 「なにをどこまでやっていたか」の「なにを」(=取り組む対象)の把握 「なにをどこまでやっていたか」の「どこまで」(=取り組んでいる対象のカーソル位置)の把握 なにを解決したいか 「なにを」の把握 TODOアイテムの内、「未着手のもの」と「仕掛り中(=WIP)のもの」を区別したい 「WIPのものの中で、現在進行系で取り組むべきもの」をフィルタしたい 「どこまで」の把握 「仕掛り中のもの」に再度取り掛かる際、「前はどこまで進めていて、次何から始めるとよいか」までのキャッチアップをスムースにしたい いかに低コストで「(雄弁な)作業途中状態のセーブ」が行えるか、が関心 まず、作業状態をセーブするための、特定タスクに紐付いたメモの書込先が必要(と抽象で書くとやや冗長な表現だけれども、たとえばタスクがtitleとなっているプレーンテキストファイルであったり、GitHub issueやカンバンツールでいうカードみたいなもの) その上で、テキストベースのメモ書きのみだと、正直足りない場合がある(作業再開する上で記憶を想起させるには十分に雄弁でない場合がある) スクショをはじめとしたメディアファイルの添付ができると、より思い出しやすい また、作業開始~中断までの作業の全体像/プロットがわかるようなアウトラインが可視化できると、よりコンテキストを思い出しやすい d.) (タスクを)実行する (一般論ではない)作業過程の流儀(?)みたいなの 実装事によっては、思考を整理する上で言語化したり、イメージを可視化しながら作業を進める 言語化 ふつうにテキストを綴れれば良い イメージの可視化 (iPad pencilで図を書いたりしていたこともあったけれども)、おおまかなコンポーネントの関係性やシーケンス図やフローチャートその他諸々については、最近はもっぱらUMLで書く方が速いと感じている(手書きで書いたときの字の汚さで読めないといったことがなく出力が安定しているのもうれしい。コピペで再利用もできるし) なにがほしいか ふつうにテキストでメモが出来て、UMLを書いてそれをプレビューできること (課題感を踏まえた上で)やりたいこと 上記のタスク管理の基本要素毎の課題感を踏まえた上で、自分なりの要件を整理してみると
Read moreContext iOSアプリ UIKitコードベースとの付き合い feature-modularizedなマルチモジュール構成をとった場合の課題 課題 Featureモジュール間の画面遷移をどう実現するか why: featureモジュール同士は依存関係をつくれない(循環参照) so: FeatureAからFeatureBの画面(VC)インスタンスを生成したい場合、globalに定義される共通の抽象インタフェースへの依存で解決する必要性が生じる VC生成にあたり、それにぶら下がる依存解決上の課題 ときに、single source of truth用途(インメモリで保持される状態アクセスなど)のsingletonインスタンスをFeatureモジュール間で引き回したい。暗黙依存でなくDI(constructor injection)の形式で。テスト容易性。 得てして、このインスタンスが実際に用いられる層はVCではなく、依存先の層(model/usecase)であるケースがほとんど 依存解決のためのインスタンスのバケツリレー 使用側(下位)のコンストラクタ(DI仕様)変更に伴う、上位側からの依存注入し直しバケツリレー変更のつらみ 依存解決やり方 モジュール内に存在する各VCを生成するためのFactoryなインタフェースをglobalに定義(eg. 各Featureモジュールが共通に依存するCoreモジュールに定義するなど) 各Featureは共通化されたFactoryインタフェースを用いて別Featureの画面を生成し、遷移先として指定する 各社の知見 cookpad社の取り組み ViewBuilderがFeature内VC生成のインタフェース Coreに定義されるEnvironmentをつかってbuild buildの中でRouter初期化時にenvironmentをDI Router内で任意のViewDescriptor(これはCoreレベルで公開)を初期化、environment.resolve(descriptor)でVCインスタンスを得る application targetで、Environment protocol準拠することで、ViewDescriptor毎のVCインスタンス生成の具象コードを実装 課題 Environmentのresolve実装するとき、Descriptorの網羅が必要 Descriptorはstaticなenumではないので、case網羅のためにコンパイラチェックの恩恵を受けられない switch caseを網羅するのに工夫が必要(実際cookpadでは存在するDescriptorのcase網羅が行われるようにコードの自動生成のしくみを構築していて、カバーされていないcaseがあればコンパイルエラーになるようにしているとのこと) しくみの構築と運用のコスト もっとラクにコンパイルタイムでの依存解決を保証するしくみがあるとうれしい はてな社の取り組み uber/needleを用いたDIコードの自動生成(≒ Dagger) 型安全なEnvironment実装生成不要でViewBuilder相当のDI定義を各Featureモジュールに寄せられる 本体app targetではRootComponentとそれにぶら下がる各FeatureComponentが必要とするFactoryを定義するだけで済む 依存解決の不足をコンパイラチェック可能に 実装How uber/needle CoreにVCインスタンス化用のBuildable定義 FeatureでComponent定義 画面に対応するBuildable型を返却するBuilderを内部で実装 app targetでRootComponentを定義 childとなる各FeatureComponentのDependencyに必要なfactory定義を施す singletonを引き回したい場合は、 NeedleFoundationのsharedでfactoryをラップすることで解決できる 図 app target RootComponent Features Feature Alpha FeatureAlphaComponent FeatureAlphaFooBuilder Feature Bravo FeatureBravoComponent FeatureBravoFooBuilder Core AlphaFooBuildable BravoFooBuildable 成果物 https://github.com/hrfmmr/ios-di-sandbox 参考 コード生成を用いたiOSアプリマルチモジュール化のための依存解決 - クックパッド開発者ブログ Mobile Act ONLINE #6 | uber/needleを用いたモジュール間の画面遷移とDI - ikesyo
Context とあるコードベースでXcodeのビルド時間がインクリメンタルビルドですら3分近くかかる問題がある 「ビルド時間おそいですね」課題感は共通認識なものの、総量として定量的にみてどれくらいの問題の大きさなのかが把握できていない状況でもある ビルド時間を開発速度のhealthcheck的な指標としてモニタリングしたい どうやるか XCMetricsというTHEソレのようなOSSをspotifyが公開していたりする github repo: https://github.com/spotify/XCMetrics (v0系ではあるものの)1st releaseが2021/01とわりと最近 vapor製 Swift大統一 dashboard UIがイケてる XCMetricsはつかえるか🤔 Backend Deploymentをみると、アプリケーション/DBサーバの運用が必要 localhostでつかう分にはDocker imageも提供されているしサクッと動かせる 業務での運用を考えると、サーバをお守りするのはセキュリティ事がついてまわるので話が変わってくる アプリケーションサーバについてはCloud Runにオフロードする構成もとれるっぽいが、別でCloud SQLもとなるとコストの割高感が否めない.. インフラ管理不要なマネージドに寄せつつ低コストでおなじようなことをやりたいというモチベーションがあったりする XCMetricsはビルドログ収集どうやってるの XCLogParserをつかっている とは Xcodeがビルドログとして~/Library/Developer/Xcode/DerivedData/に吐き出す.xcactivitylog(gzip圧縮されたSLF encodedなファイル(?))があって、 これをparseするとビルドの詳細情報(どのschemeで/どのtargetで/どのソースファイルで/どれだけビルド時間がかかったか情報etc)がとれる 解析方法の情報源として@vincentisambart氏によるcookpad社のblog記事がlinkされてる(つよい..) このparse処理をラップしてビルドメトリクス情報をhtmlやjsonで出力できる、というツール なにげにmain contributorがXCMetricsと同じspotifyの人 ビルドログ情報はXCLogParserから取得できるから、以下が揃えばXCMetricsでやってるようなビルド時間可視化はいける ログデータの加工(postBuildScript) ログデータの保存(DB) ログデータの可視化(のfrontend) 「データがあってそれを可視化」といったらBigQuery+Data Portalの構成が降ってくる(というかWebアプリ以外だとそれしかおもいつかない..) 構成 Overview a.) Xcodeでビルドが実行されたら、postBuildScriptによりxclogparserでビルドログ情報を抽出&可視化用に加工したデータ(json)をs3にupload b.) BigQuery Data Transfer Serviceでs3にuploadされたデータをBigQueryにロード(定期or手動実行) c.) Google Data Portalでビルドメトリクスを(dashboardとして)可視化 Post Build Scriptフェーズ 各フェーズ詳細 ビルド開始 ビルド完了後、Post Build Scriptとしてlauncherスクリプトを実行 launcherとは 本体のスクリプトをbackground実行するためのスクリプト なぜ .xcactivitylogがXcodeからdumpされるのはPost Build Processの後になる為 Post Build Scriptでいくらsleepしたとしてもビルドログは一生dumpされない すると「今ビルドしたもの」より1つ前のビルドログが参照されてしまう eg. 「Cleanビルドしたが、ビルドの方ではなくCleanの方のログが参照されてしまう」 ので、background実行を挟むことでPost Build Processを踏ませ、ビルドログのdumpを待ち受けるといったworkaroundが必要というかんじ cf. https://github.com/MobileNativeFoundation/XCLogParser#tips--tricks ビルド後、.xcactivitylogが/path/to/DerivedData/にdumpされる 本体のPost Build Scriptが実行される 内部処理でxclogparserによるビルドログのparse結果を取得 これをBQテーブルスキーマに合わせて加工 加工したビルドログデータをs3にupload やっていき Post Build Scriptを仕込む XcodeGen project.yml
Read more