terraform-exec の使い方

What is terraform-exec

Hashicorpにメンテナンスされているterraform cliGolangから使うためのモジュール

GitHub - hashicorp/terraform-exec: Terraform CLI commands via Go.

How to use

package main

import (
        "context"
        "encoding/json"
        "fmt"
        "log"

        "github.com/hashicorp/go-version"
        "github.com/hashicorp/hc-install/product"
        "github.com/hashicorp/hc-install/releases"
        "github.com/hashicorp/terraform-exec/tfexec"
)

func main() {
        installer := &releases.ExactVersion{
                Product: product.Terraform,
                // versionは必須
                Version: version.Must(version.NewVersion("1.0.6")),
        }

        execPath, err := installer.Install(context.Background())
        if err != nil {
                log.Fatalf("error installing Terraform: %s", err)
        }

        workingDir := "path/to/working/dir/terraform"
        tf, err := tfexec.NewTerraform(workingDir, execPath)
        if err != nil {
                log.Fatalf("error running NewTerraform: %s", err)
        }

        err = tf.Init(context.Background(), tfexec.Upgrade(true))
        if err != nil {
                log.Fatalf("error running Init: %s", err)
        }

    
       // terraform plan -out を使う場合はPlanOptionを使う
        planConfig := []tfexec.PlanOption{
                tfexec.Out("./out.txt"),
        }

        plan, err := tf.Plan(context.Background(), planConfig...)
        if err != nil {
                log.Fatalf("error running plan: %s", err)
        }
        // 差分が有る場合はtrueになる
        fmt.Println(plan)
   
        show, err := tf.Show(context.Background())
        if err != nil {
                log.Fatalf("error show: %s", err)
        }

        b, err := json.Marshal(show)
        if err != nil {
                log.Fatalf("err %s", err)
        }
        // json形式でplan結果が表示される
        fmt.Println(string(b))

        // ファイル出力したバイナリを使う場合
  
        // planStr2, err := tf.ShowPlanFile(context.Background(), "./out.txt")
        // if err != nil {
        //      log.Fatalf("err %s", err)
        // }

        // b, err := json.Marshal(planStr2)
        // if err != nil {
        //      log.Fatalf("err %s", err)
        // }
        // fmt.Println(string(b))

        // tf.Apply(context.Background())
}

NestJSのUnitTestでsqlite3を使う方法

UnitTestの時にDBにつなぐのではなくMockデーターベース(sqlite)を起動させMockデーターベースに接続する方法
ソースコードの大半はNestJSのDatabaseの項目を参考にしています。

https://docs.nestjs.com/techniques/database

1 対象のアプリを実装していきます

  • アプリを作成
    nest new app

  • 依存ライブラリのインストール
    npm install --save @nestjs/typeorm typeorm mysql2

  • app.moduleの更新

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'test',
      entities: [],
      synchronize: true,
    }),
  ],
})
export class AppModule {}
  • User moduleを作成
nest g  mo user
nest g s user
  • User entityを作成
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @Column({ default: true })
  isActive: boolean;
}
  • User moduleを更新
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entitiy';
import { UserService } from './user.service';

@Module({
  providers: [UserService],
  imports: [TypeOrmModule.forFeature([User])],
})
export class UserModule {}
  • UserServiceにロジックを追加
export class UserService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.usersRepository.find();
  }

  findOne(id: string): Promise<User> {
    return this.usersRepository.findOne(id);
  }

  async remove(id: string): Promise<void> {
    await this.usersRepository.delete(id);
  }
}

2 UnitTest 今回Mockのデータベースにsqliteを利用します
npm install --save slqite3

ちなみに今のUserServiceのテストはこのようになっています。 この状況でnpm run testを行ってもテストは通りません。

describe('UserService', () => {
  let service: UserService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [UserService],
    }).compile();

    service = module.get<UserService>(UserService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });
});

次のように変更するとテストが通るようになります。

      providers: [
        UserService,
        {
          provide: getRepositoryToken(User),
          useClass: Repository,
        },
      ],

次に接続先のDBをmysqlからsqliteに変更します。

