Foreverly

メモ帳

Kubernetes2 Advent Calendar 2020 3日目: redis-operatorの導入

この記事は Kubernetes2 Advent Calendar 2020 の 3 日目です。

直前に空きが出ていたので光速で書きました。 枠が勿体無いですからね。がんばりました。

2日目がCRDだったのでOperatorの話にしました。

これはなに?

フルマネージドサービスを使わずにHA構成のRedisをk8s上で動かして運用したい人向け。

HA構成なのでシャーディング使いたい人向けではないです。

redis-operatorは色々ありますがこちらを使います。

master/slave + sentinelsのRedis HAが爆誕します。

この記事の動機

金食い虫のRedisのマネージドサービスからの脱却

みんなで導入して運用の知見を高め隊

導入の流れ

Namespaceはお好きに作成しておいて以下の流れです。

  1. redis-operatorをapply
  2. CRをapply
  3. おしまい!

主な特徴

  • master/slave + sentinelsのRedis HAが爆誕
  • Redisエンドポイントも爆誕
  • Read Replicaのスケールが容易
  • クラスタ内アクセス想定
  • Sentinelなのでシャーディングは使えない

Operatorの復習

Operator知らずにとりあえずインストールしたらどうしたらいいかわからなくなったので、

概念理解に参考になりそうな記事を置いておきます。

OperatorはCRDとCustomControllerを実装したもので、それにCRを食わせると定義したものを爆誕させる奴と理解しました。

Sentinelの復習

そもそもRedisには高可用性の設定以下があって今回はSentinelの話。

  • Replication: マスターとレプリカのレプリケーション
  • Sentinel: 死活監視と自動フェイルオーバーを行ってくれて、ReplicationをHAにしてくれる。
  • Cluster: マルチマスター構成。データを複数サーバに分散するシャーディングでwriteがscaleする奴。

Sentinelはquorum(多数決)を取るので3台以上必要になります。

多数決でフェイルオーバーを行うかどうかを決めます。

ノードのDOWNにも2つあって、自身が検知したSDOWN (Subjectively Down),quorumの数に達したODOWN (Objectively Down)があります。

Operatorのインストール

公式でHelm Chartが用意されているので、それを使うだけでインストールできます。 サービスではhelmfileで入れています。

デフォルトのvalues.yamlではRBACが作られないので rbac.install = trueで専用のRBACを作るようにします。Namespaceも区切りました。

Redisのインストール

RedisFailoverリソースを定義します。

redisはStatefulSet, sentinelsはDeploymentとしてPodが起動します。

rfr-redisrfs-sentinelになります。

kgp -n redis
NAME                                READY   STATUS    RESTARTS   AGE
rfr-redisfailover-0                 1/1     Running   0          141m
rfr-redisfailover-1                 1/1     Running   0          145m
rfr-redisfailover-2                 1/1     Running   0          21h
rfs-redisfailover-7d6869df6-7k5gt   1/1     Running   0          137m
rfs-redisfailover-7d6869df6-82785   1/1     Running   0          3h9m
rfs-redisfailover-7d6869df6-wlxz7   1/1     Running   0          139m

EndpointもClusterIPができあがります。

NAME                TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)     AGE
rfs-redisfailover   ClusterIP   10.36.2.254   <none>        26379/TCP   46h

対マイクロサービス

マイクロサービスでKeyが衝突しないようにデータベースを分けて使います。

設定例

RedisFailoverというCRDを使ってリソースを作成します。

公式で設定例集があり色んなパターンがあるので参考になります。

またcustom-configでredisの設定を書くことも可能です。

  • imageのupdate

updateStrategyはOnDelete typeでした。更新をapplyしたあとにPodが削除される振る舞いでrollingupdateとは違った振る舞いです。

  • 高可用性の設定

Sentinelとredisはそれぞれ同じノードに乗らないようにすることで可用性が高まります。

podAntiAffinityを指定して相乗りしないようにするのがおすすめです。

podAntiAffinityの設定の書き方の場合はこれとか参考になります。

app.kubernetes.io/nameやapp.kubernetes.io/componentはredis-operationで管理されているLabelなのでこれを活用するとよいです。

  • failoverの設定

down-after-milliseconds: Master/SlaveのDOWNN検知後、SDOWNに移行するまでの時間で、デフォルトが5秒です。

failover-timeout: failoverのtimeoutです。(ms)

  • 実行ユーザの変更

root実行ではなく実行ユーザの情報が変更されていることを確認

$ k exec -it rfs-redisfailover-7d6869df6-4nfxl -n redis -- id
uid=1000 gid=1000 groups=1000

全体の設定のsample

