りんごとバナナとエンジニア

エンジニア修行の記録

AWS SysOps Administrator Associateに合格しました

少し前になってしまうが、3月末にAWS SysOps Administrator Associate試験を受験して合格した。1月末から試験勉強を始めたのだが、幸い1回で合格できた。

aws.amazon.com

受験の経緯

会社のシステムがAWS上で動いており、運用のためには体系的な知識が必要だと感じたため。社内ではSolution Architect Associateの方を受験する人もいたが、自分の興味がアプリケーションというよりもミドルウェア・インフラ寄りだったため、SysOpsを選択した。

学習法

まずUdemyのコースで一通りの範囲を学習した。

www.udemy.com

このコースは実際にAWSの各サービスを利用するハンズオンが充実しており、単なる試験対策にとどまらない実践的な知識を得ることができる。ただし量が多めで、1月末から観始めて3月上旬くらいまでかかった。自分はEC2やS3・VPC等についての基本的な知識がある状態で勉強を開始したので、何も知識がない場合はもっとかかるかもしれない。

このコースには最後にmock examがついているが、実際の試験よりも難しめで細かな内容が多い。そのため、できなくてもあまり自信を失う必要はない。そのかわり、各コースの最後についている小テストは基本的な内容なので、ここは満点を取れるまで復習した。

また、模擬試験には以下のサービスを利用した。

aws.koiwaclub.com

このサービスは無料会員でも#1~#5までの5回分の模擬試験を受けられるが、有料会員になるのがおすすめ。なぜかというと、後ろの模擬試験になるほど現在の本番試験に即した問題になっているからである。自分は#86から解きはじめ、#60くらいまでを2周したのだが、本番で全く同じ問題が何問か出てきて驚いた。

本番について

試験はオンラインで自宅から受験した。予約はAWS認定の公式サイトから行う。
受験システムはPSIかピアソンを選ぶことができるが、ピアソンの場合Karabiner-Elementsが入っているPCではうまく動作しないという情報があったため、PSIを選択した。

yoshitaku-jp.hatenablog.com

当日は特に問題なく受験できたが、PSIは試験官とのやり取りが英語のみであるため、英語ができない人はピアソンの方が良いかもしれない。

また、他のオンライン試験同様、机の周りはあらかじめ片付けておく必要がある。自分は机の上の物はもちろん、壁のカレンダーや掲示物等もすべて剥がしたうえで臨んだのだが、机の隣に本棚があったため、試験官から場所を移動するよう命じられた(他にPCを置ける場所がなかったので、無理やり本棚を動かしてなんとかした)。机の周りに物や家具がある人は、試験当日にあらかじめ移動させておくか、試験を受けられるどこか別の場所を探しておいた方が良い。

【Kustomize】patchesJson6902の使い方と利用シーン

Kustomizeを使うとき、 patches でファイルを指定してbaseファイルを部分的に変更することが多いと思う。しかし先日、 patches ではなく patchesJson6902 という指定をしているファイルを見かけたので、どう使うのか調べてみた。

patchesJson6902とは

patchesJson6902は、名前の通りRFC6902に沿った方法でPatch処理を行うことができる。RFC6902ではJSON Patchについて定義されている。

tools.ietf.org

Kustomizeは公式ドキュメントをたどりづらいが、Kubernetesのドキュメントの中に詳しい使い方が記載されている。 kubernetes.io

patchesStrategicMergepatchesJson6902 の違い

Kustomizeでは、patchを行う際に patchesStrategicMergepatchesJson6902 の2つの方式に対応している。 patch フィールドは渡したファイルまたはPatchをもとに、どちらの方式かを自動で判定して適用してくれる。
この2つの方式がどう違うのかというと、 patchesStrategicMerge はマージの挙動をKubernetesの設定ファイルに最適化されるように変更している。例えば、以下のようなdeployment.yamlがあるとする。

# base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.1
        ports:
        - containerPort: 80
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
      - name: nginx
        image: nginx:1.14.3
        ports:
        - containerPort: 80

# base/kustomization.yaml
resources:
  - deployment.yaml

overlaysを以下のようにする。