describe('UserService', () => {
  let service: UserService;
  let repository: Repository<User>;
  const testConnectionName = 'testConnection';

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: getRepositoryToken(User),
          useClass: Repository,
        },
      ],
    }).compile();
    // mock用DBの作成
    const connection = await createConnection({
      type: 'sqlite',
      database: ':memory:', // inmemoryで動かす
      dropSchema: true,
      entities: [User],
      synchronize: true,
      logging: false,
      name: testConnectionName,
    });
    repository = getRepository(User, testConnectionName);
    // 
    service = new UserService(repository);
  });

  // テストが終わる度にMockDBをクリーンにする
  afterEach(async () => {
    await getConnection(testConnectionName).close();
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });
});

あとはこんな感じでテストを書くだけ

  describe('find', () => {
    it('find', async () => {
      const user: User = {
        id: 1,
        firstName: 'test',
        lastName: 'user',
        isActive: true,
      };
      await repository.save(user);
      const inUsers = await service.findAll();
      expect(inUsers.length).toBe(1);
      expect(inUsers[0].firstName).toBe(user.firstName);
    });
  });

今回のコード

github.com

ArgoCD NotificationsでMS Teamsに通知する方法

今回の内容

ArgoCD notificationsでMS temasに通知するときに詰まった点について記載していきます。

事前準備

ArogCDは事前"Getting start"でインストールにしていると想定しています。

argo-cd.readthedocs.io

ArgoCD Notificationの準備

ArgoCDのStable版(V1.0)でMS TeamsへのNotificationがサポートされいるように見えますが、実はv1.0ではサポートされていません。Notificationのテンプレートを確認するとEmailとslackはサポートしていますがMS Teamsのテンプレートが含まれていません。
なのでv.1.1.0を使いましょう

f:id:mo121_7:20211213203044p:plain

Install ArgoCD Notification and Template

kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj-labs/argocd-notifications/v1.1.0/manifests/install.yaml
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj-labs/argocd-notifications/v1.1.0/catalog/install.yaml

通知の設定

Setp:
1 argocd-notifications-secret.yamlを作成

apiVersion: v1
kind: Secret
metadata:
  name: argocd-notifications-secret
stringData:
  channel-teams-url: <your-webhook-url>

kubectl appy -f argocd-notifications-secret.yaml

2 argocd-notifications-cmを更新

kubectl edit configmap argocd-notifications-cm -n argocd

Dataの下に追加
data:
  service.teams: |
    recipientUrls:
      channelName: $channel-teams-url

3 パイプライン一覧を確認

$ kubectl get Application -n argocd
NAME   SYNC STATUS   HEALTH STATUS
test   OutOfSync     Healthy

4 アノテーションを追加

$  kubectl edit Application test -n argocd
metadata:
  annotations:
    notifications.argoproj.io/subscribe.on-sync-succeeded.teams: channelName

これでArgoCDがSyncしたときにMS Teamsに通知ができるようになる 通知のタイミングはドキュメントを確認

argocd-notifications.readthedocs.io

JVM error test

Kubernetes上で動いているJavaアプリがOOMなどで死んだときに備えてどのようのエラーログを取得するかテストする方法 テスト環境はMinikube上で動かしています。 Minikube: Java version:

今回のベースアプリ

github.com

まずDeploymentを作成しDeployします。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hellojava
spec:
  replicas: 1
  selector:
    matchLabels:
      name: hellojava
  template:
    metadata:
      labels:
        name: hellojava
    spec:
      containers:
      - name: hellojava
        image: mo053/gs-spring-boot-docker:latest
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        env:
        ports:
        - containerPort: 8080

kubectl apply -f deployment -n <namespace> STATUSがRunningになっているのでok

kubectl get pod
NAME                                                  READY   STATUS      RESTARTS      AGE
hellojava-78cffc5867-z8sz5                            1/1     Running     3 (12m ago)   18m

PODの中に入りプロセスを落とす

kubectl exec -it hellojava-78cffc5867-z8sz5 /bin/bash

ps aux | grep java
root           1  0.7  1.1 6653628 184488 ?      Ssl  09:24   0:05 java -jar /app.jar $JAVA_OPTS
root          54  0.0  0.0   6308   728 pts/0    S+   09:37   0:00 grep --color=auto java


kill -SIGSEGV 1

Podのログは以下のように表示される