sentinel:
    replicas: 3
    image: redis:6.0.9
    resources:
      requests:
        cpu: 100m
        memory: 100Mi
      limits:
        cpu: 100m
        memory: 100Mi
    customConfig:
      - "down-after-milliseconds 2000"
      - "failover-timeout 3000"
    securityContext:
      runAsUser: 1000
      runAsGroup: 1000
      fsGroup: 1000
    affinity:
      podAntiAffinity:
        preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          podAffinityTerm:
            labelSelector:
              matchLabels:
                app.kubernetes.io/name: redisfailover
                app.kubernetes.io/component: sentinel
            topologyKey: kubernetes.io/hostname
  # redis(master/slave) pods
  redis:
    replicas: 3
    image: redis:6.0.9
    resources:
      requests:
        cpu: 100m
        memory: 200Mi
      limits:
        cpu: 100m
        memory: 200Mi
    securityContext:
      runAsUser: 1000
      runAsGroup: 1000
      fsGroup: 1000
    affinity:
      podAntiAffinity:
        preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          podAffinityTerm:
            labelSelector:
              matchLabels:
                app.kubernetes.io/name: redisfailover
                app.kubernetes.io/component: redis
            topologyKey: kubernetes.io/hostname

監視

Datadogでとれるredisのメトリクス一覧はこちら

以下のメトリクスでreplicationとEvictionとメモリ使用量の監視をしました。

他に監視したほうがいいものがあれば

  • redis.replication.last_io_seconds_ago
  • redis.keys.evicted
  • redis.mem.used

障害対応

基本はPod数かmemoryのresourceを増やす

動作確認

まずRedisのMasterでSet/Getできるか確認

$ kubectl exec -ti rfs-redisfailover-7d6869df6-h7fwr -n redis -- redis-cli -h 10.36.2.254 -p 26379
10.36.2.254:26379> SENTINEL get-master-addr-by-name mymaster
1) "10.32.4.8"
2) "6379"
$ kubectl exec -ti rfs-redisfailover-7d6869df6-h7fwr -n redis -- redis-cli -h 10.32.4.8
10.32.4.8:6379> set hello world
OK
10.32.4.8:6379> get hello
"world"

次にフェイルオーバーも確認していきます。

このコマンドで30秒間スリープしてmasterにaccessできないようにします。

masterが何かの理由でhung状態になる状況を想定した動作検証です。

$ kubectl exec -ti rfs-redisfailover-7d6869df6-h7fwr -n redis -- redis-cli -h 10.36.2.254 -p 26379

SENTINEL get-master-addr-by-name <master name> はmasterのipとport番号を名前と一緒に返します。masterが 10.32.4.8 から 10.32.13.4 になったのがわかります。

$ kubectl exec -ti rfs-redisfailover-7d6869df6-h7fwr -n redis -- redis-cli -h 10.36.2.254 -p 26379
10.36.2.254:26379> SENTINEL get-master-addr-by-name mymaster
1) "10.32.4.8"
2) "6379"
10.36.2.254:26379> SENTINEL get-master-addr-by-name mymaster
1) "10.32.13.4"
2) "6379"

ここでsentinelのログからフェイルオーバーの動作を見ていきます。

ログから以下のアクションがわかります。

  1. 各Sentinelはマスタが +sdown イベントでダウンしたことを検知します。
  2. このイベントは後で +odown に昇格されます。それは複数のSentinelがマスタが到達不可能である事実に同意したことを意味します。
  3. Sentinel は最初フェイルオーバーの試行を開始するSentinelに投票します。
  4. フェイルオーバーが発生します。

sentinelのログ

rfs-redisfailover-7d6869df6-4nfxl sentinel 1:X 25 Nov 2020 06:45:10.831 # +set master mymaster 10.32.4.8 6379 failover-timeout 3000
rfs-redisfailover-7d6869df6-4nfxl sentinel 1:X 25 Nov 2020 06:45:25.503 # +new-epoch 10
rfs-redisfailover-7d6869df6-4nfxl sentinel 1:X 25 Nov 2020 06:45:25.508 # +vote-for-leader 5051f1baca46e38724a816c720439f2960ff00e8 10
rfs-redisfailover-7d6869df6-4nfxl sentinel 1:X 25 Nov 2020 06:45:25.623 # +sdown master mymaster 10.32.4.8 6379
rfs-redisfailover-7d6869df6-4nfxl sentinel 1:X 25 Nov 2020 06:45:25.678 # +odown master mymaster 10.32.4.8 6379 #quorum 3/2
rfs-redisfailover-7d6869df6-4nfxl sentinel 1:X 25 Nov 2020 06:45:25.678 # Next failover delay: I will not start a failover before Wed Nov 25 06
:45:32 2020
rfs-redisfailover-7d6869df6-4nfxl sentinel 1:X 25 Nov 2020 06:45:26.631 # +config-update-from sentinel 5051f1baca46e38724a816c720439f2960ff00e8
 10.32.1.9 26379 @ mymaster 10.32.4.8 6379