# overlays/test/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  template:
    spec:
      containers:
        - name: busybox
          image: busybox:1.33
          ports:
            - containerPort: 80

# overlays/test/kustomization.yaml
bases:
  - ../../base

patchesStrategicMerge:
  - deployment.yaml

このとき、buildした結果は以下のようになる。overlaysで指定したコンテナが、 containers に追加されていることがわかる。

$ kustomize build . 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: busybox:1.33
        name: busybox
        ports:
        - containerPort: 80
      - image: nginx:1.14.1
        name: nginx
        ports:
        - containerPort: 80
      - image: nginx:1.14.2
        name: nginx
        ports:
        - containerPort: 80
      - image: nginx:1.14.3
        name: nginx
        ports:
        - containerPort: 80

一方で、これと同じことを patchesJson6902 で行おうとすると、overlaysを以下のようにしなければならない。

# overlays/test/kustomization.yaml
bases:
  - ../../base

patchesJson6902:
  - target:
      group: apps
      version: v1
      kind: Deployment
      name: nginx-deployment
    patch: |- # patchの内容は別ファイルに指定しても良い
      - op: add
        path: /spec/template/spec/containers/0
        value: {"image": "busibox:1.33", "name": "busybox", "ports": [{"containerPort": 80}]}

patch 内の pathJSON pathを指定する必要があり、ここで配列のインデックス番号を指定したうえで、配列に入れる新たな要素を定義する必要がある。 patchesStrategicMerge の場合と同じようにしてしまうと、 containers が丸々入れ替わって busybox のみになってしまう。

patchesJson6902 を使うべきシーン

逆に「配列の中の特定の一要素を入れ替えたい場合」は patchesJson6902 の方を使うべき。

# overlays/test/kustomization.yaml
bases:
  - ../../base

patchesJson6902:
  - target:
      group: apps
      version: v1
      kind: Deployment
      name: nginx-deployment
    patch: |-
      - op: replace
        path: /spec/template/spec/containers/0
        value: {"image": "busibox:1.33", "name": "busybox", "ports": [{"containerPort": 80}]}

opreplace に変更したことで、 containers 配列の0番目の要素のみを入れ替えることができる。結果は以下の通り。

$ kustomize build .  
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: busibox:1.33
        name: busybox
        ports:
        - containerPort: 80
      - image: nginx:1.14.2
        name: nginx
        ports:
        - containerPort: 80
      - image: nginx:1.14.3
        name: nginx
        ports:
        - containerPort: 80

これに対して patchesStrategicMerge の場合、 $patch: replace を指定することで containers 配列全体を入れ替えることはできるが、特定の要素を指定してそこだけ入れ替える機能はない。baseの containers 配列全体をoverlaysに書いてそこに新しい要素を加えるという方法はあるが、配列の要素が多い場合は非効率である。

各Patch方式の詳細な挙動は、以下が参考になる。

community/strategic-merge-patch.md at master · kubernetes/community · GitHub kustomize/inlinePatch.md at master · kubernetes-sigs/kustomize · GitHub

「レガシーコードからの脱却」を読んだ感想

昨年の10-12月まで、社内の輪読会で「レガシーコードからの脱却」を読んだ。(最初間違えて「レガシーコード改善ガイド」を買おうとしたのは内緒だ)

www.oreilly.co.jp

感想など

タイトルの通り、レガシーコードが増えていく企業ソフトウェアの開発ノウハウについて書かれた本。前半はアジャイル開発の方法論がメインで、正直この辺はより詳しいアジャイル本を読んだ方が良いと思う。しかし後半になると、単なるリファクタリングにとどまらない本質的な内容がたくさんあった。

ペアプログラミングの恐怖を乗り越える