#
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0x00007fd124332cd5 (sent by kill), pid=1, tid=1
#
# JRE version: OpenJDK Runtime Environment Microsoft-27990 (11.0.13+8) (build 11.0.13+8-LTS)
# Java VM: OpenJDK 64-Bit Server VM Microsoft-27990 (11.0.13+8-LTS, mixed mode, sharing, tiered, compressed oops, serial gc, linux-amd64)
# Problematic frame:
# C [libpthread.so.0+0xacd5] __pthread_clockjoin_ex+0x255
#
# Core dump will be written. Default location: Core dumps may be processed with "/usr/share/apport/apport %p %s %c %d %P %E" (or dumping to //core.1)
#
# An error report file with more information is saved as:
# //hs_err_pid1.log
#
# If you would like to submit a bug report, please visit:
# https://github.com/microsoft/openjdk/issues
#
[error occurred during error reporting (), id 0xb, SIGSEGV (0xb) at pc=0x00007fd124143941]

Deploymentを次のように更新すると/dumpにhs_error_pidが吐かれるようになる

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hellojava
spec:
  replicas: 1
  selector:
    matchLabels:
      name: hellojava
  template:
    metadata:
      labels:
        name: hellojava
    spec:
      containers:
      - name: hellojava
        image: mo053/gs-spring-boot-docker:latest
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        env:
        - name: JAVA_TOOL_OPTIONS
          value: "-XX:ErrorFile=/dump/error.log -Xmx50m -XX:+CrashOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dump/oom.bin"
        ports:
        - containerPort: 8080
        volumeMounts:
        - mountPath: /dump
          name: dump-volume
      volumes:
      - name: dump-volume
        emptyDir: {}

OOMを発生させる方法

OOMを疑似的に発生させヒープダンプが正しく出されるか検証
コードはすべてここに置いています。

github.com

  1. Spring bootのwebプロジェクトを作成
  2. OOMのタイミングをコントロールするために適当なコントローラーを作成
    こんな感じ
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/hello")
public class HelloController {

    @RequestMapping(method= RequestMethod.GET)
    public String hello() {
        return "Hello Spring MVC";
    }
}

次のメソッドを追加

    @RequestMapping(value="/test", method=RequestMethod.GET)
    public void test() {
        Object[] o = null; while (true) { o = new Object[] {o}; }
    }

補足 OutOfMemoryErrorをthrowするのではヒープダンプは出さない

    @RequestMapping(value="/test", method=RequestMethod.GET)
    public void test() {
        throw new OutOfMemoryError();
    }
  1. アプリケーション起動時にXmxを小さくする
    java -Xmx50m -XX:+CrashOnOutOfMemoryError -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dump/oom.bin -jar target/demo-0.0.1-SNAPSHOT.jar
    ** 制限しない場合はPCのメモリを使い果たすまでメモリを消費するので注意

`

Nginx-ingressでX-Forwarded-Protoの設定方法

Ingressのannotationsにconfiguration-snippetを追加する

  annotations:
      nginx.ingress.kubernetes.io/configuration-snippet: |
         proxy_set_header X-Forwarded-Port 443;
         proxy_set_header X-Forwarded-Proto "https";

Configmap(ingress-nginx-controller)に以下を追加

data:
  use-forwarded-headers: 'true'

これだけではHeaderの中がこのようになる

"x-forwarded-for":"xxx.xxx.xxx.xxx","x-forwarded-host":"xxxxxx","x-forwarded-proto":"http, https","x-scheme":"http","x-forwarded-port":"80, 443",

x-forwarded-protoとx-forwarded-portを"https"と'443'のみにするにはNginx.tmplの変更が必要である。
Nginx.tmplの変更手順は以下の通りである
1 動いているnginx-ingessからnginx.tmplをエクスポートする

 kubectl exec -it ingress-nginx-controller-5fd6cdcf78-qht8p -n ingress-nginx cat /etc/nginx/template/nginx.tmpl > nginx.tmpl

2 nginx.tmplから次を削除する

            {{ $proxySetHeader }} X-Forwarded-Host       $best_http_host;
            {{ $proxySetHeader }} X-Forwarded-Proto      $pass_access_scheme;

3 Configmapを作成する

kubectl create configmap nginx-template  --from-file=nginx.tmpl
  1. Nginx ingressのデプロイメントに次の項目を追加
        volumeMounts:
          - mountPath: /etc/nginx/template
            name: nginx-template-volume
            readOnly: true
      volumes:
        - name: nginx-template-volume
          configMap:
            name: nginx-template
            items:
            - key: nginx.tmpl
              path: nginx.tmpl