rfs-redisfailover-7d6869df6-4nfxl sentinel 1:X 25 Nov 2020 06:45:26.631 # +switch-master mymaster 10.32.4.8 6379 10.32.13.4 6379
rfs-redisfailover-7d6869df6-4nfxl sentinel 1:X 25 Nov 2020 06:45:26.631 * +slave slave 10.32.7.11:6379 10.32.7.11 6379 @ mymaster 10.32.13.4 63
79

先程書き込めた 10.32.4.8 がリードレプリカとなって書き込めないのも確認できます。

10.32.4.8:6379> set hey you
(error) READONLY You can't write against a read only replica.

プリエンプティブ(スポット)インスタンスを使用している場合

いつノードが落ちるかわからないのでプリエンプティブ(スポット)インスタンス上では起動させたくない場合はNodeAffinityを導入するとよいです。GKEの場合は、requiredDuringSchedulingIgnoredDuringExecutionで cloud.google.com/gke-preeptibleラベルが表示されるノードにはPodがスケジュールされないようにします。

  • requiredDuringSchedulingIgnoredDuringExecution
affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
      - matchExpressions:
        - key: cloud.google.com/gke-preemptible
          operator: DoesNotExist

また、preferredDuringSchedulingIgnoredDuringExecutionで cloud.google.com/gke-preemptible ラベルが表示されるノードに可能ならスケジュール、そうでなければ他にスケジュールするといった設定も可能です。

  • preferredDuringSchedulingIgnoredDuringExecution
affinity:
  nodeAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - preference:
        matchExpressions:
        - key: cloud.google.com/gke-preemptible
          operator: Exists
      weight: 100

以上がredis-operatorの導入になります。 その他にこうした方がいいなどありましたらブログのコメントにください。

明日の Kubernetes2 Advent Calendar 2020 4日目は @iaoiui さんの「KubeCon NA参加してみたのでKubernetesの今後を予測してみる」です!お楽しみに!

参考

Redisのドキュメント

Sentinelのドキュメント

Redisのレプリケーションのドキュメント

redis-cli、Redisコマンドライン インタフェース

Kubernates上でKVSをマネージドっぽく使いたい!

spotahome/redis-operator

ステートフル ワークロードを実行する GKE クラスタのアップグレード

ElasticCacheのイベント通知をSlackに投げ隊

これはなに?

ElasticCacheのイベント通知をSlackに通知する奴

どうして通知するんですか?

他のプロジェクトでDB周りのアラートが発報して確認したらイベント通知にメンテで再起動されていた。

イベント情報を検知できるようにイベント通知をしたくなった。

構成

シンプルにElasticCache→SNS→Lambda→Slack

ElasticCacheのイベント通知先をSNSのトピックにして、サブスクライバーであるLambdaに対して投げつける。ログはCloudWatch Logsに投げる。Lambda FunctionはPythonでSlack通知させました。

ここの一通りの設定を見ていきます。

f:id:oza__shu:20201113191816p:plain

Lambdaで使用するので、Webhook URLを作成

名前と通知先とかわいい画像を指定して作成してください。

SNS通知先の設定

これはElasticCacheの設定でSNSのarnを渡すだけ。

KMSのkeyを作成

keyを作成したら使うのにaliasも必要になるので準備すればOK

SNSでTopicとサブスクライバーの設定

Topicとサブスクリプションを作成します。

Topicは作成したら以下のポリシーを当てるぐらい

statement {
    sid    = "LambdaPublish"
    effect = "Allow"
    principals {
      type        = "AWS"
      identifiers = ["*"]
    }
    actions = [
      "SNS:GetTopicAttributes",
      "SNS:Publish"
    ]
    resources = [
      "arn:aws:sns:ap-northeast-1:${data.aws_caller_identity.self.account_id}:topic",
    ]
  }
}

サブスクリプションはLambdaに送るので送り先のARNをプロトコルでLambdaを指定すればOK.

メッセージ送信をテストで実行してSNSとLambda間で疎通が取れるかテスト可能です。

次はLambdaを用意していきます。

Lambdaに必要なIAMポリシー

Lambda用のRoleが必要なので作成します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow"
    }
  ]
}

Webhook URLはKMSで暗号化して渡すので、解読させるためにKMSのDecryptの権限ポリシーが必要です。

{
    "Version": "2012-10-17",
    "Statement": [
      {
          "Effect": "Allow",
          "Action": "kms:Decrypt",
          "Resource": "arn:aws:kms:ap-northeast-1:${data.aws_caller_identity.self.account_id}:key/${aws_kms_alias.kms_alias.target_key_id}"
      }
    ]
 }

作成したロールに上のと下2つのポリシーをアタッチすればOK

  • CloudWatchReadOnlyAccess
  • AWSLambdaBasicExecutionRole

SlackのhookのURLをKMSで暗号化

Terraformではできなかったので、手動で暗号化をして、 それをTerraformでLambdaの関数に読ませました。