過去に何度かペアプログラミングを行ったことがあるが、正直あまり良い印象を持っていなかった。会議室に一日中際限なく拘束されるうえに一挙手一投足を監視され、場の空気が非常に重くなりチームとしての生産性も非常に低かったからだ。
しかし8章を読んで、ペアプログラミングに対する誤解に気付かされた。良いペアプログラミングを行う前提として、「全員が問題を理解していること」そして「全員がメンターでありメンティーであること」が重要となる。ジュニアであっても時には問題解決のためにシニアに助言や意見を言うべきであり、全員が同時に問題解決にあたりディスカッションが自然と起こるような場作りが重要になる。ただ長く集まったり、交代でキーボードを打てば良いというものではない。
それでもペアプログラミングの気が進まないという人のために、本書では代替案も提示している。例えばある小さな問題を調査するために時間を区切って集まるスパイクや、1時間だけ2人が集まりお互いのその日のコードをレビューするバディプログラミングなどがある。大事なのは孤独な作業に徹することではなく、どのような形式であれチームメンバーと協力しあうこと・助けを求めることだ。

CLEANコードの意義

CLEAN原則についてはいろいろな本で目にしており頭では知っていたが、本書の9章を読むことで心で理解できた気がする。5つの原則(凝集性・疎結合カプセル化・断定的・非冗長)は根元では全てつながり合っており、その意義は「結合すべきものは結合し、分離すべきものは分離している状態」を作り出すことにある。こうすることで、レガシーコードを最小限に抑え、ソフトウェアの保守・拡張を容易にすることができる。
また、むやみやたらに責務の分離をさせるのも良くない。12章に書かれているが、「銀行に行く」というTODOを分解して「電車の切符を買う」というような細かすぎるタスクを作る人はいない。個々のクラスの責務が過不足なく意味が通るレベルにすることで、コード全体の抽象度が揃い、責務がわかりやすくなる。

創発設計

テストファーストで実装し、常にレガシーコードにならないように気を配りながら書いていくと、責務の分割や名前などに課題が見つかっていく。これを繰り返していくと、書いている途中でより良い設計が浮かぶ。最初にすべての設計を決めてからそのとおりに実装するよりはるかにましになる。この流れを本書では「創発設計」と呼んでいる。
このやり方は初心者には厳しいものがあるかもしれないが、アジャイル開発を行っている際に仕様や実装が大きくひっくり返る経験は何度もしたことがあるので、それが言語化されてとても共感できた。

既存のアーキテクチャに無慈悲になる

最終章に少しだけ書かれていた言葉だが、非常に刺さった。自分はどうしても「既存の設計を学んで理解する」ことに意識を向けがちだが、そこで終わらず「既存の設計の問題点を理解する」ところまで行かなければ、そもそも改善しようという発想に至らない。レガシーコードを改善するためには、まず目の前にあるものがレガシーコードではないかと認識する必要がある。
改善案も既存の設計をベースにした小さなものばかりではなく、時には全く新しい設計にする方が優れたコードになるかもしれない。そのような改善には新しい知識や経験が必要になることもあり、そのために自分たちは業務内だけでなく業務外でもより多くの技術を身につける必要がある。ここは自分の大きな弱点なので、少しずつ考え方を変えていきたい。

まとめ

真新しい概念は出てこないが、今まで知っていた概念とその意義を腹落ちさせてくれる良書だった。輪読会という形式で1章ずつ精読していったことが功を奏したのかもしれない。時間がかかってもじっくり読みたい本だと思う。

【Terraform】Autoscaling GroupのLaunch Configurationを変更するのに失敗した話

先日、新しいECS ClusterをTerraformで作った。EC2インスタンスは1台のみだが、Launch Configurationを用意してAutoscaling Groupを作成した。

resource "aws_launch_configuration" "ecs_cluster" {
  name                   = "ecs_cluster"
  image_id             = data.aws_ami.aws_optimized_ecs.id
  iam_instance_profile = aws_iam_instance_profile.ecs-cluster.name
  security_groups      = [aws_security_group.ecs-cluster.id]
  user_data            = file("${path.module}/user_data.sh")
  instance_type        = "t3.medium"

  root_block_device {
    delete_on_termination = true
    encrypted             = false
    volume_size           = "30"
    volume_type           = "gp2"
  }
}

