From 94b734ecab9d1f14614646d468b34fdf32ad71b6 Mon Sep 17 00:00:00 2001 From: Marc Gavanier Date: Tue, 5 Sep 2023 17:07:44 +0200 Subject: [PATCH] initial commit --- .github/workflows/terraform.yml | 70 ++++++++++++++++++++ .gitignore | 113 ++++++++++++++++++++++++++++++++ .terraformignore | 4 ++ LICENSE.md | 21 ++++++ README.md | 102 ++++++++++++++++++++++++++++ api-gateway.tf | 58 ++++++++++++++++ dist/goodbeye/goodbye.get.js | 18 +++++ dist/goodbeye/goodbye.get.zip | Bin 0 -> 461 bytes dist/hello/hello.get.js | 18 +++++ dist/hello/hello.get.zip | Bin 0 -> 455 bytes global.tf | 31 +++++++++ lambda.tf | 59 +++++++++++++++++ main.tf | 21 ++++++ output.tf | 17 +++++ s3.tf | 5 ++ tags.tf | 21 ++++++ 16 files changed, 558 insertions(+) create mode 100644 .github/workflows/terraform.yml create mode 100644 .gitignore create mode 100644 .terraformignore create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 api-gateway.tf create mode 100644 dist/goodbeye/goodbye.get.js create mode 100644 dist/goodbeye/goodbye.get.zip create mode 100644 dist/hello/hello.get.js create mode 100644 dist/hello/hello.get.zip create mode 100644 global.tf create mode 100644 lambda.tf create mode 100644 main.tf create mode 100644 output.tf create mode 100644 s3.tf create mode 100644 tags.tf diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml new file mode 100644 index 0000000..3f2ae3e --- /dev/null +++ b/.github/workflows/terraform.yml @@ -0,0 +1,70 @@ +name: 'Terraform' +on: + push: + branches: + - main + pull_request: +jobs: + terraform: + name: 'Terraform' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }} + - name: Terraform Format + id: fmt + run: terraform fmt -check + - name: Terraform Init + id: init + run: terraform init + env: + TF_WORKSPACE: production + - name: Terraform Validate + id: validate + run: terraform validate -no-color + - name: Terraform Plan + id: plan + if: github.event_name == 'pull_request' + run: terraform plan -no-color -input=false + continue-on-error: true + - name: Update Pull Request + uses: actions/github-script@v6.0.0 + if: github.event_name == 'pull_request' + env: + PLAN: "terraform\n${{ steps.plan.outputs.stdout }}" + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\` + #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\` + #### Terraform Plan 📖\`${{ steps.plan.outcome }}\` + #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\` + +
Show Plan + + \`\`\`\n + ${process.env.PLAN} + \`\`\` + +
+ + *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }) + - name: Terraform Plan Status + if: steps.plan.outcome == 'failure' + run: exit 1 + - name: Terraform Apply + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: terraform apply -auto-approve -input=false + env: + TF_WORKSPACE: production diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..51d7e7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,113 @@ +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# JetBrains settings +.idea/ + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# Created by .ignore support plugin (hsz.mobi) +### Terraform template +# Local .terraform directories +.terraform/ + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log + +# Exclude all .tfvars files, which are likely to contain sentitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +# +*.tfvars + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc +.terraform.lock.hcl diff --git a/.terraformignore b/.terraformignore new file mode 100644 index 0000000..a0efa2a --- /dev/null +++ b/.terraformignore @@ -0,0 +1,4 @@ +.git/ +.github/ +.gitignore +.terraformrc diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..adf239f --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2011-2023 Cartographie Nationale des lieux d'inclusion numérique + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1662d1d --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# API infrastructure + +## À propos + +API infrastructure décrit par le code l'infrastructure à provisionner pour le bon fonctionnement de [l'API de la cartographie nationale des lieux d'inclusion numérique](https://github.com/anct-cartographie-nationale/api-application). + +> Ce dépôt n'est pas utile pour faire fonctionner la Cartographie Nationale en local. + +## Table des matières + +- 🪧 [À propos](#à-propos) +- 📦 [Prérequis](#prérequis) +- 🚀 [Installation](#installation) +- 🛠️ [Utilisation](#utilisation) +- 🤝 [Contribution](#contribution) +- 🏗️ [Construit avec](#construit-avec) +- 📝 [Licence](#licence) + +## Prérequis + +- [Docker](https://www.docker.com/) ou [Terraform CLI](https://www.terraform.io/cli) + +## Installation + +La commande suivante permet d'utiliser la ligne de commande terraform via Docker : +```shell +docker run --rm -it --name terraform -v ~/:/root/ -v $(pwd):/workspace -w /workspace hashicorp/terraform:light +``` + +Pour une utilisation simplifiée, il est possible de créer un alias : +```shell +alias terraform='docker run --rm -it --name terraform -v ~/:/root/ -v $(pwd):/workspace -w /workspace hashicorp/terraform:light' +``` + +Avec cet alias, il n'y a plus de différence entre une commande terraform exécutée avec Docker ou avec Terraform CLI. + +## Utilisation + +### Vérifier et corriger la syntaxe des fichiers `.tf` + +```shell +terraform fmt +``` + +### Vérifier la cohérence de l'infrastructure + +```shell +terraform validate +``` + +### Récupérer un jeton d'authentification à Terraform Cloud en local + +```shell +terraform login +``` + +### Initialiser l'état et les plugins en local + +```shell +terraform init +``` + +### Planifier une exécution pour voir les différences avec l'état précédent de l'infrastructure + +```shell +terraform plan +``` + +## Contribution + +### Appliquer la mise à jour de l'infrastructure + +Pour que les modifications de la description de l'infrastructure soient appliquées en production, il suffit de publier les changements sur la branche `main`. + +## Construit avec + +### Langages & Frameworks + +- [Terraform](https://www.terraform.io/) est un outil de description d'infrastructure par le code qui permet de créer et de maintenir une infrastructure de manière sûre et prévisible + +### Outils + +#### CI + +- [Github Actions](https://docs.github.com/en/actions) est l'outil d'intégration et de déploiement continu intégré à GitHub + - L'historique des déploiements est disponible [sous l'onglet Actions](https://github.com/anct-cartographie-nationale/client-infrastructure/actions/) +- Secrets du dépôt : + - `TF_API_TOKEN` : Le token d'api Terraform Cloud de l'équipe Cartographie Nationale qui permet à la CI d'opérer des actions sur Terraform Cloud + +#### Déploiement + +- [Terraform Cloud](https://www.clever-cloud.com/) est la plateforme proposée par HasiCorp pour administrer les modifications d'infrastructure + - Organisation : [cartographie-nationale](https://app.terraform.io/app/cartographie-nationale/workspaces) + - Workspaces : `api-*` + - [api-production](https://app.terraform.io/app/cartographie-nationale/workspaces/api-production) +- [AWS](https://aws.amazon.com/) est la plateforme de services Cloud proposée par Amazon. + - Utilisateur : `cartographie-nationale.client.infrastructure` + - Groupe : `client.deployer` + +## Licence + +Voir le fichier [LICENSE.md](./LICENSE.md) du dépôt. diff --git a/api-gateway.tf b/api-gateway.tf new file mode 100644 index 0000000..29a152f --- /dev/null +++ b/api-gateway.tf @@ -0,0 +1,58 @@ +resource "aws_apigatewayv2_api" "cartographie_nationale" { + name = "${local.product_information.context.project}-${local.product_information.context.service}" + protocol_type = "HTTP" +} + +resource "aws_apigatewayv2_stage" "cartographie_nationale" { + api_id = aws_apigatewayv2_api.cartographie_nationale.id + + name = "production" + auto_deploy = true + + access_log_settings { + destination_arn = aws_cloudwatch_log_group.api_cartographie_nationale.arn + + format = jsonencode({ + requestId = "$context.requestId" + sourceIp = "$context.identity.sourceIp" + requestTime = "$context.requestTime" + protocol = "$context.protocol" + httpMethod = "$context.httpMethod" + resourcePath = "$context.resourcePath" + routeKey = "$context.routeKey" + status = "$context.status" + responseLength = "$context.responseLength" + integrationErrorMessage = "$context.integrationErrorMessage" + }) + } +} + +#resource "aws_apigatewayv2_integration" "hello_world" { +# api_id = aws_apigatewayv2_api.cartographie_nationale.id +# +# integration_uri = aws_lambda_function.hello_world.invoke_arn +# integration_type = "AWS_PROXY" +# integration_method = "POST" +#} + +#resource "aws_apigatewayv2_route" "hello_world" { +# api_id = aws_apigatewayv2_api.cartographie_nationale.id +# +# route_key = "GET /hello" +# target = "integrations/${aws_apigatewayv2_integration.hello_world.id}" +#} + +resource "aws_cloudwatch_log_group" "api_cartographie_nationale" { + name = "/aws/api_gw/${aws_apigatewayv2_api.cartographie_nationale.name}" + + retention_in_days = 30 +} + +#resource "aws_lambda_permission" "api_cartographie_nationale" { +# statement_id = "AllowExecutionFromAPIGateway" +# action = "lambda:InvokeFunction" +# function_name = aws_lambda_function.hello_world.function_name +# principal = "apigateway.amazonaws.com" +# +# source_arn = "${aws_apigatewayv2_api.cartographie_nationale.execution_arn}/*/*" +#} diff --git a/dist/goodbeye/goodbye.get.js b/dist/goodbeye/goodbye.get.js new file mode 100644 index 0000000..55bdc1c --- /dev/null +++ b/dist/goodbeye/goodbye.get.js @@ -0,0 +1,18 @@ +export const handler = async (event) => { + console.log('Event: ', event); + let responseMessage = 'Goodbye, World!'; + + if (event.queryStringParameters && event.queryStringParameters['Name']) { + responseMessage = 'Goodbye, ' + event.queryStringParameters['Name'] + '!'; + } + + return ({ + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: responseMessage, + }) + }); +} diff --git a/dist/goodbeye/goodbye.get.zip b/dist/goodbeye/goodbye.get.zip new file mode 100644 index 0000000000000000000000000000000000000000..0275760a7a470f861fdb77395da975d9dd3d2717 GIT binary patch literal 461 zcmWIWW@Zs#-~htk4XWV`P;iKmfq{=ffgwFVKP9O$RWCiYL@%p2G=!Id-OB8Hio5Ce z6fXboDWw(M42&!C#YQh+*pC)oNQHV|n0{-hq*4vH|8vkXVH!pQQnsWO4kDcH9_DOG(QMJLm$tSM;_MBh8 zE#>gF+qXi_Upl2+xruMHs?3we;X@kHBkW=~Ppqf;0Kvd!$wc2#lmcJ7e5X|hXi>H3>7^%iDHe{S?1?+exV zu%Y(WlRS>l_s+kbTlGyAS-#k}t#ZDx-d^uI#sF`24j2BySO0-Q0SdqXZ$>5&W<=m3 g%YgzH2L3jJSfmAYfHx}}NEagz#sKMPpnVJs0BZHFcmMzZ literal 0 HcmV?d00001 diff --git a/dist/hello/hello.get.js b/dist/hello/hello.get.js new file mode 100644 index 0000000..02e2483 --- /dev/null +++ b/dist/hello/hello.get.js @@ -0,0 +1,18 @@ +export const handler = async (event) => { + console.log('Event: ', event); + let responseMessage = 'Hello, World!'; + + if (event.queryStringParameters && event.queryStringParameters['Name']) { + responseMessage = 'Hello, ' + event.queryStringParameters['Name'] + '!'; + } + + return ({ + statusCode: 200, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: responseMessage, + }) + }); +} diff --git a/dist/hello/hello.get.zip b/dist/hello/hello.get.zip new file mode 100644 index 0000000000000000000000000000000000000000..f46ed15a408eec4ce1ba24522ddfd591e4e1bd80 GIT binary patch literal 455 zcmWIWW@Zs#-~ht64XWV`P_U1Yfq{oXfgvL`CnsMoJ+(wHt2i`-mw{d0?0d?u?+cSe z{J*D^R&X;gvblB>g%0g>wnllpzZsguIWqtTsTCNS8WRM;OJ}$QDHlhFezK= z$*-nbM;DF%vd^1a7bb0bTK(rvwR=3Twd28S+8h>t?0S`%3M>VCNch literal 0 HcmV?d00001 diff --git a/global.tf b/global.tf new file mode 100644 index 0000000..59d0268 --- /dev/null +++ b/global.tf @@ -0,0 +1,31 @@ +locals { + product_information = { + context : { + project = "cartographie-nationale" + layer = "infrastructure" + service = "api" + start_date = "2023-09-05" + end_date = "unknown" + } + purpose : { + disaster_recovery = "medium" + service_class = "bronze" + } + organization : { + client = "anct" + } + stakeholders : { + business_owner = "celestin.leroux@beta.gouv.fr" + technical_owner = "marc.gavanier@beta.gouv.fr" + approver = "marc.gavanier@beta.gouv.fr" + creator = "terraform" + team = "cartographie-nationale" + } + } +} + +locals { + projectTitle = title(replace(local.product_information.context.project, "-", " ")) + layerTitle = title(replace(local.product_information.context.layer, "-", " ")) + serviceTitle = title(replace(local.product_information.context.service, "-", " ")) +} diff --git a/lambda.tf b/lambda.tf new file mode 100644 index 0000000..5e78a04 --- /dev/null +++ b/lambda.tf @@ -0,0 +1,59 @@ +data "aws_s3_objects" "lambda_scripts_metadata" { + bucket = aws_s3_bucket.api.id + prefix = "production/" +} + +data "aws_s3_object" "lambda_scripts" { + count = length(data.aws_s3_objects.lambda_scripts_metadata.keys) + key = element(data.aws_s3_objects.lambda_scripts_metadata.keys, count.index) + bucket = data.aws_s3_objects.lambda_scripts_metadata.id +} + +#resource "aws_lambda_function" "api_routes" { +# for_each = { +# for key, object in data.aws_s3_object.lambda_scripts : key => object +# } +# +# function_name = replace(each.value.key, ".zip", "") +# +# s3_bucket = aws_s3_bucket.api.id +# s3_key = each.value.key +# +# runtime = "nodejs18.x" +# handler = "${replace(each.value.key, ".zip", "")}.handler" +# +# source_code_hash = each.value.etag +# +# role = aws_iam_role.api_routes_roles.arn +#} + +#resource "aws_cloudwatch_log_group" "api_routes" { +# for_each = aws_lambda_function.api_routes +# +# name = "/aws/lambda/${each.value.function_name}" +# +# retention_in_days = 30 +#} + +resource "aws_iam_role" "api_routes_roles" { + name = "api_routes_roles" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Sid = "" + Principal = { + Service = "lambda.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "lambda_policy" { + role = aws_iam_role.api_routes_roles.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" +} diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..686f03e --- /dev/null +++ b/main.tf @@ -0,0 +1,21 @@ +terraform { + backend "remote" { + hostname = "app.terraform.io" + organization = "cartographie-nationale" + + workspaces { + prefix = "api-" + } + } + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 4.11" + } + } +} + +provider "aws" { + region = "eu-west-3" +} diff --git a/output.tf b/output.tf new file mode 100644 index 0000000..aacc77a --- /dev/null +++ b/output.tf @@ -0,0 +1,17 @@ +#output "function_name" { +# description = "Name of the Lambda function." +# +# value = aws_lambda_function.hello_world.function_name +#} + +output "debug" { + description = "Base URL for API Gateway stage." + + value = data.aws_s3_object.lambda_scripts +} + +output "base_url" { + description = "Base URL for API Gateway stage." + + value = aws_apigatewayv2_stage.cartographie_nationale.invoke_url +} diff --git a/s3.tf b/s3.tf new file mode 100644 index 0000000..854fb96 --- /dev/null +++ b/s3.tf @@ -0,0 +1,5 @@ +resource "aws_s3_bucket" "api" { + bucket = replace("${local.product_information.context.project}_${local.product_information.context.service}", "_", "-") + force_destroy = true + tags = local.tags +} diff --git a/tags.tf b/tags.tf new file mode 100644 index 0000000..24a2562 --- /dev/null +++ b/tags.tf @@ -0,0 +1,21 @@ +data "aws_caller_identity" "current_iam" {} + +locals { + tags = { + context-project = local.product_information.context.project + context-layer = local.product_information.context.layer + context-service = local.product_information.context.service + context-start_date = local.product_information.context.start_date + context-end_date = local.product_information.context.end_date + purpose-environment = terraform.workspace + purpose-disaster_recovery = local.product_information.purpose.disaster_recovery + purpose-service_class = local.product_information.purpose.service_class + organization-client = local.product_information.organization.client + stakeholders-business_owner = local.product_information.stakeholders.business_owner + stakeholders-technical_owner = local.product_information.stakeholders.technical_owner + stakeholders-approver = local.product_information.stakeholders.approver + stakeholders-creator = local.product_information.stakeholders.creator + stakeholders-team = local.product_information.stakeholders.team + stakeholders-deployer_iam_account = data.aws_caller_identity.current_iam.account_id + } +}