暗号化したURLは次の手順で作成

  1. 転送時の暗号化に使用するヘルパーの有効化のチェックボックスをチェック
  2. 保管時に暗号化する AWS KMS キーの選択で、作った暗号化キーを選択
  3. kmsEncryptedHookUrlのvalueに、SlackのWebhookのURLを入れる
  4. 暗号化ボタンを押下
  5. 暗号化完了

暗号化したらLambdaの kmsEncryptedHookUrl 環境変数に渡します。

Lambdaでイベント通知の絞り込み

イベント内容で絞り込みしないと毎日のsnapshotのイベントとかで検知してしまうので、

それでイベント通知は以下の2つに絞り込みました。

ElastiCache:FailoverComplete
ElastiCache:CacheNodeReplaceComplete

スクリプトは以下のように暗号化したslackのURLとチャンネル名を渡して、

イベント通知に一致したらslack通知する内容です。

import boto3
import json
import logging
import os

from base64 import b64decode
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError

# The base-64 encoded, encrypted key (CiphertextBlob) stored in the kmsEncryptedHookUrl environment variable
ENCRYPTED_HOOK_URL = os.environ['kmsEncryptedHookUrl']
# The Slack channel to send a message to stored in the slackChannel environment variable
SLACK_CHANNEL = os.environ['slackChannel']

HOOK_URL = "https://hooks.slack.com" + boto3.client('kms').decrypt(
    CiphertextBlob=b64decode(ENCRYPTED_HOOK_URL),
    EncryptionContext={
        'LambdaFunctionName': os.environ['AWS_LAMBDA_FUNCTION_NAME']}
)['Plaintext'].decode('utf-8')

logger = logging.getLogger()
logger.setLevel(logging.INFO)

NOTIFICATION_EVENT_TYPE = [
    'ElastiCache:FailoverComplete',
    'ElastiCache:CacheNodeReplaceComplete',
]

def lambda_handler(event, context):
    logger.info("Event: " + str(event))
    print(event['Records'][0]['Sns']['Message'])
    message = json.loads(event['Records'][0]['Sns']['Message'])
    event_time = event['Records'][0]['Sns']['Timestamp']

    # イベントタイプがNOTIFICATION_EVENT_TYPEに含まれない場合は処理を終了
    event_set = set(message.keys())
    notification_event_type_set = set(NOTIFICATION_EVENT_TYPE)
    event_type = event_set & notification_event_type_set
    if not event_type:
        return

    logger.info("Message: " + str(message))

    slack_message = {
        'channel': SLACK_CHANNEL,
        'text': "%s ElastiCache Notification Message: %s" % (event_time, message)
    }

    req = Request(HOOK_URL, json.dumps(slack_message).encode('utf-8'))
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted to %s", slack_message['channel'])
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)

動作確認

Elasticache の SNS 通知の設定について、 Test Failover API 実行時には、 ElastiCache:FailoverComplete 等のイベントが発報されるので、 コンソール上でフェイルオーバーを実施。

通知完了!!

参考URL

KMSを使用してキーを暗号化&復号する手順

TerraformでIAMポリシーのJSONに変数を埋めたい場合はaws_iam_policy_documentを使う

Terraformでテンプレートを使ってポリシーを定義する

terraformからroleにpolicyをattachするときの話

*.tf 内で AWS アカウント ID を自動参照(取得)する aws_caller_identity Data Source)

キー ID と ARN を検索する

Amazon SNS のアクション、リソース、および条件キー

AWS Lambdaを使ったAmazon SNSへのメッセージ送受信

Terraformで構築するAmazon SNSからAWS Lambdaを呼び出すためのトリガ

【AWS】CloudWatchアラーム通知をLambdaでSlack投稿する

AWSの各種アラートをSlackで受け取る

AWS のリソースを監視して Slack に通知する方法 (または cloudwatch-alarm-to-slack の使い方)

ElastiCacheのイベント通知をLambdaを使ってフィルタしてみた

AWS CloudWatchからSlackへ通知する

ElastiCache イベントの表示

helm2to3

これはなに

helm v2からv3にマイグレするやつです。 v2のサポート期間は2020年11月13日までなので即対応必須奴。

Helmの新しいメジャーリリースへのアップグレードで最も重要な部分の1つは、データの移行で、やるにはhelm-2to3プラグインをつかうのがよい。

このプラグインは、以下をしてくれる奴

  • Helm v2の設定の移行
  • Helm v2リリースの移行
  • Helm v2の設定、リリースデータ、Tillerのデプロイメントのクリーンアップ。

やることリスト

  • [ ] helm3のインストール
  • [ ] helm2to3 pluginのインストール
  • [ ] v2のデータをバックアップ
  • [ ] Helm v2の設定を移行
  • [ ] Helm v2のReleaseを移行
  • [ ] Helm v2データをクリーンアップ(Helm v3が期待通りにHelm v2データを管理していることを確認してから)
  • [ ] cicd周りの修正
  • [ ] terraformとかでk8sクラスターでtillerとか入れてたら削除
  • [ ] helm-diff使ってたら最新にあげたほうがいいです。

