zaki work log

作業ログやら生活ログやらなんやら

オフライン環境へのK3sインストール

いろんなものを業務でオフライン動作させてきた身としては一度はやっておきたかったインターネット接続の無いオフライン環境へのインストールお試し。
K3sは軽量Kubernetesとして様々な環境で動作するのが特徴の一つで、インターネット接続の無い環境でもAir-Gap Installとしてドキュメントに手順が載っているので、比較的(過去に実施したOCP3.x/4.xとかに比べれば…)容易に構築可能。

docs.k3s.io

構成

ホストOSのネットワーク設定として、デフォルトルートの設定がされている必要がある。これはデフォルトルートが設定されているNICIPアドレスに対してクラスターが構成されるため。

$ ip route
default via 172.29.0.1 dev eth0 proto static
172.29.0.0/24 dev eth0 proto kernel scope link src 172.29.0.77

また、(RHELディストリビューションで)SELinuxが有効の場合は、k3s-selinuxパッケージも追加で必要なので別途インストールしておく。(本エントリでは未確認)

イメージの準備

K3sがデフォルトで使用するイメージは、K3sのバージョンによって固定されておりリリースページで確認可能。今回のv1.29.2で使用するイメージもGitHubのリリースページに一覧が記載されているのでそこから取得。
これはオンライン環境でDLし、プライベートのコンテナレジストリpushする。

$ curl -LO https://github.com/k3s-io/k3s/releases/download/v1.29.2%2Bk3s1/k3s-airgap-images-amd64.tar.zst
$ podman image load -i k3s-airgap-images-amd64.tar.zst

[...]

Loaded image: localhost/rancher/mirrored-library-traefik:2.10.5
Loaded image: localhost/rancher/mirrored-metrics-server:v0.6.3
Loaded image: localhost/rancher/mirrored-pause:3.6
Loaded image: localhost/rancher/klipper-helm:v0.8.2-build20230815
Loaded image: localhost/rancher/klipper-lb:v0.4.5
Loaded image: localhost/rancher/local-path-provisioner:v0.0.26
Loaded image: localhost/rancher/mirrored-coredns-coredns:1.10.1
Loaded image: localhost/rancher/mirrored-library-busybox:1.36.1

展開されたこれらのイメージをすべてプライベートレジストリへpushする。 (コンテナ名・タグ名は変更しない)

skopeo copy containers-storage:localhost/rancher/local-path-provisioner:v0.0.26 docker://gitlab.example.jp:25000/zaki/images/local-path-provisioner:v0.0.26
...

まとめるとこんな感じだったけど、、8個だしコマンドラインを生成して実行した方が楽かな?