しかしその後、key_nameの登録を忘れておりEC2インスタンスssh接続できないことに気づいたため、Launch Configurationを修正した。

 resource "aws_launch_configuration" "ecs_cluster" {
   name                   = "ecs_cluster"
   image_id               = data.aws_ami.aws_optimized_ecs.id
   iam_instance_profile = aws_iam_instance_profile.ecs-cluster.name
   security_groups      = [aws_security_group.ecs-cluster.id]
   user_data            = file("${path.module}/user_data.sh")
   instance_type        = "t3.medium"
+ key_name           = "ecs_cluster"

   root_block_device {
     delete_on_termination = true
     encrypted             = false
     volume_size           = "30"
     volume_type           = "gp2"
   }
 }

ところが、この状態で terraform apply しようとしても失敗してしまった。

Error: error deleting Autoscaling Launch Configuration (terraform-20201203072824459100000004): ResourceInUse: Cannot delete launch configuration terraform-20201203093824531400000001 because it is attached to AutoScalingGroup ecs_cluster

これは、TerraformがLaunch Configurationを一旦削除し、同名のものを新しく作って追加しようとしているため。 terraform plan の結果は Plan: 1 to add, 1 to change, 1 to destroy. となっており、 changeはAutoscaling Groupを、addとdestroyはLaunch Configurationを指している。

これを避けるためには2つの処理が必要となる。まず lifecycle の中で create_before_destroy = true を追加すること。通常であれば、resourceに対するupdateができない場合Terraformは古い方をdestroyしてから新しい方をcreateするが、この指定をすると先に新しいresourceのcreateが行われ、その後古い方をdestroyするようになる。これにより、Autoscaling Groupが起動しているのにLaunch Configurationがなくなってしまうということが起こらない。
もう一つは、 name の指定をやめるか、あるいは name_prefix の指定に変えること。これにより、新旧のLaunch Configurationが異なる名前を持つようになる。この指定を行わないと、同じ名前を持った新旧のresourceが一時的に併存することになってしまい、applyに失敗する可能性がある。

 resource "aws_launch_configuration" "ecs_cluster" {
- name                   = "ecs_cluster"
+ name_prefix            = "ecs_cluster_"   
  image_id             = data.aws_ami.aws_optimized_ecs.id
   iam_instance_profile = aws_iam_instance_profile.ecs-cluster.name
   security_groups      = [aws_security_group.ecs-cluster.id]
   user_data            = file("${path.module}/user_data.sh")
   instance_type        = "t3.medium"
   key_name           = "ecs_cluster"
    
+  lifecycle {
+    create_before_destroy = true
+  }

   root_block_device {
     delete_on_termination = true
     encrypted             = false
     volume_size           = "30"
     volume_type           = "gp2"
   }
 }

このようにすると terraform apply が成功した。

xargsの各オプションにおける引数の渡され方

普段xargsをよく使っているが、いつも -I オプションで引数をループさせる用途であることがほとんどなので、xargsはループ用のコマンドだと思いこんでいた。そのため先日、別の人のシェルスクリプトを読んだときに混乱してしまったのでまとめておく。

オプションなし

そもそもxargsは、標準入力として受け取ったデータをパラメータとして別のコマンドに渡すためのもの。ここでいうパラメータとは、標準入力を空白文字・改行の両方で区切ったものを指す。
デフォルトでは、xargsは受け取った全てのパラメータを1つのコマンドに対する引数として渡す。そのため、xargs以降に指定したコマンドは1回のみ実行される。

echo hoge fuga\\npiyo | xargs echo
hoge fuga piyo # echo hoge fuga piyoが実行された

-I オプション

-I オプションを使うと、標準入力が改行で分割され、コマンド1回の実行につき1行ずつ使用される。そのため、xargs以降に指定したコマンドは、標準入力の行の数だけ実行されるようになる。使い所が多く便利。
for ループとの違いは、引数が1つ渡されるたびにxargs以降のコマンドが実行されること。 for ループの場合、for i in... の部分のコマンドがすべて終了し、繰り返される要素がすべて揃って始めて for ループ内部のコマンドが実行される。

echo hoge fuga\\npiyo | xargs -I {} echo {}
hoge fuga # echo hoge fugaが実行された
piyo # echo piyoが実行された

-n オプション

-n コマンドは、xargs以降に指定したコマンドが、最大いくつのパラメータを使えるかを指定するもの。例えば -n 1 と指定すると、パラメータが1つずつ渡され、xargs以降に指定したコマンドはパラメータの数だけ実行される。 -n 1 の場合、 -I の場合よりもさらに細かい単位で分割されることに注意。