事前準備

Helm v2と v3 のclientのセットアップ

以下の例みたいにv2とv3のCLIを用意

  • brewで入れたhelm3.3.0(2020/11/12現在は3.4.0が最新)
    • helm (pathが通っている)
  • バイナリでいれたhelm2.14.2
$ helm version
version.BuildInfo{Version:"v3.3.0",GoVersion:"go1.14.6"}


$ helm version
Client: &version.Version{SemVer:"v2.14.2"}
Server: &version.Version{SemVer:"v2.14.2"}

helm-2to3 プラグイン

現在プラグインでは以下のことができる。

  • Helm v2設定の移行
  • Helm v2リリースの移行
  • Helm v2の設定、リリースデータ、Tillerの配置のクリーンアップ
$ helm 2to3 version
Migrate and Cleanup Helm v2 configuration and releases in-place to Helm v3

v2のデータをバックアップと差し戻し手順

Tiller はリリース情報などをすべて Kubernetes ConfigMap オブジェクトに保存しているので、これをバックアップしておけばv2に差し戻しが可能。 あとhelm v2のコマンドとフォルダを保存しておく。

  1. 一覧を出す これらの最新のconfigをバックアップしていく。
$ kubectl get configmap -n kube-system -l "OWNER=TILLER" |cut -d. -f1 |uniq -c|awk '{print $2}'
NAME
hoge-namespace
  1. 最新のconfigmapを保存しておく
kubectl get configmap -n kube-system -o yaml hogehoge-app.v17 > hogehoge-app.v17.yaml

差し戻しのapplyは以下コマンドのを適宜変更して使う。

 kubectl apply -f hogehoge-app.v17.yaml -n kube-system

手順

Helm v2の設定を移行する(localなので不要なら実行しなくてもOK)

まずはHelm v2の設定フォルダとデータフォルダを移行します。 以下を移行する。

  • Chart starters
  • Repositories
  • Plugins

まずは --dry-run をする。

$ helm 2to3 move config --dry-run

実際に移行

$ helm 2to3 move config

WARNING: Helm v3 configuration maybe overwritten during this operation.

[Move Config/confirm] Are you sure you want to move the v2 configration? [y/N]: y

2019/11/14 14:55:00 Helm v2 configuration will be moved to Helm v3 configration.
...
2019/11/14 14:55:00 Helm v2 configuration was moved successfully to Helm v3 configration.

helm v3 のrepo listを実行して確認

$ helm repo list

NAME        URL
stable      <https://kubernetes-charts.storage.googleapis.com>
jfrog       <https://charts.jfrog.io>
rimusz      <https://charts.rimusz.net>
buildkite   <https://buildkite.github.io/charts>
jetstack    <https://charts.jetstack.io>
odavid      <https://odavid.github.io/k8s-helm-charts>
elastic     <https://helm.elastic.co>
appscode    <https://charts.appscode.com/stable>

$ helm plugin list

NAME    VERSION DESCRIPTION
2to3    0.1.0   migrate Helm v2 configuration and releases in-place to Helm v3
edit    0.3.0   Edit a release.
gcs     0.2.0   Provides Google Cloud Storage protocol support.
                <https://github.com/vigles>...
linter  0.1.1   Helm plugin to find hardcoded passwords in values.yaml files
monitor 0.3.0   Query at a given interval a Prometheus, ElasticSearch or Sentry instance...

Helm v2 と同じ Helm リポジトリプラグインが使えるようになっていればOK.

※ Helm v2のプラグインがすべてHelm v3で正常に動作することを確認し、動作しないプラグインは削除すること。 move configは、Helm v3のconfigとdataフォルダが存在しない場合は作成し、存在する場合はrepositories.yamlファイルを上書きする。

このプラグインは、デフォルトではないHelm v2 homeとHelm v3の設定とデータフォルダもサポートする。

$ export HELM_V2_HOME=$HOME/.helm2
$ export HELM_V3_CONFIG=$HOME/.helm3
$ export HELM_V3_DATA=$PWD/.helm3
$ helm3 2to3 move config

Helm v2 リリースの移行

リリースの移行を開始する準備ができたので移行テスト。

listで対象を確認

$ helm list

まずは --dry-run

$ helm 2to3 convert --dry-run hoge-realease

では、実際に移行を実行してみましょう。

$ helm 2to3 convert hoge-realease

一撃ワンライナー

~/bin/helm2/helm ls --output json | jq -r '.Releases[] | select(.Namespace != "default" and .Namespace != "hoge-namespace") | .Name' | xargs -t -I {} helm 2to3 convert {}

移行が成功したかどうかをチェックしてみましょう。 v2とv3でlistで確認

