TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

939

こんにちは、最近気になっている哺乳類は オリンギート な、開発部の塩崎です。 私の所属しているMarketingAutomationチームではRealtimeMarketingシステムの開発運用を行っております。 このシステムはZOZOTOWNのユーザーに対してメールやLINEなどのコミュニケーションチャンネルを使い情報の配信を行うものです。 メルマガの配信数や開封数などの数値は自動的に集計され、BIツールであるRedashによってモニタリングされています。 このRedashは社内PCによってホスティングされていましたが、運用面で辛い部分が多々あったためパブリッククラウドに移行しました。 移行先のクラウドはawsを選択し、RedashをホスティングするためのサービスはECS/Fargateを選択しました。 この記事ではawsに構築した環境や、移行作業などを紹介します。 移行前のRedash 移行前のRedashの問題点 現行のRedashの紹介 技術選定 構成を詳細に説明 RDS(PostgreSQL) + ElastiCache(Redis) ECS/Fargate Datadog Fargateの利点欠点 利点 EC2の管理から解放される コンテナ数のスケーリングをするときにEC2のスケーリングをしなくても良い AutoNamingによるサービスディスカバリは便利 欠点 コンテナのデプロイが遅い 同一タスク中の複数のプロセスが同じポートで待受できない ログドライバーがawslogsしか選択できない previllegeモードでDockerを動かすことができない まとめ 移行前のRedash 移行前のRedashがどのような環境でホスティングされていたのかを説明します。 Windowsの物理マシンがあり、そのなかにコンテナ管理ツールの Portainer が入っています。 RedashのwebサーバーやRedashを動作させるためのミドルウェアなどはすべてDockerコンテナの中で動いており、その管理をPortainerに任せています。 Redashのデータソースは主に2種類あり、それはBigQueryとPuredataです。 BigQuery はGoogle製の完全にマネージドなDWHです。 BigQueryとの接続にはRedashがデフォルトで用意しているインテグレーション機能を使用しています。 一方、PuredataはIBM製のDWHアプライアンスです。 こちらはRedashとのインテグレーションが用意されていません。 そのため、我々のチームでPuredataと接続するためのゲートウェイを作成し、Redashとの接続に使用しています。 Puredataに接続するためのインタフェースとしてIBMからJDBCドライバーが提供されています。 しかし、RedashはPythonで実装されており、JDBCドライバーを直接読み込むことができません。 何らかの仕組みでPythonとJavaの変換が必要です。 ですので、 Py4J を使いPythonからJavaのメソッドを呼ぶことでRedashからPuredataに接続しています。 参考: RedashとBigQueryのインテグレーション機能 移行前のRedashの問題点 上記で説明したような構成のRedashにはいくつかの問題がありました。 主には物理マシンの管理が面倒という内容です。 物理マシンが落ちた時の復旧が手動 何らかの原因で物理マシンが落ちたときには、物理マシンが置いてある場所まで行き、手動で起動をするという運用が必要になってしまっていました。 ビルの法定停電の時にマシンの電源を落とす必要がある ビルの法定停電の時には停電前に電源を落とし、停電から復旧した後に正常に動作するかどうかを確認する運用が必要になっていました。 現行のRedashの紹介 次にawsに構築したRedashの紹介をします。 Redashの動作に必要なミドルウェアはPostgerSQLとRedisです。 これらはawsのフルマネージドサービスであるRelational Database Service(RDS)とElastiCacheでホスティングすることによって、管理コストを下げました。 RedashとPuredataGatewayのDockerイメージはElastic Container Registry(ECR)で管理されており、ECSはここからdocker pullを行います。 DockerhubでホスティングされているRedash公式のDockerイメージを使わない理由は、Puredataに対応させるために独自のパッチを当てているためです。 次にコンテナ間通信について説明します。 RedashとPuredataGatewayの間はPy4JによるTCP通信をさせる必要があります。 しかしPuredataGatewayコンテナのIPアドレスは起動するごとに変化するので、Redashのコンテナの中にIPアドレスをハードコーディングすることはできません。 Redash workerコンテナがPuredataGatewayコンテナのIPアドレスを取得する仕組みが必要です。 NLBを使うことはその1つの解決策ですが、今回は最近東京リージョンでも使えるようになった Route53 Auto Naming で実現しました。 これはコンテナの起動・終了のタイミングでDNSのAレコードの値が自動的に増減する仕組みです。 Redash側からPuredataGatewayに接続するときにはこのAレコードを参照すれば、現在アクティブなPuredataGatewayのIPアドレスを得ることができます。 NLBではなくAutoNamingを利用した主な理由はロードバランサーを入れないほうが全体の構成がシンプルになるためです。 これらのインフラ構成はすべてCloudFormationで管理されています。 これ以降でインフラ構成を説明するときにはCloudFormationのテンプレートファイルを使って説明します。 また、これらのサービスの監視は Datadog で行っています。 RDSとElastiCacheは aws integration でメトリクスの収集を行っています。 そして、コンテナのメトリクスの収集はDatadog Agentのサイドカーコンテナを配置することで行っています。 FargateとDatadogの連携に関する設定はこの下で詳しく説明します。 技術選定 今回のRedashホスティングにFargateを選定した理由を説明します。 まず、Redashを動作させるためのDockerイメージが公式から提供されているので、それを使うことを考えました。 その場合、何かしらの仕組みでコンテナのオーケストレーションを行う必要があります。 マーケティングオートメーションチームは既にawsでのサービスの開発・運用実績がありましたので、パブリッククラウドとしてawsを選択しました。 awsでDockerのオーケストレーションをしてくれるマネージドサービスはECSとEKSがあります。 EKSはDocker界隈で最近ますます存在感を増しているkubernatesのマネージドサービスです。 ですが、コントロールプレーンのみがマネージドであり、ワーカーノードの管理は自分たちで行う必要があります。 今回は可能な限りインフラの管理をawsに任せたいという思いがあったため、ECS/Fargateを選択しました。 構成を詳細に説明 ここからは今回構築したRedash環境の詳細をCloudFormationのテンプレートファイルを交えながら説明していきます。 RDS(PostgreSQL) + ElastiCache(Redis) まずは、Redashが必要としているミドルウェアであるPostgreSQLとRedisを構築します。 どちらともawsによるマネージドサービスが提供されているので、RDSとElastiCacheを使うことにしました。 今回は高いSLOが求められないので、値段を優先して一番安いインスタンスを使ったシングルAZ構成で構築しました。 たとえシングルAZ構成でも、 OSのセキュリティパッチやデータベースのバックアップはaws側がやってくれます。 そのため、Redash公式のdocker-composeを使った構成よりも管理コストが低いです。 また、可用性を高めるためこれをマルチAZ構成にすることも比較的容易です。 Resources : RDSDBParameterGroupForRedash : Type : 'AWS::RDS::DBParameterGroup' Properties : Description : 'rds parameter group for redash' Family : 'postgres9.6' RDSDBSubnetGroupNameForRedash : Type : "AWS::RDS::DBSubnetGroup" Properties : DBSubnetGroupDescription : 'rds subnet group for redash' DBSubnetGroupName : redash-rds-subnet-group SubnetIds : - !Ref EC2SubnetApplicationPrivateAZ1 # 予め定義しておく - !Ref EC2SubnetApplicationPrivateAZ2 # 予め定義しておく EC2SecurityGroupRDSRedash : Type : 'AWS::EC2::SecurityGroup' Properties : GroupName : redash-rds-security-group GroupDescription : 'rds security group for redash' VpcId : !Ref EC2VPC SecurityGroupIngress : - SourceSecurityGroupId : !Ref EC2SecurityGroupECSRedash # ECSタスクに紐づけているセキュリティグループ FromPort : 5432 ToPort : 5432 IpProtocol : 'tcp' RDSForRedash : Type : 'AWS::RDS::DBInstance' Properties : Engine : 'postgres' EngineVersion : '9.6' AutoMinorVersionUpgrade : true DBInstanceClass : 'db.t2.micro' DBParameterGroupName : !Ref RDSDBParameterGroupForRedash BackupRetentionPeriod : 7 # バックアップの保持期間 StorageType : 'gp2' AllocatedStorage : 20 # ストレージのサイズ(GB) MultiAZ : false AvailabilityZone : !Ref AZ1 # 予め定義しておく PubliclyAccessible : false DBSubnetGroupName : !Ref RDSDBSubnetGroupNameForRedash VPCSecurityGroups : - !Ref EC2SecurityGroupRDSRedash MasterUsername : !Ref RDSForRedashMasterUsername MasterUserPassword : !Ref RDSForRedashMasterUserPassword PreferredBackupWindow : '15:00-17:00' # UTC PreferredMaintenanceWindow : 'Sat:17:00-Sat:19:00' # UTC ## Redis ElastiCacheSubnetGroupForRedash : Type : 'AWS::ElastiCache::SubnetGroup' Properties : CacheSubnetGroupName : redash-redis-subnet-group Description : 'redis subnet group for redash' SubnetIds : - !Ref EC2SubnetApplicationPrivateAZ1 - !Ref EC2SubnetApplicationPrivateAZ2 EC2SecurityGroupRedashRedis : Type : 'AWS::EC2::SecurityGroup' Properties : GroupName : redash-redis-security-group GroupDescription : 'redis security group for redash' VpcId : !Ref EC2VPC SecurityGroupIngress : - SourceSecurityGroupId : !Ref EC2SecurityGroupECSRedash FromPort : 6379 ToPort : 6379 IpProtocol : 'tcp' ElastiCacheParameterGroupForRedash : Type : 'AWS::ElastiCache::ParameterGroup' Properties : CacheParameterGroupFamily : 'redis4.0' Description : 'Redash Redis' ElasticacheClusterForRedash : Type : 'AWS::ElastiCache::CacheCluster' Properties : ClusterName : 'redash-redis' Engine : 'redis' EngineVersion : '4.0.10' AutoMinorVersionUpgrade : true CacheNodeType : 'cache.t2.micro' NumCacheNodes : 1 CacheParameterGroupName : !Ref ElastiCacheParameterGroupForRedash PreferredAvailabilityZone : !Ref AZ1 CacheSubnetGroupName : !Ref ElastiCacheSubnetGroupForRedash VpcSecurityGroupIds : - !GetAtt EC2SecurityGroupRedashRedis.GroupId Port : 6379 ECS/Fargate 次にECS /FargateでRedashをホスティングする部分を説明します。 今回は以下のようにサービスを分割しました。 Redash Server Redash Worker Puredata Gateway PuredataGatewayはPy4JでPythonとJavaの間を取り持つコンテナなので、分離されているのは自然です。 一方でRedash系の2つが分離されているのは違和感ある人がいるかもしてないので、理由を説明します。 Redash ServerはRedashのweb UIをホスティングしているwebサーバーで、このサービスが直接クエリの実行することはありません。 クエリの実行を担うのはRedash Workerの方です。 これらの間は非同期ジョブライブラリの Celery によってクエリのやり取りが行われています。 なお、Celeryによって作られるジョブキューの実体はRedisのリストです。 そしてクエリの実行結果はPostgreSQLに書き込まれ、それがRedash Serverによって読み出されグラフィカルに表示されます。 クエリの個数が多くなった時にRedash Workerのスケーリングが必要です。 ですので、Redash ServerとRedash Workerの分離を行いました。 今回はECSサービスを3つ作りましたが、まずはRedash ServerとRedash Workerのサービス定義を説明します。 特に注意が必要なポイントは以下の2点で、それぞれ後で説明します。 Redashの待受ポートが5100なこと puredata-gateway.redash.internalというドメインでRedash WorkerがPuredata Gatewayに接続すること Resources : ## ECS ECSClusterForRedash : Type : 'AWS::ECS::Cluster' Properties : ClusterName : redash IAMRoleForRedashTaskExecution : Type : 'AWS::IAM::Role' Properties : AssumeRolePolicyDocument : Version : '2012-10-17' Statement : - Effect : 'Allow' Principal : Service : 'ecs-tasks.amazonaws.com' Action : 'sts:AssumeRole' ManagedPolicyArns : - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' IAMServiceLinkedRoleForECSRedashService : Type : 'AWS::IAM::ServiceLinkedRole' Properties : AWSServiceName : 'ecs.amazonaws.com' Description : 'Role to enable Amazon ECS to manage your cluster.' ECSTaskDefinitionApplication : Type : 'AWS::ECS::TaskDefinition' Properties : Family : redash-server RequiresCompatibilities : - 'FARGATE' Cpu : 1024 Memory : 2048 NetworkMode : 'awsvpc' ExecutionRoleArn : !GetAtt IAMRoleForRedashTaskExecution.Arn ContainerDefinitions : - Image : !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryForRedash}:last Name : 'redash-server' Command : - 'server' Environment : - Name : 'PYTHONUNBUFFERED' Value : '0' - Name : 'REDASH_LOG_LEVEL' Value : 'INFO' - Name : 'REDASH_REDIS_URL' Value : !Sub redis://${ElasticacheClusterForRedash.RedisEndpoint.Address}:${ElasticacheClusterForRedash.RedisEndpoint.Port}/0 - Name : 'REDASH_DATABASE_URL' Value : !Sub postgresql://redash:${RDSRedashUserPassword}@${RDSForRedash.Endpoint.Address}/redash # Redash用のRDSユーザーを予め作っておく - Name : 'REDASH_HOST' Value : '' - Name : 'REDASH_WEB_PORT' # Redashが待ち受けているポート番号 デフォルトの5000ではない理由は後述 Value : '5100' - Name : 'REDASH_ALLOW_SCRIPTS_IN_USER_INPUT' Value : 'true' - Name : 'REDASH_DATE_FORMAT' Value : 'YY/MM/DD' - Name : 'REDASH_ADDITIONAL_QUERY_RUNNERS' # Redashに新しいquery runnerを登録するため Value : 'redash.query_runner.puredata' - Name : 'REDASH_JAVA_GATEWAY_HOST' # このドメインはRoute53のAutoNamingで提供される Value : puredata-gateway.redash.internal Cpu : 1024 Memory : 2048 PortMappings : - ContainerPort : 5100 HostPort : 5100 Protocol : 'tcp' LogConfiguration : LogDriver : 'awslogs' Options : awslogs-group : !Ref LogsLogGroupForRedash # 予め作っておく awslogs-region : !Ref AWS::Region awslogs-stream-prefix : 'server' ECSTaskDefinitionWorker : Type : 'AWS::ECS::TaskDefinition' Properties : Family : redash-worker RequiresCompatibilities : - 'FARGATE' Cpu : 1024 Memory : 2048 NetworkMode : 'awsvpc' ExecutionRoleArn : !GetAtt IAMRoleForRedashTaskExecution.Arn ContainerDefinitions : - Image : !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryForRedash}:${RedashGithubSHA1} Name : 'redash-worker' Command : - 'scheduler' Environment : - Name : 'PYTHONUNBUFFERED' Value : '0' - Name : 'REDASH_LOG_LEVEL' Value : 'DEBUG' - Name : 'REDASH_REDIS_URL' Value : !Sub redis://${ElasticacheClusterForRedash.RedisEndpoint.Address}:${ElasticacheClusterForRedash.RedisEndpoint.Port}/0 - Name : 'REDASH_DATABASE_URL' Value : !Sub postgresql://redash:${RDSRedashUserPassword}@${RDSForRedash.Endpoint.Address}/redash - Name : 'QUEUES' Value : 'queries,scheduled_queries,celery' - Name : 'WORKERS_COUNT' Value : '10' - Name : 'REDASH_ADDITIONAL_QUERY_RUNNERS' Value : 'redash.query_runner.puredata' - Name : 'REDASH_JAVA_GATEWAY_HOST' Value : !Sub puredata-gateway.redash.internal Cpu : 1024 Memory : 2048 LogConfiguration : LogDriver : 'awslogs' Options : awslogs-group : !Ref LogsLogGroupForRedash awslogs-region : !Ref AWS::Region awslogs-stream-prefix : 'worker' EC2SecurityGroupECSRedash : Type : 'AWS::EC2::SecurityGroup' Properties : GroupName : redash-ecs-security-group GroupDescription : 'ecs security group for redash' VpcId : !Ref EC2VPC SecurityGroupIngress : - SourceSecurityGroupId : !Ref EC2SecurityGroupALBExternalRedash FromPort : 5100 ToPort : 5100 IpProtocol : 'tcp' ECSServiceRedash : Type : 'AWS::ECS::Service' DependsOn : - IAMServiceLinkedRoleForECSRedashService - ECSServicePuredataGateway Properties : Cluster : !Ref ECSClusterForRedash DesiredCount : 1 LaunchType : 'FARGATE' LoadBalancers : - ContainerName : 'redash-server' ContainerPort : 5100 TargetGroupArn : !Ref ElasticLoadBalancingV2TargetGroupExternalRedash NetworkConfiguration : AwsvpcConfiguration : AssignPublicIp : 'DISABLED' SecurityGroups : - !Ref EC2SecurityGroupECSRedash Subnets : - !Ref EC2SubnetApplicationPrivateAZ1 - !Ref EC2SubnetApplicationPrivateAZ2 PlatformVersion : '1.2.0' TaskDefinition : !Ref ECSTaskDefinitionApplication ECSServiceRedashWorker : Type : 'AWS::ECS::Service' DependsOn : - IAMServiceLinkedRoleForECSRedashService - ECSServicePuredataGateway Properties : Cluster : !Ref ECSClusterForRedash DesiredCount : 3 LaunchType : 'FARGATE' NetworkConfiguration : AwsvpcConfiguration : AssignPublicIp : 'DISABLED' SecurityGroups : - !Ref EC2SecurityGroupECSRedash Subnets : - !Ref EC2SubnetApplicationPrivateAZ1 - !Ref EC2SubnetApplicationPrivateAZ2 PlatformVersion : '1.2.0' TaskDefinition : !Ref ECSTaskDefinitionWorker 次にPuredata Gatewayのサービス定義を示します。 基本的にはRedashのサービス定義と変わりませんが、ServiceDiscoveryに関する設定が追加されているのが特徴です。 前述したRedashサービスから接続をしていたpuredata-gatewat.redash.internalのサービスディスカバリはここで行われます。 まず AWS::ServiceDiscovery::PrivateDnsNamespace リソースによって、 puredata-gateway.redash.internal というHostedZoneがRoute53に作られます。 そして AWS::ServiceDiscovery::Service リソースによって、そのHostedZoneの中にマルチバリューAレコードが作られます。 最後に AWS::ECS::Service リソースのServiceRegistriesプロパティを指定します。 これによって、タスクの起動終了のタイミングでAレコードの値が自動的に書き換わります。 https://docs.aws.amazon.com/Route53/latest/APIReference/overview-service-discovery.html Resources : ServiceDiscoveryPrivateDnsNamespaceForPuredataGateway : Type : "AWS::ServiceDiscovery::PrivateDnsNamespace" Properties : Vpc : !Ref EC2VPC Name : puredata-gateway.redash.internal ServiceDiscoveryServiceForPuredataGateway : Type : "AWS::ServiceDiscovery::Service" Properties : Name : puredata-gateway DnsConfig : NamespaceId : !Ref ServiceDiscoveryPrivateDnsNamespaceForPuredataGateway DnsRecords : - Type : 'A' TTL : 60 HealthCheckCustomConfig : FailureThreshold : 2 ECSTaskDefinitionPuredataGateway : Type : 'AWS::ECS::TaskDefinition' Properties : Family : puredata-gateway RequiresCompatibilities : - 'FARGATE' Cpu : 1024 Memory : 2048 NetworkMode : 'awsvpc' ExecutionRoleArn : !GetAtt IAMRoleForRedashTaskExecution.Arn ContainerDefinitions : - Image : !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryForPuredataGateway} Name : 'puredata-gateway' Command : [ 'command' , 'to' , 'start' , 'puregata gateway' ] Cpu : 1024 Memory : 2048 PortMappings : - ContainerPort : 25333 HostPort : 25333 Protocol : 'tcp' LogConfiguration : LogDriver : 'awslogs' Options : awslogs-group : !Ref LogsLogGroupForRedash awslogs-region : !Ref AWS::Region awslogs-stream-prefix : 'puredata-gateway' EC2SecurityGroupECSPuredataGateway : Type : 'AWS::EC2::SecurityGroup' Properties : GroupName : puredata-gateway-ecs-security-group GroupDescription : 'ecs security group for puredata gateway' VpcId : !Ref EC2VPC SecurityGroupIngress : - CidrIp : !Ref ApplicationPrivateCidrBlockAZ1 FromPort : 25333 ToPort : 25333 IpProtocol : 'tcp' - CidrIp : !Ref ApplicationPrivateCidrBlockAZ2 FromPort : 25333 ToPort : 25333 IpProtocol : 'tcp' ECSServicePuredataGateway : Type : 'AWS::ECS::Service' DependsOn : IAMServiceLinkedRoleForECSRedashService Properties : Cluster : !Ref ECSClusterForRedash DesiredCount : 1 LaunchType : 'FARGATE' ServiceRegistries : - RegistryArn : !GetAtt ServiceDiscoveryServiceForPuredataGateway.Arn NetworkConfiguration : AwsvpcConfiguration : AssignPublicIp : 'DISABLED' SecurityGroups : - !Ref EC2SecurityGroupECSPuredataGateway Subnets : - !Ref EC2SubnetApplicationPrivateAZ1 - !Ref EC2SubnetApplicationPrivateAZ2 PlatformVersion : '1.2.0' TaskDefinition : !Ref ECSTaskDefinitionPuredataGateway Redashをawsに構築するにあたって、これ以外にもALB、ACM、Route53なども使用しましたが、記事が長くなってしまうのでここでは割愛します。 Datadog これらのコンテナのメトリクスの監視にはDatadogを使用しています。 Datadogでメトリクスを収集するためには、Datadogのコンテナをサイドカーコンテナとして配置します。 例えば、Redash Serverのコンテナのメトリクスを収集したい場合は、以下のようにコンテナを配置します。 環境変数ECS_FARGATEを true に指定するだけで、そのタスクに含まれるすべてのコンテナのメトリクスを収集してくれます。 Resources : ECSTaskDefinitionApplication : Type : 'AWS::ECS::TaskDefinition' Properties : ContainerDefinitions : - Image : !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryForRedash}:last Name : 'redash-server' - Image : datadog/agent:latest Name : 'datadog' Cpu : 256 Memory : 512 Environment : - Name : 'DD_API_KEY' Value : !Ref DatadogAPIKey - Name : 'ECS_FARGATE' Value : 'true' LogConfiguration : LogDriver : 'awslogs' Options : awslogs-group : !Ref LogsLogGroupForRedash awslogs-region : !Ref AWS::Region awslogs-stream-prefix : 'redash-datadog' https://www.datadoghq.com/blog/monitor-aws-fargate/ Fargateの利点欠点 FargateでRedashを構築する最中に感じた、Fargateの利点欠点などをまとめます。 利点 EC2の管理から解放される まず真っ先に思い浮かぶFargateの利点はEC2を管理する必要がないということです。 ECS on EC2の構築をするためにはEC2インスタンスを立ち上げ、そこにECSエージェントをインストールする必要があります。 Fargateを使うことによって、そのような一手間をかけること無く、いきなりコンテナをデプロイできるので便利です。 コンテナ数のスケーリングをするときにEC2のスケーリングをしなくても良い 前述したことと少し関連しますが、EC2の管理から解放されたことでスケーリングの時に考慮する必要のある要素が減りました。 スケーリングをするときにはサービスのタスク数だけを変化させればよく、従来のECS on EC2で考える必要のあったEC2インスタンスのスケーリングは不要です。 AutoNamingによるサービスディスカバリは便利 システムをマイクロサービス化するときにサービスディスカバリの仕組みはほぼ必須です。 Route53のAutoNamingを使うとDNSレベルでのサービスディスカバリができます。 なお、これはFargateの利点というよりもECSの利点です。 欠点 コンテナのデプロイが遅い Fargateを使った時にはコンテナのデプロイには数分かかってしまいます。 そのため、高速なスケールアウトを期待してFargateを使うとがっかりすることがあるかと思います。 同一タスク中の複数のプロセスが同じポートで待受できない Fargateでは同一タスクに属するコンテナ間の通信をさせるためにはlocalhostを使います。 そのため、同一タスクに属する複数のプロセスが同じポート番号で待受しようとすると、後から待受した方が失敗します。 実際にRedashとDatadog Agentの両方が5000番ポートでlistenをしていたため、Redashのサーバーが立ち上がらないという現象が発生しました。 そのためRedashにパッチを当て、5100番ポートで待ち受けるように修正しました。 CloudFormationのテンプレートで5100番ポートを使ってRedashの待受をしている理由はこれです(伏線回収) ログドライバーがawslogsしか選択できない ECS on EC2の場合はjournaldやfluentdにログを流すことができますが、ECS/Fargateではawslogsドライバーのみがサポートされます。 awslogsドライバーが収集したログはCloudWatchLogsに送られます。 そのため、すでに別のログ収集基盤がある場合には、CloudWatchLogsからそちらにログを流す必要があります。 https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_LogConfiguration.html previllegeモードでDockerを動かすことができない これは些細な欠点ですが、privilegeモードでDockerを動かすことができないので、例えばDocker in Dockerのような構成にすることはできません。 まとめ 社内PCでホスティングされていたRedashをawsに移すことによって、Redashの動作に必要なコンポーネントをすべてフルマネージドにできました。 さらにこの設定はすべてCloudFormationで管理されています。 そのため、他のチームがawsにRedashを構築したくなったときにはテンプレートファイルを渡すことで同等の構成を簡単に作ることが出来るようになりました。 弊社ではZOZOTOWNの売上を支えているMarketingAutomationに興味がある方、クラウドを活用して既存の運用を改善することに興味がある方を募集しています。 興味のある方はぜひ以下のリンクからご応募ください。 www.wantedly.com
アバター
こんにちは。ZOZO研究所 福岡の光瀬です。Pythonを書かれている皆様は、普段どのように開発をすすめていますか? pipとvenv/virtualenvによるこれまでのデファクトの組み合わせだけではなく、最近は Pipenv を使用している開発者も増えてきたのではないでしょうか。 日々の検証や開発を効率よく進めるにあたって、依存関係を適切かつ楽に管理するのはとても重要だと感じていて、ここ半年ほどPipenvを利用しています。 今回は、その中でsetup.pyやrequirements.txtそしてPipfileの住み分け・運用について考えたことをまとめてみました。 TL;DR Pipenvが使えることで、確かに楽になった部分はあるのかなと思っています。 一方で、既存のツールとの兼ね合いがまだ微妙な部分もあります。 その上で、以下の運用がベターなのかなと考えました。 Pipenvのみで完結させようとしない(setup.pyの install_requires を併用する) setup.pyの install_requires には、直接の依存関係をゆるく記述し pipenv install -e でPipenv管理下に置く 開発・テストに必要な依存関係をPipfileの dev-packages へ記述する 依存関係のロックが必要であればrequirements.txtではなくPipfile.lockを使用する Pipenvとは何か Pipenv は、一言でいえば pipとvirtualenvをラップして、依存関係を  Pipfile およびPipfile.lockで管理するツール です。 いわゆるRubyにおけるbundlerやNode.jsにおけるnpmあるいはyarnのような立ち位置ですね。 今やごく当たり前になっているであろう開発フローをPythonにおいても実現できます。 例えば、以下に列挙する機能が実装されています。 Pipfileによる依存関係の管理 Pipfileによる開発用の依存関係の管理 Pipfile.lockによる依存関係のロック virtualenv環境の自動構築 Pipfileで指定された python_version と現在のバージョンとのチェック Pipfileで指定された python_version の処理系の自動インストール(pyenvが利用可能な時のみ) ..。など より詳しくは 公式ドキュメント をどうぞ。なお、Pipenvには 日本語ドキュメント もあります。また、 作者のKenneth Reitz氏のPyCon 2018におけるスライド もオススメです。 Pipenv以前の話 pipおよびvenv/virtualenvによる開発フロー Pythonを書かれている皆様は、普段どのように開発をすすめられていますでしょうか? Pipenv以前、私は以下のようなフローで開発していました。 使用したいバージョンの処理系をインストールする venv/virtualenvでプロジェクト用の環境を切る 「使用したいライブラリ」をsetup.pyの install_requires に列挙して pip install -e . でインストールする テスト時に利用する依存関係はsetup.pyの test_requires に列挙する pip freeze でrequirements.txtを吐きだしておく(ライブラリではない、依存関係の終端であるプリケーションの場合) 一見、特に問題がないように見えますが、 開発時に必要な依存関係の管理方法が明確ではない のが1つの問題なのかなと思います。 例えば、静的型チェッカーである mypy を使って、編集しながら型チェックをしていきたい場合があります。 この場合、mypyはどこに依存関係として記述するのが適当でしょうか? 実際にアプリケーション・ライブラリが利用される際には、通常不要なツールです。 そのため、少なくとも install_requires に書くのは適当でなさそうです。 一方、テスト時にのみ使用できれば良いということではないので test_requires に書くのも違うように思えます。 開発時に必要な依存関係の管理に対するワークアラウンド 開発時に必要な依存関係の管理については、明確な答えはないものの、いくつかのワークアラウンドで対処されてきたようです。 setup.pyの extras_require を利用してオプショナルな依存関係として扱う requirements.txtに加えて、開発用に別途requirements-dev.txtを用意して pip install -r requirements-dev.txt する いずれの場合も、 pip freeze した際に開発用の依存関係がrequirements.txtへ混入します。 requirements.txtを依存関係のロックとして扱いたい場合に、余計なパッケージが含まれるのはあまり嬉しくありません。 Pipenvが解決した問題 開発・テスト用の依存関係の管理 Pipfileには、開発・テスト用の依存関係が記述・記録される専用の dev-packages というテーブルがあります。 コマンドラインからは、 pipenv install --dev {package_name} でインストール・Pipfileへ記録できます。 Pipfile.lockには開発用の依存関係も記録されますが、 pipenv install 時に --dev を付与した場合のみインストールされます。 pipやvirtualenvなど各種ツールの隠蔽 環境構築について、pipやvirtualenvそしてpyenvを直接さわらずとも環境がつくれるようになっています。 既存ツールをラップしながら、他の言語で提供されているツールと同等の機能を提供している点は、地味ながらもひとつの利点です。 少なくとも優れたパッケージ管理ツールを備えた他の言語のユーザーがPythonを触る際の驚きは減るはずです。 Pipenvが解決できていない問題 依存関係上の終端になるようなアプリケーションであれば、setup.pyを用意せずPipenvのみで開発を進められるように思います。 ただし、pipはPipfileを読まないため、ライブラリの場合には、Pipenvのみでは完結しません。 直接pipからインストールされるライブラリが大半であると思われる現状、ライブラリの開発においてはsetup.pyを書かざるをえなくなっています。 結局どのように運用するのが楽なのか Pipenvの公式の見解が Pipfile vs Setup.py に書かれています。 他のコードから参照される「ライブラリ」であればsetup.pyでまず管理すべきで、どこからも参照されない「アプリケーション」であればPipfileで依存関係を管理するのが良いという話ですね。 この公式の見解を踏まえつつ、ライブラリ・アプリケーションの区別をせずに同じフローへ落とすのが楽なのかなと考えました。 pipからインストール可能にするため、 install_requires を使用する 不要になったパッケージを排除しやすくするため、 pipenv install -e でプロジェクトのパッケージをインストールし、依存関係はPipenv管理下に置く 開発・テスト時にのみ必要なパッケージはsetup.pyに入っている必要はないので、Pipfileに書く 依存関係のロックが必要でない場合は、 pipenv install をする際に --skip-lock するのが適当かと考えています。 もちろん、ライブラリの開発の場合は、setup.pyだけで最低限管理できます。 とはいえほとんどの場合、開発時にのみ使用するツールが入ってきますし、どのみちvenv/virtualenv環境もつくります。 そのため、初めからPipenv管理下に置いてしまった方が楽な印象です。 まとめ:Pipenvは依存関係の管理を楽にしたのか 依存関係の管理については、方法にブレが減ったという意味では楽になったと感じます。 加えて、pipとvirtualenvのラッパーとして振る舞うので、特に意識しなくとも他プロジェクトの環境と分離して管理できるのも良い点です。 一方で、pipはPipfileおよびPipfile.lockを参照できないため、「便利ではあるけど既存ツールを完全に置き換えたものではない」というのが現状のように思います。 基本はsetup.pyを書くという前提で、Pipfileを併用していこうと考えています。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.wantedly.com
アバター
こんにちは。バックエンドエンジニアの田島( @katsuyan121 )です。 弊社ではデータマートをBigQuery上に構築しています。データマートはデータベース全体のデータのうち、必要なデータだけを使いやすい形にしたデータベースです。データマート作成のためのSQLクエリは日々更新や追加があり、BigQueryのコンソールから自由にデータマートを作ってしまうと管理が大変になってしまいます。 そこで、データマートをすべてGitHub上でバージョン管理し、運用の効率化をしました。また、差分更新の導入や依存関係のあるデータマートへの対応などのデータマート構築に必要な機能を作成しました。 弊社のデータ基盤をざっくり紹介します。まずデータはBigQueryへ集約し、Digdagを用いてデータ基盤を構築しています。以下がその概要図です。S3などの分散ストレージや各種DBからデータをBigQueryへ同期し、BigQuery内部でデータマートを構築します。本ブログではこのうちのデータマート構築について紹介します。 BigQueryでのデータマートの実現方法 データマートをBigQuery上に構築していると紹介しましたがその実態は以下の2つです。 BigQueryのView BigQueryのTable 計算コストが小さいマートに関しては View を用いることでデータの鮮度を保つようにします。逆に計算コストの高いマートは事前に集計などの計算をし Table として保持することで、参照時にコストの高い計算しなくて済むようにしています。 データマート構築の問題点 冒頭でもいくつか紹介しましたがデータマートの構築には以下のような問題が存在します。 データマートは日々更新や追加がされる 現在弊社のデータマートの Table View の数は100を超えており、今でも日々追加されている データマートをBigQueryのWebコンソールから自由に作ってしまうと管理が大変 誰が作ったのかわからない 一時的に作ったデータマートなどがどこから参照されているかわからない 溜まってしまったデータマートがどこから参照されているかわからないので不用意に消せない またテーブルを用いてデータマートを構築する場合以下のことが問題となります。 更新に時間がかかる データマートがどういったクエリにより作られたかわからない 定期的にテーブルを更新する必要がある データマートの Table View が他の Table View を参照している場合、更新する順番を間違えるとデータに不整合が生じる そこで、これらの問題を解決するためのシステムを作成しました。 データマートのクエリをGitHubで管理する BigQueryのViewやTableはBigQueryのWebコンソール画面から簡単に作ることができます。しかしそれでは誰が作ったマートなのかなのかが簡単には分からないなどの運用の問題が生じてしまうと紹介しました。そこでSQLファイルを作成し、それらをAPIからBigQueryに反映をします。実際にはRubyの Google Cloud SDK を利用して反映を行っています。 また、SQLファイルのファイル名を View または Table の名前にすることで、実際のマートとSQLファイルとの対応関係が簡単にわかります。以下がその概要です。 Before After マート作成方法 BigQueryのコンソールから手動で作成 SQLファイルをGit管理しAPI経由で作成 以下に View Table をデータマートへ反映する方法を示しますが、全てのマートをSELECT文のSQLファイルとして表現できます。 BigQueryにSQLを反映する仕組みを作ったことで、すべてのマートをSQLファイルとしてGit管理できるようになりました。 Viewのマート反映 View のマートに関しては作成したSQLをそのままBigQueryに反映します。 新規作成の場合 require ' google/cloud/bigquery ' sql = File .read( ' テーブル名.sql ' ) bq_client = Google :: Cloud :: Bigquery .new( project : ' project ' , credentials : ' path ' ) dataset = bq_clent.dataset( ' データセット名 ' ) dataset.create_view( ' ビューの名前 ' , sql, standard_sql : true ) 更新の場合 require ' google/cloud/bigquery ' bq_client = Google :: Cloud :: Bigquery .new( project : ' project ' , credentials : ' path ' ) sql = File .read( ' テーブル名.sql ' ) dataset = bq_clent.dataset( ' データセット名 ' ) view = dataset.table( ' テーブル名) view.set_query(sql, standard_sql: true) テーブルのマート反映 テーブルのマートについてはBigQueryの Destination Table という機能を使うことで、SELECT文の結果をそのままテーブルに反映できます。 以下のようなコードを実行することでBigQueryにSELECT文の結果がテーブルとして作成されます。この処理は、BigQueryによってアトミックにテーブルが作成されます。 require ' google/cloud/bigquery ' bq_client = Google :: Cloud :: Bigquery .new( project : ' project ' , credentials : ' path ' ) sql = File .read( ' テーブル名.sql ' ) job = bq_client.query_job(sql, standart_sql : true , table : ' テーブル名 ' , write : ' truncate ' , create : ' needed ' ) job.wait_until_done! 差分更新 計算コストの高いマートは事前に集計などの計算をし Table として保持すると紹介しました。しかし集計するデータが大きすぎる場合、毎回全量のデータを集計・更新すると時間がかかりすぎてしまいます。 例えば日付ごとに集計するテーブルの場合、毎日全量のデータを集計し直していると最新の日付以外の集計処理は前日と同じ計算をしてしまうことになり集計処理が増えて行きます。そこで差分更新をすることでこの問題を解決します。 本システムでは差分更新にappendとoverwriteの2種類を行っています。 append appendは集計した結果を既存のテーブルに対してinsertします。 以下のようなテンプレートに対しSQLファイルの中身をそのまま埋め込むことで、「テーブルのマート反映までの流れ」で紹介した処理と同じようにBigQueryに反映できます。 SELECT * FROM `既存のテーブル` union all ( <%= sql %> ) overwrite 一部集計した結果を既存のマートに上書きしなければならない場合appendだけでは対応できません。overwriteの処理ではそのようなケースに対応するため pk などのユニークキーを利用して集計結果をマージします。 こちらもappendのときと同じように以下のようなテンプレートに対してSQLを埋め込みBigQueryに反映します。 SELECT * except(priority, row_number) FROM ( SELECT *, row_number() over (partition by <%= pk %> order by priority) as row_number FROM ( SELECT *, 1 AS priority FROM ( <%= sql %> ) union all SELECT *, 2 FROM `既存のテーブル` ) ) WHERE row_number = 1 以上のように差分更新する対象のテーブルは統一した方法で差分更新が行われるようになりました。 append更新の改善 最初は以上のようにappendとoverwriteの処理を通常の更新処理と同じように行っていました。しかし運用しているうちに SELECT * FROM により全件のデータを毎回取得していることが以下の2点で問題になりました。 処理に時間がかかる BigQueryはクエリの使用量に対して課金されるためお金がかかってしまう そこでBigQueryの append の機能を利用するように変更しました。この、 append によるデータの追記もBigQueryによりアトミックに行われることが保証されています。以下のコードで、それを実現しています。 require ' google/cloud/bigquery ' bq_client = Google :: Cloud :: Bigquery .new( project : ' project ' , credentials : ' path ' ) sql = File .read( ' テーブル名.sql ' ) job = bq_client.query_job(sql, standard_sql : true , table : ' テーブル名 ' , write : ' append ' , create : ' needed ' ) job.wait_until_done! これにより、 SELECT * FROM の処理をなくすことに成功しました。ただし、overwriteは以上の解決策は適用できないため上で紹介した通りに今も更新を行っており、課題となっています。 冪等性の担保 差分更新の仕組みを紹介しましたが、差分更新の処理があると何かの問題で処理を再実行した場合データが重複するといったデータの整合性に対する問題が生じてしまいます。 例えば、既存のテーブルに対して append の処理をします。その後再実行したい場合、もう一度 append の処理を行うと append により追加されたデータが重複してしまいます。 そこで、以下のようにすることで冪等性を担保し、何回同じ処理をしても同じ結果になるよう工夫しました。 また、 overwrite でも同じように処理を行います。 マート更新時に、マートと同じデータを保持したバックアップを作成します。そして、差分更新を行う場合はバックアップに対して差分更新を行い、その結果を実際のマートに反映します。 2018-01-06 にマートを更新すると以下のような処理になります。 これをコードにしたものが以下になります。 require ' google/cloud/bigquery ' bq_client = Google :: Cloud :: Bigquery .new( project : ' project ' , credentials : ' path ' ) dataset = bq_clent.dataset( ' データセット名 ' ) table = dataset.table( ' mart_table ' , skip_lookup : true ) backup_table_yesterday = dataset.table( ' mart_table_20180105 ' , skip_lookup : true ) backup_table_today = dataset.table( ' mart_table_20180106 ' , skip_lookup : true ) backup_table.copy_job(destination_table, create : ' needed ' , write : ' truncate ' ) # バックアップテーブルを実際のテーブルにコピー job = bq_client.query_job(sql, standard_sql : true , table : table, write : ' append ' , create : ' needed ' , &job_configuration) job.wait_until_done! raise " Fail BigQuery job: #{ job.error }" if job.failed? table.copy_job( ' バックアップテーブル.sql ' , create : ' needed ' , write : ' truncate ' ) # 更新されたテーブルをバックアップ 以上のように差分更新をすることで、何回リトライしてもデータの整合性が保たれるうようになりました。 マートの依存関係 マートが他のマートを参照している場合、更新する順番を間違えるとデータに不整合が生じると紹介しました。例えば existing_table1 existing_table2 のテーブルが存在するとし、以下のような4つのマート構築を考えます。その場合、「table3の前にtable1」「table4の前にtable1とtable3」が更新されている必要があります。 table1.sql SELECT * FROM `project.dataset.existing_table1`; table2.sql SELECT * FROM `project.dataset.xisting_table2`; table3.sql SELECT * FROM `project.dataset.table2`; table4.sql SELECT * FROM `project.dataset.table1` UNION ALL SELECT * FROM `project.dataset.table4`; そこで以下のようなグラフを作成し、実行順を確認します。このグラフは、SQLの FROM または JOIN の後ろのテーブル名とSQLファイル名を利用します。FROMの後ろに書かれているテーブルはSQLファイル名のマートよりも前に実行しなければなりません。そのため「FROMの後ろのテーブル名」->「SQLファイルのファイル名」というグラフを作成します。 例えば上の4つのSQLからマートのグラフを作成すると以下のようになります。 以下のようなコードでグラフを生成します。 graph = {} sql_files = [table1.sql, table2.sql, table3.sql, table4.sql] sql_files.each do | sql_file | sql_file_table_name = sql_file.split( ' . ' ).first sql = File .open(sql_file, ' rt:UTF-8 ' ) { | file | file.read.chomp } related_tables = sql.scan( /(?: FROM | JOIN )[\s \n]+ ` (.+?) ` /i ) graph[sql_file_table_name] = related_tables.map do | str | _, table = str.first.split( ' . ' ) next if table == sql_file_table_name # 自己参照は除外 table end .compact end この結果に対してマート以外のテーブルを削除すると、以下のような結果になります。 [ { ' table3 ' => [ ' table2 ' ]}, { ' table4 ' => [ ' table1 ' , ' table3 ' ]} ] 並列実行 以上のグラフを利用することで、マート作成のクエリの実行の順番を担保したまま並列化できます。並列実行を簡易化するために以上のようなグラフを、以下のようなテーブルのリストのリストに変換します。各テーブルのリストは先頭から順番に実行でき、テーブルのリストの中身はそれぞれ並列実行することが可能になります。 [[table1, table2], [table3], [table4]] この変換は、まず親ノードがないテーブル一覧をリストに追加します。そして追加したテーブルをグラフから削除し、もう一度親ノードがないテーブル一覧をリストに追加します。これを繰り返すことで、以上のようなリストが生成されます。 以下のようなコードでグラフからリストを生成します。 def sort_tables (graph) graph = graph.dup result = [] until graph.empty? nodes = graph.keys.select { | node | graph[node].empty? } result << nodes raise ' cyclic path detected! ' if nodes.empty? nodes.each { | node | graph.delete(node) } graph.transform_values! { | v | v - nodes } end return result end このリストを利用し、最初に「table1, table2」続いて「table3」最後に「table4」を実行します。 並列実行はDigdagで実現しており、以下のようなconfigファイルになっています。 +set_refresh_views: call> set_refresh_views # REFRESH_TABLESESにテーブルのリストのリストを格納 +update_views: for_each>: REFRESH_TABLES: ${REFRESH_TABLESES} _parallel: false _do: for_each>: REFRESH_TABLE: ${REFRESH_TABLES} _parallel: true # ここをtrueにすることで並列実行を実現 _do: _retry: 3 call> refresh # 実際にマートを更新する処理 以上のように実行する順番を考える必要があった所を、システムにより自動解決することに成功しました。さらに、並列実行をすることで更新スピードの短縮にも成功しました。 このシステムを作ってどうなったか 最後に本システムによりデータマートの運用に関してどのようになったか紹介します。 GitHubでマート管理できるようになり日々追加されるマートの管理を統一したルールで管理できるようになった 差分更新や並列実行を導入したことで、データマートの更新の時間短縮に成功した データマートのすべてのクエリをGitHubで参照できるようになった システムを定期的に実行することでテーブルの更新が自動化された データマート同士の参照が自動解決されるようになった 以上のように冒頭に挙げた問題を解決することに成功しました。またその他に以下のようなメリットが見られました。 冪等性の担保をしたことで、マート更新の再実行を気軽にできるようになった マートのSQLをGitHubで管理することによりコードレビューが活発になった GitHub上でレビューができるため誰でも気軽にマートを追加できるようになった 以上のようにデータ基盤のコード管理とマート管理をGIthubに統一、システム化することで様々なメリットが得られました。 最後に 本文でも紹介しましたが、本システムにはまだまだ問題が残されています。弊社では一緒にデータ基盤を作ってくれる方を大募集しています。 ご興味がある方は以下のリンクから是非ご応募ください! www.wantedly.com
アバター
こんにちは! 好きなスシローは 五反田店 なバックエンドエンジニアのりほやん( @rllllho ) です。 9/6,7,8に開催された builderscon tokyo 2018 へ参加しました。 カンファレンスで印象に残ったセッションをいくつかご紹介します。 buildersconとは 公式サイト にはbuildersconについて下記のように説明されています。 buildersconは「知らなかった、を聞く」をテーマとした技術を愛する全てのギーク達のお祭りです。buildersconではトークに関して技術的な制約はありません、特定のプログラミング言語や技術スタックによるくくりも設けません。 必要なのは技術者達に刺激を与えワクワクさせてくれるアイデアのみです。 あなたが実装したクレイジーなハックを見せて下さい。あなたの好きな言語のディープな知識をシェアしてください。あなたの直面した様々な問題と、それをどう解決したかを教えてください。未来技術のような未知の領域について教えてください。 記述の通り、プログラミング言語に特化したカンファレンスではなく包括的に様々な技術を扱っているカンファレンスです。 印象に残ったセッション 知らなかった時に困るWebサービスのセキュリティ対策 speakerdeck.com tnmtさんによる カラーミーショップで実際に発生したセキュリティインシデント の実例を元に、どのような攻撃が仕掛けられたのか、どのような対処・再発防止を行ったかについてのお話でした。 上記インシデントの検知は、不正なファイルが置かれたことを検知したのではなく不正なファイルが実行されたことにより発生した負荷アラートがきっかけで発覚したそうです。 またデータベース関連のログが不足しており、データベースへアクセスされた痕跡はあったものの何件抜かれたかなどがわからない状況だったそうです。 インシデントを受けセキュリティ新体制の見直し、WAF、OSコマンドの実行ログ、クエリログを残すなどの対応を行なっているそうです。 webアプリケーションに携わっている身として、セキュリティの問題は絶対避けては通れない問題です。 実際に起こってしまったインシデントに対しての対応や経験など実体験を聞くことができ、とても勉強になりました。 「Webとは何か?」あるいは「WebをWebたらしめるものは何か?」 www.youtube.com Jxckさんによるwebのこれまでの歴史、いまのwebの役割とは何なのか、これからのwebはどうなっていくのかというお話でした。 ティム・バーナーズ=リーが構想した最初のwebはHypermedia Systemとしてのwebでした。 JavaScriptやiframeなどを使用できるようになりwebによってできることが増え、webはHypemedia Systemからアプリケーションシステムへと役割が変わっていきました。 さらに現在ではwebからデバイスに接続できるようにもなり、Operation System化しているとのことでした。 webはセキュリティモデルと共に進化しているという話が印象的でした。 Ajaxの登場によりJavaScriptがブラウザの意図しない情報を通信できるようになったため、セキュリティモデルとしてoriginが導入され、このoriginというセキュリティモデルがあるおかげでAjaxを正当化できwebのアプリケーション化が進んだそうです。 そのためOS化しようとしている現在のwebのセキュリティモデルであるPermissionの必要性についてお話しされていました。 セキュリティモデルをどのように変化させて策定するかがwebの進化にとても重要であるという話がとても面白かったっです。 ソーシャルゲームが高負荷に陥っているとき、何が起こっているのか speakerdeck.com takihitoさんによるゲームの開発や運用の体制についてのお話でした。 実際にゲームを運用する上で発生した高負荷による障害とその対応について4つ紹介されました。 アプリケーションサーバーに対する高負荷検証は行なってはいたが、ログサーバーが盲点となっておりボトルネックになってしまった話 リリース後にサーバーの負荷が異常に高くなっており確認したところ、Redisのkeysコマンドがアプリケーションで使用されており高負荷になってしまっていた話 大規模な流入施策を行うために検証を行なってはいたが、アプリ申請後に重たいリクエストをアプリから頻繁に叩かれていることに気づきAPIを軽量に研ぎ澄ました話 ガチャによる高負荷によりDBが詰まり緊急メンテナンス、DBのチューニングやアプリケーションコードの修正を行なっても収まらず最終的にはテーブルの設計変更を行った話 ゲームならではの急激な高負荷についてのお話はとても興味深かったです。 高負荷対策のお話の中で、レプリケーション遅延なども考慮しなければいけないためアプリケーションのスケールアップよりDB関連のスケールアップの方が難しいということをおっしゃられていてDB設計の重要さを再確認しました。 RDB THE Right Way 〜壮大なるRDBリファクタリング物語〜 speakerdeck.com soudaiさんによるRDBのリファクタリングについてのお話でした。 アンケートフォームの設計を例に、RDB設計のアンチパターンをご紹介されていてとてもわかりやすかったです。 DBのテーブル設計を行う際に初めからデータの設計やデータストア選定などをしてしまいがちですが、最初にデータ設計をせずにモデリングを正しく行うことがとても重要だそうです。 またテーブルの責務はクラスの責務に似ており、テーブルのスコープを小さくすることを意識した方がよいとのことでした。 データベースの問題はすぐに顕在化するものではなくリリース後忘れた頃に顕在化するという話にはとても共感しました。 プロジェクトに途中から入った際にDB構成を理解する方法として、チームで『テーブル一覧を眺める会』を開催するとよいそうです。 中には誰も知らないテーブルが出てきたりするそうです。さっそくチームでやってみようと思います。 解決した問題の大きさがエンジニアの価値、大きな問題に立ち向かおう。という言葉がとても印象的でした。 自分もエンジニアとして、大きな問題に技術で立ち向かっていきたいです。 ブログサービスのHTTPS化を支えたAWSで作るピタゴラスイッチ speakerdeck.com aerealさんによる、独自ドメインで運用されているはてなブログのHTTPS証明書を配信・発行する仕組みについてのお話でした。 証明書配信に関しては、はてなブログでは万単位の独自ドメインがありリクエストごとに証明書を取得・使用するため低レイテンシであることが求められます。 そのためmemcacheに証明書をキャッシュしデータストアであるDynamoDBへの問い合わせを減らすことで、レイテンシを悪化させずに証明書の配信を行なっているそうです。 証明書発行に関しては、発行リクエストが失敗し続けるとAPIの上限回数を超えてしまうため、失敗した場合に対象から外したり外部API通信のエラーを適切に処理する必要があります。 AWSのStep Functions(SFn)というワークフローサービスとLambdaを組み合わせた仕組みになっているそうです。 DynamoDBへ保存する際に設定するTTLが切れたタイミングでトリガーが発火し証明書を更新するLambda関数が呼び出され、証明書の自動更新を行なっているとのことでした。 データ自身が更新タイミングを持つことにより、更新するLambdaが更新対象を抽出する必要がなくなります。 Lambdaの責任が証明書を更新することだけに切り分けられ、関数がシンプルになるということでした。 ワーフクローエンジンのみがバッチの状態・順番を知っており、関数やクラスは状態を保存せず入力に対し処理を実行し出力するというシンプルな形にすることでバッチの複雑性を排除したそうです。 まとめ DB設計の話や高負荷の話など、私が仕事で使っている身近な技術のお話を聞くことができとても勉強になり、たくさんの『知らなかったことを、聞く』ことができました。 カンファレンスでたくさんの知見を得たので、業務に還元していきたいと思います。 エンジニアが好きな技術や作ったもの、経験したことを熱く喋るお話はとても面白かったです! 運営の方・ご登壇の方々、素敵なカンファレンスをありがとうございました! スタートトゥディテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com
アバター
こんにちは。品質管理部エンジニアリングチームの高橋です。 今回は品質管理部として初のTECH BLOG投稿ということもあり、 「品質 / Quality」について掘り下げてみたいと思います。 「品質」の意味 「品質」という言葉の語源は古代ギリシャにまで遡ります。 「万学の祖」と称されるアリストテレスは、物質(Substance)を「量的側面」と「質的側面」に分けて定義しました。 その際に量的な側面を表現する言葉として英語の質量(Quantitiy)に相当するギリシャ語を用いました。 そして対立概念である質的な側面を表現する言葉として、英語のQualityの語源となったラテン語のQualitasを用いたと言われています。 月日は流れ現在、ISO 9000シリーズにて「品質」は「本来備わっている特性の集まりが、要求事項を満たす程度」と述べられています。※ISO 9000シリーズとは、国際標準化機構が定めた品質マネジメントシステムに関する規格の総称です。 『プログラミングの心理学』や『一般システム思考入門』を著した、ソフトウェア開発の人類学者であるジェラルド・ワインバーグは、「品質は誰かにとっての価値である」と自著内で述べています。 またソフトウェアエンジニアリングとしてのソフトウェア「品質」は以下のように定義されています。 (以下、 https://en.wikipedia.org/wiki/Software_quality より引用) In the context of software engineering, software quality refers to two related but distinct notions that exist wherever quality is defined in a business context: →ソフトウェアエンジニアリングにおいて、 ソフトウェア品質 とは、2つの関連しているが、異なる概念が定義される。 ・Software functional quality reflects how well it complies with or conforms to a given design, based on functional requirements or specifications. That attribute can also be described as the fitness for purpose of a piece of software or how it compares to competitors in the marketplace as a worthwhile product. It is the degree to which the correct software was produced. →ソフトウェアの 機能品質 は、機能要件または仕様に基づいて、与えられた設計に準拠しているかを指す。その特性はソフトウェアの目的に対する適合度、もしくは市場において競合ソフトと比べてより価値があると判断するための手段とも言うことができ、どれだけ正確にソフトウェアが作られたかの度合いである。 ・Software structural quality refers to how it meets non-functional requirements that support the delivery of the functional requirements, such as robustness or maintainability. It has a lot more to do with the degree to which the software works as needed. →ソフトウェアの構造的品質とは、堅牢性や保守性などの機能要件の提供をサポートする 非機能要件 を指す。ソフトウェアが必要に応じて動くための数多くの要件がある。 「品質」が良いとは 品質の良さに関して、「立場」から考えてみます。 もし自分が経営者の立場であったならば、最優先するべきは売上げです。 その為、会社として利益の大きいもの=品質が良いと考えられます。 では経営者ではなく研究者の立場だったらどうでしょう。 研究はそもそも莫大なお金がかかるものであり、研究者が研究中にコストやコストパフォーマンスを優先することはないでしょう。 おそらく研究者の方は、世界に比類ない発見や革新的なものを素晴らしいものと考えるはずです。 経営者 →最大限の利益が出るもの 研究者 →最新のテクノロジーが盛り込まれた革新的なもの 開発者 →要求された機能が実装された、整然としたコードで作成されたもの テスター→仕様書通りに動作する不具合のないもの このように、「品質」の基準は、判断する人の立場によって決定されます。 今度は品質の良さに関して、「物・サービス」から考えてみます。 例えば1万分の1の確率で何らかの不具合が発生する物・サービスの存在を仮定します。 そのサービスが「本の製本」だとしたら。製本ですので、1万冊に1冊の割合で落丁が発生します。 しかし出荷段階で弾いたり、無償で返品対応を行えば、そこまで大きな問題にはならないはずです。 ではそのサービスが「医療機器」だとしたら。 1万台に1台の割合で使用途中に動作が停止してしまうペースメーカーが出荷されたとしたらどうでしょう。 おそらく社会的な問題になるのではないでしょうか。 医療機器に限らず、人の命に関わる物・サービスに関しては、たとえ1万分の1であっても不具合は許されません。 このように、「品質」の基準は、物・サービスの目的によっても変わってきます。 上記のように様々な「品質」の価値基準を一概に定めることは不可能である為、必然的にどこに基準を置くかが重要になってきます。 では品質管理部の一員としての私は、「品質」の基準をどこに定めればよいのでしょうか。 ここでもう1度ジェラルド・ワインバーグの言葉を反芻してみます。 「品質は 誰か にとっての価値である」 この「誰か」とは、「 ユーザー/顧客 」と定義するべきでしょう。 すなわち、「品質」の価値基準は「ユーザー」です。 自社の提供する物・サービスを利用するユーザーの皆さんが、どうすれば便利で・安全に・楽しくサービスを利用できるか考えるのが品質管理部の使命ではないでしょうか。 「品質管理」とは 前項で品質の基準はユーザーに置くべきと述べました。自ずと「品質管理」の指針も見えてくるのではないでしょうか。 ただここで1つ留意しておきたいのが、 品質管理の成功 = ユーザーの満足 品質管理の失敗 = ユーザーの不満足 であるとは必ずしも言えないということです。 1980年代に狩野紀昭によって提唱された、狩野モデルという品質に関するモデルが存在します。 狩野は品質を「当たり前品質」「一元的品質」「魅力的品質」「無関心品質」「逆品質」の5つに分類しました。 そしてその品質がどの品質要素に該当するかによって、顧客満足度に与える効果が異なると提唱しました。 たとえ不具合が残っていたとしても、狩野モデルでいう「無関心品質」に該当する品質の不具合であればユーザー満足度に大きな影響は与えません。 逆に他の「一元的品質」や「魅力的品質」の品質の完成度が高ければ、ユーザーは十分に満足することでしょう。 ただどちらにせよ、品質の基準 = ユーザー の公式は基本的にゆるがないと思っています。 よって、「品質管理」= ユーザーが満足できるサービスが提供されるように導くこと だと私は考えています。 企画→開発→テストの全ての工程において、ユーザーを最優先に考える。そしてそれを継続することではじめて、「品質」が向上していくと思います。 「お客様は神様」という言葉が、接客を伴う業務の際に周知されたりしますよね。 多少オーバーな表現の気もしますが、ユーザーが直接目に見えないソフトウェア開発においても、お客様を第一に考えて業務にあたる姿勢が何よりも大切ではないでしょうか。 おわりに 今回は品質管理部の初投稿ということで、「品質」に関して記事を書かせて頂きました。 記事中でも述べたように、「品質」や「品質管理」に答えはないと思っています。 みなさんの考える「品質」とはなんでしょうか? ぜひ考えをお聞かせ下さい。 スタートトゥディテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.starttoday-tech.com 参考文献 / クレジット 参考文献 https://en.wikipedia.org/wiki/Software_quality https://ja.wikipedia.org/wiki/ISO_9000 https://ja.wikipedia.org/wiki/%E7%8B%A9%E9%87%8E%E3%83%A2%E3%83%87%E3%83%AB クレジット https://www.flaticon.com/free-icon/check-list_1102560#term=check&page=2&position=54 https://www.flaticon.com/free-icon/medal_1067629#term=quality&page=3&position=22
アバター
Kotlin Fest2018参加レポート 福岡研究所の渡辺(しかじろう @shikajiro)です。Kotlinのおっきなイベントが東京で開催されるということで福岡から飛んで✈いきました。 福岡でもFukuoka.ktという名前で過去に2回ほどイベントを主催しており、KotlinFest主催の太郎さんに登壇していただいたこともありました。僕自身3か月ほどKotlinから離れてましたが、直近の技術情報などをフォローできたらいいなと思い参加しました。 KotlinFestとは 2018/08/25(土)に開催された有志による日本最大のKotlinイベントです。 kotlin.connpass.com 「Kotlinを愛でる」をビジョンに、Kotlinに関する知見の共有と、Kotlinファンの交流の場を提供する技術カンファレンスです。 とあるように、Kotlinを愛するKotlinユーザーグループのメンバーにより運営されています。 参加者プレゼント すごい作りのしっかりしたトートバッグをもらいました! まじ嬉しい! 企業ブース イベントはトークセッションだけでなく企業ブースの参加もありました。CyberAgentさんによるKotlinPuzzlersが人気で、「ぱっと見では間違えてしまいそうなKotlinコード」がどのように動くのかを当てる内容です。 (ぱっと見でわからないコードってそもそも問題だよね) 僕も挑戦しましたがまんまと間違えたので早々に去りました。 Yahooさんのブースではくじ引き(外れました)とモブプロをやっていました。群衆(モブ)プログラミング(プロ)は皆に見せながらプログラミングすることで、外野と「これはこう書くよね」とコミュニケーションを取ることでコードの質を上げていくものです。 オープニング オープニングセッションでは主催の長澤太郎さん(以下太郎さん)によるKotlinの歴史やこれからのお話がありました。 Kotlinを愛でる が今回のテーマ。Kotlin in Actionの第二部のタイトルが元ネタだそうです。 Kotlinの歴史を振り返ると、発表から1.0まで5年ほど時間がかかりましたが、それからは怒涛の勢いでリリースされています。AndroidやSpring公式になることで利用ユーザーもどんどん増えていっています。 2011/7 kotlin発表 2016/2 1.0リリース 1.1コルーチン 1.2マルチプラットフォーム 2016/5 gradle 2017/4 native 2017/5 Android公式言語に 2017/9 Spring Framework5公式に 1.3コルーチンとインラインクラス 2013/7にはJapan Kotlin User Group(JKUG)も発足しており、勉強会、Slack、読本など精力的に活動しています。 サンフランシスコで開催されたKotlinConf 2017は1200人もの参加があり、世界中の開発者からのKotlinへの期待の高さが伺えます。 プレゼンでは以下のようにAndroidばかりではなく、サーバー用途でKotlinを使ってもらおうとしているようです。 Server 30% Android 24% Native 18% 次回のKotlinConf 2018は2018/10にアムステルダムで開催。 3days、61speakerになり、規模が一気に拡大しています。 (僕も行きたいと思ってたけどSold Outでした) Ktor game graphQL GCP など、Kotlinを色んな所で色んな使い方をする発表がたくさんあるようです(行きたかった)。 話は戻り、KotlinFestは多くの協賛企業に支えられており、積極的にKotlinを採用していっているようです。スポンサーは募集してから即日で埋まったみたいです。 企業名 導入内容 BIZREACH Server CA Android LINE Android, Server, Clova M3 Android, Server mercari Android mixi Android sansan Android YAHOO Android, Server dmm ほか 藤原さんからはKotlinの現状のまとめのお話でした。 Kotlinの特徴は以下の通り。 マルチプラットフォーム 型言語 関数型、オブジェクト指向 OSS そして、Kotlinには哲学があるとのこと。 実用主義 Javaの考え方のまま。学習が容易。 完結 読みやすい。ボイラープレートは少ない 安全 静的型付け、NULL安全、スマートキャスト 相互運用性 Javaと仲良し 現在の日本語本は代表的なところで以下があり、入門と中級をカバーしています。 Kotlin in Action 赤べこ、黒べこ みんなでKotlinを愛でましょう ということで、KotlinFestはスタートしました。 お昼ごはん オープニングセッションが終わったらいきなりランチタイムです。KontributorであるshirajiさんがTwitterで ぼっち飯を回避する ランチの相手を募集していたので、ご一緒させていただきました。 明日Kotlin Festでランチ誰か一緒に行かないですか?サーバサイドKotlinの話したい方、Kontributeしたい方、弊社興味ある方ぜひ!!!自分含めて四人くらいで! (自腹でお願いします。。。) — shiraji (@shiraj_i) 2018年8月24日 美味しいお肉を皆で食べました。美味しかったです。 Kotlinで改善するAndroidアプリの品質 Android界の著名人であるあんざいゆきさんに拠るアプリ品質のお話です。 JavaのソースをKotlinに移行するとして、移行の難しさ、書き直しコスト、リグレッションなどの問題が出てくるけど、「Kotlin化にはそれを上回る効果あるのか?」を考える内容です。 まず、 品質 について、[オブジェクト指向入門~原則・コンセプト~]( https://www.amazon.co.jp/dp/4798111112 ) に「品質とは何か」がまとめられているので、この発表ではこれを基準とされていました。 外的品質要因 ユーザーが認識 スピード、つかいやすさ 要因 正確さ 頑丈さ 拡張性 再利用性 内的品質要因 それ以外 モジュール性、読みやすさ JavaからKotlinにソースを書き直しても、アプリの使いやすさは変わりません。しかし、内的品質要因は外的品質要因に影響するので、アプリを使いやすくするときに内的品質を向上していると良い影響が出ると説明されています。 Javaの実装パターンをまとめた名著である Effective Java とKotlinの実装をいくつか説明していくことで、Kotlin化の正当性を説明されています。 builder pattern コンストラクタの引数を増やすのではなく、Builder Patternを使うようにするのがJavaでのテクニックでした。 ※このサンプルソースは僕が考えたものです。 コンストラクタの場合 第1引数と第2引数が同じStringのため、2つを間違えてしまう可能性があります。これがbuilder patternを使うと分かりやすくなります。 User user = new User( "shikajiro" , "fukuoka-city" , Gender.MAN); builder patternの場合。 User user = User.builder() .name( "shikajiro" ) .gender(Gender.MAN) .address( "fukuoka-city" ) .build(); nameと"shikajiro"が対になるので分かりやすいですね。 builder patternの欠点は、このための実装をUserクラスにする手間がかかることです。 Kotlinだとこのbuilder patternは名前付き引数でできます。 val user = User(name= "shikajiro" , gender=Gender.MAN, address= "fukuoka-city" ) ただし、Kotlinの名前付き引数は1つの引数に複数個指定とかできないので、場合によってはbuilderの方が良い時もあるとのことでした。 など、他に数パターン、Effective Javaに書いてある問題がKotlinでは解消される事の説明がありました。 これはつまり、Kotlin化を進めることでコードの内的品質が向上し、結果的にはユーザーの使いやすさに繋がるということです。 最後に 「明瞭で、正しく、再利用可能で、頑丈で、柔軟性があり、保守可能なプログラムが書ける」 と締めくくられていました。 How to Test Server-side Kotlin エムスリー株式会社の鈴木さん、前原さんのお二方に拠るテスト実装の実体験です。 Androidアプリはテスト実装の経験があるのですが、サーバーはあまり触ってなかったので聞かせていただきました。 基本的に標準である、JUnit4, Mockito, AssertJを選択したとのことです。テストライブラリはたくさん出てきているそうですが、まずは標準であることの信頼を選んだようです(変なところハマると辛いですもんね…)。 選択するライブラリが沢山あるので、開発チームに合うものを選定するのが大事になりそうです。 歴史的な経緯 によりDBが肥大化しているため、実装と切り離す必要があり、 いきなり全てをテストするのは困難なので、まずはアプリケーション層を主にテストしたそうです。 コンストラクタが肥大化したクラスのデータ作成が大変辛くなったので、IDEからテストコードを自動生成するものを作ったそうです。 Kotlinでもテストちゃんと書ける! ということで、サーバー実装をする際は積極的に使っていきたいです。 Kotlin linter DMM釘宮さんによるLinter開発のお話です。 KotlinにはいくつかLinterがあるので、技術選定の材料にしてほしいので壇上に上がってくださったようです。 Linterは現在有名なものが3種類。 ktlint detekt android-lint ktlint linterをカスタマイズするには AST を知る必要がある。 ASTはツリー構造。PsiViewerを使う事でASTをビューできるので、これを使いながら進めると良いとのこと。 (なんとなくAPI responseのパーサーを実装したときを思い出しました) カスタムルールのformaterも作れるけど、実装する必要がある(lintルールとformatterが乖離するのはとても辛そう) detekt ktlintより高機能だが、フォーマット機能は途中からなくなったようです。カスタムルールでは作れるらしいです。 Kotlinコルーチンを理解しよう 株式会社Lang-8八木さんによるコルーチンの解説です。 コルーチンの歴史を紐解くと、1963年にメルヴィンコンウェイがcobolコンパイラでコルーチンの概念を出したそうです。 コルーチンとは 一時停止可能な計算インスタンス であり、スレッドに似ているけど、スレッドに束縛されないのが特徴です。継続状況を持つプログラムが容易に記述できます。Future Promiseみたいに値を返すことも可能です。 プレゼンでは、コルーチンがどのように実現しているかをKotlinから生成されたバイトコードを紐解いて探求していきます。 そこから、コルーチンはステートマシンで表現されていることがわかります。 詳しい説明はプレゼン資料を見ていただくとして、ここではサンプルソースを以下にまとめます。 var rootJob: Job = Job() val job = launch(UI, parent = rootJob) { repeat( 3 ) { try { //直列 val hoge = async {} async { hoge.await() }.await() //並列 async {}.await() async {}.await() return @launch } catch (e: CancellationException ) { } catch (e: Exception ) { if (souldRetry(e)) { return @repeat } } } } job.cancel() rootJob.cancel() 他にも以下のライブラリがコルーチンに対応しているので、Androidでも使わない理由はないようです。 Retrofit EventBus, Channel gistに転がっているらしい android-coroutines onActivityResultをsuspend How to Kontribute v4 JP ランチをご一緒したUBieの磯貝さんによる、KotlinプロジェクトへのContributeの仕方の解説です。 磯貝さんはOSSであるKotlinにコントリビュートした日本人として有名になりました。 photos.google.com KotlinはKotlinで書かれているため、Kotlinを愛している方にはもってこいです。Kotlin Pluginコントリビュータが多いので、決して難しいものばかりではないようです。 Kontribute手順は発表資料に委ねたいと思います。 僕自身、磯貝さんのブログに触発され、DroidKaigi2018のAndroidアプリへのContributeを決意しました。結構エグいIssueを辛うじてfixさせることができ、大きな自信へと繋がりました。 この記事を見ているみなさんも、もしContributeに興味がありましたら磯貝さんの記事をご一読ください! クロージング Kotlin Fest 2019は現時点では未定とのことです。もしやるなら次回はBigゲストを招待したいとのことでした。 懇親会 さぁ、ついに本番が始まりました。懇親会にも多くの参加者、発表者の方が参加し、Kotlin愛を語り合っていました。 スタッフの皆さん、本当にお疲れ様でした。 こうして、しかじろうのKotlinFestは幕を下ろしました(この後Android老人会の皆さんと二次会に行きました)。 引用 Top画像は https://kotlin.connpass.com/event/91666/ より引用させていただきました さいごに スタートトゥデイテクノロジーズではKotlinを使ってアプリ・サーバーを開発するエンジニアをこれでもかというほど募集しております。 www.wantedly.com www.wantedly.com www.wantedly.com 福岡研究所でも機械学習などを使ってファッションを科学する研究者を募集しております。 www.wantedly.com
アバター
こんにちは、新事業創造部の遠藤です。現在WEARの開発を行っています。 最近はWEARのコーディネート一覧やユーザー一覧など、リスト画面にバナー型の広告を実装をしました。 リストにデータを挿入する実装は簡単なように思えますが、種類の違うデータを扱う場合には、考慮するべきポイントがいくつかあります。 本記事ではリストに広告を表示することを例に、種類の違うデータをリストに挿入する際のデータの持ち方・実装ついて紹介したいと思います。 仕様 リスト画面にバナー型の広告を表示するにあたっての仕様は以下のとおりです。 広告を取得できない場合はリストをつめる スクロールして戻っても同じ広告が表示されている 広告のインプレッションは表示時のみとなるように制御する スクロールに合わせて遅延なく広告の表示をする 上記のような元々表示していたデータと異なる仕様を扱うため、実装が複雑になります。 バナー型広告について リストにデータを挿入する実装を紹介する前に、表示しているバナー型広告について軽く触れたいと思います。 今回はGoogle Mobile Ads SDKが提供しているバナー型広告(DFP)をリストに表示しています。 このバナー型広告は広告取得リクエストをするとdelegateメソッドを通じて表示するバナーのオブジェクトが取得できます。 import GoogleMobileAds class ViewController : UICollectionViewController , GADBannerViewDelegate { let banner : DFPBannerView ! override func viewDidLoad () { super .viewDidLoad() // バナー広告のインスタンスを生成してリクエスト banner = DFPBannerView() banner.adUnitID = "xxxxxxxxxxxxxxxxx" banner.delegate = self banner.request(DFPRequest()) } func adViewDidReceiveAd (_ bannerView : GADBannerView ) { // 広告取得成功のdelegate } func adView (_ bannerView : GADBannerView , didFailToReceiveAdWithError error : GADRequestError ) { // 広告取得失敗のdelegate } } また、広告が表示されなくても広告取得が成功したタイミングでインプレッションの計測がされるようになっています。 インプレッションを手動でカウントする仕組みはあるのですが、第三者ネットワーク広告には使用できないです。 実装について データの持ち方について リストに種類の違うデータを挿入する際に肝となるのはデータの持ち方だと思います。 データ保持の方法はいろいろあると思いますが、今回は以下の考慮するポイントをもとにデータの持ち方について検討しました。 考慮するポイント   1. 広告が取得できない場合の対応 2. cellの再利用 3. インプレッション計測を正しく行う 1. 広告が取得できない場合の対応 広告を取得できなかった場合にリストをつめる簡単な方法は、cellの高さを0にすることだと思います。 しかし、UICollectionViewで minimumLineSpacing を使用して実装している場合は気をつけないといけません。 高さを0にすることでリストがつまったように見えますが、cell自体は残っています。 高さを0にしたcellの minimumLineSpacing と前のcellの minimumLineSpacing が合わさりコンテンツ間のスペースが広く見えてしまいます。 なので、広告が取得できなかった場合には高さを0にする方法ではなくデータでの制御が必要になります。 2. cellの再利用 UITableViewやUICollectionViewはcellが再利用されます。 cellを表示するタイミングで広告のリクエストを行うと、スクロールするたびに違う広告が表示されてしまいます。 この問題を解決するために、バナーのViewを保持する必要があります。 3. インプレッション計測を正しく行う スクロールに合わせて遅延なく広告を表示するために、広告の先読み処理を行うと思います。 しかし今回導入したバナー型広告は、広告を取得したタイミングでインプレッション計測が行われます。 たくさんの広告を先読みしてしまうと、広告を表示していないのに、インプレッション数が増えてしまいます。 なので、表示する直前に1件ずつ広告取得のリクエストを行う必要があります。 上記の考慮するポイントをから、以下の3種類のリストデータを持つことにしました。 リスト画面で表示するコンテンツのみのリストデータ コンテンツ、AD、ADを入れる位置を確保するためのスタブのリストデータ ADの位置を確保するためのデータです。 コンテンツと取得できた広告だけのリストデータ 表示に使用するためのデータです。 UICollectionViewのdataSource、delegateで使用します。 ADを入れるための位置を確保したデータと表示に使用するデータを分けることにしました。 これにより実際に取得できた広告のみデータとして扱われることになるので、広告の取得が失敗したときにcellの高さを0にするという対応をしなくてよくなります。 また、広告の表示位置をスタブを使用して確保するようにしました。 そうすることで広告を挿入するたびに、挿入する位置の計算をしなくてよくなります。 リストのデータについてですが、スクロールして戻っても同じ広告を表示する仕様を実現するために、表示するバナーのViewをデータとして保持することにしました。 実装 データの持ち方が決まったので、リストに種類の違うデータを挿入する実装について大まかに説明していきます。 今回はわかりやすいように、リストに3件ずつ広告を入れる仕様で説明したいと思います。 1. コンテンツのリストデータに広告のスタブを挿入する リスト画面に表示するコンテンツが取得できたら、広告のスタブを挿入していきます。 複数の型を扱うためにenumの配列で実装しています。 class ViewController : UICollectionViewController { enum CellType { case contents(Contents) case ad(DFPBannerView) case adStub } private let adInterval : Int = 3 // 3件ずつ広告を表示する private var contentsList : [Contents] = [] // コンテンツのみのリスト private var dataList : [CellType] = [] // ADスタブが入ったリスト // APIリクエスト完了後に行う処理 // contentsListにはAPIから取得できたデータが追加されている private func insertAdStub () { // データを3件ずつに分割する let chunks = stride(from : 0 , to : contentsList.count , by : adInterval ).map { contentsList[ $0 ..< Swift.min( $0 + adInterval, contentsList.count)].map { CellType.contents( $0 ) } } // 分割したデータにAdStubを挿入していく dataList = chunks.map { ( $0 .count == splitSize) ? $0 + [CellType.adStub] : $0 }.flatMap { $0 } } } 2. 広告の挿入 広告の取得リクエストを行い、実際に表示するバナーのViewが取得できたら、AdStubと置き換えていきます。 import GoogleMobileAds class ViewController : UICollectionViewController , GADBannerViewDelegate { private var ads : [DFPBannerView] = [] private var dataList : [CellType] = [] private var ad : DFPBannerView ? private var latestAdIndex : Int = 0 override func viewDidLoad () { super .viewDidLoad() requestAd() } fileprivate func requestAd () { let bannerView = DFPBannerView() bannerView.adUnitID = "" bannerView.rootViewController = self bannerView.delegate = self bannerView.load(DFPRequest()) ad = bannerView } private func insertAd (bannerView : DFPBannerView ) { for index in latestAdIndex ..< dataList.count { if case .adStub = dataSorce[index] { dataList[index] = .ad(adView) latestAdIndex = index return } } } // MARK: - GADBannerViewDelegate func adViewDidReceiveAd (_ bannerView : GADBannerView ) { guard let bannerView = bannerView as ? DFPBannerView else { return } ads.removeFirst() insertAd(adView : adView ) } } 3. 表示 実際に表示するデータはAdStubを抜いたデータを使用します。 import GoogleMobileAds class ViewController : UICollectionViewController , GADBannerViewDelegate { private func displayData () -> [CellType] { return dataList.filter { if case .adStub = $0 { return false } else { return true } } } // MARK: - UICollectionViewDataSource override func collectionView (_ collectionView : UICollectionView , numberOfItemsInSection section : Int ) -> Int { return displayData().count } override func collectionView (_ collectionView : UICollectionView , cellForItemAt indexPath : IndexPath ) -> UICollectionViewCell { if let contents = displayData()[indexPath : indexPath ] { ・・・ } else if case .ad = displayData()[indexPath.item] { ・・・ } else { fatalError( "Unexpected Display Data" ) } } } 4. 広告は1件ずつリクエストを行う インプレッション計測を正しく行うため、広告を表示する直前に広告取得のリクエストをするようにしました。 しかし表示する直前にリクエストを行うと、広告を表示したいタイミングに広告を取得できないことがあります。 広告が取得できていないと、スクロールして広告を表示する位置にきても広告を表示できません。 なので遅延なく広告を表示するために、直前に広告取得を行うのではなく、少し早いタイミングで広告の取得を行うようにしました。 今回は以下のタイミングで広告の取得リクエストを行っています。 初回表示(viewDidLoad) 広告が表示 広告の取得失敗 import GoogleMobileAds class ViewController : UICollectionViewController , GADBannerViewDelegate { override func viewDidLoad () { super .viewDidLoad() requestAd() } fileprivate func requestAd () { let bannerView = DFPBannerView() bannerView.adUnitID = "" bannerView.rootViewController = self bannerView.delegate = self bannerView.load(DFPRequest()) ad = bannerView } // MARK: - UICollectionViewDataSource override func collectionView (_ collectionView : UICollectionView , cellForItemAt indexPath : IndexPath ) -> UICollectionViewCell { if let contents = displayData()[indexPath : indexPath ] { ・・・ } else if case .ad = displayData()[indexPath.item] { requestAd() } else { fatalError( "Unexpected Display Data" ) } } // MARK: - GADBannerViewDelegate func adView (_ bannerView : GADBannerView , didFailToReceiveAdWithError error : GADRequestError ) { requestAd() } } 以上が今回バナー型広告をリストに挿入する実装についての説明です。 まとめ リスト画面に種類の違うデータを挿入する実装についての紹介でした。 広告など種類の違うデータをリスト画面に挿入して表示する際の参考になれば幸いです。 スタートトゥデイテクノロジーズではiOSエンジニアを募集しています。少しでも興味がある方は、ぜひ一度オフィスにお越しください。 下記からのエントリーもお待ちしています。 www.wantedly.com
アバター
こんにちは。スタートトゥデイ研究所の真木です。 8月5日から8月8日にかけて開催されたMIRU 2018という学会に行ってきました。また、5月下旬から約2か月間にわたって実施されてきた「MIRU若手プログラム」という 若手研究者同士 の交流プログラムにも参加してきたので、今回はその報告をします。 MIRUとは MIRUは正式名称を「画像の認識・理解シンポジウム」といい、21回目となる今年は札幌で開催されました。画像に関する研究の基礎から応用までがスコープに含まれ、この分野の学会としては 国内最大規模 です。今年は 700人 以上の参加があったようです。 通常のポスター発表とオーラル発表に加え、画像認識のトップカンファレンスであるCVPRで採択された論文の招待講演、4件のチュートリアル講演、海外のトップ研究者を招聘した特別講演、異分野の研究者が集って将来展望について議論する特別企画など盛りだくさんの内容でした。 どの発表も素晴らしかったのですが、特に高橋さん(オムロン)によるGANのチュートリアルは、数式による説明と図・動画による説明のバランスが絶妙で大変素晴らしく、勉強になりました。 チュートリアル講演の一部は以下で発表資料が公開されています。 画像認識分野における深層学習〜CNN, RNNからマルチタスク学習まで〜,山下隆義(中部大学) MIRU MIRUわかるGAN,高橋智洋(オムロン株式) スタートトゥデイと画像認識 スタートトゥデイ研究所では、ファッションに関わる基礎・応用研究を行なっており、研究の道具として画像認識をよく利用しています。例えば、商品の画像からカテゴリを自動的に認識したり、アイテム画像の組み合わせからオシャレに見えるコーディネートを推薦する研究などを行なっています。 そこで画像認識の知見をさらに深め、この分野の研究者と交流するため、スタートトゥデイ研究所はMIRU2018にゴールドスポンサーとして協賛し、企業ブースで研究を紹介しました。 大変うれしいことに、多くの方々が私たちの研究に興味を持ってくださいました。ポスターまで足を運んでくださった皆さま、ありがとうございました。 こちらは期間中展示していたポスターです。 若手プログラム MIRU若手プログラムは画像認識に興味を持つ若手研究者の交流企画で、学生を中心に大学の研究者、企業の研究者など40名以上の若手研究者が集まりました。企画内容は、「異分野サーベイ」や「若手の未来を議論する」などの真面目な企画だけでなく、自己紹介LTや写真の「インスタ映え」を競うエンタメ系の企画もあり、本会議と並んでこちらも盛りだくさんでした。 メインの企画である異分野サーベイでは、参加者が画像認識以外の7つの分野から1つを選び、その分野のサーベイを行います。これは、異分野で使われている手法から着想を得て画像認識に持ち込んだり、逆に画像認識で使われている手法をその他の分野に持ち込むことでブレークスルーを起こすことを意図したものです。サーベイ分野は(1)ロボティクス(2)自然言語処理(3)HCI(4)心理学(5)データマイニング(6)音声(7)生体の7つで、わたしはデータマイニングのグループに参加しました。 各グループには若手P実行員が1人ずつオブザーバとして参加し、議論をファシリテートしてくれました。 サーベイを始めるに際し、まず5月中旬に実行委員の米谷さん(東大)と片岡さん(産総研)からそれぞれサーベイ論文の書き方やグループサーベイの方法について以下の資料が提供されました。これを読むだけでもかなり勉強になるのでおすすめです。特に、サーベイは単に新しい論文をまとめるのではなく、分野に対する 新しい視点 を読者に与えることが重要であるという米谷さんの指摘には蒙を啓かれました。 サーベイ論文の書き方,米谷竜(東京大学) https://www.dropbox.com/s/vvqxs698en01uf2/PRMU180518_yonetani_small.pdf グループサーベイ方,片岡裕雄(産業総合研究所) http://hirokatsukataoka.net/temp/presen/180518PRMU_Kataoka.pdf これを受け、各グループの参加者たちはそれぞれ遠隔地にいながらもSlack、Git、Skypeなどのツールを駆使して、約2か月半に渡ってサーベイに取り組んできました。わたしにとっては、自分の研究分野以外の分野をサーベイすること、チームで協力しながらサーベイをすること、その両方が初めての体験でした。研究者として成長していく上で、とても貴重な機会だったと思います。 サーベイの成果はMIRU期間中にポスターでMIRU来場者に向けて発表し、さらに本会議翌日に口頭発表を行いました。他のグループの発表を聞くことで多様な分野の最新動向を把握することができ、とても勉強になりました。全チームの発表資料が下記のURLにて公開されているので、ぜひご覧ください。 MIRU若手プログラム 異分野サーベイ 発表資料 https://sites.google.com/view/miru2018sapporo/wakate_top/各チームの発表資料 若手プログラム通じてたくさんの学生や他企業の方々と仲を深めることができ、またデータマイニングに関する知見を深めることができ、非常に有意義でした。若手P参加者のみなさん、実行委員の皆さん、本当にありがとうございました! 最後に スタートトゥデイ研究所はまだできたばかりですが、ファッションという未開拓な分野を科学的な方法論で探求し、その成果を社会に還元するべく日々研究を行なっています。研究成果は、原則として会社内に留めることなく論文として公開していきます。 私たちと一緒にファッションの研究に挑戦しませんか?スタートトゥデイ研究所では豊富なデータ資産と潤沢な予算のもと研究を行うことができ、国内・国外を問わず業務として学会に参加できます。もちろん、論文を書くだけでなく研究成果を実際のサービスへ実装することでたくさんのユーザーに貢献することもできます。また、MIRU若手Pのような機会への参加を応援してくれる自由な社風があります。 ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com また、私たちの持つデータ資産を活用した共同研究の募集も行なっております。画像認識・機械学習はもちろん、社会科学や認知科学などファッションに関わる幅広い分野で研究を行いたいと考えていますので、ご興味のある方はぜひ以下のリンクからご連絡ください! https://www.starttoday-tech.com/contact/ www.starttoday-tech.com
アバター
こんにちは。新事業創造部インフラチームの内山(@k4ri474)です。 弊社が運営する IQON というサービスでは、長らくMySQLのバージョン5.6.27を利用していました。これは2018年9月にEOLを迎えるため、RDSの方針として強制アップグレードがアナウンスされています。 MySQLを継続する選択肢もありましたが、Auroraの運用知見が溜まっていたということもあり、これをキッカケにMySQLからのAurora移行を実施しました。 新事業創造部ではほぼ全てのAWSリソースをCloudFormationを使って宣言しているため、例に漏れずAuroraもテンプレートへ落とし込むことにしました。 ただ、CloudFormationだけで完結しない作業があったので、今回はCloudFormationでの宣言とコンソールからの手作業を織り交ぜるという対応を取っています。 執筆時点の公式ドキュメントではカバーできていない需要を満たすべく、 CloudFormationを使ったAmazon Auroraへの移行 の手順を皆さんに共有したいと思います。 利用したサービス AWS CloudFormation AWS CloudFormation はAWSのほぼ全てのリソースをテキストベースで管理し構築できるサービスです。 1 構築にはYAMLもしくはJSON形式で記述された テンプレート というテキストファイルを利用します。設計のタイミングで、どのリソースをどういったプロパティで作成するかをコントロールできます。 CloudFormationのテンプレートを使い回すことで、数クリックでサービス環境一式を複製できます。 また、ネットワークの詳細をテキストで表現するため、設計のノウハウをチーム内で共有できたり、他チームに提供する資料としてそのまま使えるというメリットもありますね。 Amazon Aurora Amazon Aurora はAWSの Amazon RDS というサービスで利用できる、MySQL・PostgreSQLと互換性のあるリレーショナルデータベースです。 特徴を何点か抜粋してみました。 パッチの自動適用 最大16TBまでオートスケールされるストレージ リードレプリカによる読み取りスループットの容易なスケール 読み込みエンドポイントによるリードレプリカの抽象化 僕のおすすめポイントは、読み込みエンドポイントがRDS版のロードバランサ感覚で使え、裏側のインスタンスの台数をクライアント側で管理する必要がなくなったことです。 2 Auroraへの移行 Auroraへの移行は、大雑把に言うとMySQLをAuroraで複製し、アプリケーションの接続DBをAuroraへ変更することで実現できます。 複製を作る過程でAuroraクラスタの呼称が変わるのでここで補足しておきます。 MySQLから非同期的にデータをコピー(レプリケーション)してきている段階のAuroraクラスタをAuroraリードレプリカといい、レプリケーションを止めて独立した状態のAuroraクラスタをスタンドアロンAuroraクラスタと呼びます。 今回はスタンドアロンAuroraクラスタをどうやってCloudFormationで作成するかにフォーカスして説明を行います。 AWS公式ドキュメント で示されている手法で移行を行うと、以下のような手順になります。 可能な限りCloudFormationで宣言したい部署の方針に沿って、この手順をテンプレートで最大限表現しようと思います。 CloudFormationでは次の作業ができることを期待し、検証を開始しました。 Auroraリードレプリカの作成とスタンドアロンAuroraクラスタへの昇格がCloudFormationのテンプレートで表現できれば、手作業を介さずに移行できます。 ただ、現実はこうでした。 結論として、リードレプリカの昇格はCloudFormationで実装できませんでした。 CloudFormationでは、Auroraクラスタ作成において Auroraリードレプリカの作成 か クラスタのスナップショットからクラスタを復元 することしかできません。 そのため、Auroraリードレプリカを昇格する作業は手作業となってしまいます。 ただ、普通に手作業で実施すると テンプレートの記述と実際のリソースに相違が発生 してしまいます。 これを解消するため、以下の手順で移行を実施しました。 Auroraリードレプリカの作成をテンプレートで宣言 コンソールにてAuroraリードレプリカを昇格 コンソールにてスタンドアロンAuroraクラスタのスナップショットを取得 手順1のテンプレートを再利用して手順3のスナップショットから新しいスタンドアロンAuroraクラスタを作成 この手順を踏むと、テンプレートの記述と実際のリソースが一致します。 実際に移行を行うにあたって、まずはスケジュールを説明します。 スケジュール 今回の作業ではメンテナンス前に実施する作業と、メンテナンス中に実施する作業が存在します。 メンテナンス前:Auroraリードレプリカの作成(手順1) メンテナンス中:Auroraリードレプリカの昇格以降、全ての手順(手順2・3・4) Auroraリードレプリカの昇格を実施するとレプリケーションが切れ、MySQL Masterとの差分が発生します。そのため、トランザクションが無くなってから手順2以降を実施する必要があります。 それでは、本題の各手順の内容を説明していきます。 1. Auroraリードレプリカの作成 RDS for MySQLからの移行に当たって、まずはMySQLからレプリケーションを受けるAuroraリードレプリカを作成する必要があります。 この作業はCloudFormationで宣言することが可能で、テンプレートで表すと以下のようになります。 # Auroraクラスタを作成 MyRDSDBCluster : Type : "AWS::RDS::DBCluster" Properties : # (各種必須プロパティを省略) Engine : 'aurora' EngineVersion : '5.6.10a' ReplicationSourceIdentifier : 'arn:aws:rds:<AZ>:<AccountId>:db:<SourceInstanceName>' # Auroraインスタンス1台目を作成 MyRDSDBInstanceApplicationFirst : Type : "AWS::RDS::DBInstance" Properties : # (各種必須プロパティを省略) DBClusterIdentifier : !Ref MyRDSDBCluster Engine : 'aurora' # Auroraインスタンス2台目を作成 MyRDSDBInstanceApplicationSecond : Type : "AWS::RDS::DBInstance" Properties : # (各種必須プロパティを省略) DBClusterIdentifier : !Ref MyRDSDBCluster Engine : 'aurora' ポイントは以下の2点です。 クラスタ側でReplicationSourceIdentifierというプロパティを記述し、レプリケーション元となるMySQLインスタンスのARNを指定する インスタンス側ではDBClusterIdentifierというプロパティを記述し、所属するクラスタを指定する これでMySQL Masterからレプリケーションを受けているAuroraクラスタができます。 2. Auroraリードレプリカの昇格 次に実施するのはAuroraリードレプリカをスタンドアロンのAuroraクラスタに昇格させる作業です。 この手順をなんとかCloudFormationで実装しようとしてテンプレートをぽちぽち弄ってみたのですが、実現出来ませんでした。 3 よって昇格は Aurora リードレプリカの昇格(AWS マネジメントコンソール) に記載された手順で実施しました。 先述の通り、この時点でテンプレートではAuroraリードレプリカ、実際はスタンドアロンAuroraクラスタが構築されており、差異があります。 これを解消するため、3・4の手順を踏んでテンプレートを実態に即したものへ修正します。 3. スナップショットの取得 最終的な スナップショットからの復元 の前準備として必要なのがこの手順3です。 この作業もテンプレートでは表現できないため、 DB スナップショットの作成(AWS マネジメントコンソール) に記述された手順で実施しました。 4. スナップショットからのクラスタ復元 最後に、テンプレートを使って手順3で取得したスナップショットからAuroraクラスタとAuroraインスタンスを復元します。 この手順は、手順1で作成したテンプレートを1行変更するだけで実現できます。 テンプレートに加える修正を以下にdiffで示します。 < ReplicationSourceIdentifier: 'arn:aws:rds:<AZ>:<AccountId>:db:<SourceInstanceName>' --- > # ReplicationSourceIdentifier: 'arn:aws:rds:<AZ>:<AccountId>:db:<SourceInstanceName>' > SnapshotIdentifier: arn:aws:rds:<AZ>:<AccountId>:cluster-snapshot:<SnapShotName> ReplicationSourceIdentifierの代わりにSnapshotIdentifierが有効になり、スナップショットを元にクラスタを作成するという宣言に代わります。 このテンプレートを反映すると、レプリカとして宣言していたAuroraクラスタとインスタンスが削除され、スナップショットから復元されるクラスタ・インスタンスで置換されます。 古いクラスタ・インスタンスを削除する手間が省けました。 DBClusterIdentifierで指定したAuroraクラスタが別のものになることで、インスタンスも再構築されています。 以上でCloudFormationを使って、 MySQLのAuroraレプリカが昇格したスタンドアロンAuroraクラスタ を構築できました。 後はアプリケーションの接続DBをこのスタンドアロンAuroraクラスタへ変更することで、無事移行が完了となりますね。 まとめ 今回の4つの手順を経て作成されたテンプレートは、サービスインした実際のAWSリソースを正しく表現したものとなりました。 メンテナンス中にAuroraクラスタを置換する必要があり、しかも手作業も発生するという困った状況でしたが、今回の方針で進める大きな決め手となったのは 事前に検証とリハーサルを繰り返して作業時間を正確に見積もれた ことです。本番での不確定要素を最小限に出来たことで踏み切れました。 テンプレートで表現できたため、今後はメンテナンスウィンドウやセキュリティグループなどの変更をコードベースでレビューを挟んで実施できますね。 さいごに スタートトゥデイテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! https://www.starttoday-tech.com/recruit/ サービスに追加された新機能がCloudFormationで実装されるまでにはそれなりにラグがありますが、そことはうまく付き合っていきます。 ↩ コネクションを受けているリードレプリカをクラスターからいきなり抜くとRDSはよしなに再接続処理をやってくれるんだろうか?という疑問はあります。スケールアウトは気軽にできますが・・ ↩ AuroraクラスタのReplicationSourceIdentifierを外してレプリケーション設定を削除することが有効だと思ったのですが、クラスタが削除されて新規にまっさらなクラスタが作成されるだけでした。 ↩
アバター
こんにちは! スタートトゥデイテクノロジーズ新事業創造部の塩崎です。 2018年7月24日〜26日にかけてサンフランシスコでGoogle Cloud Next '18が開催されました。 このイベントに新事業創造部の塩崎、今村、そして代表取締役CIOの金山の3名で参加してきました。 この記事では多数あった講演の中で特に印象に残ったものをいくつか紹介いたします。 講演 Building A Petabyte Scale Warehouse in BigQuery How to Do Predictive Analytics in BigQuery A Modern Data Pipeline in Action Real-Time Stream Analytics with Google Cloud Dataflow: Common Use Cases and Patterns Securing Access to GCP resources Better Practices for Cloud IAM 感想 まとめ 講演 Building A Petabyte Scale Warehouse in BigQuery DataWareHouse(DWH)のBigQuery移行をするために行ったことを各社さんが発表するセッションでした。 特にSpotifyさんの発表では移行のための具体的なtipsを4つ紹介されていました。 その4つの内訳は以下のものとのことです。 Administration オンデマンド料金ではなく、定額料金を使う ビジネス的に重要なジョブ用にスロットを予約する Education ジョブのパフォーマンス監視のために、ジョブの並列度を監視 Dremelのアーキテクチャ、特に伝統的なRDBとの違い列指向であることの理解 Integration BigQueryのAPIを使用し、自社ツールを開発 GCPサービス間でのデータ移動が簡単なので、他のGCPサービスと統合 Partnership BigQueryのサポートチームに機能のリクエストを出す また、Oathさんは各部署で独立してDWHを持っている(データサイロ)ために、部署横断的なデータ活用が難しいという課題があったそうです。 BigQuery移行のために、当時使用されていたHiveのデータをBigQueryにコピーし、BIツールであるLookerに繋ぎこむことから始めたそうです。 現在ではGCP製品をフル活用したデータ分析基盤になっているそうです。 TwitterさんはMySQL、Vertica、HDFS等に保存されたデータをBigQueryにコピーする際、一旦Apache Avro形式に変換をしているそうでした。 Avroに変換してからBigQueryにインポートすることによって高速化されるようです。 海外のBigQuery導入事例を聞くとデータ量が本当に桁違いで驚愕することが多かったです。 発表のタイトルにもあるPB(ペタバイト)という単語が発表中に頻繁に飛び交っていました。 BigQueryは大量のデータに対してもスケールするという宣伝文句はよく目にしますが、具体的な数値を見るとBigQueryにDWHを構築する時の安心感が増します。 How to Do Predictive Analytics in BigQuery BigQueryの新機能であるBigQuery MLに関する発表でした。 BigQueryに格納されているデータに対して機械学習でモデルを構築できるそうです。 データ分析を行う人にとっていつも使い慣れているSQLに似た構文で機械学習を行うことができるようになります。 お手軽に試してみたい人向けの機能としては、ハイパーパラメーターの自動決定やデータを学習用とテスト用に自動的に分けてくれる機能があるそうです。 また、アドバンストな機能として、学習率の設定やデータを学習用とテスト用に分ける時の方法の設定を自分で変更できる機能もあるそうです。 現在は線形回帰と二項ロジスティック回帰のモデルがサポートされているそうですが、使用できるモデルはこれから拡充されるようです。 対象としているユーザー層はAutoMLやCloudML APIを使う層とTensorFlowやCloudML Engineを使う層の中間層らしいです。 発表の後半ではHearst Newspapersさんでの導入事例の紹介がありました。 オンラインで提供している新聞購読サービスの解約予測モデルの構築をBigQuery MLを使って行ったそうです。 利用ログやデモグラ情報などから、そのユーザーが解約するかどうかを二項ロジスティック回帰で予測するモデルだそうです。 このモデルを構築することによって、どの属性がユーザーの解約に寄与するのかを可視化することが出来たそうです。 プロトタイプの構築はわずか1日で行うことができ、本番投入もわずか2スプリントで行うことができたそうです。 BigQueryにすべてのログやマスタデータが集約されている環境では非常にパワフルな機能であると感じました。 大量のデータに対する機械学習をシンプルなSQL記法で行うことができるため、アナリストが自分で予測モデルをつくることの敷居が下がるかと思います。 まだモデルの種類は2種類しかありませんが、将来的に増えていくことを強く期待しています。 A Modern Data Pipeline in Action モダンなデータパイプラインを構築するためのパターンの紹介でした。 なお、データパイプラインとはデータに対して以下の一連の処理を行うためのシステムのことです。 Ingest → Transform → Store → Analyze → Visualize 発表中では以下のパターンが紹介されていました。 一度取得したデータに対して、再度処理をかける時のパターン プログラムのバグやビジネスロジックの変更に備えるためのパターンです。 PubSubからのデータをDataFlowでBigQueryに入れるための流れとは別に、GCSにAvro形式でデータを保存する流れを作ります。 もしもの時にはこのデータをDataFlowで再度処理してBigQueryに入れ直すことができます。 データのバックフィルをする時のパターン CloudComposer(マネージドAirflow)を使い過去分のデータをBigQueryに入れるのがベストプラクティスらしいです。 また、データパイプラインのアーキテクチャを考える上で重要なことも紹介されていました。 Decoupling DBやログストリームなどのデータを生成する部分をデータを消費する部分をPubSubを使って分離させる。 Simple, Elastic, low-cost マネージドサービスの活用によって、スケーリングとロードバランスに関する問題を考えなくても良いようにする。 Low Latency ETL処理の負荷を可能な限り小さくして、低レイテンシでBigQueryにデータを入れる。 Stream and Batch DataFlowを使うとストリーム処理、バッチ処理の両方を同一のソースコードで処理可能になる。 Easy re-processing DataFlowによる変換処理前のデータをGCSに保存することによって、容易に再処理をできるようにする。 High-Level DataFlowならば簡単な記述で大きな処理を行わせることができる。 Portability DataFlowはApache Beamでプログラミングできるため、ロックインされない。 バッチ処理とストリーム処理を同じソースコードで実現できることにDataFlowの実力を感じました。 現在DigdagとEmbulkを活用してETLを構築していますが、CloudComposerやDataFlowを使用したパターンとの比較も真面目に検討する必要性を感じました。 Real-Time Stream Analytics with Google Cloud Dataflow: Common Use Cases and Patterns ストリーミングデータをリアルタイム分析をするためのパターンの紹介でした。 ストリーミングデータの特徴として、途切れることがないこと、予期せぬディレイが発生することなどが紹介されていました。 また、そのためにWindowingやWaterMarkなどのバッチでデータを処理するときには考えなくてもよい概念も紹介されていました。 後半ではCloudDataFlowを使った時の頻出パターンの紹介がなされていました。 Exactly-onceの実現 PubSubはAtLeastOnceの取り出ししかサポートしないために、DataFlow側でExactly-onceを実現するための方法です。 各々のイベントのAttributeにイベントID(乱数)を埋め込み、Dataflow側でwithIdAttributeすることによってExactly-onceが実現されます。 壊れたデータの扱い PubSubにはリトライ機能があるため、壊れたデータがやってくるとそのデータを無限に処理しようとしてしまいます。 そのため、予期せぬ例外をtry-catchで拾い、それをdead letter用のTopicにpublishすることでこの問題に対処できるそうです。 また、壊れたデータを後から解析するためBigQueryに永続化をしたり、アラートを上げるためのTopicにpublishするなどの派生パターンもあるそうです。 スキーマ変更への対応 イベントにフィールドが追加された時に対処するためのパターンです。 BigQueryのテーブルに対するスキーマ変更はダウンタイムなしで実行できるため、フィールド追加を検知した時にテーブルの列追加を行います。 また、前述したリトライ機能によって、テーブルへの挿入に失敗したイベントの取得もできます。 リアルタイムにデータの非正規化をする BigTableにもデータを入れ、DataFlow上でそのデータとJOINすることによってリアルタイムでデータの非正規化を行います。 @SetupでBigTableへのコネクションを確立し、@Teardownでコネクションを閉じるようにすると良いそうです。 ストリーミングデータを処理するにあたって、データの到着が遅延するという特性はバッチ処理にはない概念です。 ですが、DataFlowを活用することによって、これらの問題に対処できる可能性も同時に感じました。 Securing Access to GCP resources VPCの新機能であるVPC Service Controlについての発表でした。 VPCの中にセキュリティ境界を作ることができ、そこに出入りする通信のルールを定めることができるそうです。 GCSバケットに対するアクセスを接続元IPによって制限し、専用線で接続されたオンプレ環境のみからのアクセスに限定するという使用例が紹介されていました。 オペミスによってIAMの設定をミスしてしまった時や、内部の人が悪意を持ってアクセス権を変えてしまった時の防波堤として機能するようです。 たとえIAMでアクセスが許可されていてもVPC Service Controlの設定が優先されるそうです。 この機能はまだアルファ版なので使うためには申請が必要ですが、早くも利用してみたい気持ちが高まります。 データ流出事件のレポートを読んでいると、その原因の多くがアクセス権設定のオペミスであることに驚かされます。 人間は誰しもオペミスをするので、このような仕組みで2重3重に防御することでデータ流出の危険性を低減できるのではと期待できます。 Better Practices for Cloud IAM Cloud IAMの説明とベストプラクティスの紹介でした。 発表の前半では、Cloud IAMの説明として、以下のようなことが紹介されていました。 プロジェクトは信頼境界を引くためのもの Resourceは階層構造を持っており上位階層の設定は下位階層に引き継がれる 後半では、具体的なベストプラクティスの紹介がされていました。 主に以下のようなことが紹介されていました。 ロールを割り振る対象はユーザーではなく、グループにする Google Groupで作成したグループに対して権限を割り振ることで新しい人が入った時の運用負荷が下がる。 可能な限り小さな権限を与える Primitive Roleはプロジェクト全体に対する権限を与えてしまうので、Predefined RoleかCustom Roleかを使うようにする。 追跡するための情報を残す 監査ログをGCSかBigQueryに保存する。 組織の構造をCloudIAMの階層構造に反映させる 組織全体の設定はOrganization Policyで管理し部署ごとの設定はFolderで管理する。 そして、サービス毎、環境(production、development、test)毎にプロジェクトを分ける。 プロジェクトは信頼境界を引くためのものということが目から鱗な発表でした。 今までは開発用のリソースと本番用のリソースを同一プロジェクトに入れていたため、アクセス権の設定に頭を悩ませることが多かったです。 あの時にプロジェクトを分けるという発想に至っていれば…という後悔も同時に感じました。 「とりあえず使って見よう!」という状態では後回しにされがちなIAMの設定を体系的に知ることができ、今後のGCP運用のための大きな知見でした。 感想 Google Cloud Next'18に参加することによって今まで知らなかったGCPの機能を知ることができました。 今まではAWSをメインのクラウドとして使用し、GCPはBigQueryを使うのみであったため、GCP全体を本格的に学習することはありませんでした。 しかし、いざGCPについて学習するとどのサービスもとても奥深く、自分の理解不足を強く実感しました。 また、今回はセッションを聞くことを中心に時間を過ごしましたが、Googleの人と直接話すことができる場にもっとを顔を出せばよかったと感じました。 そして、英語でのコミュニケーション力不足も強く痛感しました。 発表の最中に上手く聞き取れなかった部分を後で字幕付き動画で確認するととても平易な英語なことが分かり、落ち込むことが多かったです。 これからの弊社の大きな目標に海外展開がありますので、英語の継続的な勉強の必要性を実感しました。 まとめ 今回のGoogle Cloud Next'18は会社の出張という形で参加しました。 参加期間はすべて業務扱いとなり、交通費、宿泊費の他、出張手当も会社からサポートされました。 国外のカンファレンスへの参加のサポートはエンジニアとして非常にありがたいことであると感じました。 今回の出張で得た知見を日頃のサービス開発に生かしていきたいと思います。 スタートトゥディテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! https://www.wantedly.com/companies/starttoday-tech/projects
アバター
こんにちは。新事業創造部の荒井です。 今回はiOSアプリの多言語対応について紹介します。 はじめに 私は今までいくつかのiOSアプリを運営してきましたが、どのアプリも日本語のみのサポートでした。現在関わっているWEARでは、すでに多言語対応が進められており、良い機会ですので個人的に知見がなかった多言語対応について調査をしました。今回は基本となる文字列の翻訳について触れていきたいと思います。 多言語対応 幅広いユーザーにサービスを使用してもらうには、言語と地域が非常に重要になってきます。日付、通貨、長さの単位など、言語、地域、文化によって異なることが多く、アプリケーションを世界的に出すには地域や文化に対応していく必要があります。現在WEARでは4つの言語対応を進めており、設定言語に応じて、アプリケーションをその言語に翻訳しています。対応している言語は以下の通りです。 日本語 簡体字中国語 繁体字中国語 英語 多言語対応をすることで、日本人以外の方にも使用していただく機会が増え、ユーザー増加が見込めます。 Localizationの手順 それでは設定言語によってアプリケーション内の文字列を複数の言語に変更する方法を紹介します。大まかに以下の流れになります。 PROJECTのLocalizationsに言語の追加をする Localizable.stringsを用意する NSLocalizedStringを使用し言語の切り替えをする 1. Localizationに追加 ローカライズの設定は PROJECT -> Info -> Localizations で行います。 はじめにBase Internatioalizationを有効にします。 Base Internationalizationを有効にする PROJECT -> Info -> Use Base Internationalization にチェックを付けます。これによりアプリケーションで使う文字列を.storyboardと.xibから分離します。こちらはXcode 5以降ではデフォルトで有効になっています。それ以前のバージョンから作成されているプロジェクトは公式の「 Base Internationalizationを有効にする 」が参考になるかと思います。 言語を追加する 次に PROJECT -> Info -> Localizations の「 + 」を押して言語を追加します。例では英語をBaseにしているので日本語を追加しています。 追加する言語を選ぶと、対応するファイルと File Types を選択する画面が表示されます。表示する文字列のみを対応する場合は Localizable Strings を選択してください。 2. Localizable.stringsの作成 翻訳したい文字列を定義しておくLocalizable.stringsを作成します。 メニューの Fille -> New -> File から Strings File を選択します。 作成したLocalizable.Stringsを選択し「 Localize... 」から言語を選択すると選択した言語がローカライズされます。 Base にチェックをつけることで Localizable.strings(Japanese) と Localizable.strings(Base) が作成されます。 作成したLocalizable.stringsを編集していきます。 ここではアプリケーションでよく使う文字列をサンプルとして定義しました。 Localizable.strings(Japanese) "Yes" = "はい" ; "No" = "いいえ" ; "Cancel" = "キャンセル" ; "Confirm" = "確認" ; "Search" = "検索" ; "Else" = "その他" ; "Edit" = "編集" ; "Done" = "完了" ; "Friend" = "友達" ; "Save" = "保存" ; "Title" = "タイトル" ; "Decide" = "決定" ; Localizable.strings(Base) "Yes" = "Yes" ; "No" = "No" ; "Cancel" = "Cancel" ; "Confirm" = "Confirm" ; "Search" = "Search" ; "Else" = "Other" ; "Edit" = "Edit" ; "Done" = "Done" ; "Friend" = "Friends" ; "Save" = "Save" ; "Title" = "Title" ; "Decide" = "Enter" ; 3. NSLocalizedString ここまでで準備は完了です。NSLocalizedStringを使用し言語の切り替えを実装します。 override func viewDidLoad () { super .viewDidLoad() let yes = NSLocalizedString( "Yes" , comment : "" ) let no = NSLocalizedString( "No" , comment : "" ) let cancel = NSLocalizedString( "Cancel" , comment : "" ) let confirm = NSLocalizedString( "Confirm" , comment : "" ) let search = NSLocalizedString( "Search" , comment : "" ) print(yes) print(no) print(cancel) print(confirm) print(search) } このコードを実行すると以下のように出力されます。 Yes No Cancel Confirm Search 言語を日本語に切り替えることによって文字列が日本語になるか確認します。 Edit Scheme の Application Language を変更すると簡単に確認ができます。 実行結果は以下のようになります。 はい いいえ キャンセル 確認 検索 Application LanguageでJapanese選択すると日本語に切り替わることが確認できました。 このようにアプリケーションで必要な文字列を定義して使用していきます。 翻訳のフロー 多言語対応されたアプリケーションを運用するには普段と違った工程が必要となります。 1つの言語のみサポートするサービスとの大きな違いは機能追加ごとに翻訳のプロセスが入ることです。 翻訳サービスを使用したり、社内に翻訳を担当するチームがあったりと様々だと思いますが、 現在のWEARはチーム内で完結しています。 開発段階で翻訳が必要なリストを作成し、各言語を母国語とするメンバーが翻訳を担当しています。 Localizable.stringsをエンジニアが管理する方法もありますが、Apple社が案内している翻訳の手順にXLIFF(XML Localization Interchange File FormatXLIFF)を使用した運用があります。 画像出展: インターナショナライゼーションとローカリゼーションのガイド XcodeからXLIFFファイルにエクスポートし、翻訳者がXLIFFを編集し翻訳完了後にXLIFFファイルをインポートをします。Androidなど複数プラットフォームで多言語対応を進める際に有用です。 まとめ 今回はiOSアプリの多言語化について紹介しました。今回は文字列だけでしたが、実際には日付・価格などの表示やレイアウト対応など考慮するべきことは多々あります。より多くのユーザーにサービスを使って頂けるよう日々改善を続けています。少しでも興味がある方は、ぜひ一度オフィスにお越しください。 下記からのエントリーもお待ちしています。 www.wantedly.com 参考文献 https://developer.apple.com/jp/internationalization/
アバター
(Icon Credit *1 ) こんにちは。スタートトゥデイ研究所の後藤です。 今回は、集合を入力として扱うネットワークモデルの紹介をしたいと思います。機械学習の多くのモデルは、固定長の入出力や順序のある可変長の入出力を扱うように設計されます。画像データやテーブルデータは各サンプルの入出力の次元を合わせて学習しますし、自然言語処理のコーパスや時系列データは入出力の順序を保持して利用します。 その一方で、可変長で順序のない集合データを扱うモデルの研究は最近になって取り組み始められたばかりです。我々が研究しているファッションの領域において、入力データを集合として扱いたくなる状況がたびたびあるため、理解を深めておきたい問題設定です。 コーディネートをアイテムの集合とみなす コーディネートに使われたアイテムの例 コーディネートをアイテムの組み合わせとして捉えた場合、1つのコーディネートはアイテムを要素とする集合であると見なすことができそうです。ここでは、1つのコーディネート内に同じアイテムは1度しか登場しないことと、アイテムには順序がないことを前提としています。後ほど触れますが、アイテムのカテゴリによる順序を設定して系列データとして扱う研究もあります(Han et al. 2017, Nakamura & Goto 2018)。 コーディネートデータを扱う過去の取り組み コーディネートデータの学習をするための過去の取り組みを紹介します。 Siamese Networks 集合データから2つのアイテムを取り出して固定長の入力として扱う例です。Veit et al. 2015では、ファッションスタイルの違いを反映した空間を学習する試みで、この手法を取っています。集合全体を評価することは一旦諦めて、入力を固定長にしてしまうことで、従来の枠組みを用いてモデルを構築できます。 (Veit et al. 2015, Figure 2) このタスクでは、ペアが集合全体の性質を持っている場合は良いですが、集合全体として初めて成り立つ性質を持っているようなタスクには使えません。1つ1つのアイテムからはスタイルは見えてこないが、トータルコーディネートを見て初めてスタイルが明らかになるような例を学習することは難しそうです。 Bidirectional LSTM コーディネートに使われるアイテムの数は可変長なので、系列データとみなして扱った例です。Han et al. 2017では、アイテムのカテゴリにより入力の順序を決め、コーディネートを系列データとしてBidirectional LSTMを学習しています。 (Han et al. 2017, Figure 2) この手法ではカテゴリによる入力の順序を定義しているため、同じコーディネートの評価をする場合でも入力するデータの順序を変えると、出力が変わると言う性質があります。論文中では、インプットするカテゴリの順番を入れ替えて学習すると、得られるモデルのクオリティ自体が変わることも指摘されています。入力する順序によって組み合わせの評価値が変わる性質は、タスクによっては不都合なこともあります。 集合データを直接扱う方法 Zaheer et al. 2017では集合データをストレートに扱う方法を提案しています。この論文では、集合を入力とする際に満たされるべき性質を持った、必要十分なネットワーク構造を示しています。ここでは、permutation invariantとpermutation equivariantについて紹介します。実装に関しては、著者自身のコードがgithubにあるため理解の助けになるかと思います。 github.com Permutation invariant Invariant modelのアーキテクチャ (Zaheer et al. 2017, Figure 5) permutation invariantのモデルを模式的に表したのが上図です。 集合データを入力とし、スカラーを出力する問題です。集合に対して1つの値を返すため、入力データの順序を変えても出力が同じになります。この性質をpermutation invariantと呼びます。例えば、コーディネートに点数をつける、といったタスクが当てはまると考えられます。 関数 がpermutation invariantであるとは、任意の並べ替え に対して、 が成り立つことです。 論文では、集合の各要素の特徴を関数\( \phi(x)\)により独立に抽出し、それを足し上げたものを非線形関数\( \rho\)に渡すことで、permutation invariantな関数が実現できることを証明しています。各要素の特徴 \( \phi(x) \)の和は、任意の並べ替え \( \pi \)に対して変わらないので、\( f(X) = \rho (\sum_{x \in X} \phi(x)) \)が入力する順序を入れ替えても出力は変わらないことが直感的にわかるかと思います。 活用例 コーディネートデータの活用事例として、オートエンコーダーによるスタイルの抽出があります。この実験ではコーディネートを固定長のベクトルで表現する方法として、concat型とreduce型の方法を試しています。このうちのreduce型の手法は先ほど述べたpermutation invariantの条件を満たしています。この手法はコーディネートデータからスタイルを抽出するというタスクにおいて優れた性能を発揮することがわかっています。 Permutation equivariant 例えば、集合の中から仲間はずれを検知する、コーディネートデータの中からスタイルが一致しないアイテムを発見するなどの問題設定の場合、集合の入力に対して要素数分の出力が必要になります。さらに各入力要素と出力の各要素は対応関係を持っています。このような対称性をpermutation equivariantと呼び、以下のように表現されます。 ] モデルパラメータを\( \Theta\)を持つモデルを と表現した際、\( \Theta\)が を満たす変換であればpermutation equivarinatであることが示されています。さらにこの定式化の拡張として、 も、permutation equivariantを満たしています。 集合方向のmaxpoolingは入力データの順序に対して不変なので、これ自体はpermutation invariantです。 Equivariant modelのアーキテクチャ (Zaheer et al. 2017, Figure 7) このようなネットワークは、pytorchで表現すると以下のように記述することができます。PermutationEquivariantクラスが\(\Theta\)に対応する部分です。集合を扱うという新しいことができるようになっているのですが、とてもシンプルです。実際に入力の順序を入れ替えると対応した出力の順序も入れ替わることが確認できます。 import torch import torch.nn as nn class PermutationEquivariant (nn.Module): def __init__ (self, in_dim, out_dim): super (PermutationEquivariant, self).__init__() self.Gamma = nn.Linear(in_dim, out_dim) self.Lambda = nn.Linear(in_dim, out_dim, bias= False ) def forward (self, x): xm, _ = x. max ( 1 , keepdim= True ) xm = self.Lambda(xm) x = self.Gamma(x) x = x - xm return x class DeepSets (nn.Module): def __init__ (self, x_dim, d_dim): super (DeepSets, self).__init__() self.x_dim = x_dim self.d_dim = d_dim self.phi = nn.Sequential( PermutationEquivariant(self.x_dim, self.d_dim), nn.ELU(inplace= True ), ) self.rho = nn.Sequential( nn.Dropout(p= 0.5 ), nn.Linear(self.d_dim, self.d_dim), nn.ELU(inplace= True ), nn.Dropout(p= 0.5 ), nn.Linear(self.d_dim, 10 ), ) print (self) def forward (self, x): phi_output = self.phi(x) sum_output = phi_output.mean( 1 ) rho_output = self.rho(sum_output) return rho_output まとめ 今回は、集合を入力として扱うネットワークの表現のうち、permutation invariantとpermutation equivariantの2つの場合について紹介しました。入力に集合を扱えるようになることで、IQONやWEARのコーディネートデータを活用しやすくなります。例えば、コーディネートの採点やスタイルの抽出、スタイル不一致のアイテムの発見など、様々な応用例が考えられるでしょう。 さいごに 研究所ではアイテムの集合以外にも「似合う」ということについて多角的に研究を進めています。機械学習に限らず、専門性を活かしてファッションを分析できる環境を提供しています。ファッションに関する研究テーマに挑戦したい方はぜひ下のリンクから応募して、ぜひ一度オフィスに来ていただければと思います。 www.wantedly.com 参考 Han, X., Wu, Z., Jiang, Y., Davis, L. Learning Fashion Compatibility with Bidirectional LSTMs. In ACM Multimedia, 2017. Nakamura, T., Goto, R. Outfit Generation and Style Extraction via Bidirectional LSTM and Autoencoder. In KDD workshop 2018. Veit, A., Kovacs, B., Bell, S., McAuley, J., Bala, K., Belongie, S. Learning Visual Clothing Style with Heterogeneous Dyadic Co-occurrences. In ICCV, 2015. Zaheer, M., Kottur, S., Ravanbakhsh, S., Poczos, B., Salakhutdinov, R., Smola, A. Deep Sets. In NIPS, 2017.
アバター
こんにちは。新事業創造部インフラチームの光野(kotatsu360)です。 先日、VASILY時代 1 から長らく使われていたCapistranoによるデプロイを見直し、CodePipeline+CodeDeployによるデプロイフローを導入しました。 CodeDeployはEC2 AutoScalingとよく統合されており、この新しいデプロイフローによって最新のアプリケーションコードをどう反映するかという悩みから開放されました。この記事ではそのフローについて設計と運用を交えつつ紹介します。 AWS CodePipeline / AWS CodeDeploy CodePipeline AWS CodePipeline はアプリケーションのCI/CDパイプラインを作るためのサービスです。 Source 、 Build 、 Test 、 Deploy の4ステージに対して1つ以上のアクションを割り当てることで、任意のパイプラインを構築します。 設定項目が多く初めは戸惑いますが、全てのステップを使う必要はありません。 実際、今回構築したパイプラインでも Source と Deploy のみを使っています。 Build や Test に相当する部分は既存のCIで事足りるためです。 CodeDeploy AWS CodeDeploy はアプリケーションの自動デププロイを管理するためのサービスです。 エージェント式になっており、Amazon EC2だけでなくオンプレミスに対してもデプロイが可能です 2 。 デプロイの具体的な作業は、実行可能な形式で置いておきます。 # appspec.yml # CodeDeployに対してデプロイ処理を指定するファイル。リポジトリルートに置かれる version : 0.0 os : linux files : - source : / # zipに含まれるファイル全部を destination : /tmp/sample # sample以下に展開 hooks : ApplicationStop : - location : codedeploy/stop-application.sh timeout : 30 # Install: # CodeDeployが最新のコードをデプロイターゲットに配布する AfterInstall : - location : codedeploy/start-application.sh 上のappspec.ymlでは「既存プロセスをkillする」「新しいプロセスを起動する」といった内容を .sh で表現しています。これをエージェントが順次実行します。デプロイ先で実行可能であれば、表現方法は自由です。 デプロイフロー 新しいデプロイフローは次のようになりました。 画像の左下がスタートです。 アプリケーションコードがGitHub上で特定のブランチにマージされる CircleCI によるテストの後、zipで圧縮してS3に保存 S3はオブジェクトの更新をCodePipelineに通知 CodePipelineはCodeDeployを呼び出す CodeDeployは事前に定められたAutoScalingグループに対してデプロイを実行 詳細について触れていきます。 CodePipelineのパイプライン戦略 CodePipelineのデプロイパイプラインは、リポジトリ単位で分離します。 複数の役割を持っているリポジトリについては、 Deploy フェーズで分岐させ、それぞれをCodeDeployが実行します。 3 具体的には、APIのようにロードバランサにアタッチする必要のあるAutoScalingグループと、非同期処理を担当するAutoScalingグループで分離しています。 CodeDeploy + OpsWorksとの連携 CodeDeployのデプロイターゲットとしてAutoScalingグループを指定すると、新規追加されたインスタンスに対して自動でデプロイを実行します 4 。便利な一方、新規に起動したインスタンスの構成管理について考慮する必要があります。 私が管理するいくつかのサービスでは、構成管理を全てOpsWorksに集約しています 5 。これはAutoScalingグループに所属するインスタンスであっても例外ではありません 6 。 何のフォローも行わない場合、構成管理中にCodeDeployのデプロイ処理だけが先行し、デプロイが失敗します。デプロイに失敗した新規インスタンスはAutoScaling側で失敗とみなされ破棄されるため、延々と起動・失敗・破棄を繰り返してしまいます。 これを避けるため、CodeDeployよるデプロイ処理中にOpsWorks側のステータスを確認する処理を含めています。 version : 0.0 os : linux files : - source : / # zipに含まれるファイル全部を destination : /tmp/sample # sample以下に展開 hooks : ApplicationStop : - location : codedeploy/stop-application.sh timeout : 30 # この処理を追加 BeforeInstall : - location : codedeploy/setup-wait.sh timeout : 900 AfterInstall : - location : codedeploy/start-application.sh #!/bin/bash set -e OPSWORKS_INSTANCE_ID = $( grep ' OpsWorks Instance ID ' /etc/motd | cut -d: -f2 | tr -d ' ' ) /usr/local/bin/aws opsworks --region us-east-1 wait instance-online --instance-ids $OPSWORKS_INSTANCE_ID 素朴なシェルスクリプトですが、この処理を挟むことで構成管理を待った上でデプロイを実行する事ができます。 インプレースデプロイかBlue/Greenデプロイか CodeDeployのデプロイターゲットとしてAutoScalingグループを指定すると、デプロイの方法として2パターンを選択できます。 インプレースデプロイ:既存のインスタンスに対してデプロイを行う Blue/Greenデプロイ:新規のインスタンスを作成しデプロイを行う 本件では前者のインプレースデプロイを採用しています。後者の長所は障害時の高速なロールバックかと思いますが、前述の構成管理と合わせて検討すると、revertコミット+インプレースデプロイが十分に高速かつシンプルという判断です 7 。 なお、インプレースデプロイかつデプロイターゲットがロードバランサに所属する場合、CodeDeploy側がデプロイの前後で適切にアタッチ・デタッチをしてくれます。サービスインしたままデプロイが行われるということはありません。 CodeDeployを採用してハマった事 完成した後のデプロイフローはとても安定しています。しかし、構築中にいくつかハマった部分もありました。 複数のロードバランサに所属する場合のフォロー CodeDeployは、デプロイターゲットがロードバランサ(ALBならターゲットグループ)に所属している場合、適切にアタッチ・デタッチしてくれます。しかし複数のロードバランサまでは面倒を見てくれません(2018年7月時点)。 そのため、何らかの理由で複数のロードバランサに所属するAutoScaingグループであれば、CodeDeployに渡す処理中でフォローしてやる必要があります。 #!/bin/bash set -e readonly INSTANCE_ID = $( curl -s http:// 169 . 254 . 169 . 254 /latest/meta-data/instance-id ) readonly TARGET_GROUP_INTERNAL_API = ' xxxxx ' /usr/local/bin/aws --region ap-northeast-1 elbv2 register-targets --target-group-arn ${TARGET_GROUP_INTERNAL_API} --targets Id = ${INSTANCE_ID} /usr/local/bin/aws --region ap-northeast-1 elbv2 wait target-in-service --target-group-arn ${TARGET_GROUP_INTERNAL_API} --targets Id = ${INSTANCE_ID} CodeDeployのBlockTraffic/AllowTraffic CodeDeployのデプロイは幾つかのステップがAWS側に予約されています。ロードバランサとのインテグレーションもその予約されたステップで行われるのですが、なぜか2〜3分も待たされる事があります。 どうやらCodeDeployの挙動はロードバランサのヘルスチェック設定に依存しているようです 8 。 BlockTraffic/AllowTrafficにかかる時間は次のとおりです。 Interval: 30sec 、 Healthy threshold: 10 ならそれぞれ300秒程度 Interval: 5sec 、 Healthy threshold: 2 ならそれぞれ10秒程度 運用のポリシーが許す範囲で短くしておくことをおすすめします。 CodeDeployを採用して良かったこと AutoScalingとの統合以外にもコスト面で効いた部分がありました。 集約率の向上 これはアプリケーションレベルでの Graceful Restart にこだわる必要が無くなったため得た恩恵です。 当初はAutoScalingによる柔軟なリソース配分を重視して始めた施策でしたが、改めて考えると一台あたりのパフォーマンスも上げることが可能でした。 本件で紹介しているデプロイフローを採用しているアプリケーションは Rails + unicorn + nginx という、Railsでよくみる鉄板構成です。 旧デプロイフローではunicornに対してCapistranoでUSR2シグナルによる Graceful Restart をしていました。 Graceful Restart の問題は、古いプロセスを残したまま新しいプロセスを作るため瞬間的にメモリ消費量が倍になることです。そのため、デプロイを安全に終了するためには、1インスタンス毎のメモリ消費量を40%程度に管理しておく必要があります。 一方、CodeDeployによるデプロイであれば一時的にサービスアウトしつつデプロイが進行します。そのため、既存のプロセスを一旦完全にkillできます。これによりデプロイ中という瞬間的な状態を気にすること無く集約率を検討できるようになりました。 まとめ 本記事では、CodePipeline+CodeDeployによる新しいデプロイフローについて紹介しました。 AutoScalingとの統合や集約率の向上といった恩恵はもちろんですが、デプロイを小さなステップに分割して整理できたため、保守性という意味でも向上したように感じています。 スタートトゥディテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 https://www.starttoday-tech.com/recruit/ 株式会社スタートトゥデイテクノロジーズはスタートトゥデイ工務店 + VASILY + カラクルの3社を統合し発足されました ↩ AWS Lambdaに対するデプロイも可能ですが、本記事では扱いません ↩ 画像中でCodeBuildを呼び出していますが、これはパイプライン設計中に後からタスクを任せようと思った名残です。結局、既存のCircleCIで十分と気づき何もしないステージとしてそのままになっています。本記事でも触れません。 ↩ ライフサイクルフック の仕組みを用いています。 ↩ CloudFormationとOpsWorksでインフラを育てる ↩ UserData、OpsWorks、Lambdaを組み合わせ、常に新鮮なSpotFleetインスタンスでサービスを運用する (引用はSpotFleetに関する話題だが、SpotFleetでないAutoScalingでも同様) ↩ ゴールデンイメージによる構成管理時間の短縮を検討しなかったわけではありませんが、既存のOpsWorksによるフローを変更することのデメリットが遥かに大きく、早々にアプローチから外れました。 ↩ AWS Developer Forums: BlockTraffic/AllowTraffic durations ↩
アバター
(Icon Credit *1 ) こんにちは。PB開発部インフラチームの @inductor です。最近はすっかり インフラ勉強会 というオンライン勉強会の運営が趣味になっています。 今回はLambda@EdgeというAWSのサービスを使って、CloudFrontへのアクセスを「細かいルール」を設定して振り分けてみたいと思います。 Lambda@Edgeについてもう詳しく知っているよ! という方は、次のセクションはスキップしてもらって構いません。もしよく知らないという方は、一緒に勉強してみましょう! AWS Lambda@Edgeとは Lambda@Edge は以下の2つのサービスから成り立ちます。 AWS Lambda Amazon CloudFront CloudFrontのエッジロケーションにおいて、Lambdaで定義した任意のコードを実行できるというのがLambda@Edgeで、具体的には以下のような恩恵を得ることができます。 エッジロケーション(地域)に応じて表示させるコンテンツを変えたい User-Agentなどに応じて取得するコンテンツを変えたい アクセス元のIPアドレスによって表示させるコンテンツを変えたい 開発環境に、アクセス元IPが自社でない場合のみBasic認証を導入したい 実際に叩かれるURLと、S3などから実際に取得する資源のURIを変更したい この他にも、CloudFrontから取得できるデータに応じて様々な対応を柔軟に行えるのが特徴です。 AWS Lambdaのおさらい AWS Lambda は、任意のプログラムをサーバを用意することなく実行できるFaaS(Function as a Service)のサービスです。主に、バッチ処理やログの処理、サーバレスアーキテクチャにおけるバックエンドの処理などで利用されます。 現在Lambdaで提供されている実行環境には、Node.js、Java、C#、Go、Pythonがあり、それぞれ好きな言語を選んで実行させることができます。 詳しくは、 AWSの公式ページ や、 クラスメソッドさんなどの記事 をご覧いただければと思います。 Amazon CloudFrontのおさらい Amazon CloudFront は、AWSのCDNサービスです。世界中に存在する「エッジロケーション」と呼ばれるAmazonの各拠点に対して、S3に保存した静的コンテンツ(HTML、CSS、JSなど)をキャッシュさせておくことができます。 キャッシュのルールを細かく設定できる他、SSL証明書を ACM などから投入しつつHTTP/2の有効化やTLSのバージョン指定などのエンドポイントとしても機能的に利用できるなど、開発者にとって面倒な設定も簡単にできるという利点もあります。 改めてLambda@Edgeとは CloudFrontのエッジロケーションにおいて、Lambdaを使って細かいルールを自由に設定できるサービスです。 CloudFrontでは大きく分けて以下の4つのデータの流れがあり、それぞれ、1つのリクエスト(レスポンス)に対して一意のLambdaのコードを割り当てることができます(割り当てなくても動作はします)。 ※引用元スライド: https://www.slideshare.net/AmazonWebServicesJapan/aws-blackbelt-online-seminar-2017-amazon-cloudfront-aws-lambdaedge Lambda@Edge導入前の注意事項と事前準備 注意事項 Lambda@Edgeにて2018年7月現在で対応しているのはNode.js(6.10と8.10)のみです Lambda関数はバージニアリージョンで作成する必要があります Lambda関数が実行されたときに吐き出されるログが保管されるリージョンは、ユーザーから 最も近いエッジロケーション になります 例えばバージニアリージョンでコードを上げていても、ベトナムのユーザーがアクセスした場合、LambdaのCloudWatchログはムンバイなどのリージョンに表示されます CloudFrontの仕様上、特定のHTTPヘッダについて書き換えられないなどの制限があります 詳しくは 公式のドキュメント へどうぞ 必要な事前準備 Lambda@Edgeを適用するためには以下の準備が必要となります。 CloudFrontとS3の準備 Lambda@Edge用のIAMロールの作成 これらについては今回は省略します。それぞれ、該当リンクなどを参考に準備をしてみてください。 まず、IPアドレスに応じて必要な振り分け先を変えるという対応をしてみましょう。 サンプルとして、以下のようなコードを作成し、Lambda@Edgeの設定をしていきます(IPアドレスはダミーのため、各自試したいものに変更しましょう)。 'use strict' const permitIp = [ '172.0.0.1' , //IPアドレスは各自設定すること! '172.0.0.2' , ] ; exports.handler = ( event , context, callback) => { const request = event .Records [ 0 ] .cf.request; const httpVersion = request.httpVersion; const clientIp = request.clientIp; const isPermittedIp = permitIp.includes(clientIp); if (isPermittedIp) { // 許可されているIPであればそのまま次の処理へ callback( null , request); } else { // 許可されていないIPに対しては許可されていない旨のメッセージを返す const body = '<!DOCTYPE html> \n ' + '<html> \n ' + '<head><title>Hello From Lambda@Edge</title></head> \n ' + '<body> \n ' + 'Your IP address is not permitted to access! \n ' + '</body> \n ' + '</html>' /* Generate HTTP response */ const customResponse = { status : '200' , statusDescription: 'HTTP OK' , httpVersion: httpVersion, body: body, headers: { 'cache-control' : [{ key: 'Cache-Control' , value: 'max-age=100' }] , 'content-type' : [{ key: 'Content-Type' , value: 'text/html; charset=utf-8' }] , } , } ; callback( null , customResponse); } } ※特定の条件下で何もせず次の処理に渡したい場合は、上記コードのように callbackでrequestをそのまま返してあげる と良いです。 Lambdaの設定 Lambdaに対して上記コードを仕込んでいきます。 まず Lambdaのコンソール にアクセスし、関数を新しく作成します。 ※このとき、かならずバージニアリージョンを選びましょう! 続いて、以下スクリーンショットのように関数名に適当な名前を付け、ロールに作成済みのLambda@Edge用ロールを紐づけます。 関数が作成されますので、「関数コード」の欄に該当コードを貼り付け、画面上部、右上の「保存」ボタンをクリックします。 「アクション」から新しいバージョンを発行するを選択し、「発行」をクリックします。 バージョンが1になったことを確認したら、「Designer」よりトリガーの追加→CloudFrontを選択します。 「トリガーの設定」から、ディストリビューションにCloudFrontのDistribution IDを入力し、イベントトリガーにはビューアーリクエストを追加します。 上記が確認できたら「追加」をクリックし、再度、画面右上の保存ボタンをクリックします。 CloudFrontのコンソール画面 からBehaviorsタブを選択し、「Path Pattern」にて「Default(*)」または自分の選択したいパスを選択し、「Edit」をクリックします。 最下部のLambda Function Associationsにバージョン番号含めLambda関数が紐付いていることを確認します。 最後に、該当のCloudFrontのURLをブラウザで確認します! 該当しないIPアドレスからのアクセスに対して、以下のようなレスポンスが返ってくれば問題ないです。 以上、長くなってしまいましたが、Lambda@Edgeを使ったIPアドレスフィルタの実装手順でした。 さいごに Lambda@Edgeでは、CloudFrontだけでは今までできなかった細かいルールの振り分けが実現でき、より柔軟な使い方が可能になっています!本ブログでも、役に立った便利な使い方を定期的に更新していこうと思います。 みなさまも、この機会に是非導入を検討してみてはいかがでしょうか! スタートトゥディテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! https://www.starttoday-tech.com/recruit/
アバター
スタートトゥデイ研究所リサーチャーの中村です。 今回は、コーディネートからスタイルを自動抽出する技術に関するアイデアの紹介です。こちらは、 企業研究所による研究発表カンファレンス (CCSE2018) でも同様の内容で発表させていただきました。 そのときに使用した資料はこちらです。 ファッションのスタイルについて 服は組み合わせによって見た目の印象が変化します。例えば同じスラックスを履いたとしても、トップスがYシャツのときとTシャツのときでは印象が異なるはずです。ファッションではこの現象をスタイルやテイストといった言葉で表現します。 スタイルはコーディネートを考える際の指針となります。頻出するスタイルにはエイリアスが設定されており、印象を伝える際に利用されます。例えば以下のようなものです。 ところが多くの人にとって、このスタイルという概念はとても厄介です。服の組み合わせは無限に存在し、組み合わせを印象へマッピングできるようになるためには高度な専門知識が必要になるからです。 しかし服をおしゃれに着る際にはスタイルは最も重要な指針のひとつなので、この背後にある法則を解析し、機械で再現することには意義があります。 スタイルを解釈するためのフレームワーク 日本ファッションスタイリスト協会 という団体がStyling Mapというフレームワークを提供しています。 ファッションに関係する物 *1 と人 *2 を、それぞれの個性に応じて4つのテイストに分類するための考え方です。 4つのテイストとはアクアテイスト(Aq)、ブライトテイスト(Br)、クリスタルテイスト(Cr)、アーステイスト(Ea)で、各テイストについて与える印象と属するオブジェクトの有する特徴が定義されています。 ( https://stylist-kyokai.jp/stylingmap ) Styling Mapでは、スタイルを先述の4つのテイストの混合で表現することを許容しています。 4値判別でなくあえて曖昧さを含む表現を用いることで、表現能力の高いフレームワークとなっています。このアイデアは非常に有用だと思います。 解きたい問題 Styling Mapはファッションの専門家が経験に基づいて組み上げたフレームワークであり、実績を上げています。 この知識を再現してECに導入できれば、推薦を高度化すると同時に説得力をもたせることも可能です。 今回はコーディネートを対象に、背後に存在するスタイルの抽出とコーディネートの分類に取り組みます。 なお、Styling Mapでは4つのテイストに分類していましたが、本記事では基底となるスタイルをデータから求めます。 スタイルの定式化 Styling Mapからアイデアを拝借し、コーディネートのスタイルを混合表現で記述することを考えます。 基底のスタイルベクトル と混合比 を与えたとき、コーディネートのベクトル をそれらの線形結合で表現します。 例えば、基底スタイルを先述の4種類のテイストとしたとき、あるコーディネートは以下のように表現できます。 今回の問題では と は未知であり、これらをデータから求めることに挑戦します。 モデル 3層の砂時計型ニューラルネットワークを使います。入力はコーディネートベクトル 、出力は復元したコーディネートベクトル 、中間層はスタイル混合比 に対応します。 スタイル混合比の獲得 スタイル混合比 の各要素は対応する基底スタイルの重みに相当します。コーディネートベクトル を入力とし、全結合層にfeedしてこれを求めます。 比であることを維持するため、softmax関数を適用します。 コーディネートベクトルの復元 とモデルパラメータ の内積によりコーディネートベクトルの復元を行います。 ここで、 の各行は基底スタイルのベクトルであると仮定すると、この操作は定式化の章で述べたスタイルの表現に対応します。 学習 2種類のloss関数を定義します。はじめに復元時の誤差として、 と の類似度をhinge関数で評価します。 ここで、負例 はミニバッチから選択します。2点の類似度を返す関数 はコサイン類似度を使います。 続いて の正則化です。 の各行はスタイルの基底であると仮定しているため、各行は独立であることが望まれます。 これを式にすると、以下の様になるかと思います。 ここで、 は の各行を正規化した行列です。 コーディネートベクトルの抽出 コーディネートベクトル はCNNを用いて抽出します。入力のコーディネートデータはアイテム画像の集合であるため、それぞれの画像をResNet50にfeedして得られた特徴量を平均したベクトルをコーディネートベクトルとします。 データ IQONのコーディネートデータセットを使います。データセットの詳細は以前のブログでも触れているのでそちらをご参照ください。 参考: コーディネートの自動生成 特徴は、1つの標本が画像の集合であり、標本ごとに画像枚数が異なるということです。 結果 の次元数を8に設定し、学習させました。得られた基底スタイルの9近傍を表示します。 各スタイルの1行が、1つのコーディネートに対応します。 やはりファッションに詳しくないと分類に成功したか否かの判断に困りますが、少なくとも色調の違いはかなりはっきりと現れているかと思います。 本家Styling Mapでも説明できそうなクラスタも見受けられます。例えば、#5はアクアテイスト *3 とかなり似ていることが確認できます。 スタイルを混合表現にしたことで、基底スタイルを混ぜ合わせたようなスタイルも表現できるようになりました。 ここに示した例は、混合した基底スタイルの特徴が現れているかと思います。 まとめ 今回はコーディネートからのスタイル抽出に挑戦してました。 スタイルに混合表現を導入し、データから基底スタイルと混合比を教師無しで抽出しました。 poolingの方法の高度化やattentionの導入など、まだまだ改良の余地はあるかと思います。 一方で、コーディネートの特徴量さえうまく作れれば、スタイル抽出はもっとシンプルな手法でも達成できる気がしました。LDAやICAなどの手法は相性が良さそうです。 さいごに 研究所ではスタイルの抽出も含め「似合う」ということについて多角的に研究を始めています。 機械学習に限らず、専門性を活かしてファッションを解析したい方はぜひ下のリンクから応募して、ぜひ一度オフィスに来ていただければと思います。 www.wantedly.com *1 : 服やアクセサリーなど *2 : 骨格や性格など *3 : 本家が定義しているアクアテイストの特徴をご確認ください
アバター
こんにちは。スタートトゥデイテクノロジーズ新事業創造部の id:takanamito です。 今日はVASILY時代から活用されているOpenAPI(Swagger)の定義からRubyのクラスを自動生成するgemを作ったので、その紹介をしようと思います。 Swaggerの定義と実際のAPIが返すレスポンスの内容がズレている 弊社ではVASILY時代からSwaggerの導入が進んでいましたが、徐々に「Swaggerの定義と実際のAPIが返すレスポンスの内容がズレている」といった問題が発生しはじめていました。 その問題を解決するために今回つくったのがこのgemです。 github.com 例えばこんなOpenAPI Specification 3.0のYAMLの定義から schemas : user : type : object properties : username : type : string uuid : type : string repository : type : object properties : slug : type : string owner : $ref : '#/components/schemas/user' pullrequest : type : object properties : id : type : integer title : type : string repository : $ref : '#/components/schemas/repository' author : $ref : '#/components/schemas/user' こんなクラスが自動生成できます。 # cliで生成 
$ openapi2ruby generate ./path/to/link-example.yaml --out ./ $ ls . pullrequest_serializer.rb repository_serializer.rb user_serializer.rb class PullrequestSerializer < ActiveModel :: Serializer attributes :id , :title , :repository , :author def repository RepositorySerializer .new(object.repository) end def author UserSerializer .new(object.user) end def id type_check( :id , [ Integer ]) object.id end def title type_check( :title , [ String ]) object.title end private def type_check (name, types) raise " Field type is invalid. #{ name }" unless types.include?(object.send(name).class) end end 開発の経緯 先述の「Swaggerの定義と実際のAPIが返すレスポンスの内容がズレている」問題 APIの改修時に必ずSwagger定義も更新した上でアプリケーションを書き換えるという運用が人の手によって行われていたため、当然起こりうる事象だったのですが 実際にコードを読んでみるとController内で単にHashのオブジェクトを to_json してレスポンスデータを生成している処理が散見されました。 そのためAPIに型の概念を持ち込んでSwagger上の定義とレスポンスを一致させる方法を考え始めました。 実際にはOpenAPIのschema定義から ActiveModel::Serializer のクラスを自動生成しています。 調査 「コードの自動生成」という用途においては swagger-codegen が有名だったので、まずは今回やりたいRubyクラスの自動生成ができないか調査してみました。 デフォルトでは ruby , sinatra , rails5 のコードジェネレータが用意されており、以下のようにDockerを使ってコードの自動生成ができます。 # 通常のコードジェネレート $ ./run- in -docker.sh generate -i modules/swagger-codegen/src/ test /resources/2_0/petstore.yaml -l ruby -o /path/to/output # 独自テンプレートでコードジェネレート $ ./run- in -docker.sh generate -i modules/swagger-codegen/src/ test /resources/2_0/petstore.yaml -l ruby -t path/to/template_dir -o /path/to/output 実際にコードジェネレートすることはできますが、以下の点が気になりました。 テンプレートにmustache記法を使うことを強制される 欲しいのはschema定義された数ファイルだけなのに不要なファイルが大量に生成されてしまう 回避しようと 新しい対応言語を定義 してみたがjarを実行するなどの手順が必要 たまたまやる気があったので、シンプルにOpenAPI Specificaton 3.0のschema定義からRubyのクラスだけを生成するgemを作ることにしました。 (後から知ったんですが --ignore-file-override と .swagger-codegen-ignore を使えば指定したテンプレートのみ使ってコードジェネレートできるようです。) 導入の利点 現状、Rubyアプリケーションにおいてスキーマ定義どおりにレスポンスが返っているか検証するにはテストを書くか もしくはGraphQLなどのスキーマと実装が密接に紐付いている仕組みを採用することになると思います。 しかしテストを書くか否かは実装者に依存してしまいますし、既存APIをRESTからGraphQLに置き換えるのは工数的にもなかなか選べないことが多いはずです。 そういう状況において「スキーマから自動生成したシリアライザーでレスポンスの型を保証できる」今回のようなアプローチは有用かと思います。 スキーマファーストで開発をしていても、スキーマを更新した後アプリケーションにその変更を反映することを忘れてしまうと同じ問題が起こってしまいますが 今回のgemはcliを提供しているので「シリアライザーのファイルをgit管理下から外し、CIでテストやデプロイ時に最新のスキーマから自動生成して配置する」といったことも可能です。 「人が忘れてたことによってOpenAPIのスキーマ定義と実際のレスポンスがズレる」といった問題が防げるところに導入の利点があると考えています。 サンプル このgemを使いつつ、Twitterのような簡単なRails APIを作ってみます。 まずはGemfileに以下を追加 gem 'active_model_serializers' 登場するモデルは User , Profile , Tweet の3種類です。 class User < ApplicationRecord has_one :profile has_many :tweets end class Profile < ApplicationRecord belongs_to :user end class Tweet < ApplicationRecord belongs_to :user end スキーマはこんな感じ。 create_table " profiles " , options : " ENGINE=InnoDB DEFAULT CHARSET=utf8 " , force : :cascade do | t | t.bigint " user_id " t.string " description " t.datetime " created_at " , null : false t.datetime " updated_at " , null : false t.index [ " user_id " ], name : " index_profiles_on_user_id " end create_table " tweets " , options : " ENGINE=InnoDB DEFAULT CHARSET=utf8 " , force : :cascade do | t | t.bigint " user_id " t.string " tweet_text " t.datetime " created_at " , null : false t.datetime " updated_at " , null : false t.index [ " user_id " ], name : " index_tweets_on_user_id " end create_table " users " , options : " ENGINE=InnoDB DEFAULT CHARSET=utf8 " , force : :cascade do | t | t.string " name " t.datetime " created_at " , null : false t.datetime " updated_at " , null : false end seeds.rbでサンプルデータも。 User .create( name : ' takanamito ' ) Profile .create( user : User .first, description : ' プロフィールです ' ) Tweet .create( user : User .first, tweet_text : ' 我が問いに空言人が焼かれ死ぬ ' ) Tweet .create( user : User .first, tweet_text : ' オレは太刀の間合い(半径4m)までで十分...!!(つーか これが限界) ' ) Tweet .create( user : User .first, tweet_text : ' 私の垂直跳びベストは16m80cm!!! ' ) 以下のようなユーザー情報を返すAPIをOpenAPIで定義します。 schemas : user : type : object properties : name : type : string profile : $ref : '#/components/schemas/profile' tweets : type : array items : $ref : '#/components/schemas/tweet' profile : type : object properties : description : type : string tweet : type : object properties : tweet_text : type : string ActiveModel::Serializerを使う前提でControllerを書いてゆきます。 class UsersController < ApplicationController def show @user = User .find(params[ :id ]) render json : @user end end ここまで用意すればあとはgemでシリアライザを自動生成するだけ。 $ openapi2ruby generate ./path/to/openapi.yaml --out ./app/serializers/ Railsを立ち上げて、ブラウザでアクセスしてみると... シリアライザを通して生成したjsonが返せています。 現状の問題点 開発を始めたばかりということもあり、いくつかの問題を抱えています。 OpenAPI上の各schemaのpropertyがActiveRecordのassociatonなのかわからない 上記の問題の解決のためにassociationであっても、シリアライザのattributes定義を使っているため循環参照による無限ループに陥る場合がある まず1点目 例えば上記ユースケース内で紹介しているモデルはすべて ActiveRecordのassociation として関係性が明示されていますが OpenAPIの定義からはその関係性がassociationなのか、単なるクラスのメンバ変数としてアクセスするのかを知るすべはありません。 そのためシリアライザ内で has_one , has_many の定義は使用しておらず 全て attributes として定義し $ref で参照しているクラスのシリアライザで初期化した値を返すメソッドを定義しています。 これによりassociationかどうかを意識せずシリアライザを扱うことができるようになりました。 ( --template オプションにより自作のテンプレートを使うこともできます。 参照: Use original template ) しかし2点目 associationでの対応を諦めたことにより has_one <-> belongs_to な関係性のモデルのシリアライザ生成時した場合、循環参照が生まれてしまいました。 例えば Profile モデルは belongs_to :user な関係にありますが これをschema定義上のProfileのpropertyとして定義してしまうと実行時にお互いのシリアライザを呼び合ってしまい循環参照から抜けられない状態になります。 # profileのschemaにuserへの参照を追加 profile : type : object properties : user : $ref : '#/components/schemas/user' description : type : string class UserSerializer < ActiveModel :: Serializer attributes :name , :profile , :tweets # Profileをシリアライズ def profile ProfileSerializer .new(object.profile) end # ..略.. end class ProfileSerializer < ActiveModel :: Serializer attributes :user , :description # Userをシリアライズしてるので循環参照を引き起こす def user UserSerializer .new(object.user) end # ..略.. end ActiveRecordのassociationが前提であれば Controllerで includeオプション を渡すことによりこの問題は回避可能です。 しかし先述の通りこのgemではschemaのpropertyがassociationなのか判定できず 全てのpropertyをシリアライザのattributesとして実装しているため、今回の問題を引き起こしてしまいます。 ※回避する方法をご存知の方がいればこっそり教えていただけると幸いです。 おわりに 開発の経緯からサンプル実装までご紹介させていただきました。 実際に現場のアプリケーションで導入を検討しているので、これからProduction環境にのせるにあたってgemの改修をしていく予定です。 また弊社では「レスポンスに型をもたせる」という文脈でGraphQL, gRPCなどの技術の採用について普段から議論しています。 RESTにとらわれずAPIを開発したい方はぜひ下のリンクから応募して、ぜひ一度オフィスに来ていただければと思います。
アバター
こんにちは、最近のマイブームは マヌルネコ動画 な新事業創造部バックエンドエンジニアの塩崎です。 今回のテックブログでは、以前にDigdagを紹介した記事の続編として、DigdagをHA構成にするためのTipsなどを紹介します。 Digdagとは Digdagはワークフローエンジンと呼ばれるソフトウェアです。 複数個のタスク間の依存関係からなるワークフローを定義し、そのワークフローの実行及び管理を行います。 この説明だけですと、何が便利なのかいまいちピンとこない方が多いかと思います。 ですが、かゆいところに手が届く便利ソフトウェアです。 具体的なかゆいところの紹介は以前にDigdagを紹介した記事の前半部分に書かれています。 Digdagを使用したことのない方はこちらを読んでから本記事を読み進めると理解しやすいかと思います。 tech.starttoday-tech.com さて、前回の記事ではDigdagを使うメリットの1つとしてHA構成を紹介しましたが、それを実現するための具体的な設定などについては紹介していませんでした。 今回はDigdagでHA構成を実現するために必要な構成要素や設定ファイルなどを紹介します。 HA構成にするための知識 DigdagをHA構成にするためには digdag server コマンドで起動しているプロセスが担っているいくつかの役割を理解することから始めると分かりやすいです。 以下でそれぞれのコンポーネントの説明をします。 API server このコンポーネントはHTTPサーバーとして動作し、以下の機能を提供します。 タスクの状態をブラウザから確認できる機能 digdag push などのdigdagのクライアントモードのコマンドを受け付けるためのREST API これはdigdag serverを起動した時に必ず有効化されます。 Agent Agentは実際にタスクの実行を行う部分です。タスクキューからタスクを取り出し、実行をします。 信用できない環境で動かすことも考慮されているため、この処理を行うスレッドはワークフロー情報が格納されているDBと直接通信をしません。 Workflow executor Workflow executorはワークフローの状態を監視し、次に実行するべきタスクをタスクキューにプッシュします。 Schedule executor Schedule executorはスケジュール実行が設定されているワークフローを監視し、設定された時刻になったらワークフローを実行状態にします。 一部の機能の無効化 これらの機能の一部は digdag server 起動時にコマンドにオプションを渡すことによって無効化できます。 指定するオプション API server Agent Workflow executor Schedule executor なし ○ ○ ○ ○ --disable-executor-loop ○ ○ ☓ ☓ --disable-local-agent ○ ☓ ○ ○ --disable-executor-loop --disable-local-agent ○ ☓ ☓ ☓ なお、これらのより詳細な説明はDigdag公式ドキュメントの中の以下の部分にかかれています。 Internal architecture 構成 HA構成のときの典型的なシステム構成図を以下に示します。 上で紹介した4つの機能のすべてがいずれかのサーバーで有効になっています。 以下ではそれぞれの構成要素について説明します。 PostgreSQL ワークフロー定義やタスクキューなどはPostgreSQLに保存されます。 システム全体をHA構成にする場合には当然ここもHA構成にする必要があります。 今回構築したシステムではAmazon AuroraのPostgreSQL互換モードをMultiAZ構成にすることによって、HA構成としました。 API Server API Serverはそれ専用のインスタンスを用意し、ロードバランサーの後段に2台を配置しました。 これらのサーバー上でのタスクの実行を止めるために、 --disable-executor-loop --disable-local-agent のオプションを指定しています。 また、ここではタスクの実行を行わないため、小さなEC2インスタンスを用いています。 Agent + Workflow executor + Schedule executor Agent、Workflow executor、Schedule executorの機能は同一のサーバーに載せています。 digdag server を起動する時に、 --disable-* オプションを指定せずに起動しています。 タスクの実行はこれらのサーバーのみで行われます。 実際には、これらもAPI Serverとしての機能も有していますが、HTTPリクエストを受けることはありません。 タスクの実行ログの場所について digdag serverを1つのサーバーだけで運用する場合、タスクの実行ログ(タスクが標準出力に書き出した内容)をサーバー上のローカルストレージに保存することが多いかと思います。 しかし、上で紹介したような構成を取る場合、全てのサーバーが読み書きできる場所に実行ログを配置する必要があります。 その1つの方法は、NFSなどの方法を使いネットワーク内の全てのDigdagで同じディレクトリを共有する事です。 また、Digdagはaws S3に実行ログを保存することも出来るので、今回はこちらを採用しました。 以下のような設定ファイルを全てのDigdag serverに読み込ませることによって、実行ログがS3に保存されます。 direct_downloadオプションはログをS3から直接ダウンロードするか否かを指定するためのオプションです。 log-server.type=s3 log-server.s3.bucket=<バケット名> log-server.s3.path=<ログを配置する場所のパス> log-server.s3.direct_download=false # S3から直にダウンロードする場合はtrueにする HA構成にしたときにハマったこと 構成例の次に、HA構成にすることによって発生した問題とその対処法を紹介します。 ローカルストレージのファイルの扱い DigdagをHA構成すると一般的にサーバー台数が2台以上になるため、1つのワークフローに属するタスクたちが複数台のサーバーで実行されることがあります。 そのため、タスク間でのファイルの受け渡しにサーバーのローカルストレージを使用すると、ファイルが見つからずエラーになることがあります。 +task1: sh>: echo 'hoge' > /tmp/hoge.txt +task2: sh>: cat /tmp/hoge.txt # ファイルが見つからない場合がある この問題を解消するためには、サーバーのローカルストレージを介したデータの受け渡しをなくす必要があります。 ローカルストレージの代わりにS3やDBなどの全サーバーから参照することのできるストレージを使うことによって、問題に対処をしました。 PostgreSQLのコネクション数 DigdagがPostgreSQLと接続する時に使用しているコネクションが多すぎると、Digdagの起動時にPostgreSQLとの接続を確立できないことがありました。 これはPosgreSQL側が受けることのできるコネクション数の上限に達すると発生する現象です。 コネクション数のデフォルトはCPUコアの数*32とかなり多めになっているので、以下のようにして、コネクション数を絞って問題に対処しました。 database.maximumPoolSize=32 サーバーが落ちた時の挙動 HA構成をとっているときにサーバーが突然死したときの挙動がどうなるのかの検証を行いました。 以下のようなタスクを実行し、sleep 10を実行している最中にDigdagプロセスを kill -9 で落としてみました。 timezone: UTC +task1: sh>: "echo task1 start && sleep 10 && echo task1 end" kill -9 を行ったdigdagプロセスのログは以下のようになり、task1の実行途中でサーバーが突然死をしたような挙動になっています。 2018-06-15 13:50:24 +0900: Digdag v0.9.25 2018-06-15 13:50:25 +0900 [INFO] (main): secret encryption engine: disabled 2018-06-15 13:50:25 +0900 [INFO] (main): XNIO version 3.3.6.Final 2018-06-15 13:50:25 +0900 [INFO] (main): XNIO NIO Implementation Version 3.3.6.Final 2018-06-15 13:50:25 +0900 [INFO] (main): Starting server on 0.0.0.0:65434 2018-06-15 13:50:25 +0900 [INFO] (main): Bound on 0:0:0:0:0:0:0:0:65434 (api) 2018-06-15 13:50:26 +0900 [INFO] (0042@[0:ha_sample]+sample^failure-alert): type: notify 2018-06-15 13:50:34 +0900 [INFO] (0042@[0:ha_sample]+sample+task1): sh>: echo task1 start && sleep 10 && echo task1 end task1 start # ここでdigdagプロセスに対してkill -9を行う。プロセスが突然死ぬので、これ以降のログは無い。 このワークフローの実行状況をweb UIで確認すると、タスクが未だ実行中という状態になっています。 そして、約5分後になると別のdigdagプロセスでタスクが実行され、ワークフローの実行が成功しました。 2018-06-15 13:55:37 +0900 [INFO] (0042@[0:ha_sample]+sample+task1): sh>: echo task1 start && sleep 10 && echo task1 end task1 start task1 end Agentはキューからタスクを取り出すときにqueued_task_locksテーブルの lock_expire_time に現在時刻から5分後のUNIX timeを書き込みます。 Agentは1分毎にこの値を現在時刻の5分後に設定するため、Agentが生きている限りは 現在時刻 < lock_expire_time になります。 一方、Agentが死んだ場合には lock_expire_time の更新がストップするため、約5分後になると現在時刻 > lock_expire_time という状態になります。 この状態のタスクの存在を他のAgentが検知することによって、突然死したAgentが担当していたタスクを他のAgentが代わりに実行するという機能が実現されています。 この動作はタスクのリトライ機能とは関係ないため、リトライ回数を設定していないタスクに対しても行われます。 このような内部実装になっているため、Digdagのワークルフローを書くときには可能な限り各タスクを冪等にするべきです。 オートスケーリングとの組み合わせ HA構成を組んだことによってオートスケーリングによるスケールアウトが簡単に行えるようになったので、その紹介もします。 構成図中でタスクを実行する役割を持っているEC2インスタンスたちに対してAutoScalingGroupの設定を行い、サーバーの起動時にDigdagが自動的に起動するよう設定します。 そして以下のようにすることによって、効率よく複数個のタスクを実行できます。 なお、Agentのサーバー台数をゼロにしてしまうとその後にサーバー台数を増やすためのリクエストを実行することすらできなくなりますので、注意が必要です。 +scale_out: sh>: autoscaling.sh 10 +load_tables: for_each>: table: ["table1", "table2", "table3", ... "table100"] _parallel: true _do: call>: load_table.dig +scale_in: sh>: autoscaling.sh 1 #!/bin/bash REGION='<region>' AUTO_SCALING_GROUP_NAME='<auto scaling group name>' aws --region $REGION autoscaling set-desired-capacity --auto-scaling-group-name $AUTO_SCALING_GROUP_NAME --desired-capacity $1 まとめ 今回はDigdagをHA構成で構築する方法を紹介しました。 ワークフロー管理ツールはDigdag以外にもAirflowやLuigiなどいろいろあります。 それら他のツールと比べるとDigdagはHA構成を簡単に組むことが出来るように最初から考えられている印象を受けます。 バッチをスケジュール実行するサーバーは多くのシステムでSPOFになりやすいので、HA構成にすることによって可用性をより高めましょう。 スタートトゥディテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください!
アバター
こんにちは、バックエンドエンジニアの田島( @katsuyan121 )です。 5/31〜6/2にかけて仙台で開催されたRubyKaigi2018に、スタートトゥデイテクノロジーズから5人が参加しました。 今年のRubyKaigiは3日間で50を超える講演があり、参加者も1000人を超える大変大規模なカンファレンスでした。たくさんの講演の中で、スタートトゥデイテクノロジーズのエンジニアが興味を持ったものを、この記事でいくつか紹介します。 また今回スタートトゥデイテクノロジーズはスポンサーブースを出展したので、そこで得られたことを共有します。 セッション Proverbs(Matz) Rubyのお父さんであり、弊社スタートトゥデイテクノロジーズの技術顧問でもある Matz さんの発表です。 今回の発表では3つの諺を例にお話を展開されました。 名は体を表す 「名は体を表す」という通り、名前は重要であるというお話でした。特にソフトウェアには実態がないため、名前付けが重要になるとのことでした。 名前には2種類あり、1つは振る舞いに対する名前でもう1つがプロジェクトに対する名前です。 振る舞いに対する名前付けが難しいという場面はよく訪れます。しかし名前付けが難しいということはその概念を十分に理解していないことであるとのことでした。 プロジェクト名という側面では、Rubyのような求心力のある名前が好ましいとの話でした。また、今の時代だとgooglabilityが非常に大切であるとも語られていました。それを解決するのためにTensorFlowのように単語を組み合わせたり、Jupyterのようにスペルをいじるのは良い手段であるとのことでした。 時は金(価値)なり 時は金(価値)なりが示すように、時間を有効活用することが非常に大切であるとの話でした。開発における時間には開発時間と実行時間という2つの側面があります。Rubyでは主に開発時間という部分に重きをおいてきました。開発時間を短くすることで人件費の節約など直接的に費用削減につながるということでした。 また、実行時間についてもRubyにJITを導入するなど改善を常に行っています。実行時間を短縮することでサーバの台数が減るなど直接的に費用を削減できると例をあげて説明していました。 塞翁が馬 塞翁が馬のように、良いこと悪いことに一喜一憂するなというお話でした。例えば、静的型が流行っているからと言ってRubyには絶対に静的型は導入しないとのことでした。長い目で見た時に安易に静的型を導入しないほうが有利になるとMatzさんは考えているそうです。 コスチュームスポンサー 今回のRubyKaigiではMatzさんには事前にZOZOSUITを着ていただき、測定結果を元にMatzさんピッタリサイズのジーンズを履いていただきました。 またkeynoteの最後でコスチュームスポンサーとしてスタートトゥデイテクノロジーズを紹介していただきました! Improve Ruby coding style rules and Lint(Koichi ITO) RuboCopのコミッタである Koichi ITO さんによる、RuboCopのStyleとLintについての発表でした。 Styleはコーディングスタイルのことを表します。コーディングスタイルは様々な文化から形成され、会社やプロジェクトによって異なったルールが存在します。RuboCopのデフォルトのStyleは Ruby Style Guide に従って作成されているそうです。Lintは潜在的なバグを指摘するもので、コーディングスタイルとは違い言語仕様的に適切でない箇所を指摘します。 Styleについては、会社やプロジェクトによってコーディングスタイルは違うものなので、積極的にデフォルト設定からカスタマイズしてほしいとのことでした。 また、Koichi ITOさん自身がRuboCopのルールをどのような経緯で新しく追加したかについてもお話されていました。会社のコードレビューを受けて気づいたルールや、RailsのカスタムコップからRuboCopに採用したルールもあるとのことでした。 発表を通して、自分たちが使ってよかったものを他の人にも広めていこうというKoichi ITOさんの意思がとても伝わってきました。 Architecture of hanami applications(Anton Davydov) Anton Davydov さんによる、hanamiを長期的にメンテナンスしていくうえで採用したアーキテクチャを振り返るという発表でした。 ファットモデルなコードから段階的にビジネスロジックを切り離していく方法としてhanamiに採用されている手法の説明がありました。具体的にはInteractor(Service object), Repositoryや、Event Sourcingなどが紹介されていました。 また、Event SourcingのPoC実装としてhanami-eventsというgemについての紹介もありました。 実際にInteractorやRepositoryを入れた意図やhanamiでの実装例が示されていました。 様々なデザインパターン紹介の最後に『必要かわからないときは使わないでおく』ということを書かれていた事は、エンジニアとしてすごく共感できました。 ビジネスロジックの切り分けはhanamiだけでなく、webアプリケーション開発をしている人全体の問題でもあります。良さそうなパターンは実際に検証して自分たちのプロジェクトに取り入れていこうと思います。 bancor: Token economy made with Ruby(Yuta Kurotaki) Yuta Kurotaki さんによるBancor Protocolというスマートトークンを発行するためのプロトコルをRubyで実装したという発表でした。ちなみにBancorはバンコールと読みます。 ブロックチェーン関連の実装はPythonやGo・JavaScriptで実装されているものが多いですが、Rubyで実装されているものがあまりなく実装してみたということでした。 実際のRailsアプリケーションに組み込んでbancor protocolを利用したデモでは、流動的に価格が決定されているところを見ることができました。 Ruby実装が少ないブロックチェーンを実装するという試みは、新しいものに挑戦していくという部分ですごく共感を得られました。 参考: bancorのリポジトリ Hijacking Ruby Syntax in Ruby(joker1007/Satoshi "moris" Tagomori) Hijacking Ruby Syntax in Ruby from SATOSHI TAGOMORI joker1007 さん・ Satoshi "moris" Tagomori さんによるメタプログラミングを最大限に活用しようという発表でした。メタプログラミングというRubyの黒魔術を最大限に活用して、Rubyの文法が変わったのかと錯覚するレベルの機能が示されていました。 肝になる機能は Binding#local_variable_set と Tracepoint で、これらの機能を利用していたるところにフックを仕込みます。そうすることで、いたるところの変数を書き換え邪悪な機能を実現しているとのことでした。 Javaでおなじみのfinal、override、abstractをRubyでも実現し、なおかつこれらはクラス定義時点でエラーを出せるという点に感動しました。 その他にPythonでおなじみのwithや、Goでおなじみのdeferなどと同等な機能をRubyのメタプロだけで実現している例が示されました。 Rubyはメタプロ文化があるとはいえ、ここまでのことができたのかという驚きがありました。 TTY - Ruby alchemist’s secret potion(Piotr Murach) Piotr Murach さんによるTTYというCLI作成用のライブラリを作ったという発表でした。 最初に以下のようにwebのアーキテクチャと比較している点が興味深かったです。 cli - router command - controller stdin - request stdout - response template - view TTYには複数のプラグインがあり、プラグインを導入することでCLIをリッチにできます。 実際にデモではTTYを利用したCLIが示されており、ギュインギュイン動いていて感動しました。 TTYは簡単にリッチなCLIツールを作るにはすごくいいライブラリだと思います。 またアーキテクチャも上に示した通りwebエンジニアにとってすごくわかりやすい構成となっていて、複雑なCLIツールを作るのにも向いているライブラリであると感じました。 参考: TTYリポジトリ Firmware programming with mruby/c(Hitoshi HASUMI) Hitoshi HASUMI さんによる、mruby/cを使って日本酒を醸す(かもす)というお話でした。 日本酒の製造にあたっては麹(こうじ)や醪(もろみ)の温度管理が必要で、そのために温度センサーのデータを収集するためのファームウェアをmruby/cで作ったそうです。 こういう用途ではラズパイの使用も考えられます。しかし冬でも35度である麹室での運用を考えた時に、低消費電力かつ低発熱なことを重視しARM Cortex-M3が搭載されたPSoc5LPマイコンにしたそうです。 このシステムは2018年1月から実際に稼働しているそうです。 mrubyとmruby/cはどちらとも軽量Rubyですが、mruby/cの方がより少ないメモリでも動くようにできているとのことでした。 大きな違いとしてmrubyはRTOS(RealTimeOS)の上で動くことが前提ですが、mruby/cはRTOSなしでその下に直接ハードウェアがあるという構成になっている点です。 そのためにHAL(ハードウェア抽象化レイヤー)があり、ソースコードの中のその部分を読むと面白いのではとのことでした。 開発中に困ったこととしては、デバッガでのステップ実行ができないのでprintデバッグしかできなかったとのことでした。また、 Array#each が未実装なので while と break で代替しなければならないという点でした。このようなことから、mruby/cはまだまだ成長の余地があって楽しみです。 また発表されていた十旭日は、東京では以下の酒販店で取り扱われているそうです。 生原酒と火入れ:新宿の三伊井上酒店 主に生原酒:笹塚のマルセウ本間商店 主に火入れ:中目黒の出口屋、練馬の大塚屋 定番品種:横浜君嶋屋の銀座店、恵比寿店 十旭日のなかでもmruby/cで醸されたお酒は29BY(平成29酒造年度の醸造)のものだそうです。 https://twitter.com/hasumikin/status/1006899085570334727 Ruby code from the stratosphere - SIAF, Sonic Pi, Petal(Kenichi Kanai) Kenichi Kanai さんによる、mrubyを使ったlivecodingの発表でした。 ここでいうlivecodingとは、「コンピュータの言語であるプログラムコードを直接操作することで、さまざまな音や映像をリアルタイムに生成する即興演奏の方法」とのことでした。 Petalというプログラミング言語(実際はRubyのDSL)を使うことで、シンプルな記法で多様な音を出すことができるそうです。 気象観測用の気球にラズパイとセンサーを載せて、上空でセンサーから取得したデータをもとにPetalのコードを生成します。そこから音を生成することによって、成層圏からの音楽を地上に届けるということを札幌国際芸術祭でやったそうです。 最初は気球に搭載したラズパイで音を生成してそれを地上に無線で届けるということをしていたそうです。しかしそれだとノイズが多くのってしまったので、2回目からは音の生成を地上でやるという方法で問題解決をしたらしいです。 発表の最後ではこの時に取得したセンサーデータのログから音楽を生成し、会場のみんなで成層圏からの音楽を楽しみました。 参考: スライド スライド Petalリポジトリ Deep Learning Programming on Ruby(Kenta Murata/Yusaku Hatanaka) Kenta Murata さん・ Yusaku Hatanaka さんによるRubyでDeep Learningをできるようにする試みについての発表でした。 このセッションは2つ分かれており、前半では「mxnet.rb」のについての発表。後半は「Red Chainer」の発表となっていました。 1つめのmxnet.rbのお話は、MXnetのRubyバインディングを開発したというお話でした。MXnetはDeepLearning用のライブラリで複数言語のバインディングが存在するが、Rubyには対応していなかったとのことで開発を初めたようです。 MXnet自体はKerasのbackendにも利用されています。TensorFlowでのバックグラウンドと比較して良いパフォーマンスであったことが確認されており、有用なライブラリであるとのことでした。 ONNXにも対応しており、一度MXnetで作ったモデルを他のONNXに対応したDeepLearningライブラリで利用できるそうです。また逆に他のライブラリで作成したモデルをMXnetで利用できるとのことでした。 mxnet.rbは絶賛開発中で、プルリクをお待ちしておりますとのことでした。 2つ目のRed ChainerはChainerをRuby実装として実現するというお話でした。こちらも絶賛開発中とのことで、まだPythonチックなRubyを書かないと行けない部分があったり、CPUにのみしか対応していないとのことでした。しかし、今後はもっとRubyらしく記述できるようにし、後述するCUMOを利用することで近々GPUに対応予定とのことでした。 Red ChainerはRed Data Toolsというコミュニティに属しています。Red Data Toolsは理念が素敵で誰でも気軽に参加できそうなコミュニティだと感じました。 このように、RubyでDeepLearningをするという試みが活発になってきていることが分かりました。Rubyで楽しくDeep Learningできるのはすごく楽しみです。 参考: Yusaku Hatanakaさん振り返りブログ mxnet.rbリポジトリ redchainerリポジトリ Fast Numerical Computing and Deep Learning in Ruby with Cumo(Naotoshi Seo) Naotoshi Seo さんによる、NumoというRubyの計算用ライブラリをCUDAに対応させようという試みの発表でした。 Numo自体はRubyの計算用ライブラリですが、CPUにのみ対応しています。そこで、Numo互換のCUDA対応ライブラリであるCUMOを作成したそうです。 CPUを前提とした実装をGPUに対応した時、GPUによる並列計算どのように計算させるのかということがわかりやすくまとまっていました。CUDAプログラミングをしたことがない人でも理解できるような内容となっていました。 実際にCUMOを利用する場合、ソースコード中のNumoをCumoに全置換するだけでGPUに対応できるのは感動しました。 またこのプロジェクトはRuby Associationに採択されたプロジェクトだそうです。個人的にやるプロジェクトでもなにかしらの締切が設けられることで、それがマイルストーンになるということも語られていました。 参考: 発表者振り返りブログ CUMOリジトリ NUMOリポジトリ スポンサーブース アンケート 1日目 お題 初日のお題は、「ブロック開始の中括弧とブロックパラメーターの間にスペースを開けるか」というものでした。 このお題の理由としては、技術顧問のMatzさんとの月イチのミーティングでRubCopの話になったことが切っ掛けです。RuboCopのデフォルトではスペースを開けるようになっています。しかし、Matzさんはスペースを開けたくないとのことだったのでこのお題を提示しました。 結果 考察 結果はスペースを開ける派が多数となりました。スペースを開けるという理由として多く挙げられたのはやはりRuboCopに指摘されるからというのが多かったです。また、中括弧でなく do end で記述する場合は do とブロックパラメータの間にスペースを開けるから統一するためとの解答もいただきました。 スペースを開けない派の意見としては、ArrayやHashを作るときにはカッコのあとにスペースを開けないからそちらと統一するためなどの意見をいただきました。 「Improve Ruby coding style rules and Lint」で示されていたように、styleは文化によって違います。そのためプロジェクト内で記法が統一されていれば、どちらでも良いと考えました。 2日目 お題 2日目のお題としては、「クラスメソッドの定義をするときに self.クラスメソッド名 と書くか、 class << self で特異クラスをオープンするのか」というものでした。このお題の意図としてはまたもRuboCopでは後者を使うように示されますが、前者のほうがクラスメソッドとしてわかりやすいのではないか? という意見が社内であったことからこのお題を提示しました。 結果 考察 結果は class << self で特異クラスをオープンするほうが多数となりました。特に多かった意見としては、クラスメソッドが1つの場合は self.クラスメソッド名 、2つ以上の場合は class << self を使うというものでした。 また、 class << self を使う理由としてはprivateクラスメソッドがこっちの書き方でしか書けないから、grepする時に探しやすいからというものがありました。 self.クラスメソッド名 を使う理由としてはパット見てクラスメソッドかどうかがわかりやすいからという意見が多かったです。 このアンケートは、一日目よりも迷う人が多かった様子でした。このケースはプロジェクトで柔軟に対応していくのがいいと考えます。 3日目 お題 最終日のお題は「文字列の配列を定義するときに、%記法を使うか」というものでした。このお題に関してもRuboCopでは前者が示されます。後者のほうが文字列の配列としてシンプルでわかりやすいという意見があったことからこのお題を提示しました。 結果 考察 結果は%記法を使うという方が多数となりました。%記法を使う理由としては、単純にタイプ数が減る。文字列以外が配列の中に入らないなどが挙げられました。ここでもRuboCopに指摘されるからという意見もありました。 %記法を使わない理由としては、%の次に何を書けばいいか忘れてしまう。Ruby以外の言語から来た人でもわかりやすい。文字列の配列であることが直感的にわかりやすということが挙げられました。 これについても、それぞれの利点をちゃんと知った上であれば好みのスタイルで書けばいいと考えました。 gem お題 最終日には好きなgemのアンケートを取りました。 結果 考察 今回の好きなgemアンケートでは pry や byebug など手元で動かすgemが多く挙げられました。 実際に自分が助けられたり便利だなって感じたgemを好きだと感じるのかなと思います。 知らないgemも多く挙げられすごく勉強になりました。ぜひアンケート結果から知らないgemを調べてみてください。 まとめ やはりRubyKaigiは日本一のテックカンファレンスだなと改めて実感しました。Rubyコミッタ級のエンジニアがゴロゴロ転がっているカンファレンスは他にはないです。また来年のRubyKaigiが楽しみです! 今回初めてスポンサーブースを出展させていただきました。ブースを出展することによってRubyのお話をたくさんの方とできました。Rubyコミッタの方たちも立ち寄って頂き気軽に話すことができたのはすごく勉強になりました。 また、今回のRubyKaigi参加に関する費用はすべて会社が負担してくれました。さらに出張手当をもらえたうえに、休日出勤分の振休まで取得させてくれました。 来年のRubyKaigiは福岡で開催される予定です。一緒にいこう! という方がいましたら、ぜひ以下のリンクからご応募ください。 https://www.wantedly.com/companies/starttoday-tech/projects おまけ RubyKaigi参加メンバー 弊社技術顧問であるMatzさんとの記念撮影 仙台は美味しいものが多すぎてお腹がいっぱい 牛タン からの牛タン からの牛タン うに 海鮮丼 東北大生名物さわき RubyKaigi最終日の次の日に猫島こと田代島にお出かけ いざ猫島 ねこにちょっかい 猫になるリーダー 猫神社 ねこー
アバター
こんにちは!スタートトゥデイテクノロジーズ新事業創造部の堀江( @Horie1024 )です。 2018年5月8日〜5月10日にかけて、カリフォルニア州マウンテンビューにあるショアライン・アンフィシアターで行われたGoogle I/O 2018(以下I/O)に新事業創造部の堀江、権守、茨木、そして代表取締役CIOの金山の4名で参加してきました。 また、帰国後の5月29日にDMM.comグループさん主催の Google I/O 2018 参加報告会 で堀江、権守、茨木が登壇しI/Oで得た知見を発表しました。 本記事では、 参加報告会での発表内容の紹介とI/Oで印象に残ったセッションの紹介、そして参加した感想をお伝えしたいと思います。 Google I/O 2018参加報告会での発表 堀江、権守、茨木が発表した内容について紹介します。 堀江「Paging Library Overview」 「Paging Library Overview」というタイトルで今回のI/Oで1.0がリリースされた Paging Library について発表しました。 Jetpack のArchitecture Componentsに含まれるPaging Libraryは、無限にスクロールするようなListの実装を非常に簡単にしてくれます。スクロール位置に応じたデータの追加ロードやPlaceHolder、Roomを利用したデータのキャッシュとDiffUtilによる差分更新など実装のしやすさとパフォーマンスにフォーカスされていてぜひ業務で使ってみたいと感じています。 デフォルトではRoomを使用する前提となっていますが、DataSourceを継承したクラスを自作することで様々なケースに柔軟に対応することが可能です。 こちらはPaging Libraryについてのセッションの動画です。ネットワークからのデータの取得からUIへの反映までアニメーションを交えて説明されていてわかりやすいです。PageKeyedDataSource、ItemKeyedDataSource、PositionalDataSourceの各種DataSource型についても解説されています。 Paging Libraryについてより理解を深めたい場合、 Codelab や Android Developers のドキュメントが参考になります。 権守「新しくなったMaterial Designを触ってみた」 I/Oで発表された Material Theme Editor の使い方を中心に、新しくなったMaterial Designを実装していく上での具体的な手順について発表しました。 Material ThemeはMaterial Designで利用するためのパーツ(ボタンやカードなど)のカタログのようなものです。Material Theme Editorを使うとTheme上のパーツの色やフォントなどを簡単に変えることができ、プロダクトに合ったThemeを作ることができます。プロダクトのデザインデータを作成する際にはThemeからパーツを選ぶ形で行うことになります。 Material Componentsの実装状況もあり、Material Themeから作ったデザイン全てをAndroid上にスムーズに実装できるとはまだ言えない状況だと思います。ですが、今後の発展次第ではデザイン実装が容易になることも期待できるものだと感じました。 茨木「フロントエンドの動向 From Google I/O 2018」 I/Oでフロントエンドに関するセッションを中心に聴講しました。また、CodelabsでWebAssemblyやPWAに関するコースを体験しました。報告会では、PWA・WebAssembly・JavaScriptの3点にフォーカスして発表しました。 PWA PWAに関してはログイン・決済APIや実際の運用例が紹介されていました。スターバックスの例が紹介されていましたが、ネイティブアプリに近いUXが印象的でした。 WebAssembly WebAssemblyに関してはコンパイル用ツールチェインであるemscriptenや、実際にWebAssemblyを使っているサービスの実例が紹介されました。サービスの実例で特に印象的だったのがAutoCADでした。ヘビーな計算やグラフィックスを伴うCADアプリケーションがブラウザで動作しているのが衝撃的でした。 JavaScript JavaScriptに関しては色々な発表がありましたが、特に印象的だったのがTensorFlow.jsのNode.js対応でした。GPUに対応していてパフォーマンスもPythonに匹敵するので、今後の機械学習ではJavaScriptも多く使われるようになるかもしれません。 Google I/O 2018で印象に残ったセッションの紹介 I/Oでは3日間に渡り様々なセッションが行われます。その中から堀江、権守、茨木が聴いて印象に残ったセッションをいくつか紹介します。 ML Kit: Machine Learning SDK for mobile developers ML Kitについてデモを交えながら紹介されています。Firebaseの一機能として公開されていて、ベースとなる5つの機能(Image labeling、Text recognition、Face detection、Barcode scanning、Landmark detection)を簡単に利用できます。それらで仕様を満たせない場合、カスタムモデルをアップロードすることでアプリから簡単に使用でき便利です。カスタムモデルのサイズを軽量化しつつ精度を維持する仕組みも興味深かったです。 Android Jetpack: easy background processing with WorkManager JetpackのArchitecture Componentsの1つとして公開されたWorkManagerについてのセッションです。 複数のbackground処理を繋げたり並列で実行するなど柔軟に処理を実行でき非常に便利です。 WorkManagerがどのように動作しているかの解説もあり面白いです。 Code beautiful UI with Flutter and Material Design Flutterで開発されたアプリ「SHRINE」に対してライブコーディングでMaterial Designを適用していくセッションです。 登壇者2人の発表が上手く、サクサクと進むライブコーディングは聴いていてとても楽しかったです。 また、Flutterでこんなに簡単にMaterial Designを適用できるのかと感動しました。 What's new with ConstraintLayout and Android Studio design tools Motion Editorによるアニメーションの作成は必見です。 What's new in Android Runtime アプリ実行中のログを集め、配布前にAPKを最適化することで起動を高速化するという話には驚きました。 Web performance made easy Lighthouseを使ったWebサイトのパフォーマンス改善について実例を用いて説明していてとてもわかりやすかったです。 Lighthouseの進歩を感じられるセッションでもありました。 Best practices for text on Android テキスト周りの細かい話も興味深かったですが、特にRecyclerView中のリンクに関するテクニックはぜひ取り入れたいと思いました。 Introducing .app domain names, and how to secure them .appドメインはHTTPSのみのドメインで、Google I/O初日の5/8に登録の受付が開始されました。Google I/Oの参加者には.appドメインを無料で取得する権利が与えられたので実際に取得してみました。 What’s new in Chrome DevTools Lighthouseとの連携やasync/awaitのデバッグが印象的でした。 Make your WordPress site progressive Webの30%を占めるWordPressに対し、メジャーなテンプレートをAMP対応させていくという内容でした。 Googleらしいアプローチで、なるほどと思いました。 感想 I/Oへ参加することで非常に良い経験ができました。 セッションを聴くことを中心に3日間を過ごしましたが、Office HoursとDesign Reviewにもっと積極的に行くべきだったと感じています。 生でセッションを聴くのも大切ですが、Googlerにその場で質問できる機会は貴重ですしセッションは後から見返すことができます。 次回のI/Oに参加できる場合にはこの点を踏まえスケジュールを組もうと思います。 加えて、KeynoteでのAIに関する発表やML Kitの発表を聴いてフロントエンドエンジニアでも 機械学習やディープラーニングの基礎を理解し、その上でサービスの開発に取り入れていく必要があると考えています。 そして、英語で聴く、話す力が足りないことを身に染みて実感しました。 I/Oに限らず英語を使う機会は弊社が海外展開を進めていく中で増えていくので継続的に勉強したいと思います。 最後に 今回のI/Oは会社の出張という形で参加しました。参加期間は業務扱いとなり、交通費なども含め会社からサポートされます。 国内外のカンファレンス参加についてサポートする体制が整っており非常にありがたく思います。 得られた知見を普段のサービス開発に生かしていこうと思います! スタートトゥディテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! https://www.wantedly.com/companies/starttoday-tech/projects おまけ Day2の夜にコンサートが開かれるのですが完全にフェスでした。照明の使い方がすごい。 空いた時間でサンフランシスコを観光しました。西海岸の気候は最高ですね。 サンフランシスコにあるGitHub本社オフィスを訪ねました。事前に連絡せず行ったにも関わらずフレンドリーなお兄さんが案内してれました。 有名なOctocatの像が。残念ながら本社のGitHubストアはクローズしたとのこと。今後はOnline Shopのみでグッズを販売するとのことです。
アバター
(Headphones icon credit *1 ) こんにちは、2018新卒エンジニアの田島( @katsuyan121 )です。 新卒研修の一環としてZOZOTOWNカスタマーサポートセンターにてカスタマーサポート研修に参加しました。 研修を受けるまではエンジニアがカスタマーサポート研修を受ける必要があるのだろうかと疑問に思っていましたが、実際にやってみると学びが多く会社としてもメリットがあると感じました。 そこで、本記事ではカスタマーサポート研修の内容とそのメリットをご紹介します。 ZOZOTOWNカスタマーサポートセンターとは ZOZOTOWNカスタマーサポートセンターはお客様からのZOZOTOWNに関するお問い合わせを受け付けています。 「お客様と友達になる」を理念として掲げており、困っている友達を助けるように全力でお客様をサポートしお客様が笑顔になれるよう日々努めています。 また高品質なお客様サポートの結果、ZOZOTOWNカスタマーサポートセンターは「HDI-Japan」が定める「HDI 五つ星認証プログラム」において最高評価の「五つ星認証」をアパレル業界で初めて取得しています。詳細につきましては以下のリンクをご参照ください。 https://www.starttoday.jp/news/20170608-2030/ 研修の流れ 研修は以下のように行われました。 前半: 座学・ロープレで実践練習 後半: 実践(電話対応・チャット対応) 研修では実際にお客様対応を行うため、ZOZOTOWNの仕様やルールを学ぶ必要があります。そこで最初にZOZOTOWNについて研修担当者にみっちり教わりました。カスタマーサポートの方々はZOZOTOWNに詳しく、エンジニア以上にZOZOTOWNの仕様やルールを把握しているのではないのかと思うほどでした。 その後学んだことをふまえて研修担当者や研修生がお客様役としてお問い合わせをし、それに対応するというロープレを行いました。ロープレを行うことで学んだことを定着させることができ、座学だけでは見つけることができなかったイレギュラーケースや疑問点を解消できました。 一通りのことを学び終えると実践ということで実際にお客様からのお問い合わせに対する電話対応を行い、研修全体で1人約20件のお問い合わせに対応しました。最初のうちはうまく対応できるか不安でしたが、ロープレを何回も行っていたお陰でお客様の問題を解決することがでました。 またZOZOTOWNではカスタマーサポートとして電話だけでなく、メールやチャットでもお問い合わせを頂きます。そこで、チャット対応についても経験しました。電話とチャットでお問い合わせの内容がぜんぜん違うということが興味深かったです。 カスタマーサポート研修のメリット カスタマーサポートで感じたメリットを6つ紹介します。 サービス開発へのモチベーション向上 相手の背景を理解して話ができるようになる お客様が困っている温度感や細かいニュアンスを知ることができる サービス全体を把握できる 他部署がなにをやっているのかを知れる 部署外に頼れる仲間ができる 1. サービス開発へのモチベーション向上 エンジニアとして仕事をしているとお客様の声を直接聞くという機会がほとんどないですが、カスタマーサポート研修ではお客様の声を直接聞くことができました。お客様の声を直接聞いて感動したことがあります。それはZOZOTOWN側の不備に対してもすごく丁寧にお問い合わせをくださったり、お問い合わせの回答に対して感謝の言葉を頂くことが多かったことです。 お客様の声を直接聞いたことで、サービスの向こう側には実体をもったユーザがいるということに改めて気づくことができました。この経験から「もっとお客様のために良いサービスを作っていきたいな」と、サービス開発へのモチベーションが向上しました。 2. 相手の背景を理解して話ができるようになる 最初にも紹介しましたが、ZOZOTOWNカスタマーサポートセンターは「お客様と友達になる」を理念として掲げています。お客様と友達になるということを意識して会話をすることで、相手の背景を理解しベストな回答をできるようになります。 カスタマーサポートセンターにお問い合わせをしてくださるお客様は、お問い合わせしたい内容がまとまっているとは限りません。そのため相手の背景を理解したうえで会話をするということが非常に大切であることを身をもって体感しました。 「お客様と友達になる」というマインドは、カスタマーサポートにかぎらずエンジニアの領分でも必要になります。業務ではエンジニアに限らず様々なバックグラウンドを持つ人が関わってきます。そんな中で「友達になる」というマインドを忘れずに会話をすることで、スムーズにコミュニケーションを取ることができるようになりました。 3. お客様が困っている温度感や細かいニュアンスを知ることができる 普段の業務ではお客様の声はほとんど間接的に聞くことしかないため、それに対する対応の重要度が分からず後回しにしてしまうことがあります。 しかし直接お客様の声を聞くことで、どれだけお客様が困っているかの「温度感」を感じることができました。これをきっかけとして、後回しにしがちな問題が重要なものであると気づき取り組むきっかけになりました。 また直接お客様の声を聞くことで、サービスのどんな部分が使いにくいのかをエンジニア目線で細かいニュアンスまで感じることができました。これによって、お客様が使いやすいようなサービスを細部までこだわって作ることができます。そして今後のサービス開発においてどんなサービスが使いやすいか考える助けになると考えています。 4. サービス全体を把握できる カスタマーサポートセンターではお問い合わせに間違った情報を返答してはいけません。そこで、研修ではサービスの仕様やルールを覚える必要があります。 ZOZOTOWNの商品をユーザが購入し、どのような流れでお客様のところへ届くのかといったサービスの基本的な部分。ZOZOTOWNの商品が発送されてから大体いつお客様のもとへ届くのか。どういった支払い方法があり、どういった条件であればキャンセルができるのかといった細かい部分までを学びました。普段のエンジニアリング業務ではあまり気にすることがない、配送や決済の詳細な部分までも知ることができたことは大きな学びとなりました。 これらの内容はただ覚えるだけではなく、実際のお客様サポートの中でさらに理解を深めることができたと感じています。ZOZOTOWNをあまり使うことがなかった私ですが、今ではZOZOTOWNのサービス全体を把握できています。 5. 他部署がなにをやっているのかを知れる 3回目の紹介になりますが、ZOZOTOWNカスタマーサポートセンターは「お客様と友達になる」を理念に掲げています。カスタマーサポートセンターの人たちはこの理念に基づき、お客様が笑顔になってもらうためにどうすればいいのか考えながら行動していることが感じられてすごく感動しました。実際に同じ現場で仕事をすることによって、カスタマーサポートセンターの業務はもちろんその場で働く人達がどんな思いで働いているのかということを肌で感じることができました。 また現在ZOZOTOWNに関わる部署は20を超え、さらにオフィスや倉庫と働いている場所もバラバラでどんな人達が一緒に働いているのかなかなか把握しにくい状況となっています。カスタマーサポートセンターではあらゆるお問い合わせを頂くため、他部署との連携が最も多い部署です。そのため他部署がどんなことをやっているのか、どんな人達が同じ会社で働いているのかということを知ることができました。短い期間でサービスに誰がどのように関わっているのか、どんな思いで働いているのかを知ることができたのはカスタマーサポート研修であったからこそと感じています。 6. 部署外に頼れる仲間ができる 今回の研修は新卒エンジニアが6人参加しましたが、バックグラウンド・専門分野がバラバラなメンバーでした。研修に参加した全員がカスタマーサポートの経験がなかったため、カスタマーサポートという共通の話題により気軽に会話ができたと感じています。 研修の期間中は一緒の目的に対して取り組み、研修の中で感じたことや問題を共有することでかけがえのない仲間となりました。部署内で解決できない問題など気軽に聞ける仲間がいることは、個人的にすごく喜ばしいことであると感じています。 まとめ 研修を受けるまではエンジニアがカスタマーサポート研修を受ける必要があるのだろうかと疑問に思っていましたが、エンジニアとして大切なことを学ぶことができました。学んだことをサービスに還元して、カスタマーサポートセンターの人たちはじめ研修に関わってくださった人たちへの恩返しとなればと思っています。 この記事を読んで、カスタマーサポート研修を導入してみようと思っていただけたら幸いです。直接カスタマーサポート研修はどうだったか聞きたいという方がいましたらぜひ弊社まで遊びに来てください。 スタートトゥデイテクノロジーズではお客様に最高のサービスをお届けするためにエンジニアを募集しています。興味がありましたら、以下のリンクからご応募ください。 connpass.com *1 : Headphones icon: Icons made by Freepik from www.flaticon.com is licensed by Flaticon Basic License
アバター