echo hoge fuga\\npiyo | xargs -n 1 echo    
hoge # echo hogeが実行された
fuga # echo fugaが実行された
piyo # echo piyoが実行された

【AWS Lambda】dead_letter_queueからPagerDutyにアラートは飛ばない

本番システムで使われているAWS Lambda functionが失敗した際にPagerDutyでアラート通知を受けたいと思い、開発用のLambda functionにdead_letter_queueを設定し、そのSNS queueにPagerDutyのエンドポイントをsubscribeさせた。
しかし、Lambda functionを失敗させてからいくら待ってもアラートが来ない。確認のため、Slackに通知を投げる別のLambda functionも合わせてsubscribeさせたが、そちらには通知が飛ぶ。

PagerDuty側の原因ではないかと思い調べてみたところ、PagerDutyとAWSのIntegrationで表示されるのはAWS CloudWatchのみだった。PagerDutyは、CloudWatchからSNS queueに送られたメッセージを受信し、そこからAlarm状態を検知することでAlertを飛ばす。しかし、今回は aws lambda invoke コマンドでLambda functionを実行させていたため、dead_letter_queueから送られたメッセージは空だった。これではPagerDutyがAlarm状態だとみなさない。

メッセージの中身を解析して人為的に同じ内容を飛ばすようにしても良いが、そうするくらいであればCloudWatchのAlarmを設定した方が早い。結局dead_letter_queueの使用をやめ、CloudWatchからSNS queue経由でPagerDutyにメッセージを飛ばすように変更した。dead_letter_queueは失敗検知には便利だが、あくまで「実行に失敗したイベント」を拾うためのものであり、アラートの完全な代用にはならない。

ArgoCDを使ってみる

ArgoCDをローカルで動かしてみたいと思い、以前使ったKubernetes講座のmanifestファイルでやってみた。

github.com

まず、以下のチュートリアルのステップ4まで行い、 argocd namespaceでArgoCDのサーバを起動させる。

argoproj.github.io

その後、 argocd namespaceにAppProjectとApplicationのmanifestファイルを作れば、宣言的にArgoCDの設定を定義できる。ローカルのKubernetesクラスタで動かしたので、namespaceはdefaultに設定した。

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: default
  namespace: argocd
spec:
  description: argocd project
  clusterResourceWhitelist:
  - group: '*'
    kind: '*'
  namespaceResourceBlacklist:
  - group: ''
    kind: ResourceQuota
  - group: ''
    kind: LimitRange
  - group: ''
    kind: NetworkPolicy
  destinations:
  - namespace: 'default'
    server: 'https://kubernetes.default.svc'
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: wordpress-challenge
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default

  source:
    repoURL: https://github.com/Udomomo/kubernetes-challenge-1-wordpress
    targetRevision: argocd
    path: app

  destination:
    server: https://kubernetes.default.svc
    namespace: default

これらのファイルをapplyするだけで設定は完了する。レポジトリの argocd ブランチをトラックするように設定したので、後は app ディレクトリ下にあるアプリケーションコードを更新し、syncさせれば設定が反映される。
AppProjectやApplicationの設定を更新したい場合は、当該manifestファイルを更新してapplyし直せば良い。

f:id:Udomomo:20201115235229p:plain

このようにArgoCDを使ってアプリケーションを管理する場合、ArgoCDの管理下のレポジトリにmanifestファイルをプッシュすれば、自分が権限を持っていない種類のresourceでもArgoCDを通してapplyできてしまうことが考えられる。そこで、resourceの種類ごとにブラックリスト/ホワイトリストを設定することも可能。 clusterResourceWhitelist clusterResourceBlacklist namespaceResourceWhitelist namespaceResourceBlacklist の4種類が設定できる。
上記のmanifestファイルでは、 namespaceResourceBlacklist にいくつか指定してみている。試しにLimitRangeのmanifestファイルをプッシュすると、ArgoCDがSyncを拒否して失敗した。

f:id:Udomomo:20201115234923p:plain