(v2) helm list


$ helm list -n hoge_namespaces

※ --delete-v2-releasesフラグを指定していないので、Helm v2 のリリース情報がそのまま残るが、 後からhelmの2to3クリーンアップで削除することができる。

すべてのリリースを移動する準備ができたら、hellmリストをループで実行し、hellm v2の各リリースに対してhellm 2to3の変換RELEASEを適用することで、 自動化することができる。

Helm v2データのクリーンアップ

最後のステップは、古いデータのクリーンアップ。 以下がクリーンアップされる

  • Configuration (Helm home directory)
  • v2 release data
  • Tiller deployment

まずは --dry-run

どのリリースが削除されるか、kube-system名前空間からTillerサービスが削除されるか、Helm v2のホームフォルダが削除されるかが表示される。

$ helm 2to3 cleanup --dry-run

2019/11/14 15:06:59 NOTE: This is in dry-run mode, the following actions will not be executed.
2019/11/14 15:06:59 Run without --dry-run to take the actions described below:
2019/11/14 15:06:59
WARNING: "Helm v2 Configuration" "Release Data" "Release Data" will be removed.
This will clean up all releases managed by Helm v2. It will not be possible to restore them if you haven't made a backup of the releases.
Helm v2 may not be usable afterwards.

[Cleanup/confirm] Are you sure you want to cleanup Helm v2 data? [y/N]: y
2019/11/14 15:07:01
Helm v2 data will be cleaned up.
2019/11/14 15:07:01 [Helm 2] Releases will be deleted.
2019/11/14 15:07:01 [Helm 2] ReleaseVersion "postgres.v1" will be deleted.
2019/11/14 15:07:01 [Helm 2] ReleaseVersion "redis.v1" will be deleted.
2019/11/14 15:07:01 [Helm 2] Home folder "/Users/rimasm/.helm" will be deleted.

Hem v2 のデータをクリーンアップする準備ができたら、--dry-run フラグを指定せずにコマンドを実行。

$ helm 2to3 cleanup --dry-run

ci/cd周りの修正

helmとかもろもろversion upする

terraformの修正

helmのupgradeとtillerの削除 ここらへん↓を修正

https://ghe.ca-tools.org/odessa/infrastructure/blob/394cc738b716d2d6b332d8b2ff6065f9a762f2bc/gcp/env/common/gke/locals.tf#L2-L96

helm-diffプラグインのupgrade

installシェルでhelm homeというコマンドが使われているけど、helm v3ではhelm homeが消えてインストールできなかったが、helm-diffのバージョンを最新にしたところインストールできました。

参考

How to migrate from Helm v2 to Helm v3helm-2to3プラグインhelm v3.3.0helm2からhelm3に(削除履歴も含めて)マイグレーションできるのか試してみたHelm_v2->v3_移行How Helm Uses ConfigMaps to Store Datahelm2からhelm3に(削除履歴も含めて)マイグレーションできるのか試してみた

Terraform0.13へのUpgrade

Terraform0.13にUpgradeの手順と詰まったPointをメモ

事前準備

こんなアドバイスを受けていたので、先にproviderのバージョンをあげます。

古い環境からterraform v0.13.0+aws providerv3に一気に上げるとtfstateが不整合っぽいエラーでまくるので、
terraformv0.12.x+aws provider v3で一度apply通してから(何のリソースでもok)だとエラーなくなるので、その順番で0.13に上げると良さそうです

手順

各providerも0.13に対応させる terraform 0.13upgradeを各ステートに実行 terraform init,applyを実行して通ればOK

詰まったところ

  1. providerが読み込めないと怒られる

workspaceごとにterraform init,applyをしてstateを最新にしたら突破できました。

Error: Could not load plugin


Plugin reinitialization required. Please run "terraform init".

Plugins are external binaries that Terraform uses to access and manipulate
resources. The configuration provided requires plugins which can't be located,
don't satisfy the version constraints, or are otherwise incompatible.

Terraform automatically discovers provider requirements from your
configuration, including providers used in child modules. To see the
requirements and constraints, run "terraform providers".

7 problems:

- Failed to instantiate provider "registry.terraform.io/-/archive" to obtain
schema: unknown provider "registry.terraform.io/-/archive"
- Failed to instantiate provider "registry.terraform.io/-/aws" to obtain
schema: unknown provider "registry.terraform.io/-/aws"

以下のように直せばOK