$ podman image list localhost/rancher/* -n | while read line; do src=$(echo $line | awk '{print $1":"$2}'); echo $src; dst=$(echo $src | sed 's#localhost/rancher#gitlab.example.jp:25000/zaki/images#'); skopeo copy containers-storage:$src docker://$dst --dest-tls-verify=false; done
localhost/rancher/mirrored-library-busybox:1.36.1

skopeo copy tarball:/path/to/k3s-airgap-images-amd64.tar.zstだと複数リポジトリにpushしてくれず、うまいやり方わからなかったので1つずつやってる…

プライベートレジストリのリダイレクト設定

通常通りのインストールを行うと、K3sのデプロイ時に使用するイメージはdocker.ioからpullしようとして失敗する。そこでdocker.ioへのアクセスをプライベートレジストリへリダイレクトする設定を事前に行う。

インストールするノードに/etc/rancher/k3s/registries.yamlを作成する。

mirrors:
  docker.io:
    endpoint:
      - "https://gitlab.example.jp:25000"
    rewrite:
      "^rancher/(.*)": "zaki/images/$1"
configs:
  "gitlab.example.jp:25000":
    tls:
      insecure_skip_verify: true

これはプライベートレジストリのホストがオレオレ証明書設定のhttps://gitlab.example.jp:25000にあり、イメージをpushするリポジトリzaki/images/*を使用している構成の場合。
もう少し具体的に言うと、GitLabのコンテナレジストリ機能を使って、ユーザーzakiimagesプロジェクトのリポジトリを使う場合で、K3sで使用するイメージ(rancher/*)を全てここへpushしている構成。

ちなみにendpointhttpsから書けるので、非SSLのコンテナレジストリでもhttp://から記述すれば使用可能。

docker.iorancher以外のリポジトリを追加するなら以下のように加えていく。

mirrors:
  docker.io:
    endpoint:
      - "https://gitlab.example.jp:25000"
    rewrite:
      "^rancher/(.*)": "zaki/images/$1"
      "^library/(.*)": "zaki/images/$1"

docker.io以外のレジストリのリダイレクト設定は追加する場合は以下の通り。

mirrors:
  docker.io:
    endpoint:
      - "https://gitlab.example.jp:25000"
    rewrite:
      "^rancher/(.*)": "zaki/images/$1"
      "^library/(.*)": "zaki/images/$1"
  gcr.io:
    endpoint:
      - "https://gitlab.example.jp:25000"
    rewrite:
      "^kubebuilder/(.*)": "zaki/images/$1"

リダイレクト設定はすべてのノードに必要。

インストールスクリプトの配置

https://get.k3s.io

通常インストール時にcurl -sfL https://get.k3s.io | sh -するときに使用するhttps://get.k3s.ioスクリプトをローカルに配置する。

online$ curl -L https://get.k3s.io -o install.sh
online$ scp install.sh  k3s-offline1:~

実行バイナリの配置

対象バージョンのK3s実行バイナリを取得。

online$ curl -LO https://github.com/k3s-io/k3s/releases/download/v1.29.2%2Bk3s1/k3s
online$ scp k3s k3s-offline1:~

インストール対象ノードで/usr/local/binへ配置。

$ chmod 755 k3s
$ sudo cp k3s /usr/local/bin/
$ k3s --version
k3s version v1.29.2+k3s1 (86f10213)
go version go1.21.7

インストール

イメージ・バイナリ・インストールスクリプトの準備ができたら、あとはインストールスクリプトINSTALL_K3S_SKIP_DOWNLOAD=trueを付与して実行する。

$ INSTALL_K3S_SKIP_DOWNLOAD=true ./install.sh 
[INFO]  Skipping k3s download and verify
[INFO]  Skipping installation of SELinux RPM
[INFO]  Creating /usr/local/bin/kubectl symlink to k3s
[INFO]  Creating /usr/local/bin/crictl symlink to k3s
[INFO]  Creating /usr/local/bin/ctr symlink to k3s
[INFO]  Creating killall script /usr/local/bin/k3s-killall.sh
[INFO]  Creating uninstall script /usr/local/bin/k3s-uninstall.sh
[INFO]  env: Creating environment file /etc/systemd/system/k3s.service.env
[INFO]  systemd: Creating service file /etc/systemd/system/k3s.service
[INFO]  systemd: Enabling k3s unit
Created symlink /etc/systemd/system/multi-user.target.wants/k3s.service → /etc/systemd/system/k3s.service.
[INFO]  systemd: Starting k3s
$ sudo kubectl get node
NAME           STATUS   ROLES                  AGE   VERSION
k3s-offline1   Ready    control-plane,master   18s   v1.29.2+k3s1
$ sudo kubectl get pod -A
NAMESPACE     NAME                                      READY   STATUS              RESTARTS   AGE
kube-system   helm-install-traefik-gvq48                0/1     ContainerCreating   0          97s
kube-system   helm-install-traefik-crd-xsblz            0/1     ContainerCreating   0          97s
kube-system   metrics-server-67c658944b-r655h           0/1     ContainerCreating   0          97s
kube-system   local-path-provisioner-6c86858495-qznr7   0/1     ContainerCreating   0          97s
kube-system   coredns-6799fbcd5-rwg9x                   0/1     ContainerCreating   0          97s

余談だけどこのときいつまでもRunningにならないと思ったらプライベートネットワークはDNSが無かったので/etc/hosts追加(笑)

$ sudo kubectl get pod -A
NAMESPACE     NAME                                      READY   STATUS      RESTARTS   AGE
kube-system   local-path-provisioner-6c86858495-qznr7   1/1     Running     0          2m29s
kube-system   coredns-6799fbcd5-rwg9x                   1/1     Running     0          2m29s
kube-system   helm-install-traefik-crd-xsblz            0/1     Completed   0          2m29s
kube-system   svclb-traefik-10a1b448-6h6c9              2/2     Running     0          35s
kube-system   helm-install-traefik-gvq48                0/1     Completed   1          2m29s
kube-system   traefik-f4564c4f4-gg6mv                   1/1     Running     0          35s
kube-system   metrics-server-67c658944b-r655h           1/1     Running     0          2m29s

動いた。

httpdのデプロイお試し

$ sudo kubectl apply -f http.yaml 
namespace/sample-app created
deployment.apps/sample-http created
service/sample-http created
$ sudo kubectl get pod,svc -n sample-app
NAME                               READY   STATUS    RESTARTS   AGE
pod/sample-http-5cd4944c69-l7xnb   1/1     Running   0          11s
pod/sample-http-5cd4944c69-6knfd   1/1     Running   0          11s

NAME                  TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
service/sample-http   LoadBalancer   10.43.134.90   172.29.0.77   8080:31629/TCP   11s

別ホストからアクセス

online$ curl 172.29.0.77:8080
<html><body><h1>It works!</h1></body></html>

大丈夫。

(追記) http.yamlの中身は以下の通り。

apiVersion: v1
kind: Namespace
metadata:
  name: sample-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: sample-http
  name: sample-http
  namespace: sample-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: sample-http
  template:
    metadata:
      labels:
        app: sample-http
    spec:
      containers:
      - image: gitlab.example.jp:25000/zaki/images/httpd:latest
        name: httpd
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: sample-http
  name: sample-http
  namespace: sample-app
spec:
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 80
    name: http
  selector:
    app: sample-http
  type: LoadBalancer

オフラインの仲間たち

qiita.com

zaki-hmkc.hatenablog.com

zaki-hmkc.hatenablog.com

zaki-hmkc.hatenablog.com

zaki-hmkc.hatenablog.com

[Kubernetes] Helmを使ってインストールすると生成されるシークレットの内容についてのメモ

Helmチャートをインストールすると、sh.helm.release.v1.<リリース名>.v1というシークレットリソースが作成されるけど、これ何だっけ?と思って調べてみた。
結論から言うと、Helmでインストールしたリリース情報がgzip形式で格納されている。これを手動で消したりするとHelmによる制御・管理ができなくなり、リリースの削除等のhelmを使った管理ができなくなる。なので手動で消したりするのは厳禁。
また、アップグレードすると、シークレットは増えていく。

チャートのインストール

$ helm upgrade --install fluentbit fluent/fluent-bit -n logging --create-namespace --version 0.43.0 -f values.yam
l
Release "fluentbit" does not exist. Installing it now.
NAME: fluentbit
LAST DEPLOYED: Thu Feb 29 15:45:51 2024
NAMESPACE: logging
STATUS: deployed
REVISION: 1

[...]

$ kubectl get secret -n logging 
NAME                              TYPE                 DATA   AGE
sh.helm.release.v1.fluentbit.v1   helm.sh/release.v1   1      9s

この通り、チャートをインストールするとシークレットが生成される。

シークレットを削除すると…

$ kubectl delete secret -n logging sh.helm.release.v1.fluentbit.v1 
secret "sh.helm.release.v1.fluentbit.v1" deleted

以下の通りリリースが削除できなくなる。

$ helm uninstall -n logging fluentbit
Error: uninstall: Release not loaded: fluentbit: release: not found

ので、シークレットを削除するのはやめましょう。

リリースをアップグレードすると

デプロイ済みのアプリがある状態で、次のバージョン2をデプロイ

$ helm upgrade --install fluentbit fluent/fluent-bit -n logging --create-namespace --version 0.43.0 -f values.yam
l
Release "fluentbit" has been upgraded. Happy Helming!
NAME: fluentbit
LAST DEPLOYED: Thu Feb 29 15:46:13 2024
NAMESPACE: logging
STATUS: deployed
REVISION: 2
NOTES:
Get Fluent Bit build information by running these commands:

export POD_NAME=$(kubectl get pods --namespace logging -l "app.kubernetes.io/name=fluent-bit,app.kubernetes.io/instance=fluentbit" -o jsonpath="{.items[0].metadata.name}")
kubectl --namespace logging port-forward $POD_NAME 2020:2020
curl http://127.0.0.1:2020

すると、v2のシークレットが作成される。

$ kubectl get secret -n logging 
NAME                              TYPE                 DATA   AGE
sh.helm.release.v1.fluentbit.v1   helm.sh/release.v1   1      26s
sh.helm.release.v1.fluentbit.v2   helm.sh/release.v1   1      3s

ここでv2のシークレットを削除すると、Helmとしては「Revision 2はなく、Revision 1の状態」と認識してしまうため、その状態からhelm upgradeを実行するとRevision 2としてデプロイされる。

$ helm upgrade --install fluentbit fluent/fluent-bit -n logging --create-namespace --version 0.43.0 -f values.yam
l
Release "fluentbit" has been upgraded. Happy Helming!
NAME: fluentbit
LAST DEPLOYED: Thu Feb 29 15:50:08 2024
NAMESPACE: logging
STATUS: deployed
REVISION: 2

[...]

で、このシークレットの中身って何なの?

$ kubectl describe -n logging secrets sh.helm.release.v1.fluentbit.v1 
Name:         sh.helm.release.v1.fluentbit.v1
Namespace:    logging
Labels:       modifiedAt=1709189817
              name=fluentbit
              owner=helm
              status=deployed
              version=1
Annotations:  <none>

Type:  helm.sh/release.v1

Data
====
release:  28832 bytes

サイズはそこそこある。
何のデータか確認するためBase64デコードした内容をみると、何かがBase64エンコードされた文字列に見えるので、さらにBase64デコードしてみるとバイナリデータだった。
ファイルへ出力すると中身はgzip圧縮されたデータだった。

$ kubectl get -n logging secrets sh.helm.release.v1.fluentbit.v1 -o jsonpath='{.data.release}' | base64 -d | base64 -d > zzz
$ file zzz
zzz: gzip compressed data, max compression, original size modulo 2^32 108244
$ mv zzz zzz.gz
$ gunzip zzz.gz
$ file zzz
zzz: JSON text data

中身を確認するとチャートとリリースの情報一式が入ってるようだった。

ドキュメント

自力で中身調べてたけど、ドキュメントに載ってましたね。。

https://helm.sh/docs/topics/kubernetes_apis/

[K3s / Helm] K3sのHelmChartカスタムリソースを使ったチャートのインストール (Fluent Bit)

K3sにHelmを使って何かをデプロイする場合、K3s自体にHelmChartカスタムリソースを使ったチャートのインストールがサポートされており、helmCLIコマンドを使わずにマニフェストを作成するだけでチャートのインストールが可能なので試してみた。

docs.k3s.io

HelmChartカスタムリソースのマニフェストファイル

お題は以下の「クラスタ上のPodのログをsyslog転送するFluent Bitのデプロイ」

zaki-hmkc.hatenablog.com

これと同じ内容でHelmチャートをデプロイするには、HelmChartカスタムリソースを以下のように記述。

---
apiVersion: helm.cattle.io/v1
kind: HelmChart
metadata:
  name: fluentbit
  namespace: kube-system
spec:
  targetNamespace: logging
  createNamespace: true
  repo: https://fluent.github.io/helm-charts
  chart: fluent-bit
  version: "0.43.0"
  valuesContent: |
    image:
      repository: cr.fluentbit.io/fluent/fluent-bit
      tag: 2.2.2
    config:
      ## https://docs.fluentbit.io/manual/pipeline/filters
      filters: |
        [FILTER]
            Name kubernetes
            Match kube.*
            Merge_Log On
            Keep_Log Off
            K8S-Logging.Parser On
            K8S-Logging.Exclude On
        [FILTER]
            Name     grep
            Match    *
            Exclude  $kubernetes['namespace_name'] logging
        [FILTER]
            name       nest
            match      kube.*
            operation  lift
            nest_under kubernetes
            add_prefix kubernetes_

      ## https://docs.fluentbit.io/manual/pipeline/outputs
      outputs: |
        [OUTPUT]
            name syslog
            match kube.*
            host  192.168.0.75
            syslog_message_key  log
            syslog_hostname_key kubernetes_namespace_name
            syslog_appname_key  kubernetes_pod_name
            syslog_procid_key   kubernetes_container_name

この内容のマニフェストを適用すると、

  1. kube-systemネームスペースにHelmチャートをインストールするためのhelm-install-fluentbit podがデプロイされる
  2. helm-install-fluentbit podによって、loggingネームスペースにFluent Bitのpodがデプロイされる

という処理が起動する。

HelmChartリソースの作成先をkube-systemにしてるが特にどこでもよい。
Helmチャートインストール時に新しくネームスペースを作成する場合(初期状態では存在しない場合)にすでに確実に存在するネームスペースを指定している程度。

チャートのリポジトリrepoで、チャート名をchartで、チャートのバージョンをversionで指定しているが、これはhelmCLI実行時の事前実行で行う処理や引数指定に相当。
values.yamlの内容がvaluesContentに相当で、コピペで問題ない。(マニフェストファイル内のインデントに位置を合わせる必要はあるが)

設定変更したい場合

CLIだとhelm upgradeする場面は、HelmChartカスタムリソースを更新してapplyすればOK
前述のFluent Bitの例であれば、valuesContent部分の設定を変更してapplyすれば、ConfigMapが更新されPodが再作成される。

HelmChartConfigカスタムリソースを使った設定の分離と上書き

HelmChartカスタムリソースとは別に、HelmChartConfigカスタムリソースも用意されており、HelmChartでデプロイされているチャートのvaluesContent部分の設定を上書きできる。

上書き対象のHelmチャートは、metadata.namemetadata.namespaceHelmChartリソースの値と一致させておく必要があり、前述のFluent Bit用の設定を上書きするには以下の通り。
この内容のマニフェストによって、Podのリソース制限とtailインプットモジュールのバッファサイズが更新される。

---
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
  name: fluentbit
  namespace: kube-system
spec:
  valuesContent: |
    resources:
      limits:
        cpu: 100m
        memory: 128Mi
      requests:
        cpu: 100m
        memory: 128Mi
    config:
      inputs: |
        [INPUT]
            Name tail
            Path /var/log/containers/*.log
            multiline.parser docker, cri
            Tag kube.*
            Mem_Buf_Limit 20MB
            Skip_Long_Lines On

        [INPUT]
            Name systemd
            Tag host.*
            Systemd_Filter _SYSTEMD_UNIT=kubelet.service
            Read_From_Tail On

これを使うと、設定のみ定義を分離できる。 また、K3sデフォルトでHelmインストールされるTraefikの設定もこれで上書きできる。


というか、ドキュメントの大項目に「Helm」とあるのにちゃんと読んでなくて、最近なんとなく眺めてたら「え、こんな機能あったんだ!?」と思ったのが実情

[Terraform] Data Sourcesを使った対象リージョンのAZの実行時取得

指定のリージョンにおけるアベイラビリティゾーンは何があるか?のように、自分が実装する内容でなくAWS(プロバイダ)の情報を取得したい場合は、データソースを使うことで情報参照ができる。

developer.hashicorp.com

アベイラビリティゾーンを参照するためのデータソースはaws_availability_zoneが用意されており、簡単なコードで実現できる。

registry.terraform.io

before (AZをハードコーディング)

2つのサブネットをそれぞれ別のAZとしたい場合、ハードコーディングするとこんな感じ。

resource "aws_subnet" "example_subnet1" {
  vpc_id            = aws_vpc.example_vpc.id
  cidr_block        = "10.89.1.0/24"
  availability_zone = "ap-southeast-2a"

  tags = {
    Name = "example_subnet1"
  }
}

resource "aws_subnet" "example_subnet2" {
  vpc_id            = aws_vpc.example_vpc.id
  cidr_block        = "10.89.2.0/24"
  availability_zone = "ap-southeast-2b"

  tags = {
    Name = "example_subnet2"
  }
}

after (データソースを使ったAZの取得)

前述のハードコーディングの場合、リージョンをap-southeast-2から変更したい場合に、AZの指定の箇所も修正が必要になるのでまったくスマートではない。
そこで使用するのがデータソースで、これを使うことで「現在のリージョンで定義されているAZを実行時に取得」することができる。

data "aws_availability_zones" "available" {
  state = "available"
}

resource "aws_subnet" "example_subnet1" {
  vpc_id            = aws_vpc.example_vpc.id
  cidr_block        = "10.89.1.0/24"
  availability_zone = data.aws_availability_zones.available.names[0]

  tags = {
    Name = "example_subnet1"
  }
}

resource "aws_subnet" "example_subnet2" {
  vpc_id            = aws_vpc.example_vpc.id
  cidr_block        = "10.89.2.0/24"
  availability_zone = data.aws_availability_zones.available.names[1]

  tags = {
    Name = "example_subnet2"
  }
}

このコードでplanすると以下のように自動でAZの値が取れていることが確認できる。

  # aws_subnet.example_subnet1 will be created
  + resource "aws_subnet" "example_subnet1" {
      + arn                                            = (known after apply)
      + assign_ipv6_address_on_creation                = false
      + availability_zone                              = "ap-southeast-2a"
      + availability_zone_id                           = (known after apply)

[...]

  # aws_subnet.example_subnet2 will be created
  + resource "aws_subnet" "example_subnet2" {
      + arn                                            = (known after apply)
      + assign_ipv6_address_on_creation                = false
      + availability_zone                              = "ap-southeast-2b"
      + availability_zone_id                           = (known after apply)

[...]

CFnの場合

CloudFormationの場合はこんな感じのはず (YAML)

  ExampleSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Select [ 0, !GetAZs '' ]

※ ちなみに今、他の人が実装したCFnのコードを微修正して実装してたシステムを最近Terraformに置き換えを進めてるところで、CFn自体はまったく理解してないというのが現状


zaki-hmkc.hatenablog.com

Ansibleからterraform applyを実行するタスクの実装

Terraformでインフラをプロビジョニングしたあとに上物に対してAnsibleで何か(アプリをインストールしたりデータを投入したり)処理をするようなことが多いので、AnsibleからTerraformを実行するようにして一発で処理できるようにするにはどうするか実装を確認してみた。

Ansibleモジュールはcommunity.general.terraformを使用する。

docs.ansible.com

applydestroyを間違えてたか所を修正(23:55)

project_pathのパス指定

/path/to/tffiles 以下でterraform applyをする構成であれば以下の通り。

  - name: apply terraform
    community.general.terraform:
      project_path: /path/to/tffiles
      state: present

このタスクを実行すると、/path/to/tffiles以下でterraform apply -auto-approveを実行するのと同様の動作になる。
TerraformのコードをAnsibleとは別リポジトリで管理してあり、AnsibleからTerraformのコードをgit cloneしてapplyするような構成であればこれで良いと思う。

一方で、構成によってはTerraformのコードをAnsibleと同じディレクトリで管理する場合もあるかと思う。その場合は実行に使うプレイブックからの相対パスproject_pathを指定する。
ただ、現時点でのcommunity.general version 8.3.0だと、ディレクトリ名だけだとパスを認識してくれないため、末尾に/を付与する必要があるっぽい。

例えば以下のディレクトリ構成でtffiles以下でterraform applyを実行する想定の場合。

.
├── inventory.ini
├── playbook.yml
└── tffiles
    ├── main.tf
    ├── outputs.tf
    ├── provider.tf
    └── variables.tf

タスクのコードとしては以下の通り。

  - name: apply terraform
    community.general.terraform:
      project_path: tffiles/
      state: present

このときにproject_pathに単にtffilesと指定すると、以下エラーとなる。

fatal: [localhost]: FAILED! => {"changed": false, "msg": "Path for Terraform project can not be None or ''."}

コード見ればわかるんだろうけど、末尾に/が無いとエラーになるのはなぜだろうね。
ちなみに./tffilesでもOK

変数指定

Terraformでvariableを使った変数定義については、community.general.terraformモジュールのvariablesパラメタで指定できる。

  - name: apply terraform
    community.general.terraform:
      project_path: ./tffiles
      state: present
      variables:
        infra_region: "{{ region }}"
        app_ami_id: "{{ app_ami }}"
        db_ami_id: "{{ db_ami }}"

この定義で、Terraform側で定義している変数infra_regionapp_ami_iddb_ami_idをAnsibleのタスク側から上書きする動作になる。

出力変数

Terraformでプロビジョニングされたリソースの情報を取得するためのoutputを使った定義については、Ansibleではタスクの実行結果の戻り値として参照できる。なので、registerディレクティブを使って後続のタスクなどで参照できる。

output "route_table_id" {
    value       = aws_route_table.example_rtb.id
    description = "ID of Route Table"
}

output "vpc_id" {
    value       = aws_vpc.example_vpc.id
    description = "ID of VPC"
}

output "nic_id" {
    value       = aws_network_interface.example_ni.id
    description = "ID of Network Interface"
}

こんなoutput定義を行っておけば、タスク定義でregisterを使って戻り値を参照でき、クラウドリソースのIDやIPアドレスを使った後続のタスクに使用できる。
以下は、AWSのルートテーブルを書き換える例。
(デフォルトで作成されるルートテーブルは作成の段階でカスタマイズできないため、作成済みのリソースをAnsibleで更新している)

  - name: apply terraform
    community.general.terraform:
      project_path: ./tffiles
      state: present
    register: result

  - name: update routetable
    amazon.aws.ec2_vpc_route_table:
      region: "{{ region }}"
      route_table_id: "{{ result.outputs.route_table_id.value }}"
      lookup: id
      vpc_id: "{{ result.outputs.vpc_id.value }}"
      routes:
        - dest: 10.1.0.0/16
          network_interface_id: "{{ result.outputs.nic_id.value }}"

環境

$ cat /etc/fedora-release 
Fedora release 39 (Thirty Nine)
$ terraform providers -version
Terraform v1.7.4
on linux_amd64
+ provider registry.terraform.io/hashicorp/aws v5.38.0
$ ansible --version
ansible [core 2.16.3]
  config file = None
  configured module search path = ['/home/zaki/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/zaki/venv/ansible9/lib64/python3.12/site-packages/ansible
  ansible collection location = /home/zaki/.ansible/collections:/usr/share/ansible/collections
  executable location = /home/zaki/venv/ansible9/bin/ansible
  python version = 3.12.2 (main, Feb  7 2024, 00:00:00) [GCC 13.2.1 20231205 (Red Hat 13.2.1-6)] (/home/zaki/venv/ansible9/bin/python)
  jinja version = 3.1.3
  libyaml = True
$ ansible-galaxy collection list community.general

# /home/zaki/venv/ansible9/lib/python3.12/site-packages/ansible_collections
Collection        Version
----------------- -------
community.general 8.3.0  

お、今年初めてプレイブック書いた気がするぞ。

[Terraform / AWS] EventBridge Schedulerを使ったVMの自動起動と停止

EventBridge Schedulerを使ったVMの自動on/off設定をCloudFormationを使って設定するには以下のClassmethodさんの記事を見れば一通り実装できます。

dev.classmethod.jp

ここではTerraformを使った実装についてのまとめ。
こうしたらうまくいった、という感じの内容なので、コードの解説はほぼ無いです。

※ 実際に使ってるコードをコピペして変数をblog用に更新してるので、辻褄が合ってない箇所がもしあったらゴメンナサイ

EventBridgeスケジューラ用IAMロールの作成

CloudFormationだとこんな感じ(YAML)

  SchedulerEC2StopStartRole:
    # Start/Stop by EventBridge用IAMロール
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - scheduler.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      Policies:
        - PolicyName: EC2StopStart
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:StartInstances
                  - ec2:StopInstances
                Resource:
                  - "*"

これをTerraformで書くとこんな感じ。
使うのはaws_iam_roleリソース。

registry.terraform.io

resource "aws_iam_role" "stopstart_role" {
  name = "ec2-stop-start-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "scheduler.amazonaws.com"
        }
      },
    ]
  })

  path = "/"

  inline_policy {
    name = "start_stop_policy"
    policy = jsonencode({
      Version = "2012-10-17"
      Statement = [
        {
          Action   = ["ec2:StartInstances", "ec2:StopInstances"]
          Effect   = "Allow"
          Resource = "*"
        },
      ]
    })
  }
}

nameはドキュメントにはOptionalと書かれてて実際無くてもTerraform的にデプロイはできるんだけど、リソースが作られなかったので実質必須。

自動停止のスケジューラ設定

EC2を自動停止するスケジューラ設定。
CloudFormationだとこんな感じ。(YAML)
InstanceIdsに対象EC2を指定。
毎日17時25分に停止するには以下の通り。

  ScheduleEC2Stop:
    # EC2の停止EventBridge
    Type: AWS::Scheduler::Schedule
    Properties:
      Name: ec2-stop
      Description: Stop EC2 Instance
      ScheduleExpression: cron(25 17 * * ? *)
      ScheduleExpressionTimezone: Japan
      FlexibleTimeWindow:
        Mode: "OFF"
      State: ENABLED
      Target:
        Arn: arn:aws:scheduler:::aws-sdk:ec2:stopInstances
        Input: !Sub |-
          {
            "InstanceIds": ["${Server1}", "${Server2}", "${Server3}"]
          }
        RoleArn:
          Fn::GetAtt:
          - SchedulerEC2StopStartRole
          - Arn

これをTerraformで書くとこんな感じ。
使うのはaws_scheduler_scheduleリソース。

registry.terraform.io

resource "aws_scheduler_schedule" "schedule_stop" {
  name       = "ec2-stop"
  group_name = "default"

  flexible_time_window {
    mode = "OFF"
  }

  schedule_expression = "cron(25 17 * * ? *)"
  schedule_expression_timezone = "Japan"
  target {
    arn = "arn:aws:scheduler:::aws-sdk:ec2:stopInstances"
    role_arn = aws_iam_role.stopstart_role.arn
    input = jsonencode({
      InstanceIds = ["${aws_instance.server1.id}",
                     "${aws_instance.server2.id}",
                     "${aws_instance.server3.id}"]
    })
  }
}

自動起動のスケジューラ設定

最後にEC2を自動起動するスケジューラ設定。
CloudFormationだと(ry
毎朝8時45分に起動する設定。

  ScheduleEC2Start:
    # EC2の開始EventBridge
    Type: AWS::Scheduler::Schedule
    Properties:
      Name: ec2-start
      Description: Start EC2 Instance
      ScheduleExpression: cron(45 8 * * ? *)
      ScheduleExpressionTimezone: Japan
      FlexibleTimeWindow:
        Mode: "OFF"
      State: ENABLED
      Target:
        Arn: arn:aws:scheduler:::aws-sdk:ec2:startInstances
        Input: !Sub |-
          {
            "InstanceIds": ["${Server1}", "${Server2}", "${Server3}"]
          }
        RoleArn:
          Fn::GetAtt:
          - SchedulerEC2StopStartRole
          - Arn

これをTerraformで書くとこんな感じ。
使うのは停止と同様にaws_scheduler_scheduleリソース。

resource "aws_scheduler_schedule" "schedule_start" {
  name       = "ec2-start"
  group_name = "default"

  flexible_time_window {
    mode = "OFF"
  }

  schedule_expression = "cron(45 8 * * ? *)"
  schedule_expression_timezone = "Japan"
  target {
    arn = "arn:aws:scheduler:::aws-sdk:ec2:startInstances"
    role_arn = aws_iam_role.stopstart_role.arn
    input = jsonencode({
      InstanceIds = ["${aws_instance.server1.id}",
                     "${aws_instance.server2.id}",
                     "${aws_instance.server3.id}"]
    })
  }
}

(おまけ)cron式で「平日のみ」にする場合

業務で使う場合だと、毎日起動でなく平日だけ起動にしたい場合がほとんどだと思う。
その場合はこのように書けば「月曜から金曜」になる。

cron(45 8 ? * MON-FRI *)

docs.aws.amazon.com

www.tegos.co.jp

[HashiCorp Vault] KVシークレットエンジンのデータの読み書きメモ (CLI/REST)

HashiCorp VaultのKVシークレットエンジンを使ったkey/value形式のデータの読み書きを試した。
触り始めて間もないので解釈や表現がおかしいところはあるかも。(あったら教えてください)

developer.hashicorp.com

HashiCorp VaultにおいてシークレットエンジンにはAWSAzureSSHなど様々なデータ構造の定義があり、本エントリではその中の一つであるkey/value形式のデータを扱うKVシークレットエンジンを使ったデータの読み書きについての簡単にまとめ。

ほんとはイケてないんだけど、作業はすべてrootで実施(root tokenでログイン済み状態)
RESTについては、使用するトークン(例では環境変数 VAULT_TOKEN にセットしてる)は、同様にroot tokenで、X-Vault-Tokenヘッダで指定する。X-Vault-Tokenでなく、一般的な"Authorization: Bearer $VAULT_TOKEN"でも認証可能。

KVシークレットエンジンの有効化

「KVシークレットエンジンの有効化」というと始めはピンとこなかったけど、「KV形式のデータを扱うための箱を作る」が近いイメージだと思う。
C言語風にいうとKV構造体変数を保持する領域を確保する、みたいな。

my-kv-secretという名前(パス)のKVシークレットエンジンを有効にするには以下のコマンド。

/ $ vault secrets enable -path=my-kv-secret -version=2 kv
Success! Enabled the kv secrets engine at: my-kv-secret/

-pathオプションを省略すると、kvというパスのKVシークレットエンジンが作成される。
-versionオプションを省略するとKVシークレットエンジンversion1で作成される。version2で作成する場合は-version=2を付加。
普通のLinuxコマンドと違って、オプションを末尾に付与できないので注意。

バージョンの違いについてはドキュメント参照

developer.hashicorp.com

KVシークレットエンジンの無効化

有効化の逆なので、要は削除。

/ $ vault secrets disable my-kv-secret
Success! Disabled the secrets engine (if it existed) at: my-kv-secret/

KVシークレットエンジンの一覧

/ $ vault secrets list
Path             Type         Accessor              Description
----             ----         --------              -----------
cubbyhole/       cubbyhole    cubbyhole_fa65f299    per-token private secret storage
identity/        identity     identity_0917a11b     identity store
my-kv-secret/    kv           kv_838b2e62           n/a
sys/             system       system_8a5e2f0e       system endpoints used for control, policy and debugging

-detailedを付与すると詳細が表示される。

読み書きと一覧

最初わかりづらかったんだけど、「一つのKVシークレットエンジンにkey/value形式のデータを保存」ではなく「一つのKVシークレットエンジン内にあるキー毎にkey/value形式のデータを保存」というデータ構造になってる。

という表現もやっぱりわかりづらいんで、百聞は一見に如かずで実行例。

write

CLI

my-kv-secretシークレットエンジンに、server1server2いうキーで、ユーザー名・パスワード・IPアドレスを保存。

/ $ vault kv put my-kv-secret/server1 username=zaki password=curry_tabetai host=192.168.10.89
Success! Data written to: my-kv-secret/server1
/ $ vault kv put my-kv-secret/server2 username=zaki password=curry_tabeta host=192.168.10.13
Success! Data written to: my-kv-secret/server2

ちなみにこの書式はKVシークレットエンジンversion1のもので、version2では非推奨。
version2では-mountオプションでシークレットエンジン名を指定する書式が推奨される。

/ $ vault kv put -mount=my-kv-secret server3 username=zaki password=asuha_curry host=192.168.10.10
Success! Data written to: my-kv-secret/server3

JSONファイル

JSON形式のファイルを使ったwriteも可能。

/ $ cat /var/tmp/sample.json 
{
  "username": "zaki",
  "password": "json-data",
  "host": "192.168.18.6"
}
/ $ vault kv put -mount=my-kv-secret server4 @/var/tmp/sample.json
Success! Data written to: my-kv-secret/server4

REST

developer.hashicorp.com

POSTするデータはdata以下に作成するのがポイント (ここ見落として30分くらい溶かした笑)

$ curl -s -H "X-Vault-Token: $VAULT_TOKEN" \
  -X POST \
  -d '{ "data": { "username":"zaki", "password":"curl-post", "host":"192.168.18.21" } }' \
  http://192.168.0.75:8200/v1/my-kv-secret/data/server5
{"request_id":"45c406a2-9d38-b46c-fd48-32c887366fd8","lease_id":"","renewable":false,"lease_duration":0,"data":{"created_time":"2024-02-20T13:42:48.263373535Z","custom_metadata":null,"deletion_time":"","destroyed":false,"version":1},"wrap_info":null,"warnings":null,"auth":null}

JSONファイルを使う場合も同様

$ cat tmp/post.json 
{ "data": { "username":"zaki", "password":"curl-json-post", "host":"192.168.18.26" } }
$ curl -s -H "X-Vault-Token: $VAULT_TOKEN" \
  -X POST \
  -d @tmp/post.json \
  http://192.168.0.75:8200/v1/my-kv-secret/data/server6
{"request_id":"3a52eeff-2be4-9b01-94cf-d6316d64f208","lease_id":"","renewable":false,"lease_duration":0,"data":{"created_time":"2024-02-20T13:45:11.476087387Z","custom_metadata":null,"deletion_time":"","destroyed":false,"version":1},"wrap_info":null,"warnings":null,"auth":null}

エンドポイントはシークレットエンジンのバージョンで異なるので注意。
前述の例はversion2の場合。
version1の場合は/v1/<secret-engine-name>/<key-name>となり、/dataが無くなる。
(先頭の/v1APIバージョンであってシークレットエンジンのバージョンではない)

developer.hashicorp.com

ここまで全部ユーザー名・パスワード・ホストと、データ構造をそろえているけど、これはたまたまそうしてるだけで、別にそろってる必要はなく柔軟に書き込み可能。

list

/ $ vault kv list my-kv-secret
Keys
----
server1
server2
server3
server4
server5
server6

writeで作った6件分のキーがリストされていることがわかる。

RESTの場合は以下の通り。

$ curl -s -H "X-Vault-Token: $VAULT_TOKEN" -X LIST http://192.168.0.75:8200/v1/my-kv-secret/metadata | python -m json.tool
{
    "request_id": "fb371bc4-b31b-7cb1-a870-e3fedc8411ef",
    "lease_id": "",
    "renewable": false,
    "lease_duration": 0,
    "data": {
        "keys": [
            "server1",
            "server2",
            "server3",
            "server4",
            "server5",
            "server6"
        ]
    },
    "wrap_info": null,
    "warnings": null,
    "auth": null
}

ちなみにAPIリファレンスにこれ乗って無さそうなんだけど。。(/metadataのあとのパス記載が全てrequiredになってる)

read

CLI

putで作成したデータは当然getで参照できる。

/ $ vault kv get my-kv-secret/server1
====== Secret Path ======
my-kv-secret/data/server1

======= Metadata =======
Key                Value
---                -----
created_time       2024-02-20T13:30:15.916047056Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1

====== Data ======
Key         Value
---         -----
host        192.168.10.89
password    curry_tabetai
username    zaki

KVシークレットエンジンversion2を使っていると、この通りメタデータも含まれる。(version1の場合はデータのみ) 書式についてもputと同様、-mountの使用が推奨されている。

/ $ vault kv get -mount=my-kv-secret server2
====== Secret Path ======
my-kv-secret/data/server2

======= Metadata =======
Key                Value
---                -----
created_time       2024-02-20T13:30:15.990461672Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1

====== Data ======
Key         Value
---         -----
host        192.168.10.13
password    curry_tabeta
username    zaki

また、-formatを使えばjsonyaml形式でも出力できる。

/ $ vault kv get -format=json -mount=my-kv-secret server3
{
  "request_id": "804b3606-ee34-7734-1ff3-efa9011ce5d2",
  "lease_id": "",
  "lease_duration": 0,
  "renewable": false,
  "data": {
    "data": {
      "host": "192.168.10.10",
      "password": "asuha_curry",
      "username": "zaki"
    },
    "metadata": {
      "created_time": "2024-02-20T13:30:16.03813777Z",
      "custom_metadata": null,
      "deletion_time": "",
      "destroyed": false,
      "version": 1
    }
  },
  "warnings": null
}

REST

$ curl -s -H "X-Vault-Token: $VAULT_TOKEN" http://192.168.0.75:8200/v1/my-kv-secret/data/server4 | python -m json.tool
{
    "request_id": "b6370992-2cc7-9e63-97ff-ea7daf9d0e9b",
    "lease_id": "",
    "renewable": false,
    "lease_duration": 0,
    "data": {
        "data": {
            "host": "192.168.18.6",
            "password": "json-data",
            "username": "zaki"
        },
        "metadata": {
            "created_time": "2024-02-20T13:30:16.101513188Z",
            "custom_metadata": null,
            "deletion_time": "",
            "destroyed": false,
            "version": 1
        }
    },
    "wrap_info": null,
    "warnings": null,
    "auth": null
}

環境

  • HashiCorp Vault on Kubernetes
    • CHART: vault-0.27.0
    • APP VERSION: 1.15.2

更新と削除はまた別途…


ところでSecrets EngineだったりSecrets EnginesだったりSecrets enginesだったり公式ドキュメントの表記揺れが結構あるように見える。。どれが正解なの。。