Podをヘルスチェックする3つの方法 - probeを理解する
KubernetesにはPodをヘルスチェックする方法として3つのprobeという機能を用意しています。その3つのprobeの使い方や違いを解説します。
probeはKubernetesの重要な機能で、エンドユーザーにとって使いやすいサービス提供のために欠かせないものです。基本的な機能としてはPodがトラフィックを適切に処理できる状態かどうかを一定の間隔で監視し、再起動などのアクションを実行できます。
ReadinessProbe
ReadinessProbeとは
ReadinessProbeはServiceを構築するときに一緒に検討するべきリソースの一つです。
ServiceはYAMLのlabelに一致するPodを見つけたら、そこにトラフィックを流すしくみになっています。Podを見つけるとすぐにトラフィックを流し始めるため、Podの状況を見てはいません。もし、Podが起動後に何か設定が必要で、それに対して時間がかかる場合、トラフィックが流れてきても処理できず、エラーになってしまいます。
この問題を解決するためにReadinessProbeが使われます。RedinessProbeはPodのYAMLに書き加えることで設定できます。ReadinessProbeが設定されたPodがデプロイされたら、Podの準備ができていないうちはServiceからトラフィックを流さないようにコントロールプレーにシグナルを送れるようになります。
ReadinessProbeの書き方
ReadinessProbeの設定パターンは3種類あります。
- HTTP: Podの 準備ができたことを、HTTPリクエストを送ることで確認する方法です。200以上400未満のレスポンスコードが返ってきたら準備完了とみなします。
- Command: Podの準備ができたことを、コマンドを実行することで確認する方法です。exit code 0で返ってきたら準備完了とみなします。
- TCP: Podの準備ができたことを、TCPのコネクションを生成することで確認する方法です。コネクションを生成できたら準備完了とみなします。
それぞれの事例を見ていきましょう。
1.HTTPパターン
apiVersion: v1 kind: Pod metadata: name: nginx-pod-readinessprobe spec: containers: - name: nginx image: nginx readinessProbe: httpGet: path: /ready port: 80 initialDelaySeconds: 5 periodSeconds: 5
initialDelaySecondesには、最初のヘルスチェックを実行するまでに待つ時間を設定します。periodsSecondsには、ヘルチェックの実行間隔を設定します。 この設定の場合は、Podが起動して5秒後、/readyエンドポイントに対してHTTP GETリクエストを送り、その後5秒ごとにリクエストを繰り返します。HTTPステータスコードが200-399の範囲内であればPodを準備完了とみなし、PodはServiceからのリクエストを受け入れるようになります。
2.Commandパターン
apiVersion: v1 kind: Pod metadata: name: myapp-pod spec: containers: - name: myapp-container image: myapp readinessProbe: exec: command: - cat - /tmp/ready initialDelaySeconds: 5 periodSeconds: 5
このパターンでは、コマンドを指定します。上記の書き方の場合、コンテナ内でcat /tmp/ready
コマンドを実行し、ファイルが存在すれば(exitコードが0なら)Podは準備完了とみなされます。Podで稼働するアプリの実装として、準備が完了したら当該ファイルを作成する処理を入れておく必要があります。
3.TCPパターン
apiVersion: v1 kind: Pod metadata: name: myapp-pod spec: containers: - name: myapp-container image: myapp readinessProbe: tcpSocket: port: 80 initialDelaySeconds: 5 periodSeconds: 5
このパターンではTCPソケットのポート番号を指定します。PodがTCPポート80で接続を受け入れられるかどうかで準備完了を判断します。このパターンはPodで稼働するアプリケーションでHTTPを起動させていない場合や、所定のファイルを置けない(置きたくない)場合に利用できます。
実際のアプリ開発の場面では、アプリケーションの状態に関連するエンドポイントを呼び出すことで、より有効性が増します。たとえば、アプリがDBを使用している場合、内部的にMySQLへの接続を開くページを呼び出して、アプリケーションがそのDBと通信できることを確認するようにできます。ただ単に200を返すようなページを用意するのではなく、実際にDBに接続しにいくようなテスト用のページを用意することで、より堅牢なアプリになるというわけです。
LivenessProbe
LivenessProbeとは
LivenessProbeはReadinessProbeと似ています。ReadinessProbeはPodの準備完了状況を伝えるためのヘルスチェックとして使えるのに対し、LivenessProbeは定期的なヘルスチェックとして利用します。
ServiceではPodの生死を検知することができません。Podが死んでいてもトラフィックを送り続けてしまいます。
ivenessProbeをPodに設定しておくことで、Podに対して常にヘルスチェックを実行し、異常が生じた場合にはPodを再起動してくれます。
LivenessProbeの書き方
設定パターンはReadinessと同じ3種類です。それぞれの設定方法を見ていきましょう。
1.HTTP Probe
apiVersion: v1 kind: Pod metadata: name: nginx-pod-livenessprobe spec: containers: - name: nginx image: nginx livenessProbe: httpGet: path: /health port: 80 initialDelaySeconds: 15 periodSeconds: 20
この例では、HTTP GETリクエストを用いてヘルスチェックします。Podが起動して15秒後に/health
エンドポイントにリクエストを送り、その後20秒ごとにチェックを繰り返します。レスポンスコードが200-399ならPodは正常とみなされます。
2.Command Probe
apiVersion: v1 kind: Pod metadata: name: myapp-pod spec: containers: - name: myapp-container image: myapp livenessProbe: exec: command: - cat - /tmp/healthy initialDelaySeconds: 15 periodSeconds: 20
この例ではcat /tmp/healthy
コマンドを実行し、exitコードが0ならPodは正常と判断されます。
3.TCP Probe
apiVersion: v1 kind: Pod metadata: name: myapp-pod spec: containers: - name: myapp-container image: myapp livenessProbe: tcpSocket: port: 80 initialDelaySeconds: 15 periodSeconds: 20
TCPソケットを使ったLivenessProbeの例です。PodがTCPポート80で接続を受け入れるかどうかをチェックし、接続できればPodは正常とみなされます。
StartupProbe
StartupProbeとは
StartupProbeは比較的新しい機能で、Kubernetes 1.16でアルファ版として導入され、その後のバージョンで改善が進められました(LivenessProbeとReadinessProbeはKubernetesの初期のバージョンから存在しています)。正式に安定した機能としてリリースされたのはKubernetes 1.20です。
StartupProbeは、特に起動に時間がかかるアプリケーションの健康状態をチェックするために設計されています。LivenessProbeやReadinessProbeとは異なり、StartupProbeはコンテナが初めて起動する際にのみ実行されます。
起動が遅いアプリケーションでは、LivenessProbeが早すぎる段階でPodを異常と判断し、再起動を引き起こす可能性があります。StartupProbeはこの問題を解決するために、アプリケーションが起動して安定するまでの間、LivenessProbeとReadinessProbeのチェックを一時的に停止します。
StartupProbeの書き方
例によって3種類あります。
1.HTTP Probe
apiVersion: v1 kind: Pod metadata: name: slow-start-app spec: containers: - name: myapp image: myapp startupProbe: httpGet: path: /start port: 8080 failureThreshold: 30 periodSeconds: 10
この例ではHTTP GETリクエストを用いてStartupをチェックします。/start
エンドポイントに対してHTTPリクエストを送り、10秒ごとにリクエストを繰り返します。failureThreshold
は30に設定されており、これはProbeが30回失敗するまでPodを起動し続けることを意味します。
2.Command Probe
apiVersion: v1 kind: Pod metadata: name: myapp-pod spec: containers: - name: myapp-container image: myapp startupProbe: exec: command: - cat - /tmp/start failureThreshold: 30 periodSeconds: 10
コマンドを実行してStartupを確認する例です。cat /tmp/start
コマンドを実行し、exitコードが0ならアプリケーションは起動中と判断されます。このチェックは10秒ごとに実行され、30回の失敗まで許容されます。
3.TCP Probe
apiVersion: v1 kind: Pod metadata: name: myapp-pod spec: containers: - name: myapp-container image: myapp startupProbe: tcpSocket: port: 8080 failureThreshold: 30 periodSeconds: 10
TCPソケットを使ったStartupProbeの例です。PodがTCPポート8080で接続を受け入れるかどうかをチェックし、接続できればアプリケーションは起動中とみなされます。チェックは10秒ごとに実行され、最大30回の失敗まで許容されます。
3つのprobeをうまく組み合わせて利用する
Kubernetesにおいて、ReadinessProbe、LivenessProbe、およびStartupProbeを組み合わせて使用することは、異常な通信を発生させにくくできるという信頼性と状態監視を自動化できるという効率性などを高めるために有効です。説明してきたように、それぞれのProbeは異なる目的と機能を持っており、合わせて使用することで、アプリケーションの稼働状況をより細かく監視し、適切に対応することが可能になります。
3つのProbeの役割
改めて3つのProbeを整理します。
- StartupProbe: アプリケーションが初めて起動する際に使用され、起動に時間がかかる場合に特に有用。このProbeが成功するまで、LivenessProbeとReadinessProbeは無効化される。
- LivenessProbe: アプリケーションが動作しているかどうかを定期的に確認し、問題がある場合にはPodを再起動させる。
- ReadinessProbe: アプリケーションがトラフィックを受け入れる準備ができているかどうかをチェックし、準備ができていない間はトラフィックを受け入れない。
設定例
以下は、3つのProbeを組み合わせたYAML設定の例です。
apiVersion: v1 kind: Pod metadata: name: myapp-pod spec: containers: - name: myapp-container image: myapp startupProbe: httpGet: path: /startup port: 8080 failureThreshold: 30 periodSeconds: 10 livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5
上記の例では
- StartupProbe: /startup
エンドポイントをチェックし、アプリケーションの起動を監視します。最大30回まで失敗を許容し、10秒ごとにチェックします。つまり最大で300秒間は監視を続けます。
- LivenessProbe: StartupProbeのチェック完了後から5秒後に/health
エンドポイントでアプリケーションが正常に動作しているかを確認し、問題があればPodを再起動します。10秒ごとにチェックを行います。
- ReadinessProbe: StartupProbeのチェック完了後から5秒後に/ready
エンドポイントでトラフィックを受け入れる準備ができているかを確認し、準備ができていない場合はトラフィックを受け入れません。5秒ごとにチェックします。
このように、3つのProbeを適切に組み合わせることで、アプリケーションが正常に起動し、安定して動作し、適切なタイミングでトラフィックを処理することが可能になります。
なお、最近のKubernetesのバージョンでは、grpcによるヘルスチェックの機能もアルファ版として提供されています。詳細は公式ページを参考にしてください( Podのライフサイクル | Kubernetes)
Go言語のtesting.Tとtesting.Mとtesting.Bとtesting.Fの違いと使い方
Go言語のtestingパッケージには、ユニットテストやベンチマークテスト、Fuzzingテストを行うためのツールが組み込まれています。
このパッケージには、いくつかの重要な型が定義されています。
testing.T
testing.T
は、ユニットテストを行うための主要な型で、ユニットテストを行うためのメソッドが用意されています。エラーや失敗を報告するためのメソッド、サブテストを作成するためのメソッドなどがあります。
package main import ( "testing" ) func Add(a int, b int) int { return a + b } func TestAdd(t *testing.T) { result := Add(2, 3) if result != 5 { t.Errorf("add(2, 3) = %d; want 5", result) } }
上記コードをThe Go Playgroundで実行したい場合は https://go.dev/play/p/i5kAxJY4wt4
testing.M
testing.M
は、テスト前のセットアップやテスト後のクリーンアップを行うための型です。TestMain
関数を定義することで、全てのテストの前後に実行したい処理を定義できます。この関数内でm.Run()
を呼び出すことで、全てのテストが実行されます。
func TestMain(m *testing.M) { // セットアップ処理を行います setup() // 全てのテストを実行します code := m.Run() // テストが完了したらクリーンアップ処理を行います cleanup() // テストの結果に基づいて終了ステータスを返します os.Exit(code) }
testing.B
testing.B
は、ベンチマークを行うための型です。ベンチマークは処理にかかる時間を計測します。testing.B
はN
というフィールドを持っており、このフィールドはベンチマークを実行する回数を示します。
func BenchmarkAdd(b *testing.B) { // ベンチマークをb.N回実行します // b.Nの値はtestingライブラリで自動的に決定されます for i := 0; i < b.N; i++ { add(2, 3) } }
testing.PB
testing.PB
は、ベンチマークを並列で実行するためのものです。for pb.Next(){}
のfor文の中に並列実行させたい処理を書きます。
func main() { testing.Benchmark(func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { DoAnyThing() } }) }) }
testing.TB
testing.TB
は、testing.T
とtesting.B
が共通に持つメソッドを持っている型です。テストとベンチマークの両方で再利用できます。
func assertCorrectSum(tb testing.TB, got, want int) { tb.Helper() if got != want { tb.Errorf("got %d; want %d", got, want) } } func TestAdd(t *testing.T) { assertCorrectSum(t, add(2, 3), 5) } func BenchmarkAdd(b *testing.B) { for i := 0; i < b.N; i++ { assertCorrectSum(b, add(2, 3), 5) } }
testing.F
testing.F
は、Go 1.18から追加された、Fuzzing(ランダムなデータを使ったテスト)を行うための型です。ランダムな入力に対する処理をテストすることができます。下記の記事にわかりやすく書かれています。
func FuzzParseMyInput(f *testing.F) { // 実行したいインプットをAddで追加します // Addした文字列以外のテストはランダムなデータが自動的に使われます f.Add("example input") // f.Fuzz内に、ランダムな入力に対するテストの挙動を記述します f.Fuzz(func(t *testing.T, s string) { // テスト対象の関数を実行します _, err := ParseMyInput(s) // 何らかのエラーが発生した場合 if err != nil { t.Fatal(err) } }) }
最後に
これ以上の詳細は、公式ドキュメントが参考になります。
React HooksのuseReducerとuseContextとカスタムフック
useReducer
useReducerの基本
Stateとdispatch(actionを送信する関数)を返すフック。
これを使うことでコンポーネント内でstate管理ができる。
書き方は以下。
const [state, dispatch] = useReducer(reducer, stateの初期値)
reducer
にはstateの更新方法を記述する。
useReducerを生成して返ってくるdispatchはstateを更新ために利用する。dispatchにactionという更新内容を渡すことでstateを更新できる。
使い方は以下。
const action = {
type: 'ADD_TODO',
text: 'Learning React'
};
// reducerではactionをswitch文で分岐して書くことで可読性が上がる
function reducer(state, action) {
switch(action.type) {
case 'ADD_TODO':
return [...state, [{ text: action.text, completed: false }]];
default:
return state;
}
}
const [state, dispatch] = useReducter(reducer, {})
dispacth(action);
別の例。
// import省略
// 現在のstateとactionを受け取り、actionに応じて更新したstateを返す関数
function reducer(state, action){
switch (action.type) {
case 'INCRE':
return {count: state.count + 1}
case 'DECRE':
return {count: state.count - 1}
case 'RESET':
return {count: 0}
default:
return state;
}
}
export default App() {
const [state, dispatch] = useReducter(reducer, { count:0 })
}
return(
<>
<p>count: {state.count}</p>
<button onClick={dispatch(state, {type: 'INCRE'})}>+</button>
<button onClick={dispatch(state, {type: 'DECRE'})}>-</button>
<button onClick={dispatch(state, {type: 'RESET'})}>reset</button>
</>
)
useReducerの使い所
複雑なstateを扱う時にはuseStateよりもuseReducerの方が良い。たとえば以下のような場合。
- stateを更新するために他のstateを参照する
- stateを更新したら別のstateも更新する必要がある
- stateを更新するロジックが複雑である
useReducerを利用すると、
- stateの更新ロジックをreducerに集約できる
- stateの更新がdispatchの記述に統一される
というメリットがある。
また、dispatchは同一性が保たれるため、React.memoにdispatchを渡しても、親コンポーネントの再レンダーによる、子コンポーネントのレンダリングを回避できる。useCallbackを使う必要はない。
useContext
useContextの基本
useContextはReactのContextと併用することで力をはっきする。
ReactのContextとは、Propsを利用せずに様々な階層のコンポーネントに値を共有できる機能。
共有したい値をContextオブジェクトとして生成し、Providerで使えるコンポーネント範囲を指定し、使いたいコンポーネントでuseContextを使って利用する、という流れ。Contextオブジェクトを利用するコンポーネントのことをConsumerと呼ぶ。
使い方は以下。
// import省略
// Contextオブジェクトの生成
const HogeContext = React.createContext();
function Child() {
// コンポーネントでの利用
const name = useContext(HogeContext);
return <h1>Hello, {name}</h1>;
}
// Providerタグで各層のコンポーネントを囲むことで、囲まれたコンポーネントはContextを共有できるようになる
// 渡したいContextをvalueで渡す
export default function App() {
const name = 'React'
return (
<HogeContext.Provider value={name}>
<Child />
</HogeContext.Provider>
)
}
Contextのメリット
Contextを利用することで得られるメリットは
- どのコンポーネントからもアクセスできるstateを作成できる
- Prop drilling問題を解消できる(階層が深いとPropsを渡すのが煩わしくなる問題)
の2点。
たとえば、共通利用するデータをContextで一元化し、Contextの更新を全てのコンポーネントにもれなく反映させることができる(下記例)。
import React, { useState, useContext, createContext } from 'react';
const LangContext = createContext();
export default function App(){
const [lang, setLang] = useState('JP')
return (
<>
<button onClick={() => setLang('JP')}>日本語</button>
<button onClick={() => setLang('EN')}>English</button>
<LangContext.Provider value={lang}>
<Header />
<Body />
</LangContext.Provider>
<>
);
}
function Header(){
const lang = useContext(LangContext);
const text = {
JP: "Reactハンズオンラーニング",
EN: "React Hands-on Learning"
};
return (
<header>
<h1>{text[lang]}</h1>
</header>
)
}
function Body() {
const lang = useContext(LangContext);
const text = {
JP: "Reactの練習",
EN: "React Training"
};
return <p>{text[lang]}</p>
}
Context利用時の注意
Contextオブジェクトを使うConsumerは、Providerのvalueが更新されるたびに再レンダーされてしまう。 これを防ぐ方法は、
- Contextを分割する
- React.memoを利用する
- useMemoを利用する
の3パターンある。
- Contextを分割する
Contextを細かく分割することで、関係あるContextの更新のみでConsumerが再レンダーされるようにする。
- React.memoを利用する
useContextを直接利用したコンポーネントの再レンダーは避けられない。ただ、そのコンポーネントでさらにReact.memo化したコンポーネントを呼ぶ形で、再レンダーを少し減らすことはできる。
//import省略
const CountContext = createContext();
function countReducer(state, action) {
switch (action.type) {
case 'INCRE':
return {count: state.count + 1}
case 'DECRE':
return {count: state.count - 1}
case 'RESET':
return {count: 0}
default:
return state;
}
}
function CountProvider({ chileren }) {
const [state, dispatch] = useReducer(countReducer, { count: 0 })
const value = { state, dispacth }
return (
<CountContext.Provider value={value}>{children}</CountProvider>
)
}
function Count() {
const { state } = useContext(CountContext)
return <h1>{state.count}</h1>
}
function Counter() {
const {dispatch} = useContext(CountContext)
return <DispatchButton dispatch={dispatch} />
}
// 上記コンポーネントでは DispatchButtonにdispatchを渡している
// valueの変更によるCounterの再レンダーは避けられないため、
// メモ化したDispatchButtonにdispatchを渡すことでDispatchButton自体の再レンダーを回避する対応を取っている
const DispatchButton = React.memo( ({ dispatch }) => {
return (
<>
<button onClick={() => dispatch({ type: "DECRE" })>-</button>
<button onClick={() => dispatch({ type: "INCRE" })>+</button>
</>
)
})
export default function App() {
return (
<CountProvider>
<Count />
<Counter />
</CountProvider>
)
}
- useMemoを利用する
useContextを使ったコンポーネントのreturnでuseMemoを返す形にすることで、再レンダーを防ぐことができる。上記のReact.memoの例のCounterを下記のように変更することで実現される。
function Counter() {
const { dispatch } = useContext(CountContext)
return useMemo(() => {
return (
<>
<button onClick={() => dispatch({ type: "DECRE" })>-</button>
<button onClick={() => dispatch({ type: "INCRE" })>+</button>
</>
)
}, [dispatch])
}
つまり、Counterのレンダリング結果(return結果)をメモ化することで、dispatchの変更以外の要因でCounterが再レンダーされたとしてもreturn内容は再レンダーされないようになる。
Contextのデメリットと利用事例
デメリットは以下。
- コンポーネントがContextに依存するため、再利用性が低下する
- グローバルなstateなので、値がどこで更新されたり利用されたりしているのかが分かりづらくなる
上記デメリットを許容してでもContextを使いたい事例
- 認証情報:ログインしているかどうかで要素の出し分けをするために利用
- テーマ:選択しているテーマに応じて、コンポーネントに適用するスタイルを制御
- 言語:選択している言語に応じて、表示する言語を切り替えるために利用する
カスタムフック
カスタムフックの基本
自作のフックのことをカスタムフックと呼ぶ。
カスタムフックのメリットは、
- 独自のロジックをカスタムフックとして切り出すことで再利用ができる
→複数のフックを組み合わせた内容を再利用できるため便利。
- 複雑なロジックをカスタムフックとして切り出すことで可読性が上がる
事例1
import React, { useState } from 'react';
// stateとstate更新ロジックを持つカスタムフックを定義
function useCounter(initialCount) {
const [count, setCount] = useState(initialCount)
const increment = () => {
setCount(count + 1)
}
const decrement = () => {
setCount(count -1)
}
return { count, increment, decrement }
}
function App() {
const {count, increment, decrement} = useCounter(0);
return (
<div>
<p>count: {count}</p>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
)
}
export default App;
事例2
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
カスタムフックの名前は必ずuseから始める必要がある。useを使うことで、linterプラグインが動作してバグ検知してくれる。
公開されているカスタムフックもあるので、便利なものがあるかもしれない。 GitHub - streamich/react-use: React Hooks — 👍 Collection of React Hooks useHooks - Easy to understand React Hook recipes
参考
React.memoとReact HooksのuseCallbackとuseMemo
React.memo
React.memoの基本
処理した結果をキャッシュとして保持することを「メモ化」と呼ぶ。React.memoはそのメモ化をするための機能。
メモ化することでコンポーネントの再レンダーを回避し、パフォーマンスを上げることができる。レンダリングに時間がかかるコンポーネントや、親が頻繁に再レンダーされる子コンポーネントなどがある場合に効果的。
逆に上記のようなコンポーネント以外では使っても意味がない。
使い方
以下のように使うことで、コンポーネントの再レンダーを避けることができる。
const Hello = React.memo(({ name }) => {
return <h1>Hello {name}>/h1>;
});
再レンダーの判断は、Propsが等価かどうかでチェックされる。上記の場合は、nameが変更されない限り、再レンダーされることはない。
以下の例ではChildコンポーネントに無駄なループをセットしているため、React.memoの効果を理解できる。
import React, { useState } from "react";
const Child = React.memo(({ count }) => {
let i = 0;
while (i < 300000000) i++; //無駄なループを設定しておく
console.log('render Child')
return <p>Child: {count}</p>
})
export default function App() {
console.log('render App')
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<>
<button onClick={() => setCount1(count1 + 1)}>countup App count</button>
<button onClick={() => setCount2(count2 + 1)}>countup Child count</button>
<p>App: {count1}</p>
<Child count={count2} />
</>
)
}
React.memoを定義せずに実行した場合は、count1が更新されたとしてもChildコンポーネントも再レンダーされ、count2には関係なくても毎回時間がかかることになってしまう。
上記の例ではChildコンポーネントをReact.memoで定義しているため、Childコンポーネントのprops(count2)が更新された時だけ再レンダーされる。count1を更新しても再レンダーされず、素早くレンダリングが完了させることができる。
React.memoではカバーできない部分
React.memoにコールバック関数を渡す場合は、React.memoを使っても再レンダーされてしまう。
// import 省略
// React.memoに関数を渡している
const Child = React.memo(({ handleClick }) => {
console.log('render Child');
return <button onClick={handleClick}>Child</button>
});
export default function App() {
console.log('render App');
const [count, setCount] = useState(0);
const handleClick = () => {
console.log('click');
};
return (
<>
<p>Counter: {count}</p>
<button onClick=() => setCount(count + 1)}>Increment count</button>
<Child handleClick={handoleClick} />
);
}
これは、関数がAppコンポーネントの再レンダリングの度に再生成されるため。オブジェクトとしては前回と異なるものと判断され、Childコンポーネントも再レンダーされるという結果になる。
この問題を解消するためには、useCallbackを使って関数をメモ化する必要がある。
useCallback
useCallbackの基本
useCallbackは関数を受け取り、コンポーネントの再レンダーを防ぐ目的で利用される。React.memoでメモ化したコンポーネントに、useCallbackでメモ化したコールバック関数をPropsとして渡すことで、不要な再レンダーを回避できるようになる。
使い方
書き方は、const 変数 = useCallback(関数, 依存配列)
となる。
一つ前の例を書き換えると以下のようになり、関数がメモ化され再レンダーされなくなる。
// import省略
const Child = React.memo(({ handleClick }) => {
console.log('render Child');
return <button onClick={handleClick}>Child</button>
});
export default function App() {
console.log('render App');
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('click');
}, []);
return (
<>
<p>Counter: {count}</p>
<button onClick=() => setCount(count + 1)}>Increment count</button>
<Child handleClick={handoleClick} />
);
}
ただし、上記の場合は依存配列を空で設定しているためuseCallbackで生成した関数は初回しかレンダリングされない。
コールバック関数が他の状態に依存する場合は、依存配列に状態変数(下の例だとcount)を追加する必要がある。
const Button = React.memo((props) => {
return <button onClick={props.onClick}>Click Me</button>
});
const App = () => {
const [count, setCount] = useState(1);
const onClick = useCallback(() => {
alert(count);
setCount(count+1);
}, [count]);
return <Button onClick={onClick}/>
};
また、React.memoしていないコンポーネントにuseCallbackでメモ化した関数を渡しても再レンダーのスキップはできないので注意。
useMemo
useMemoの基本
useMemoはメモ化された値を返すフック。
書き方は以下。
useMemo(() => 計算ロジック, 依存配列);
依存配列は計算ロジックが依存している値を格納することで、値が変わると再計算される。 また、計算ロジックだけではなくレンダー結果もメモ化できるため、React.memoと同じようにコンポーネントの再レンダーをスキップできる。
例は以下。
// import省略
export default function App() {
console.log('render App')
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
const double = (count) => {
let i = 0;
while (i < 100000000) i++; // 無駄なループをセットしてわざと時間をかけている
return count * 2;
};
const Counter = useMemo(() => {
console.log('render Counter');
const doubleCount = double(count2);
return (
<p>
Counter: {count2}, {doubledCount}
</p>
);
}, [count2]);
// Counterはcount2にしか依存しないため、count1が更新され、Appコンポーネントが再レンダーされてもCounterは再レンダーされない。
return (
<>
<h2>Increment count1</h2>
<p>Counter: {count1}</p>
<button onClick={() => setCount1(count1 + 1)}>count1 up</button>
<h2>Increment count2</h2>
{Counter}
<button onClick={() => setCount2(count2 + 1)}>count2 up</button>
</>
);
}
React.memo / useCallback / useMemoの違い
- React.memo
const コンポーネント = React.memo((props) => { return <p> props.hoge </p> })
メモ化する対象はコンポーネント。
コンポーネントを定義するときに使い、そのコンポーネントの不要なレンダーを回避する目的で使われる。
再レンダーはpropsが更新された時だけ実行される。
- useCallback
const 変数 = useCallback(() => {hoge}, [依存変数など])
メモ化する対象は関数。
React.memoのpropsが関数の場合に、そのコンポーネントの不要なレンダーを回避する目的で使われる。
関数を定義しているコンポーネントが再レンダーされると、関数は等価ではなくなるので、その関数をpropsとして受け取っている別コンポーネントはReact.memoを使っていても再レンダーされてしまう。
React.memoしているコンポーネントにuseCallbackでメモ化した関数を渡すことで、別コンポーネントの不要なレンダーを回避できる。
ただ、React.memoとか関係なくなるべく使え、って話もある。
useCallbackはとにかく使え! 特にカスタムフックでは - uhyo/blog
- useMemo
const 変数 = useMemo(() => {hoge}, [依存変数など])
メモ化する対象は計算結果の値(数値やレンダー結果)。
計算結果やレンダー結果をメモ化し、所属するコンポーネントが再レンダーされても、メモ化した変数内容が再計算されるのを回避する目的で使われる。他と違って必ずしも再レンダーの回避だけが目的ではない。
再計算のコストとuseMemoの実行コストを比較検討して、使うかどうかを判断する。
- まとめ
useCallbackはuseCallback自身がpropsとして設定されたコンポーネントの再レンダーを回避する目的で使うのに対し、useMemoはuseMemo自身に設定した関数の再実行を回避する目的で使う。
参考
React HooksのuseEffectとuseRef
useEffect
useEffectの基本
- useEffectとは何か?
コンポーネントのレンダー後か、アンマウント後に処理が稼働するフック。
「レンダー後」に稼働するため、DOMを操作できる。また、アンマウントとはコンポーネントをDOMから削除して破棄することを指す。
【React】マウントとレンダリングとその違い | Milestones ReactのuseEffectとレンダリング|NOBORI Tech|note
- 何ができるのか?
DOMの変更、APIとの通信、非同期処理などに使われる。
最新版のReact17ではいくつか破壊的変更があったらしい。
React17におけるuseEffectの破壊的変更を理解する
- どんな使い方をするか?
以下の書き方。
useEffect(処理, 依存配列);
依存配列に格納された変数などが変更されたタイミングで処理が実行される。
下記の場合はmessageが更新されるたびに処理が実行される。
useEffect(() => {
console.log(message);
}, [message]}
依存配列を省略すると、レンダリングの度に処理が実行される。
useEffect(() => {
console.log(document.getElementById('texthook').innerText);
}}
なお上記は<p id='texthook'>hogehoge</p>
の場合、コンソールにhogehoge
と表示される。
空の配列を指定すると、最初の1回だけ処理が実行される。
useEffect(() => {
console.log(message);
}, []}
処理内で関数を返すと、初回は何も実行されず、依存配列によって再度処理実行タイミングが来た時、もしくはアンマウントされた時に処理が実行される。 これは「クリーンアップ関数」と呼ばれる。
useEffect(() => {
return () => {
console.log('cleanup');
}
}
何かのステータスを変えたり、変数定義をクリアにしたいなどに使われる。
ただし、注意点としてクリーンアップ関数は、アンマウントというタイミング上、useEffectの再レンダリングの前にも実行されてしまう。
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]);
依存配列の書き忘れ防止などのためにLintツールもある(eslint-plugin-react-hooks パッケージの exhaustive-deps ルール)。
- クラスコンポーネントとの違い
useEffectはDOM更新の後に実行したいことがある場合に利用する。たとえば、APIの実行、ログ記録など。
クラスで書く場合は、componentDidMount
とcomponentDidUpdate
の2つのタイミングで処理を書く必要がある。もし同じ処理を実行させたい場合は2回書く必要がある。
一方で、useEffectの場合はコンポーネントのマウント直後か更新後かは区別しない。レンダリング後に実行する、という仕組みがあるだけなので、1回useEffectを書くだけで同じ挙動を実現できる。
また、クリーンアップについても、クラスの場合はcomponentWillUnmount
を使って定義する必要があったが、フックの場合はreturnに処理を書くだけで実現できる。
- DOM操作の例
レンダリング後でないとDOM上に要素が存在しない(取得しようとするとnullになる)ため、useEffectを使わないとDOM要素を取得できない。
const Message = () => {
// ここだとレンダリング前なのでタイミングによっては
// DOMで要素を取得できない可能性がある
const elem = document.querySelector('#message')
console.log(elem); // null
// useEffectを使うとレンダリング後に実行されるので
// 確実に取得できる
useEffect(() => {
const elem = document.querySelector('message');
console.log(elem); // HTMLDivElement
elem.innerText = 'Hello!'
);
return <div id='mesage'></iv>
};
クリーンアップ関数の例 const Timer = () => { const [time, setTime] = useState(0);
// クリーンアップ関数を登録(return)する useEffect(() => { const timerId = setInterval(() => setTime(new Date().getTime()), 1000); return () => clearInterval(timerId); }); return <div>{time}</div>
};
APIからデータを取得する例
import React, { useState, useEffect } from "react";
import axios from 'axios';
export default function App() {
const [items, setItems] = useState([]);
const [inputValue, setInputValue] = useState("react");
const [query, setQuery] = useState(inputValue);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
const result = await axios(
`https://hn.algolia.com/api/v1/search?query=${query}`
)
setItems(result.data.hits);
setIsLoading(false);
}
fetchData();
}, [query]);
return (
<>
<form onSubmit={(event) => {
event.preventDefault();
setQuery(inputValue);
}}>
<input type='text' value={inputValue} onChange={(event) => setInputValue(event.target.value)} />
<button type='submit'>検索</button>
</form>
{isLoading ? (
<p>Loading</p>
) : (
<ul>
{items.map((item) => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)}
</>
)
}
useEffectの依存配列としてqueryを指定しているため、検索ワードが変わらないと処理は実行されない。つまり、同じ検索ワードのまま連打しても処理は1回だけ実行されることになる。
event.preventDefault();
については以下を参照。
【JavaScript】event.preventDefault()が何をするのか - Qiita
useEffectを同期的に実行したい場合は
useEffectは非同期で処理されるため、ブラウザの動きをブロックしない。もし、同期させて処理させたい場合(レイアウトによって分岐させるなど)は、useLayoutEffect という別のフックが使える。機能はuseEffectと同じで、実行タイミングが同期的になり、レンダリング→useLayoutEffect→ブラウザ表示 の順に実行される。 useEffectはレンダリングの後にブラウザ表示・useEffectが同時実行されるイメージ。
useRef
useRefの基本
- useRefとは何か
useStateと同じく値を保存できるが、値を更新してもレンダリングが発生しない違いがある。
- 何ができるのか
DOMを参照する、もしくは前回の値を参照する時などに利用される。
- どんな使い方をするのか
useRef(初期値)
currentプロパティで現在の値を参照できる。
const count = useRef(0);
console.log(count.current); // 0
count.current = count.current + 1
console.log(count.current); // 1
DOM参照の例
useRefで参照を作り、それをJSXに渡しておくことで、useEffectで参照できる。
const Message = () => {
const divRef = useRef(null);
useEffect(() => {
// ref.currentで現在の参照の値を取得できる
// ここではdiv要素のDOM
divRef.current.innerText = ‘Hello!’;
}, []);
// refに渡しておく
return <div ref={divRef}></div>
};
前回の値を参照する例
import React, { useState, useEffect, useRef } from "react";
export default function App() {
const [count, setCount] = useState(10);
const prevCountRef = useRef(0);
useEffect(() => {
prevCountRef.current = count;
});
return (
<>
<p>
現在のcount: {count}, 前回のcount: {prevCountRef.current}
</p>
<p>前回のcountより{prevCountRef.current > count ? "小さい" : "大きい"}</p>
<button onClick={() => setCount(Math.floor(Math.random() * 11))}>update</button>
</>
)
}
上記のようにすることで、useRefで定義されたオブジェクトが更新されても、再レンダーは実行されず、useEffect内の処理も稼働しない。
currentの値が更新された時のみ再レンダーが稼働し、useEffect内の処理も稼働する。
もしuseRefを使わずに let prevCountRef = 0;
のようにして実装しようとすると、再レンダー時に毎回0に初期化されてしまう。
もしuseRefを使わずにuseStateを使ってしまうと、countが更新され、再レンダー→useEffect稼働となり、prevCountRefがcurrentと同じ値になった後に、画面が描画される。つまり、常に同じ値が表示されてしまい、意図とは異なる挙動になってしまう。
参考
React Hooksの基本とuseState
Hooksとは
HooksはReactの16.8.0
バージョンから追加された新しい機能。
Hooksを使うことで、ReactのComponentを関数で定義することできる。従来はクラスによる定義のみであったが、関数でも定義できるようになったことで、
- 同等の機能を実装する場合、クラスで実装するよりもコード量が少なくなる
- ロジックを分離できるため再利用やテストがしやすい
といったメリットを得られる。
仕組み
- フックの呼び出しとコンポーネントをどう関連づけているか?
- Reactはどのコンポーネントが現在レンダー中なのかを把握しており、メモリとして保持している。
- また、Reactはフックが呼ばれる順番をメモリとして保持している。そのため、フックとコンポーネントを紐付けて管理できている。
注意点
- フックはトップレベルでのみ定義すべき。上記の通り、フックが呼ばれる順番をReactは覚えるため、ループやif文の中で定義すると条件によっては順番が変わってしまい、エラーやおかしな挙動の原因になる。もし必要ならが、useEffectの中でループやif文を実行する、という回避策をとる。
- Reactの関数コンポーネントの中でのみ定義すべき。
上記を2点をチェックできるlinterツールがあるので使う。create react appにはデフォルトで含まれている。インストールする場合は npm install eslint-plugin-react-hooks —-save-dev
を実行する。
eslint-plugin-react-hooks - npm
ESLint confファイルの事例
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
useState
useStateの基本
書き方は、const [変数名, 更新関数名] = useState(初期値)
const [hogeNum, setHogeNum] = useState(0);
const [gehoStr, setGehoStr] = useState("hoge");
setHogeNum(10) // hogeNumに 10 に更新される
setGehoStr('geho') // gehoStrが 'geho' に更新される
更新関数に値を渡す → stateが渡された値に更新される
更新関数に関数を渡す → その関数の引数にstateの現在の値が入り、returnで渡された値が新たなstateになる。以下事例。currentCount
に現在のcountの中身が格納される。
import React, { useState } from "react";
export default function App() {
const [count, setCount] = useState(10);
const decrement = () => {
setCount((currentCount) => currentCount - 1);
}
const increment = () => {
setCount((currentCount) => currentCount + 1);
}
return (
<>
<p>Count: {count}</p>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</>
)
}
setCount((currentCount) => currentCount - 1);
ではなく、setCount(count - 1);
でも同等の結果になる。しかし、関数を渡した方が、stateを更新するロジックが外部に依存しない形になる。
こうすることで、更新するロジックが長くなって読みにくくなった場合でも、関数ごと外出しすることで読みやすさを保つことができる。
オブジェクトやリストもstateとして扱える
数値や文字列だけではなく、オブジェクト型やリスト型も利用できる。
- オブジェクト型の例
更新メソッドを利用するときには、スプレッド演算子を使って更新する必要がある(後述の「いつ再レンダーされるか」を参照)。
import React, { useState } from "react";
export default function App() {
const [count, setCount] = useState({ men: 0, women: 0 })
const comingMen = () => {
setCount((currentCount) => ({ ...currentCount, men: currentCount.men + 1 }));
}
const comingWomen = () => {
setCount((currentCount) => ({ ...currentCount, women: currentCount.women + 1 }))
}
return (
<>
<p>Men: {count.men}</p>
<p>Women: {count.women}</p>
<button onClick={comingMen}>Men</button>
<button onClick={comingWomen}>Women</button>
</>
)
}
- リスト型の例
追加はオブジェクト型と同じくスプレッド構文を使って更新できるが、削除は色々方法がある。ここではfilterを利用している。
import React, { useState } from "react";
export default function App() {
const [years, setYears] = useState([2019, 2020, 2021])
const addYear = () => {
setYears((current) => ([...current, current[current.length - 1] + 1]));
}
const deleteYear = () => {
setYears(years.filter((_, i) => i != years.length - 1))
}
return (
<>
<p>Years: {years.map((year) => year + ',')}</p>
<button onClick={addYear}>add year</button><button onClick={deleteYear}>del year</button>
</>
)
}
再レンダーされるタイミング
オブジェクト型のstateを更新するときに、以下のような書き方をしてしまうと再レンダーされない(上記の例の一部を抜粋)。
const comingMen = () => {
count.men = count.men + 1;
setCount(count);
}
公式ページ(フック API リファレンス – React)にあるように、ReactではObject.jsによる比較アルゴリズムを利用している。 たとえば
const hoge = { num: 1 };
const geho = { num: 1 };
console.log(Object.is(hoge, hoge)); // trueになるため同一とみなされる
console.log(Object.is(hoge, geho)): // 値は同じでもfalseになる
つまり、オブジェクトが同じであればtrueになり、別であればfalseになる。
count.men = count.men + 1
としただけでは、countオブジェクトは同一であり何も変わっていないとみなされるため、再レンダーされない。
newCount = { ...count, men: count.men + 1 }
のようにスプレッド演算子を利用して新しいオブジェクトを生成すれば別のオブジェクトとしてみなされ、再レンダーが実行される。
リスト型の場合でも、list.push()
やlist.splice()
を使うとオブジェクトが変化していないと判断されてしまうため注意。
参考
文章を書くときのチェックリスト
書く前にやること
- 記事を書く目的を明確にする
- 読み手を意識する
- 構成を考え、要素を箇条書きにする(結論、理由、具体例、結論など定型を考える)
- 「である調」「ですます調」のどちらにするか決める →「である」は強い印象を与えるが端的に書ける。「ですます」は丁寧な印象を与えるが冗長になりがち。
- 見出しを考える(具体的な内容にする)
書くときに意識すること
見た目/読みやすさ
- リズムが良い、声を出して呼吸する場所で読点(、)を打っているか
- 5行以内で改行ができているか
- 適度な余白があるか
- 漢字とひらがなのバランスが良いか
- 同じ言葉の重複がないか(省略するか、言い換える)
- 話し言葉が混ざっていないか
理解しやすさ
- 1文の長さは60文字以内か
- ワンセンテンス、ワンメッセージになっているか
- 主語と述語が遠くなっていないか
- 修飾する語と修飾される語を近くに置いているか(修飾語が多く読みにくいときは文章を分ける)
- 見出しは具体的か、本文と合っているか
- 単純接続の「が」を使っていないか →「XXについてですが、どう思いますか?」という文章は単純接続の「が」の使い方。分かりにくくなるため、文章を分けるか、表現を変える。「が」は逆説の時だけ使うようにする。
その他
- 比喩、例え話を積極的に使っているか
- 体験談が入っているか(説得力を高めたい内容の場合)
書き終わった後にやること
- 上記チェックポイントを再度チェックする
- 削れる言葉を削る →接続詞、主語、副詞(とても、すごく)、指示語など。ただし、削るか残すか迷ったときは残すようにする。
- 時間をおいて読み直す
- 声に出して読み直す
- 誰かに読んでもらう
参考
pythonの関数とclosureのメモ
python function is first object
>>> def outer(): ... def inner(): ... print('hoge') ... return inner() ... >>> outer() hoge
inner()を返している点に注目。ここではouter()を呼び出すことでprint文の実行結果が返される。 innerを返す内容に変えてみる。
>>> def outer(): ... def inner(): ... print('hoge') ... return inner ... >>> outer() <function outer.<locals>.inner at 0x102931b80>
今度は、outer()を呼び出すとfunctionであることが返される。 ではこれをtestという変数に格納し、()をつけて呼び出すとどうなるか。
>>> test = outer() >>> test() hoge
print文の実行結果が返される。つまり下みたいなこともできてしまう。
>>> outer()() hoge
クロージャ(Function closure)について 関数はローカルスコープとして変数の名前空間を持っている。この名前空間は関数の処理が終わると無くなる。
>>> def hoge(x): ... print(x) ... >>> hoge(2) 2 >>> print(x) Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'x' is not defined
当たり前と言えば当たり前。では以下の場合はどうなるか。
>>> def outer(x): ... y = 2 ... def inner(): ... print(x, y) ... return inner ... >>> hoge = outer(1)
最後の行ではouter(1)の処理が終わった後にreturnsrされるinner関数がhogeに代入されているので、名前空間が消えているのではないか?上記と同じエラーになるのではないか?という疑問が出てくる。 しかし、実行してみると、
>>> hoge <function outer.<locals>.inner at 0x104afdb80> >>> hoge() 1 2
ちゃんと意図した通りに実行できる。これは、PythonがFunction closure(クロージャ)という機能を持っているから。クロージャとは、グローバルスコープ以外で定義された関数(この場合inner)が、定義時の自分を囲むスコープの情報を保持する、というもの。
hogeのクロージャプロパティ(closure)を確認すると、intオブジェクトを2つ持っていることがわかる。
>>> hoge.__closure__ (<cell at 0x102927c10: int object at 0x10276e108>, <cell at 0x1029a7f40: int object at 0x10276e128>)
AWSのVPCなどのネットワークをTerraformで作成する
はじめに
AWSのVPCやサブネットなどのネットワークリソースを新規にTerraformで作成します。ネットワーク内でのローカル通信、外部からのアクセス、外部へのアクセスが可能なネットワークにします。
Terraformの基本的な使い方や、インストール方法などは、
を参考にしてみてください。
この先はTerraformがすでにインストールされている前提で進めます。
目次
実行環境
- Ubuntu:18.04
- Terraform: v0.13.4
作成するリソース
AWSにて新たにネットワーク環境を構築するにあたって必要になるリソースは、
- VPC:ネットワークアドレスを持った論理プライベートネットワーク
- サブネット:AZを跨いで配置するVPCを分割したネットワーク
- インターネットゲートウェイ:インターネットとVPC内を繋ぐためのサービス
- ルートテーブル:VPC内部やインターネットゲートウェイなどとのルーティングを設定する表
です。
それぞれをTerraformにて定義していきます。
各リソースをTFファイルとして作成
各リソースの書き方のルールについては、
を参考にできます。
VPC
まずはVPCを作成します。
resource "aws_vpc" "sample_vpc" { cidr_block = "10.10.0.0/16" # VPCに設定したいCIDRを指定 enable_dns_support = "true" # VPC内でDNSによる名前解決を有効化するかを指定 enable_dns_hostnames = "true" # VPC内インスタンスがDNSホスト名を取得するかを指定 instance_tenancy = "default" # VPC内インスタンスのテナント属性を指定 assign_generated_ipv6_cidr_block = "false" # IPv6を有効化するかを指定 tags = { Name = "sample_vpc" } }
必須設定はcidr_block
のみです。
上記では、任意の設定のうち、デフォルト値から変えていない設定もあえて記載しています。なぜ設定しているかと言うと、Infrastructure as Codeとしてインフラの設定をコードとして残しておきたい場合は、デフォルトの設定でも必要なものは明記しておいた方が賢明だからです。「デフォルト値で良いと判断して何も書かなかった場合」と「その任意設定を知らない(意識していない)ことで何も書かなかった場合」の混在を避けることができるためです。
なお、instance_tenancy
は、VPC内で起動するインスタンスのテナント属性を設定できます。テナント属性の種類は下記のように3種類あります。
さらなる詳細は、Terraform Registry AWS VPCを参考にしてください。
サブネット
次にサブネットを作成します。
resource "aws_subnet" "sample_subnet_a" { vpc_id = aws_vpc.sample_vpc.id # VPCのIDを指定 cidr_block = "10.10.1.0/24" # サブネットに設定したいCIDRを指定 assign_ipv6_address_on_creation = "false" # IPv6を利用するかどうかを指定 map_public_ip_on_launch = "true" # VPC内インスタンスにパブリックIPアドレスを付与するかを指定 availability_zone = "ap-northeast-1a" # サブネットが稼働するAZを指定 tags = { Name = "sample_subnet_1a" } } resource "aws_subnet" "sample_subnet_c" { vpc_id = aws_vpc.sample_vpc.id cidr_block = "10.10.1.0/24" assign_ipv6_address_on_creation = "false" map_public_ip_on_launch = "true" availability_zone = "ap-northeast-1c" tags = { Name = "sample_subnet_1c" } }
2つのAZそれぞれにサブネットを作成する内容にしています。
必須設定はvpc_id
とcidr_block
です。
インターネットゲートウェイ
続いてインターネットゲートウェイを作成します。
resource "aws_internet_gateway" "sample_igw" { vpc_id = aws_vpc.sample_vpc.id # VPCのIDを指定 tags = { Name = "sample_igw" } }
必須設定はvpc_id
のみです。
ルートテーブル
さて、VPC/サブネットとインターネットゲートウェイを作成したので、それぞれでルーティングできるようにルートテーブルと、紐付け設定を作成していきます。
まずはルートテーブルです。ルートテーブルは「送信されたパケットの宛先IPアドレスを確認し、どこに通信を流すかを判断する時に使う表」です。
そのため、ルートの設定には、宛先IPアドレスとターゲットの属性(local、gateway、特定のEC2、など)を指定する必要があります。以下では、外部向け通信を可能にするためのルート設定を加えています。
resource "aws_route_table" "sample_rt" { vpc_id = aws_vpc.sample_vpc.id # VPCのIDを指定 # 外部向け通信を可能にするためのルート設定 route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.sample_igw.id } tags = { Name = "sample_rt" } }
なお、サブネット内での通信を可能にするためのルートの設定も必要ですが、これは自動的に作成されるので、設定していません。
ルートテーブルを作成したので、実際のVPCやサブネットと紐づける設定を作成します。
resource "aws_main_route_table_association" "sample_rt_vpc" { vpc_id = aws_vpc.sample_vpc.id # 紐づけたいVPCのIDを指定 route_table_id = aws_route_table.sample_rt.id # 紐付けたいルートテーブルのIDを指定 } resource "aws_route_table_association" "sample_rt_subet_a" { subnet_id = aws_subnet.sample_subnet_a.id # 紐づけたいサブネットのIDを指定 route_table_id = aws_route_table.sample_rt.id # 紐付けたいルートテーブルのIDを指定 } resource "aws_route_table_association" "sample_rt_subnet_c" { subnet_id = aws_subnet.sample_subnet_c.id route_table_id = aws_route_table.sample_rt.id }
それぞれ、VPC本体、サブネット2つとルートテーブルを紐づけています。
作成したTFファイルを適用して実際にAWSリソースを作成する
ここまでの設定を全て書いたnetworks.tf
というファイルを用意します。
また以下の内容でproviders.tf
を作成し、AWSへのリソース作成であることを明示します。
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 3.10.0" } } } provider "aws" { region = "ap-northeast-1" }
これらのファイルがあるディレクトリで、以下のコマンドを実行し、AWSリソースを作成します。(アクセスしたいAWSアカウントのconfigureの設定は完了している前提です。)
terraform init # 初期化 terraform plan # 設定が正しいかを事前確認 terraform apply # 適用
以下のメッセージが出れば問題なく設定完了です。
Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
AWSのコンソールを確認することで、設定した通りのVPCやルートテーブルが作成されていることがわかると思います。
別途、作成したVPCを紐付けたEC2を作成することで、sshによるログインや外部からのライブラリのインストールなどが可能なことを確認できます。
環境変数およびmoduleを利用して扱いやすくする
さて、以上で問題なく新たなネットワークを作成できましたが、ここではTFファイルをさらに扱いやすくするため、環境変数化とmodule化をしていきます。
環境変数化
環境変数を利用してTFファイルを再利用可能にします。
環境変数を利用するには、variables.tf
をいうファイルで環境変数を定義しておく必要があります。
今回は、VPC/サブネットのCIDRと環境(devかprodか)を環境変数として設定してみました。
以下はvariables.tf
の例です。
variable ENV { type = string description = "環境(prod/dev)" default = "dev" } variable VPC_CIDR { type = string description = "VPCのCIDR" default = "10.0.0.0/16" } variable SUBNET_A_CIDR { type = string description = "subnet aのCIDR" default = "10.0.1.0/24" } variable SUBNET_C_CIDR { type = string description = "subnet cのCIDR" default = "10.0.2.0/24" }
また、networks.tf
を以下のように変更します。
変更点としては、各所で環境変数を利用した点と、tagにENVの情報を付与した点です。
resource "aws_vpc" "sample_vpc" { cidr_block = var.VPC_CIDR # VPCに設定したいCIDRを指定 enable_dns_support = "true" # VPC内でDNSによる名前解決を有効化するかを指定 enable_dns_hostnames = "true" # VPC内インスタンスがDNSホスト名を取得するかを指定 instance_tenancy = "default" # VPC内インスタンスのテナント属性を指定 assign_generated_ipv6_cidr_block = "false" # IPv6を有効化するかを指定 tags = { Name = "sample_vpc_${var.ENV}" Env = var.ENV } } resource "aws_subnet" "sample_subnet_a" { vpc_id = aws_vpc.sample_vpc.id # VPCのIDを指定 cidr_block = var.SUBNET_A_CIDR # サブネットに設定したいCIDRを指定 assign_ipv6_address_on_creation = "false" # IPv6を利用するかどうかを指定 map_public_ip_on_launch = "true" # VPC内インスタンスにパブリックIPアドレスを付与するかを指定 availability_zone = "ap-northeast-1a" # サブネットが稼働するAZを指定 tags = { Name = "sample_subnet_${var.ENV}_1a" Env = var.ENV } } resource "aws_subnet" "sample_subnet_c" { vpc_id = aws_vpc.sample_vpc.id cidr_block = var.SUBNET_C_CIDR assign_ipv6_address_on_creation = "false" map_public_ip_on_launch = "true" availability_zone = "ap-northeast-1c" tags = { Name = "sample_subnet_${var.ENV}_1c" Env = var.ENV } } resource "aws_internet_gateway" "sample_igw" { vpc_id = aws_vpc.sample_vpc.id # VPCのIDを指定 tags = { Name = "sample_igw_${var.ENV}" Env = var.ENV } } resource "aws_route_table" "sample_rt" { vpc_id = aws_vpc.sample_vpc.id # VPCのIDを指定 # 外部向け通信を可能にするためのルート設定 route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.sample_igw.id } tags = { Name = "sample_rt_${var.ENV}" Env = var.ENV } } resource "aws_main_route_table_association" "sample_rt_vpc" { vpc_id = aws_vpc.sample_vpc.id # 紐づけたいVPCのIDを指定 route_table_id = aws_route_table.sample_rt.id # 紐付けたいルートテーブルのIDを指定 } resource "aws_route_table_association" "sample_rt_subet_a" { subnet_id = aws_subnet.sample_subnet_a.id # 紐づけたいサブネットのIDを指定 route_table_id = aws_route_table.sample_rt.id # 紐付けたいルートテーブルのIDを指定 } resource "aws_route_table_association" "sample_rt_subnet_c" { subnet_id = aws_subnet.sample_subnet_c.id route_table_id = aws_route_table.sample_rt.id }
これでterraform apply
時に自動的にvariables.tf
の中身を読み込み、デフォルト値に設定した環境変数が反映されます。
module化
TFファイルをさらに共有して使いやすくするために、module化します。
以下のようなディレクトリ構成を作り、各ファイルを格納します。
├── modules │ ├── networks.tf │ ├── provider.tf │ └── variables.tf └── services └── dev └── main.tf
main.tf
には環境変数とともに実行するソースモジュールを指定します。
module "samples" { source = "../../modules/" ENV = "dev" VPC_CIDR = "10.10.0.0/16" SUBNET_A_CIDR = "10.10.1.0/24" SUBNET_C_CIDR = "10.10.2.0/24" }
この状態で、services/dev
ディレクトリでapplyすることで、main.tf
がmodulesの全てのファイルを読み込み、AWSリソースを作成してくれます。
cd services/dev
terraform init
terraform apply
以上のようにmodule化することでmain.tf
の中身を変えるだけで様々な設定のネットワークを作成することができるようになります。
Terraformを実際に使えるようになるためのTerraform入門+ちょっと実践
はじめに
AWSのリソース作成を参考にTerraformの基本的な使い方と、実際に開発する時に必要になるファイル構成や環境変数を使い方などを解説します。
目次
実行環境
- Ubuntu:18.04
- Terraform: v0.13.4
インストール
以下、公式ページに各環境でのインストール方法が記載されています。(英語)
Install Terraform | Terraform - HashiCorp Learn
今回は、Linux/Ubuntuなので以下のコマンドでインストールします。
$ curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - $ sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" $ sudo apt-get update && sudo apt-get install terraform $ terraform -v
バージョンが表示されればインストール完了です。
タブ補完ができると非常に便利なので、以下のコマンドで有効化しておきます。
$ terraform -install-autocomplete
まずは使ってみる(QuickStart的な)
AWSのconfig設定
AWSリソースにアクセスするための~/.aws/
ディレクトリ内にconfigが用意されている必要があります。
設定していない場合は、aws configure
コマンドにてIAMユーザのアクセスキー /シークレットアクセスキー などを入力して設定してください。
参考:設定の基本 - AWS Command Line Interface
EC2インスタンスを作成する
以下2つのファイルを作成します。
provider.tf
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 3.10.0" } } } provider "aws" { region = "ap-northeast-1" }
ec2.tf
resource "aws_instance" "sample" { ami = "ami-02b658ac34935766f" # Ubuntu Server 18.04 LTS (HVM), SSD Volume Type(64bit x86) instance_type = "t2.micro" tags = { Name = "sample-instance" } }
ここではt2.micro
のEC2インスタンスを1つ立ち上げる内容になっています。
では実際にTerraformコマンドを使って、定義した内容を適用していきます。
最初に初期化が必要です。初期化することで.terraform
ディレクトリが作成され、その中にいくつかterraform関連のファイルが作成されます。
$ terraform init Initializing the backend... Initializing provider plugins... - Finding hashicorp/aws versions matching "~> 3.10.0"... - Installing hashicorp/aws v3.10.0... - Installed hashicorp/aws v3.10.0 (signed by HashiCorp) Terraform has been successfully initialized! You may now begin working with Terraform. Try running "terraform plan" to see any changes that are required for your infrastructure. All Terraform commands should now work. If you ever set or change modules or backend configuration for Terraform, rerun this command to reinitialize your working directory. If you forget, other commands will detect it and remind you to do so if necessary.
続いて、事前のドライランとして定義内容に問題がないかのチェックをします。どんなリソースが追加されるか一気に表示されます。(出力内容は長いので割愛します)
terraform plan
最後の方に
Plan: 1 to add, 0 to change, 0 to destroy.
と表示されるため、想定通りの内容かどうかの確認ができます。
では実際にTFファイルを適用していきます。以下のコマンドを実行します。
terraform apply
ドライランの時と同じ出力が表示され、
Plan: 1 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value:
と聞かれますので、yesを入力します。
実際に適用が開始され、以下の表示が出ればOKです。
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
ここまで来たら、AWSコンソールを確認してみましょう。
実際にEC2インスタンスが作成されているはずです。
今回はネットワーク設定などは明記していないため、デフォルトの設定が適用されていると思いますが、必要であれば細かい設定を記述することができます。
また、terraform show
コマンドにて「現在、どんな内容がAWSに適用されているのか」を確認できます。
EC2インスタンスの設定を変更する
さて次に、ec2.tf
の中身を以下のように変更してみます。
resource "aws_instance" "sample" { ami = "ami-02b658ac34935766f" # Ubuntu Server 18.04 LTS (HVM), SSD Volume Type(64bit x86) instance_type = "t2.micro" tags = { Name = "changed-instance" } }
インスタンスの名前を変更しているだけですが、これを適用してみます。
ドライランさせることで差分を確認できます。
terraform plan
差分は以下のように表示されます。
~ tags = { ~ "Name" = "sample-instance" -> "changed-instance" }
差分が想定通りであることを確認したら、インスタンスを作成する時と同様terraform apply
コマンドを実行します。
この時の出力は、changedのみが1になっていると思います。このことから想定どおり変更されたことがわかります。
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
リソースの変更もリソース作成の時と同様に、terraform apply
をするだけで実現できました。
全てのリソースを削除する
全てのリソースを削除するには、
terraform destroy
というコマンドを実行するだけです。
また、一部のリソースを削除したい場合は、TFファイルの削除したい部分をコメントアウトし、terraform apply
するだけで実現できます。
Terraformの基本的な使い方
ここまでで超基本的なTerraformの使い方、使い勝手がわかったと思います。
ここからは実際にTerraformを使って様々なリソースを作成/変更するときに必要になる使い方を説明していきます。
基本的なフォルダ構成
provider.tf
: プロバイダ設定ができるファイルですvariables.tf
::変数設定ができるファイルですmain.tf
:実際に作成したいAWSリソースを記載します(ファイル名はなんでもOKですが、mainとしておいた方がこのファイルが起点になっている、ということが分かると思います)
provider.tf
AWSのアクセス先などの基本的な情報を格納します。
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 3.10.0" } } } provider "aws" { region = "ap-northeast-1" }
詳細は公式ページを確認ください。
直接AWSのアクセスキーとシークレットアクセスキー を指定することもできますが、セキュリティ上は避けた方が良いです。
variables.tf
TFファイルで環境変数を利用するには、variables.tf
にて定義する必要があります。
書き方は、
variable 変数名 { type: 変数の型 stringなど description: 説明 default: デフォルト値 }
です。
使い方は、var.変数名
を指定するだけです。
例えば、
variable aws_instance_type { type = string description = "AWSインスタンス種別" default = "t2.micro" }
という環境変数を定義した場合、
resource "aws_instance" "sample" { count = 1 ami = "ami-02b658ac34935766f" # Ubuntu Server 18.04 LTS (HVM), SSD Volume Type(64bit x86) instance_type = var.aws_instance_type tags = { Name = "instance-${var.aws_instance_type}" # 文字列内で使いたい場合は${}で囲むだけ } }
といった使い方ができます。
変数への値の設定方法
変数を設定する方法は、3通りあります。
- オプションで渡す:
terraform apply -var 'aws_instance_type=t2.large'
のように-var
を使う - 環境変数で渡す:
TF_VAR_変数名
として環境変数を設定しておくことで、terraformコマンド実行時に自動的に読み込まれる - ファイルで渡す:
terraform.tfvars
という名前でファイルを作成し変数名 = 値
を書いておくことで、自動的に読み込まれる
上記の方法で変数の値を設定しなかった場合は、デフォルト値が設定されます。デフォルト値が設定されていなければ、terraform
コマンド実行時にエラーになります。
リソースの書き方
基本的な書き方は、
resource "リソースの種類" "リソース名" { (リソースの設定値) }
のように、リソースの種類とリソース名(任意の文字列)を設定し、設定値を中に記載していく形になります。
リソースの種類は、Terraformであらかじめ定義されており、AWSであればaws_
という接頭語が付けられています。(EC2はaws_instance
、S3はaws_s3
、など)
リソースの種類やブロック内の各リソースの設定項目は、公式ページにまとまっていますので、実際に作成する時に参考になります。例えば、ECSクラスターの設定項目は以下のArgument Reference
に記載されています。
他リソースの属性を参照する
QuickStartではEC2のみを定義して作成しましたが、実施に利用する時には複数のリソースを定義することが多いと思います。
その時に、「同時に複数リソースを定義してapplyしたいけど、その時に作成されるリソースの情報が他のリソースで必要、、、」となる時があります。例えば、「セキュリティグループにVPCを指定したいけど、VPC自体も今回作成するため識別子がわからない、、、」といった時です。
そんな時は、他リソースの属性をリソースの種類.リソース名.属性名
と指定して参照することできます。
以下はVPCとセキュリティグループの定義です。
resource "aws_vpc" "sampleVPC" { cidr_block = "10.1.0.0/16" instance_tenancy = "default" enable_dns_support = "true" enable_dns_hostnames = "false" tags { Name = "sampleVPC" } } resource "aws_security_group" "admin" { name = "admin" description = "Allow SSH" vpc_id = aws_vpc.sampleVPC.id # ここで同時に作成するvpcの属性を指定 ingress { from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } }
もし、他のリソースから上記のセキュリティグループを参照したい場合は、aws_security_group.admin.id
といった具合に利用できます。
リソース間の依存関係は自動で解決してくれるため、適用順序は基本的には気にしないでOKです。depends_on
を使って明記することもできます。
resource "aws_security_group" "admin" { name = "admin" description = "Allow SSH" vpc_id = aws_vpc.sampleVPC.id depends_on = aws_vpc.sampleVPC.id # vpcが作成されてから作成することを明記 ingress { from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } }
参考
さらに細かく理解したい人は
以下のサイトや本に実用的な使い方が細かくまとまっていますので、一読し、Referenceとして使うと良いと思います。