export AWS_ACCOUNT_ID=${AWS_ACCOUNT_ID}; eval $(aws sts assume-role --role-arn arn:aws:iam::${AWS_ACCOUNT_ID}:role/AdminAssumeRole --role-session-name "terraform_session" --output json | jq -r '.Credentials|"export AWS_ACCESS_KEY_ID="+.AccessKeyId+"\nexport AWS_SECRET_ACCESS_KEY="+.SecretAccessKey+"\nexport AWS_SESSION_TOKEN="+.SessionToken')
terraform init -backend-config "key=env-terraform.tfstate" 
terraform get                                                                                                                                                                                                  
terraform workspace select env
terraform state replace-provider 'registry.terraform.io/-/archive' 'registry.terraform.io/hashicorp/archive' -auto-approve 
terraform state replace-provider 'registry.terraform.io/-/aws' 'registry.terraform.io/hashicorp/aws' -auto-approve 
terraform state replace-provider 'registry.terraform.io/-/kubernetes' 'registry.terraform.io/hashicorp/kubernetes' -auto-approve 
terraform state replace-provider 'registry.terraform.io/-/local' 'registry.terraform.io/hashicorp/local' -auto-approve 
terraform state replace-provider 'registry.terraform.io/-/null' 'registry.terraform.io/hashicorp/null' -auto-approve 
terraform state replace-provider 'registry.terraform.io/-/random' 'registry.terraform.io/hashicorp/random' -auto-approve 
terraform state replace-provider 'registry.terraform.io/-/template' 'registry.terraform.io/hashicorp/template' -auto-approve 
  1. regionを指定しろと怒られる
provider.aws.region
  The region where AWS operations will take place. Examples
  are us-east-1, us-west-2, etc.
  Enter a value: 
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

providerのregionの指定の仕方が変わっていたこれを参考に書き直しました。

参考手順

Upgrading to Terraform v0.13 Terraform AWS Provider Version 3 Upgrade Guide Terraform Google Provider 3.0.0 Upgrade Guide

CNDT2020で発表してきた

CloudNativeDays2020で以下のタイトルで発表してきました。

Amebaアフィリエイト基盤のGKEアーキテクチャとマイクロサービス | CloudNative Days Tokyo 2020

会社のスポンサー枠ということで40分の発表枠を頂きました。 無論40分話せる自信がないので、サーバサイドの @youta1119 君に無理言って一緒に発表してもらいました。

資料はこちら

会社の名前を出しての発表とオンラインの発表が共に初なので、結構発表を準備して望みました。 オンラインの発表とか大変そう〜とか思っていたぐらいなので、発表することになると決まってすぐにマイクをポチりました。

今まで運用構築請負業務をやってきたので、 AmebaPickは自分が担当した自社サービスということで 割と思い入れのあるサービスなので発表する機会を頂けたのは幸運でした。 社内のKubernates自信ニキが多数いて、色々構成を相談してできたものなので、 世の中に公開できて、供養できてのは本当によかったです。

発表の元ネタ的にはいくつかの会社の発表資料を参考にしたりしてたりします。 伝えたかったこととしては、システムの価値を維持・向上し続けていくぞ!でした。 あえて言わなかったこともあるし、もっと言いたかったこともありますので、 今後も継続的にシステム改善できたら、どっかで言いたいです。

発表内容の後半のサービスや開発面についても語れるようになればSREにジョブチェンジできるのかもしれないなあと思って、語れるようにならんとなあというお気持ちでいっぱいです。

ほなまた

sendgridのeventdataをS3に送るやつです

構成

SendGridのエラーをS3に格納するfunctionを実行する構成です。

f:id:oza__shu:20200717200041p:plain

各サービスの役割とポイントとなる設定についてみていきましょう。 S3→Lambda→APIGateway→SendGridの順にみていきましょう〜〜〜

S3

  • eventdataの保存先にS3を使用します。bucketを用意してlifecycleで保存期間を設定して終わりです。

AWS Lambda

  • API Gatewayで受けたeventdataをS3に保存するために、AWS Lambdaを使います。公式ドキュメントをみるとイベントデータはJSON配列で送信されるようです。今回はPython3でfunctionを書きました。forで回して1件ずつ処理してS3に格納していきます。内容をざっと見ると、timestampのunixstampをJSTに変換しログ名にもいれて、dict型からstr型bytes型に変換しioモジュールでファイルに書き出し、それをboto3でアップロードさせました。Lambda関数のコード抜粋を載せます。
# configure with env vars
BUCKET_NAME = os.environ['LOG_S3_BUCKET']

def put_to_s3(data: dict, bucket: str, key: str):
    xray_recorder.begin_subsegment('s3 upload')
    strdata = json.dumps(data)
    bindata = strdata.encode()
    try:
        with io.BytesIO(bindata) as data_fileobj:
            s3_results = s3.upload_fileobj(data_fileobj, bucket, key)

        logger.info(f"S3 upload errors: {s3_results}")

    except S3UploadFailedError as e:
        logger.error("Upload failed. Error:")
        logger.error(e)
        import traceback
        traceback.print_stack()
        raise
    xray_recorder.end_subsegment()

def handler(event, context):
    logger.info(event)
    data_list = event['body']
    data_dicts = json.loads(data_list)
    logger.info(data_dicts)
    for _, data in enumerate(data_dicts):
        unix_timestamp = data['timestamp']
        jst_time = datetime.fromtimestamp(unix_timestamp)
        key = data['event'] + "/" + jst_time.strftime("%Y-%m/%d/%H/%Y-%m-%d-%H:%M:%S-") + "-" + data['sg_event_id'] + ".log"
        put_to_s3(data, BUCKET_NAME, key)

eventに何が受け取るのか最初わからずハマりました。文字列がきていたのでjson.loadsでdictに変更して値がとれるようになりました。 Lambda関数には、S3へのUpload権限を与えるのを忘れないようにしてください。

公式ドキュメントのここらへんが参考になりそう。

functionのzip化はarchive_fileを使うでもいいと思います。

Pythonのコードはとりあえず、ここらへんから参考に育てていきました。あとここらへんとかも。

API Gateway

  • Event WebhookのHTTPリクエストをLambda Proxyを経由してAPI Gatewayで受けます。API Gatewayはリソースを作成後、ANYメソッドを作成します。プロキシリソースとの Lambda プロキシ統合を参考に設定します。設定内容としては、greedy パス変数 {proxy+} を使用してプロキシリソースを作成します。そしてプロキシリソースに ANY メソッドを設定します。API Gateway REST APIAWS_PROXYで指定されるLambda プロキシ統合は、バックエンドの Lambda 関数と統合するために使用します。Lambda プロキシ統合を使うと何がよいかなのですが、API Gatewayがリクエストとレスポンスのマッピング設定をよしなに設定してくれるので、マッピングテンプレートを書かなくなることです。以上でAPI Gatewayの設定は完了です。最後にAPIをterraformでapplyしてデプロイしてAPI Gatewayの設定は完了です。こっちのドキュメントも参照した方がいいと思います。 この記事も見やすかったです。

SendGrid

  • Event WebhookはSendGridダッシュボードの「Settings > Mail Settings > Event Notification」で設定します。HTTP POST URLAPI GatewayのエンドポイントのURLを設定します。SELECT ACTIONSで受け取りたいイベントのチェックボックスをONにして設定を保存します。今回はエラーを受け取りたいのでDropped, Deffered, Bouncedに✅を入れます。Event Notification設定画面で「Test Your Integration」ボタンを選択して、S3にテストデータが保存されていることが確認できれば設定は完了です。ここらへんに書いてあります。

まとめ

いかがでしたか?SendgridのeventdataをAWSを利用して保存する方法をみてきました。S3は耐障害性が高く、Lifecycleでログ保存期間についても簡単に設定することができます。今回の構成をeventdataの保存を考える際に参考にしてみてください。

datadogのアラート通知先を環境毎に変えたい

環境毎にアラート項目を作り分けてて面倒くさかったので、一つの監視で複数環境を監視できるようにした。 環境毎に閾値を変えたいという要望は捨て置く。

datadogでタグを使って1モニターから複数の通知先を出し分け設定する こちらを参考にした。

ここでの想定は

  • envタグとかenviromentタグにprd,dev,stg.sbx,shd,lrd...などなど付いている
  • slackチャンネルが hoge_alert 部屋と hoge_dev_alert 部屋があり、slackのwebhookのurlをdatadogのslackのintegrationで設定し、通知可能であることです。

slackが落ちた時に大丈夫なように、prdのcriticalだけMail通知もすると安全そう

以下のように書けば、一つの監視で通知先を分けれるし、ステータス毎にメンションも分けられるのでおすすめです。

{{#is_alert}}  
Criticalだよ!
{{#is_match "env" "prd"}}  <!channel>  @slack-Slack_Account_Hook_PRD-hoge_alert {{/is_match}}
{{^is_match "env" "prd"}}  <!here>  @slack-Slack_Account_Hook_DEV-hoge_dev_alert  {{/is_match}}
{{/is_alert}}


{{#is_warning}}
WARNINGだよ。 
{{#is_match "env" "prd"}} <!here> @slack-Slack_Account_Hook_PRD-hoge_alert {{/is_match}}
{{^is_match "env" "prd"}} @slack-Slack_Account_Hook_DEV-hoge_dev_alert  {{/is_match}}
{{/is_warning}} 

{{#is_no_data}}
NoDataだよ!
{{#is_match "env" "prd"}} <!here>  @slack-Slack_Account_Hook_PRD-hoge_alert {{/is_match}}
{{^is_match "env" "prd"}} @slack-Slack_Account_Hook_DEV-hoge_dev_alert  {{/is_match}}
{{/is_no_data}} 

{{#is_recovery}}
復旧したよ!
{{#is_match "env" "prd"}} @slack-Slack_Account_Hook_PRD-hoge_alert {{/is_match}}
{{^is_match "env" "prd"}} @slack-Slack_Account_Hook_DEV-hoge_dev_alert  {{/is_match}}
{{/is_recovery}}